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.

56 lines 2.96 kB
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