import { spawn as defaultSpawn } from "node:child_process"; import { existsSync as defaultExistsSync } from "node:fs"; import type { ClientName, ExecResult, DebugInfo, ExecuteOptions } from "./types.js"; import { ClientNotFoundError, ExecError } from "./types.js"; export const CLIENT_ARGS: Record string[]> = { codex: (p) => ["exec", "--yolo", p], claude: (p) => ["-p", p, "--dangerously-skip-permissions"], opencode: (p) => ["run", "--dangerously-skip-permissions", p], }; 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, client, durationMs: 0, }); } const spawnImpl = options.spawn ?? defaultSpawn; const existsSyncImpl = options.existsSync ?? defaultExistsSync; const timeoutMs = options.timeoutMs ?? 600_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, client, durationMs: 0, }); } const args = argBuilder(prompt); return new Promise((resolve, reject) => { let settled = false; let timedOut = false; let stdout = ""; let stderr = ""; let exitSignal: NodeJS.Signals | null = null; const startMs = Date.now(); 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); const durationMs = Date.now() - startMs; if (options.debug || options.onDebug) { const effectiveExitCode = result?.exitCode ?? (err instanceof ExecError ? err.result.exitCode : null); const debugInfo: DebugInfo = { command, args, pid: child.pid ?? undefined, exitCode: effectiveExitCode, exitSignal, durationMs, stderrLength: stderr.length, stdoutLength: stdout.length, noisySuccess: effectiveExitCode === 0 && stderr.length > 0, }; options.onDebug?.(debugInfo); } if (err) { if (err instanceof ExecError) { err.result.client = client; err.result.durationMs = durationMs; } reject(err); } else { resolve({ ...result!, client, durationMs }); } } child.on("error", (err: NodeJS.ErrnoException) => { if (err.code === "ENOENT") { settle(new ClientNotFoundError(client)); } else { settle( new ExecError(err.message, { stdout, stderr, exitCode: -1, client, durationMs: 0 }) ); } }); child.on("close", (code: number | null, signal: NodeJS.Signals | null) => { exitSignal = signal; if (timedOut) { settle( new ExecError(`Execution timed out after ${timeoutMs}ms`, { stdout, stderr, exitCode: -1, client, durationMs: 0, }) ); } else { settle(undefined, { stdout, stderr, exitCode: code ?? -1, client, durationMs: 0 }); } }); }); }