feat(S-301): Test-drive and implement src/execute.ts

This commit is contained in:
2026-05-18 18:01:51 -05:00
parent 360e27d952
commit a99041f910
2 changed files with 295 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
import { spawn as defaultSpawn } from "node:child_process";
import { existsSync as defaultExistsSync } from "node:fs";
import type { ClientName, ExecResult } from "./types.js";
import { ClientNotFoundError, ExecError } from "./types.js";
const CLIENT_ARGS: Record<ClientName, (prompt: string) => string[]> = {
codex: (p) => ["exec", p],
claude: (p) => ["-p", p],
opencode: (p) => [p],
};
export interface ExecuteOptions {
clientPath?: string;
timeoutMs?: number;
spawn?: typeof defaultSpawn;
existsSync?: typeof defaultExistsSync;
}
export async function executePrompt(
client: ClientName,
prompt: string,
options: ExecuteOptions = {}
): Promise<ExecResult> {
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 args = CLIENT_ARGS[client](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 });
}
});
});
}