72 lines
1.7 KiB
TypeScript
72 lines
1.7 KiB
TypeScript
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<RunResult> {
|
|
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 });
|
|
});
|
|
});
|
|
}
|