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.
91 lines • 4.62 kB
JavaScript
import { execFile } from 'node:child_process';
import { DRIVER_SPECS } from './driver-cli.js';
function defaultProbe(bin, args) {
return new Promise(resolvePromise => {
execFile(bin, [...args], { timeout: 10_000 }, (err, stdout, stderr) => {
resolvePromise({ ok: !err, output: `${String(stdout)}${String(stderr)}` });
});
});
}
/** Whether this process runs as root. Windows has no uid, and is never root by this test. */
function runningAsRoot() {
return process.getuid?.() === 0;
}
/**
* Run the preflight checks: Node is implicit (we are running), the picked driver's CLI must be
* installed, and it must be logged in. Returns every check plus an overall `ok`, which counts
* failures only, so a warning travels without blocking anything.
*
* The auth probe is skipped when the CLI is missing: a binary that does not resolve cannot
* answer a second question, and one "not found" beats two lines saying the same thing.
*/
export async function preflight(opts = {}) {
const agent = opts.driver ?? 'claude';
const spec = DRIVER_SPECS[agent];
const bin = opts.bin ?? spec.bin;
const probe = opts.probe ?? defaultProbe;
const checks = [{ name: 'node', ok: true, detail: process.version }];
const cli = await probe(bin, ['--version']);
checks.push(cli.ok
? { name: agent, ok: true, detail: cli.output.trim() || 'installed' }
: { name: agent, ok: false, detail: `\`${bin}\` not found — ${spec.installHint}` });
if (cli.ok) {
const loggedIn = spec.auth.loggedIn(await probe(bin, spec.auth.args));
// Only an explicit no fails. `undefined` means the CLI would not say, and an agent that might
// work is worth more than a warning we cannot stand behind.
if (loggedIn === false) {
checks.push({
name: `${agent} auth`,
ok: false,
detail: `\`${bin}\` is not logged in. Run \`${spec.auth.fix}\`, then start the session again.`,
});
}
else if (loggedIn === true) {
checks.push({ name: `${agent} auth`, ok: true, detail: 'logged in' });
}
}
// The publish half (#1419): an armed PR/merge fires `gh` at the finish, hours after the Start
// that could have said it will not work. Warnings, not failures — the session's own work needs
// no gh, and the push rung is plain git, so the agent is worth starting either way.
if (opts.publish) {
const ghCli = await probe('gh', ['--version']);
if (!ghCli.ok) {
checks.push({
name: 'gh',
ok: true,
warn: true,
detail: '`gh` not found — the armed PR cannot be opened, so publishing stops at the pushed branch. Install the GitHub CLI (`brew install gh`, or https://cli.github.com) and run `gh auth login`.',
});
}
else if (!(await probe('gh', ['auth', 'status'])).ok) {
// `gh auth status` exits non-zero when no host is logged in, and says so on stderr —
// which the probe folds into `output`, though the exit code alone decides here.
checks.push({
name: 'gh auth',
ok: true,
warn: true,
detail: '`gh` is not logged in — the armed PR cannot be opened, so publishing stops at the pushed branch. Run `gh auth login`, then start the session.',
});
}
}
// Root is the sudo-HOME trap (#1323), and it fails every agent identically: `sudo` moves `HOME`, the CLI
// looks for its credentials under root's home instead of the user's, finds none, and every agent
// dies the same way with the same empty log. Named here because the failure it causes says
// nothing at all about its cause.
if ((opts.isRoot ?? runningAsRoot)()) {
const sudoUser = opts.sudoUser !== undefined ? opts.sudoUser : process.env.SUDO_USER;
const asUser = sudoUser ? `\`${sudoUser}\`` : 'your own user';
checks.push({
name: 'root',
ok: true,
warn: true,
detail: `running as root, so the agent looks for credentials under root's home and will not find yours. Restart the daemon as ${asUser}, without \`sudo\`.`,
});
}
return { ok: checks.every(c => c.ok), checks };
}
/** The failing checks, one line each, the way a user is told what to fix. */
export function preflightProblems(result) {
return result.checks.filter(c => !c.ok).map(c => `${c.name}: ${c.detail}`);
}
//# sourceMappingURL=preflight.js.map