Files
stack/packages/quality-rails/src/evaluator/adapter.ts
T

68 lines
2.1 KiB
TypeScript

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<AdapterOutcome> {
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 });
});
});
},
};
}