import { execFile } from 'child_process'; import path from 'path'; import { pathToFileURL } from 'url'; export { computeVerificationSnapshotId, } from '../../../shared/verification-snapshot.js'; export const VERIFICATION_PROFILES = [ 'test', 'typecheck', 'build', ] as const; export type VerificationProfile = (typeof VERIFICATION_PROFILES)[number]; type VerificationHelperEnvKey = | 'PATH' | 'HOME' | 'USER' | 'LOGNAME' | 'SHELL' | 'LANG' | 'LC_ALL' | 'LC_CTYPE' | 'NODE_PATH' | 'NODE_OPTIONS' | 'TERM' | 'COLORTERM' | 'FORCE_COLOR'; const VERIFICATION_HELPER_ENV_KEYS: readonly VerificationHelperEnvKey[] = [ 'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'LC_ALL', 'LC_CTYPE', 'NODE_PATH', 'NODE_OPTIONS', 'TERM', 'COLORTERM', 'FORCE_COLOR', ]; const HELPER_TIMEOUT_MS = 20 * 60 * 1000; const HELPER_MAX_BUFFER = 20 * 1024 * 1024; export interface VerificationRequest { requestId: string; profile: VerificationProfile; expectedSnapshotId?: string; } export interface VerificationResponse { requestId: string; ok: boolean; profile: VerificationProfile; command: string; stdout: string; stderr: string; exitCode: number; snapshotId: string; runtimeVersion: string; workdir?: string; error?: string; } export function isVerificationProfile( value: unknown, ): value is VerificationProfile { return ( typeof value === 'string' && VERIFICATION_PROFILES.includes(value as VerificationProfile) ); } function buildVerificationHelperEnv(): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { TZ: 'Asia/Seoul', CI: '1', }; for (const key of VERIFICATION_HELPER_ENV_KEYS) { const value = process.env[key]; if (value) { env[key] = value; } } return env; } function execFileCapture( file: string, args: string[], options?: { cwd?: string; env?: NodeJS.ProcessEnv; }, ): Promise<{ stdout: string; stderr: string }> { return new Promise((resolve, reject) => { execFile( file, args, { encoding: 'utf8', timeout: HELPER_TIMEOUT_MS, maxBuffer: HELPER_MAX_BUFFER, cwd: options?.cwd, env: options?.env, }, (error, stdout, stderr) => { if (error) { reject( Object.assign(error, { stdout, stderr, }), ); return; } resolve({ stdout, stderr }); }, ); }); } export async function runVerificationRequestDirect( repoRoot: string, request: VerificationRequest, ): Promise { const helperPath = path.join( repoRoot, 'shared', 'verification-request-runner.js', ); const helperEnv = buildVerificationHelperEnv(); const { stdout, stderr } = await execFileCapture( 'bun', [helperPath, repoRoot, JSON.stringify(request)], { cwd: repoRoot, env: helperEnv, }, ); try { return JSON.parse(stdout) as VerificationResponse; } catch (error) { const detail = stderr.trim() || stdout.trim(); throw new Error( `Failed to parse verification response from ${pathToFileURL(helperPath).href}: ${ error instanceof Error ? error.message : String(error) }${detail ? `\n${detail}` : ''}`, ); } } export function formatVerificationResponse( response: VerificationResponse, ): string { const parts = [ `Verification profile: ${response.profile}`, `Snapshot: ${response.snapshotId}`, `Runtime: ${response.runtimeVersion}`, `Exit code: ${response.exitCode}`, response.workdir ? `Workdir: ${response.workdir}` : null, response.command ? `$ ${response.command}` : null, response.stdout ? response.stdout.trimEnd() : null, response.stderr ? `[stderr]\n${response.stderr.trimEnd()}` : null, response.error ? `[error] ${response.error}` : null, ].filter((value): value is string => Boolean(value)); return parts.join('\n'); }