import { spawn, execFile, type ExecException, type ExecFileOptions } from "node:child_process"; import { createInterface } from "node:readline"; import { promisify } from "node:util"; type ExecProps = { command: string; args?: string[]; env?: NodeJS.ProcessEnv; } & ExecFileOptions; export const exec = async ({ command, args = [], env = {}, ...rest }: ExecProps) => { const options = { env: { ...process.env, ...env }, }; try { const { stdout, stderr } = await promisify(execFile)(command, args, { ...options, ...rest, encoding: "utf8", }); return { exitCode: 0, stdout, stderr }; } catch (error) { const execError = error as ExecException; return { exitCode: typeof execError.code === "number" ? execError.code : 1, stdout: execError.stdout || "", stderr: execError.stderr || "", }; } }; export interface SafeSpawnParamsBase { command: string; args: string[]; env?: NodeJS.ProcessEnv; signal?: AbortSignal; onStderr?: (error: string) => void; onSpawn?: (child: ReturnType) => void; } export interface SafeSpawnParamsLines extends SafeSpawnParamsBase { stdoutMode?: "lines"; onStdout?: (line: string) => void; } export interface SafeSpawnParamsRaw extends SafeSpawnParamsBase { stdoutMode: "raw"; onStdout?: never; } export type SafeSpawnParams = SafeSpawnParamsLines | SafeSpawnParamsRaw; export type SpawnResult = { exitCode: number; summary: string; error: string; }; export function safeSpawn(params: SafeSpawnParamsLines): Promise; export function safeSpawn(params: SafeSpawnParamsRaw): Promise; export function safeSpawn(params: SafeSpawnParams): Promise { const { command, args, env = {}, signal, onStderr, onSpawn } = params; const stdoutMode = params.stdoutMode ?? "lines"; const onStdout = stdoutMode === "lines" ? params.onStdout : undefined; let lastStdout = ""; let lastStderr = ""; return new Promise((resolve) => { const child = spawn(command, args, { env: { ...process.env, ...env }, signal: signal, stdio: ["ignore", "pipe", "pipe"], }); onSpawn?.(child); child.stderr.setEncoding("utf8"); const rlErr = createInterface({ input: child.stderr }); if (stdoutMode === "lines") { child.stdout.setEncoding("utf8"); const rl = createInterface({ input: child.stdout }); rl.on("line", (line) => { if (onStdout) onStdout(line); const trimmed = line.trim(); if (trimmed.length > 0) { lastStdout = line; } }); } rlErr.on("line", (line) => { if (onStderr) onStderr(line); const trimmed = line.trim(); if (trimmed.length > 0) { lastStderr = line; } }); child.on("error", (err) => { resolve({ exitCode: -1, summary: lastStdout, error: err.message || lastStderr, }); }); child.on("close", (code) => { resolve({ exitCode: code ?? -1, summary: lastStdout, error: lastStderr, }); }); }); }