feat(S-301): Test-drive and implement src/execute.ts
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user