import type { ChildProcess } from "node:child_process"; import { spawn as defaultSpawn } from "node:child_process"; import type { PathLike } from "node:fs"; import { existsSync as defaultExistsSync } from "node:fs"; import type { ClientName, ExecResult } from "./types.js"; import { ClientNotFoundError, ExecError } from "./types.js"; const CLIENT_ARGS: Record string[]> = { codex: (p) => ["exec", "--full-auto", p], claude: (p) => ["-p", p, "--dangerously-skip-permissions"], opencode: (p) => ["run", "--dangerously-skip-permissions", p], }; export interface ExecuteOptions { clientPath?: string; timeoutMs?: number; spawn?: (command: string, args: string[], options?: { shell?: boolean }) => ChildProcess; existsSync?: (path: PathLike) => boolean; } export async function executePrompt( client: ClientName, prompt: string, options: ExecuteOptions = {} ): Promise { if (prompt.trim() === "") { throw new ExecError("Prompt cannot be empty", { stdout: "", stderr: "", exitCode: -1, }); } const spawnImpl = options.spawn ?? defaultSpawn; const existsSyncImpl = options.existsSync ?? defaultExistsSync; const timeoutMs = options.timeoutMs ?? 300_000; const command = options.clientPath ?? client; if (options.clientPath && !existsSyncImpl(options.clientPath)) { throw new ClientNotFoundError(client); } const argBuilder = (CLIENT_ARGS as Record string[]>)[client]; if (!argBuilder) { throw new ExecError(`Unknown client: ${client}`, { stdout: "", stderr: "", exitCode: -1, }); } const args = argBuilder(prompt); return new Promise((resolve, reject) => { let settled = false; let timedOut = false; let stdout = ""; let stderr = ""; const child = spawnImpl(command, args, { shell: false, }); child.stdout?.on("data", (chunk: Buffer | string) => { stdout += chunk.toString(); }); child.stderr?.on("data", (chunk: Buffer | string) => { stderr += chunk.toString(); }); const timeout = setTimeout(() => { timedOut = true; child.kill(); }, timeoutMs); function settle( err?: Error | undefined, result?: ExecResult | undefined ): void { if (settled) return; settled = true; clearTimeout(timeout); if (err) reject(err); else resolve(result!); } child.on("error", (err: NodeJS.ErrnoException) => { if (err.code === "ENOENT") { settle(new ClientNotFoundError(client)); } else { settle( new ExecError(err.message, { stdout, stderr, exitCode: -1 }) ); } }); child.on("close", (code: number | null) => { if (timedOut) { settle( new ExecError(`Execution timed out after ${timeoutMs}ms`, { stdout, stderr, exitCode: -1, }) ); } else { settle(undefined, { stdout, stderr, exitCode: code ?? -1 }); } }); }); }