import { spawn } from "node:child_process"; export type CommandResult = { command: string; args: string[]; cwd: string; exitCode: number | null; signal: NodeJS.Signals | null; stdout: string; stderr: string; }; export type CommandRunner = (command: string, args: string[], options: { cwd: string; timeoutMs?: number; env?: NodeJS.ProcessEnv }) => Promise; export const runCommand: CommandRunner = (command, args, options) => new Promise((resolve, reject) => { const child = spawn(command, args, { cwd: options.cwd, env: options.env ?? process.env, stdio: ["ignore", "pipe", "pipe"] }); let stdout = ""; let stderr = ""; let settled = false; let timedOut = false; const timer = options.timeoutMs ? setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, options.timeoutMs) : undefined; child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString("utf8"); }); child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); }); child.on("error", (error) => { if (settled) return; settled = true; if (timer) clearTimeout(timer); reject(error); }); child.on("close", (exitCode, signal) => { if (settled) return; settled = true; if (timer) clearTimeout(timer); if (timedOut) { stderr = `${stderr}\nCommand timed out after ${options.timeoutMs}ms`.trimStart(); } resolve({ command, args, cwd: options.cwd, exitCode, signal, stdout, stderr }); }); }); export function formatCommand(command: string, args: string[]): string { return [command, ...args].join(" "); } export function tail(text: string, max = 12_000): string { return text.length > max ? text.slice(-max) : text; }