import { execFile, type ExecFileOptions } from 'child_process' /** * Normalized result of a one-shot child-process run. `ok` is true on a clean * exit-0; `timedOut` distinguishes a kill-by-timeout (Node sets `err.killed` when * it terminates the process on the `timeout` option) from an ordinary non-zero * exit, so callers can show "Timed out …" versus the captured stderr. */ export interface ExecResult { ok: boolean stdout: string stderr: string timedOut: boolean /** The raw error on a non-zero exit / spawn failure (undefined when ok). */ error?: Error } /** * `execFile` as a promise that never rejects (audit SIMP2/CC4/CC5). Replaces the * six hand-rolled `new Promise((resolve) => execFile(…, cb))` wrappers across * probe / ytdlp (×2) / ffmpeg / indexer / download.probeMeta — each caller now maps * this normalized result to its own shape instead of repeating the `err.killed` * timeout check and the resolve/reject plumbing. (schedule.ts keeps its own wrapper: * it needs the numeric exit code, which this helper deliberately doesn't surface.) * * `windowsHide` defaults on (every caller wants the console window suppressed); * pass `timeout` / `maxBuffer` per call exactly as before. */ export function execFileAsync( file: string, args: readonly string[], options: ExecFileOptions = {} ): Promise { return new Promise((resolve) => { execFile(file, [...args], { windowsHide: true, ...options }, (err, stdout, stderr) => { // With the default (utf8) encoding stdout/stderr are strings, but ExecFileOptions // permits a Buffer encoding, so normalize defensively. const out = typeof stdout === 'string' ? stdout : stdout.toString() const errOut = typeof stderr === 'string' ? stderr : stderr.toString() if (err) { resolve({ ok: false, stdout: out, stderr: errOut, timedOut: (err as { killed?: boolean }).killed === true, error: err }) } else { resolve({ ok: true, stdout: out, stderr: errOut, timedOut: false }) } }) }) }