import { spawn, spawnSync } from "node:child_process"; import { closeSync, createWriteStream, existsSync, openSync, readSync, statSync } from "node:fs"; export interface CommandResult { command: string[]; cwd: string; exitCode: number | null; stdout: string; stderr: string; signal: NodeJS.Signals | null; timedOut: boolean; } export function runCommand(command: string[], cwd: string, options: { timeoutMs?: number; env?: NodeJS.ProcessEnv } = {}): CommandResult { const result = spawnSync(command[0], command.slice(1), { cwd, encoding: "utf8", env: options.env, maxBuffer: 1024 * 1024 * 8, timeout: options.timeoutMs, }); const error = result.error as (Error & { code?: string }) | undefined; return { command, cwd, exitCode: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? error?.message ?? "", signal: result.signal, timedOut: error?.code === "ETIMEDOUT", }; } export function commandOk(command: string[], cwd: string): boolean { return runCommand(command, cwd).exitCode === 0; } export async function runCommandToFiles(command: string[], cwd: string, stdoutFile: string, stderrFile: string): Promise { const stdout = createWriteStream(stdoutFile, { flags: "a" }); const stderr = createWriteStream(stderrFile, { flags: "a" }); stdout.write(`$ ${command.map((part) => JSON.stringify(part)).join(" ")}\n`); const child = spawn(command[0], command.slice(1), { cwd, env: process.env }); child.stdout.pipe(stdout, { end: false }); child.stderr.pipe(stderr, { end: false }); const exitCode = await new Promise((resolve) => { child.on("close", (code) => resolve(code)); child.on("error", (error) => { stderr.write(`${error.stack ?? error.message}\n`); resolve(127); }); }); stdout.write(`\n[exit_code=${exitCode}]\n`); stdout.end(); stderr.end(); return exitCode; } export function tailFile(path: string, maxBytes = 8192): string { if (!existsSync(path)) return ""; const safeMaxBytes = Math.max(0, Math.floor(maxBytes)); if (safeMaxBytes === 0) return ""; const size = statSync(path).size; const bytesToRead = Math.min(size, safeMaxBytes); const buffer = Buffer.alloc(bytesToRead); const fd = openSync(path, "r"); try { readSync(fd, buffer, 0, bytesToRead, size - bytesToRead); } finally { closeSync(fd); } return buffer.toString("utf8"); }