import { spawn } from 'node:child_process'; import type { ChildProcess } from 'node:child_process'; import type { AdapterOutcome, AdapterRequest, ProcessAdapter } from './types.js'; /** * Default thin process adapter (spawn-based). Runs a command to completion with * a hard timeout and reports exit code + captured output — it owns NO verdict * logic. Interpreting the outcome is always the check implementation's job. */ export function createSpawnProcessAdapter(): ProcessAdapter { return { run(request: AdapterRequest): Promise { return new Promise((resolve) => { let child: ChildProcess; try { child = spawn(request.file, request.args, { cwd: request.cwd, stdio: ['ignore', 'pipe', 'pipe'], }); } catch (error) { resolve({ ok: false, kind: 'spawn-error', message: error instanceof Error ? error.message : String(error), }); return; } let stdout = ''; let stderr = ''; let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; child.kill('SIGKILL'); resolve({ ok: false, kind: 'timeout', message: `process timed out after ${request.timeoutMs}ms: ${request.file}`, }); }, request.timeoutMs); const settle = (outcome: AdapterOutcome): void => { if (settled) return; settled = true; clearTimeout(timer); resolve(outcome); }; child.stdout?.on('data', (chunk: Buffer) => { stdout += chunk.toString('utf8'); }); child.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString('utf8'); }); child.on('error', (error: Error) => { settle({ ok: false, kind: 'spawn-error', message: error.message }); }); child.on('close', (code: number | null) => { settle({ ok: true, exitCode: code, stdout, stderr }); }); }); }, }; }