import { spawn } from "node:child_process"; export interface ProcessRequest { command: string; args: string[]; cwd: string; input?: string; signal?: AbortSignal; env?: NodeJS.ProcessEnv; } export interface ProcessResult { stdout: string; stderr: string; exitCode: number; } export type ProcessRunner = (request: ProcessRequest) => Promise; export const runProcess: ProcessRunner = (request) => new Promise((resolve, reject) => { const child = spawn(request.command, request.args, { cwd: request.cwd, env: request.env, shell: false, stdio: ["pipe", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; let aborted = false; let closed = false; let killTimer: NodeJS.Timeout | undefined; child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString("utf8"); }); child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); }); const abort = () => { aborted = true; child.kill("SIGTERM"); killTimer = setTimeout(() => { if (!closed) child.kill("SIGKILL"); }, 5000); }; if (request.signal?.aborted) abort(); else request.signal?.addEventListener("abort", abort, { once: true }); child.on("error", (error) => { request.signal?.removeEventListener("abort", abort); reject(new Error(`Failed to start ${request.command}: ${error.message}`)); }); child.on("close", (exitCode) => { closed = true; if (killTimer) clearTimeout(killTimer); request.signal?.removeEventListener("abort", abort); if (aborted) { reject(new Error(`${request.command} was aborted`)); return; } resolve({ stdout, stderr, exitCode: exitCode ?? 1 }); }); if (request.input !== undefined) child.stdin.end(request.input); else child.stdin.end(); }); export async function runChecked( runner: ProcessRunner, request: ProcessRequest, ): Promise { const result = await runner(request); if (result.exitCode !== 0) { const detail = result.stderr.trim() || result.stdout.trim() || "no diagnostic output"; throw new Error( `${request.command} exited with ${result.exitCode}: ${detail}`, ); } return result; }