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.
99 lines • 4.95 kB
JavaScript
import { join } from 'node:path';
import { nodeGitRunner } from './project.js';
import { THE_FRAMEWORK_DIR } from './framework-dir.js';
import { frameworkGitignore, gitignorePath } from './framework-gitignore.js';
import { layoutMarker, layoutMarkerPath } from './layout.js';
import { nodeStoreFs } from './store/index.js';
import { materializePresets } from './presets.js';
import { errorMessage } from './error-message.js';
/**
* Activate the repo at `cwd`: commit any pre-existing dirty changes, create `.the-framework/` with
* its ignore file and layout marker (#1575), and commit the install. A repo whose ignore file is
* already there is a no-op (`alreadyActivated`) — the ignore file is the activation marker.
* Forgiving: any git/fs failure surfaces as `{ ok: false, error }`.
*/
export async function installProject(cwd, deps = {}) {
const git = deps.git ?? nodeGitRunner();
const fs = deps.fs ?? nodeStoreFs();
if (await fs.exists(gitignorePath(cwd)))
return { ok: true, alreadyActivated: true };
try {
// Auto-initialize a repo when the folder isn't one yet: The Framework treats
// git as the source of truth, so `git init` it for the user rather than erroring.
const insideRepo = await git(['rev-parse', '--is-inside-work-tree'], cwd)
.then(out => out.trim() === 'true')
.catch(() => false);
if (!insideRepo)
await git(['init'], cwd);
// Commit pre-existing changes first so the install commit is clean.
const status = await git(['status', '--porcelain'], cwd);
if (status.trim()) {
await git(['add', '-A'], cwd);
await git(['commit', '-m', '[The Framework] uncommitted changes'], cwd);
}
await fs.mkdir(join(cwd, THE_FRAMEWORK_DIR));
// Keep the transient agent state (events.jsonl / agent.json / agents/) out of git and the session
// archive in it (#313/#1179). The early return above established the file is absent.
await fs.write(gitignorePath(cwd), frameworkGitignore());
// Record the bookkeeping layout this build writes (#1575), so a build whose layout differs —
// a stale published one, say — refuses to run here instead of committing wrong-layout files.
await fs.write(layoutMarkerPath(cwd), layoutMarker());
// Materialize the quality presets so an on-before-mergeable TODO entry's filePath resolves to a
// real file the agent can open (#326). The .the-framework/.gitignore above keeps them out
// of git, so they are regenerated on install and track the installed framework version rather
// than going stale in the repo's history.
await materializePresets(cwd, fs);
// The ticket-format spec is NOT materialized (#674): it ships inside the package and the
// #683 context fragment points at its node_modules path, so it versions with the package.
await git(['add', '-A'], cwd);
await git(['commit', '-m', '[The Framework] install The Framework'], cwd);
return insideRepo ? { ok: true } : { ok: true, initialized: true };
}
catch (err) {
return { ok: false, error: errorMessage(err) };
}
}
/**
* A {@link DirLister} backed by `node:fs/promises`. The import is dynamic so
* the module core stays free of a hard `node:fs` dependency, same convention
* as {@link nodeStoreFs}; any error reads as `[]`.
*/
export function nodeDirLister() {
return {
async childDirs(dir) {
try {
const { readdir } = await import('node:fs/promises');
const entries = await readdir(dir, { withFileTypes: true });
return entries.filter(entry => entry.isDirectory()).map(entry => join(dir, entry.name));
}
catch {
return [];
}
},
};
}
/**
* The immediate child directories of `dir` that are their own git repo roots. A
* child is a root when `git rev-parse --show-prefix` (the path from the repo root
* down to the cwd) is empty; a subdir of an outer repo yields a non-empty prefix,
* and a non-repo makes git error. This beats comparing `--show-toplevel` to the
* child path, which breaks where the path crosses a symlink (e.g. macOS `/var` ->
* `/private/var`). Returns the surviving paths, deduped and sorted; never throws.
*/
export async function enumerateGitRepos(dir, deps = {}) {
const git = deps.git ?? nodeGitRunner();
const dirs = deps.dirs ?? nodeDirLister();
const repos = new Set();
for (const child of await dirs.childDirs(dir)) {
try {
const prefix = await git(['rev-parse', '--show-prefix'], child);
if (prefix.trim() === '')
repos.add(child);
}
catch {
// Not a repo (or git failed): skip.
}
}
return [...repos].sort();
}
//# sourceMappingURL=install.js.map