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.
56 lines • 2.96 kB
JavaScript
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { basename, dirname, join, resolve } from 'node:path';
/** The env var naming the directory session specs are written to. Set by tests; defaults to the OS temp dir. */
const SPEC_DIR_ENV = 'FRAMEWORK_SESSION_SPEC_DIR';
/** The per-spec mkdtemp directory's prefix, which marks the directory as this module's to remove. */
const SPEC_DIR_PREFIX = 'framework-session-';
/**
* Write a spec and return its path, for `framework --agent <path>`.
*
* A file rather than a pipe or an fd: the child is spawned detached with its stdio closed, so
* there is no channel to inherit, and a path survives the spawn without either side blocking on
* the other. The child removes it once read, so a spec never outlives the session it started.
*/
export async function writeAgentSpec(spec, env = process.env) {
const dir = await mkdtemp(join(env[SPEC_DIR_ENV] ?? tmpdir(), SPEC_DIR_PREFIX));
const path = join(dir, 'session.json');
await writeFile(path, JSON.stringify(spec, null, 2) + '\n');
return path;
}
/**
* Remove a spec — and the directory {@link writeAgentSpec} made for it, or removing one spec per
* session would leave one empty directory per session behind forever. Only a directory this
* module verifiably made goes whole: it must carry the mkdtemp prefix AND sit directly in the
* configured spec home. `--agent <path>` accepts any path, and neither a hand-written spec nor a
* user's own directory that happens to be named like ours may be taken with the file.
*
* Also the cleanup for a spawn whose child never consumed the spec — the spawn failed outright,
* or the child died before reading it — which otherwise left the whole prompt sitting on disk.
* Idempotent: removing a spec an exiting child already consumed is a no-op.
*/
export async function removeAgentSpec(path, env = process.env) {
const dir = dirname(path);
const home = resolve(env[SPEC_DIR_ENV] ?? tmpdir());
if (basename(dir).startsWith(SPEC_DIR_PREFIX) && resolve(dirname(dir)) === home) {
await rm(dir, { recursive: true, force: true }).catch(() => { });
}
else {
await rm(path, { force: true }).catch(() => { });
}
}
/**
* Read a spec, and remove it. Consumed rather than merely read: it is a one-shot handoff, and a
* session's options can name a device token (`options.remote`), which has no business staying on
* disk after the session that used it has started.
*/
export async function readAgentSpec(path, env = process.env) {
const raw = await readFile(path, 'utf8');
await removeAgentSpec(path, env);
const spec = JSON.parse(raw);
if (typeof spec.prompt !== 'string' || typeof spec.cwd !== 'string' || !spec.kind) {
throw new Error(`${path} is not a session spec`);
}
return { ...spec, options: spec.options ?? {} };
}
//# sourceMappingURL=agent-spec.js.map