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.

130 lines 5.66 kB
import { cliRunner } from './cli-exec.js'; import { nodeFs } from './node-fs.js'; import { gitignorePath } from './framework-gitignore.js'; /** A {@link ProjectFs} backed by `node:fs/promises`. See {@link nodeFs}. */ export function nodeProjectFs() { const { exists } = nodeFs(); return { exists }; } /** * A repo is "activated"/installed for The Framework when it has the * `.the-framework/.gitignore` install writes — the same marker install's own * no-op check reads (#1600), so a `.the-framework/` directory something else * created can never read as activated while the repo still lacks the ignore * file that keeps framework state off its branches. Read-only check; writing * the marker + the install commit is a separate, deferred concern. */ export async function isActivated(cwd, fs = nodeProjectFs()) { return fs.exists(gitignorePath(cwd)); } /** * A local read: the index, a ref, or objects already on disk. Kept at the budget that used to * cover everything, so a hung read still fails fast instead of holding the daemon longer (#997). */ export const GIT_READ_TIMEOUT_MS = 10_000; /** A local mutation. Bounded by disk, but an index write on a large repo outlives a read. */ export const GIT_WRITE_TIMEOUT_MS = 30_000; /** * The network, or a whole checkout. `git worktree add` writes every tracked file and `git push` * uploads a packfile; on a large repo both routinely pass 10s, which is what #997 is about. Well * past the 60s `gh` allows its write actions (dashboard/gh.ts), because those are API calls. */ export const GIT_SLOW_TIMEOUT_MS = 120_000; /** Subcommands that only read. Everything unlisted is treated as a mutation. */ const GIT_READ_OPS = new Set([ 'branch', 'cat-file', 'diff', 'log', 'ls-files', 'merge-base', 'remote', 'rev-list', 'rev-parse', 'show', 'status', 'symbolic-ref', ]); /** Subcommands bounded by the network rather than by this machine. */ const GIT_SLOW_OPS = new Set(['clone', 'fetch', 'pull', 'push', 'ls-remote']); /** Global options whose value is the next word, so that word is not the subcommand. */ const GIT_GLOBAL_VALUE_OPTIONS = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path']); /** * The subcommand and its own words, with the leading global options dropped. A bare flag filter * would read `git -C /repo push` as the subcommand `/repo`, costing `push` its slow budget. */ function gitWords(args) { let i = 0; while (i < args.length) { const arg = args[i] ?? ''; if (!arg.startsWith('-')) break; // The `--opt=value` form carries its value inline; the separate form eats the next word. i += GIT_GLOBAL_VALUE_OPTIONS.has(arg) ? 2 : 1; } return args.slice(i).filter(arg => !arg.startsWith('-')); } /** * The timeout for one git invocation, chosen by subcommand (#997). One flat 10s budget covered * the repo's ~20 call sites, so the slowest two ran under what is really a read's budget: a * SIGTERM'd `worktree add` drops an agent into the user's main checkout, a SIGTERM'd `push` may * have half-landed. Mirrors the read/write split `gh` already has (dashboard/gh.ts). */ export function gitTimeoutMs(args) { const words = gitWords(args); const op = words[0] ?? ''; if (GIT_SLOW_OPS.has(op)) return GIT_SLOW_TIMEOUT_MS; if (op === 'worktree') { // Only `add` writes a checkout; `list` is a read, and remove/prune are ordinary mutations. if (words[1] === 'add') return GIT_SLOW_TIMEOUT_MS; return words[1] === 'list' ? GIT_READ_TIMEOUT_MS : GIT_WRITE_TIMEOUT_MS; } return GIT_READ_OPS.has(op) ? GIT_READ_TIMEOUT_MS : GIT_WRITE_TIMEOUT_MS; } /** * A {@link GitRunner} backed by `execFile('git', ...)`. Rejects on any git error, and with a * `CliTimeoutError` when the operation outran its {@link gitTimeoutMs} budget. * * The buffer is raised well past the default because a repo crawl (`git ls-files`) prints a * line per file, and a large checkout overruns it. */ export function nodeGitRunner() { return cliRunner({ bin: 'git', timeoutMs: gitTimeoutMs, maxBuffer: 16 * 1024 * 1024 }); } /** * Whether `cwd` sits inside a git working tree (#997). Lets a caller tell "this project cannot * host a worktree at all" from "git was there and the operation failed", which are the same * rejection out of `git worktree add` but call for opposite handling. * * Forgiving in one direction only: an unreadable / missing git reads as "no repo", which is the * conservative answer for the caller that treats a repo's failure as fatal. */ export async function isGitRepo(cwd, agent = nodeGitRunner()) { return agent(['rev-parse', '--is-inside-work-tree'], cwd) .then(out => out.trim() === 'true') .catch(() => false); } /** * List every file git sees in the repo at `cwd`: tracked + untracked, honoring * .gitignore. Uses `git ls-files -z --cached --others --exclude-standard` (the * same approach Vike uses). Returns repo-relative paths, deduped and sorted. * Forgiving: a non-repo / missing git / any failure yields `[]`, never throws. */ export async function crawlRepoFiles(cwd, agent = nodeGitRunner()) { try { const out = await agent(['ls-files', '-z', '--cached', '--others', '--exclude-standard'], cwd); const files = new Set(); for (const entry of out.split('\0')) { const trimmed = entry.trim(); if (trimmed) files.add(trimmed); } return [...files].sort(); } catch { return []; } } //# sourceMappingURL=project.js.map