import { assert, codedError, hashValue, sleep, throwIfShutdownRequested, } from '../assertions/core.mjs'; import { fs, path, spawn, withLoopbackNoProxy } from '../dependencies.mjs'; import { activeCommandChildren, appRoot, commandOutputLimit, manifestPath, state, } from '../runtime-state.mjs'; import { isSupervisorSwarmTransientRetrySuite, recordSupervisorSwarmChatSessionFailureDiagnostic, } from '../suites/supervisor-swarm.mjs'; import { isIsolatedRunnerSuite } from './reporting.mjs'; export async function prepareCliBinary() { const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo'; await runProcess( cargo, ['build', '--quiet', '--manifest-path', manifestPath], { cwd: appRoot, timeoutMs: 15 * 60 * 1000, }, ); const metadata = await runProcess( cargo, [ 'metadata', '--format-version', '1', '--no-deps', '--manifest-path', manifestPath, ], { cwd: appRoot, timeoutMs: 120_000 }, ); const parsed = JSON.parse(metadata.stdout); const executable = path.join( parsed.target_directory, 'debug', `genarrative-ai-game-creator-shell${process.platform === 'win32' ? '.exe' : ''}`, ); const binary = await fs.stat(executable).catch(() => null); assert(binary?.isFile(), 'cli-binary-missing'); return executable; } export async function runCli(args, options = {}) { assert(Boolean(state.cliBinary), 'cli-binary-not-ready'); assert(Boolean(state.runtimeConfigDir), 'runtime-config-dir-not-ready'); if ( isIsolatedRunnerSuite() && state.options?.configDir && path.resolve(state.runtimeConfigDir) === path.resolve(state.options.configDir) ) { state.isolatedRunner.sourceConfigCliCallCount += 1; } return runProcess( state.cliBinary, [...args, '--config-dir', state.runtimeConfigDir], { cwd: appRoot, timeoutMs: options.timeoutMs ?? 60_000, stdin: options.stdin, allowNonZero: options.allowNonZero ?? false, env: buildCliChildEnvironment(), }, ); } export function buildCliChildEnvironment() { const environment = { ...process.env, NO_COLOR: '1', RUST_BACKTRACE: '0', }; return isSupervisorSwarmTransientRetrySuite() ? withLoopbackNoProxy(environment) : environment; } export function safeProcessFailureDiagnostic({ code = null, signal = null, error = null, stdout = '', stderr = '', } = {}) { const stdoutText = Buffer.isBuffer(stdout) ? stdout.toString('utf8') : String(stdout); const stderrText = Buffer.isBuffer(stderr) ? stderr.toString('utf8') : String(stderr); const combined = `${error?.message ?? ''}\n${stdoutText}\n${stderrText}`; const processErrorCode = /^[A-Z0-9_]+$/u.test(error?.code ?? '') ? error.code : 'none'; const closeSignal = /^[A-Z0-9]+$/u.test(signal ?? '') ? signal : 'none'; let failureKind = 'closed'; if ( processErrorCode === 'ENOSPC' || /(?:\bENOSPC\b|No space left on device|os error 28)/iu.test(combined) ) { failureKind = 'enospc'; } else if (processErrorCode !== 'none') { failureKind = 'process-error'; } else if (closeSignal !== 'none') { failureKind = 'signal'; } else if (Number.isInteger(code) && code !== 0) { failureKind = 'nonzero-exit'; } return { failureKind, exitCode: Number.isInteger(code) ? String(code) : 'none', signal: closeSignal, processErrorCode, stderrChars: [...stderrText].length, stderrSha256: hashValue(stderrText) ?? 'none', }; } export function codedProcessError(code, details) { const error = codedError(code, details?.error); error.safeFailureDiagnostic = safeProcessFailureDiagnostic(details); return error; } export function startInteractiveCli(args) { assert(Boolean(state.cliBinary), 'interactive-cli-binary-not-ready'); assert(Boolean(state.runtimeConfigDir), 'interactive-config-dir-not-ready'); if ( isIsolatedRunnerSuite() && state.options?.configDir && path.resolve(state.runtimeConfigDir) === path.resolve(state.options.configDir) ) { state.isolatedRunner.sourceConfigCliCallCount += 1; } const child = spawn( state.cliBinary, [...args, '--config-dir', state.runtimeConfigDir], { cwd: appRoot, env: buildCliChildEnvironment(), stdio: ['pipe', 'pipe', 'pipe'], }, ); activeCommandChildren.add(child); const session = { child, stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), closed: false, closeInfo: null, closePromise: null, }; session.closePromise = new Promise((resolve) => { child.on('error', (error) => { activeCommandChildren.delete(child); session.closed = true; session.closeInfo = { code: null, signal: null, error }; resolve(session.closeInfo); }); child.on('close', (code, signal) => { activeCommandChildren.delete(child); session.closed = true; session.closeInfo = { code, signal, error: null }; resolve(session.closeInfo); }); }); child.stdout.on('data', (chunk) => { state.transcriptScanner?.scan('interactive-stdout', chunk); state.formalConfigPathTranscriptScanner?.scan('interactive-stdout', chunk); session.stdout = appendBounded(session.stdout, chunk, commandOutputLimit); }); child.stderr.on('data', (chunk) => { state.transcriptScanner?.scan('interactive-stderr', chunk); state.formalConfigPathTranscriptScanner?.scan('interactive-stderr', chunk); session.stderr = appendBounded(session.stderr, chunk, commandOutputLimit); }); return session; } export function interactiveCliOutput(session) { return `${session.stdout.toString('utf8')}\n${session.stderr.toString('utf8')}`; } export function writeInteractiveCliLine(session, line) { assert(!session.closed, 'interactive-cli-already-closed'); assert(session.child.stdin.writable, 'interactive-cli-stdin-not-writable'); session.child.stdin.write(`${line}\n`); } export async function waitForInteractiveCliOutput( session, predicate, code, timeoutMs, ) { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const output = interactiveCliOutput(session); if (predicate(output)) return output; if (session.closed) { if (session === state.supervisorSwarmCliSession) { recordSupervisorSwarmChatSessionFailureDiagnostic(session); } throw codedError(`${code}-cli-closed`); } await sleep(50); } throw codedError(code); } export async function waitForInteractiveCliExit(session, timeoutMs) { const result = await Promise.race([ session.closePromise, sleep(timeoutMs).then(() => null), ]); if (!result) throw codedError('interactive-cli-exit-timeout'); if (result.error) throw codedError('interactive-cli-process-error'); assert( result.code === 0 && result.signal === null, 'interactive-cli-exit-invalid', ); return result; } export async function closeInteractiveCli(session) { if (!session || session.closed) return; if (session.child.stdin.writable) { session.child.stdin.write('/quit\n'); } let result = await Promise.race([ session.closePromise, sleep(3_000).then(() => null), ]); if (!result && !session.closed) { session.child.kill('SIGTERM'); result = await Promise.race([ session.closePromise, sleep(2_000).then(() => null), ]); } if (!result && !session.closed) { session.child.kill('SIGKILL'); result = await session.closePromise; } assert(Boolean(result), 'interactive-cli-cleanup-timeout'); } export async function runProcess( program, args, { cwd, timeoutMs, stdin, allowNonZero = false, env = null }, ) { throwIfShutdownRequested(); return new Promise((resolve, reject) => { const child = spawn(program, args, { cwd, env: env ?? { ...process.env, NO_COLOR: '1', RUST_BACKTRACE: '0' }, stdio: [stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'], }); activeCommandChildren.add(child); let stdout = Buffer.alloc(0); let stderr = Buffer.alloc(0); let timedOut = false; const timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, timeoutMs); child.stdout.on('data', (chunk) => { state.transcriptScanner?.scan('stdout', chunk); state.projectPathTranscriptScanner?.scan('stdout', chunk); state.formalConfigPathTranscriptScanner?.scan('stdout', chunk); stdout = appendBounded(stdout, chunk, commandOutputLimit); }); child.stderr.on('data', (chunk) => { state.transcriptScanner?.scan('stderr', chunk); state.projectPathTranscriptScanner?.scan('stderr', chunk); state.formalConfigPathTranscriptScanner?.scan('stderr', chunk); stderr = appendBounded(stderr, chunk, commandOutputLimit); }); child.on('error', (error) => { clearTimeout(timer); activeCommandChildren.delete(child); reject( codedProcessError('process-spawn-failed', { error, stdout, stderr, }), ); }); child.on('close', (code, signal) => { clearTimeout(timer); activeCommandChildren.delete(child); const result = { stdout: stdout.toString('utf8'), stderr: stderr.toString('utf8'), code, signal, }; if (timedOut) { reject(codedProcessError('process-timeout', result)); } else if (state.shutdownSignal && !state.cleanupInProgress) { reject( codedProcessError( `interrupted-${state.shutdownSignal.toLowerCase()}`, result, ), ); } else if (code !== 0 && !allowNonZero) { reject(codedProcessError('cli-command-failed', result)); } else { resolve(result); } }); if (stdin !== undefined) child.stdin.end(stdin); }); } export function appendBounded(current, chunk, limit) { const combined = Buffer.concat([current, chunk]); return combined.length <= limit ? combined : combined.subarray(combined.length - limit); }