UNPKG

framework

Version:

The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.

117 lines 5.54 kB
import { nodeGitRunner } from '../project.js'; import { cutToPreview, readConfinedFile, safeRepoPath } from './file-read.js'; // `safeRepoPath` moved to file-read.js with #828, where the unchanged-file preview shares it. // Re-exported so this stays the import site it has been. export { safeRepoPath }; /** Drop git's `diff --git` / `index` / `mode` preamble, keeping the `---`/`+++`/hunk body. */ function hunksOnly(patch) { const lines = patch.split('\n'); const start = lines.findIndex(line => line.startsWith('--- ') || line.startsWith('@@')); return (start === -1 ? [] : lines.slice(start)).join('\n').trimEnd(); } /** Count the +/- lines of a unified patch, ignoring the `---`/`+++` file headers. */ function countChanges(patch) { let added = 0; let removed = 0; for (const line of patch.split('\n')) { if (line.startsWith('+++') || line.startsWith('---')) continue; if (line.startsWith('+')) added++; else if (line.startsWith('-')) removed++; } return { added, removed }; } /** An untracked file has no blob to diff against, so render its contents as all-added. */ function asAllAdded(text) { const lines = text.split('\n'); if (lines[lines.length - 1] === '') lines.pop(); // a trailing newline is not a line return lines.map(line => `+${line}`).join('\n'); } /** * The diff for one changed file in the checkout at `cwd`. * * Tracked files diff against `HEAD`, not the index, so a change the agent staged still shows; * that also matches `git status --porcelain`, which is what dotted the file in the first place. * An untracked file is rendered as all-added from its contents, since `git diff --no-index` * exits non-zero on a difference and would read as a failure here. * * Returns null when the path is unsafe, unreadable, or has no diff to show. */ export async function readFileDiff(cwd, path, status, git = nodeGitRunner()) { if (!safeRepoPath(path)) return null; if (status === 'untracked') { const raw = await readConfinedFile(cwd, path); if (!raw) return null; if (raw.includes(0)) return { path, status, patch: '', added: 0, removed: 0, truncated: false, binary: true }; const { body: patch, truncated } = cutToPreview(asAllAdded(raw.toString('utf8'))); return { path, status, patch, ...countChanges(patch), truncated, binary: false }; } // `HEAD` is missing in a repo with no commits yet; the working-tree diff is the honest answer // there rather than an error. const raw = await git(['diff', '--unified=3', 'HEAD', '--', path], cwd) .catch(() => git(['diff', '--unified=3', '--', path], cwd)) .catch(() => ''); if (!raw.trim()) return null; if (/^Binary files /m.test(raw)) return { path, status, patch: '', added: 0, removed: 0, truncated: false, binary: true }; const { body: patch, truncated } = cutToPreview(hunksOnly(raw)); if (!patch) return null; return { path, status, patch, ...countChanges(patch), truncated, binary: false }; } /** * Parse `git diff --numstat` output: `added<TAB>removed<TAB>path`, with `-` for both counts * on a binary file. The one parser for the grammar — it was written here and in run-handoff, * and the copies had already drifted on a path containing a tab (this side rejoined it, the * other dropped the line). */ export function parseNumstat(out) { const entries = []; for (const line of out.split('\n')) { const [added, removed, ...rest] = line.split('\t'); const path = rest.join('\t'); if (!path || added === undefined || removed === undefined) continue; const binary = added === '-' || removed === '-'; entries.push({ path, added: binary ? 0 : Number(added) || 0, removed: binary ? 0 : Number(removed) || 0, binary }); } return entries; } /** * Every changed file in the checkout at `cwd`, with its line counts: the session's Changes list * (#817). One `git status` plus one `git diff --numstat`, not a diff per file, so a session that * touched forty files still costs two git calls. * * Untracked files have no numstat entry (they are not in the diff at all), so their added count * is their line count, read from disk. Sorted by path, so the list does not reshuffle as a live * session edits. */ export async function readFileChanges(cwd, statuses, git = nodeGitRunner()) { const paths = Object.keys(statuses).filter(safeRepoPath); if (paths.length === 0) return []; const numstat = await git(['diff', '--numstat', 'HEAD'], cwd) .catch(() => git(['diff', '--numstat'], cwd)) .catch(() => ''); const counted = new Map(parseNumstat(numstat).map(entry => [entry.path, entry])); const changes = await Promise.all(paths.map(async (path) => { const status = statuses[path]; const known = counted.get(path); if (known) return { ...known, status }; if (status !== 'untracked') return { path, status, added: 0, removed: 0, binary: false }; // An untracked file is not in any diff, so its whole content is the addition. const diff = await readFileDiff(cwd, path, status, git).catch(() => null); return { path, status, added: diff?.added ?? 0, removed: 0, binary: diff?.binary ?? false }; })); return changes.sort((a, b) => a.path.localeCompare(b.path)); } //# sourceMappingURL=file-diff.js.map