import { spawn } from 'node:child_process'; export interface RunResult { stdout: string; stderr: string; code: number; } export function run( command: string, args: string[] = [], timeoutMs = 120000, ): Promise { return new Promise((resolve, reject) => { const printable = [command, ...args].join(' '); console.log(`[shell] running: ${printable}`); const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env, }); let stdout = ''; let stderr = ''; const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error(`Command timed out after ${timeoutMs}ms: ${printable}`)); }, timeoutMs); child.stdout.on('data', (chunk) => { stdout += chunk.toString(); }); child.stderr.on('data', (chunk) => { stderr += chunk.toString(); }); child.on('error', (err) => { clearTimeout(timer); console.error(`[shell] failed to start: ${printable}`); console.error(`[shell] error: ${err.message}`); reject(err); }); child.on('close', (code) => { clearTimeout(timer); const exitCode = code ?? -1; if (stdout.trim()) { console.log(`[shell] stdout for ${printable}:\n${stdout.trim()}`); } if (stderr.trim()) { console.warn(`[shell] stderr for ${printable}:\n${stderr.trim()}`); } if (exitCode !== 0) { const err = new Error( `Command failed with exit code ${exitCode}: ${printable}\n${stderr || stdout}`, ); reject(err); return; } console.log(`[shell] completed: ${printable}`); resolve({ stdout, stderr, code: exitCode }); }); }); }