/** * Local shell exec on the BooCoder host (replaces deprecated ssh.ts for worktrees). */ import { spawn } from 'node:child_process'; export interface HostExecResult { exitCode: number; stdout: string; stderr: string; } export async function hostExec( command: string, opts?: { signal?: AbortSignal; timeoutMs?: number }, ): Promise { return new Promise((resolve, reject) => { const child = spawn('bash', ['-lc', command], { stdio: ['pipe', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; let killed = false; child.stdout!.on('data', (chunk: Buffer) => { stdout += chunk.toString(); }); child.stderr!.on('data', (chunk: Buffer) => { stderr += chunk.toString(); }); const cleanup = () => { if (!killed) { killed = true; child.kill('SIGTERM'); } }; if (opts?.signal) { if (opts.signal.aborted) { cleanup(); reject(new Error('host exec aborted before start')); return; } opts.signal.addEventListener('abort', cleanup, { once: true }); } let timer: ReturnType | undefined; if (opts?.timeoutMs) { timer = setTimeout(() => { cleanup(); reject(new Error(`host exec timed out after ${opts.timeoutMs}ms`)); }, opts.timeoutMs); } child.on('close', (code) => { if (timer) clearTimeout(timer); if (opts?.signal) opts.signal.removeEventListener('abort', cleanup); resolve({ exitCode: code ?? 1, stdout, stderr }); }); child.on('error', (err) => { if (timer) clearTimeout(timer); if (opts?.signal) opts.signal.removeEventListener('abort', cleanup); reject(err); }); child.stdin!.end(); }); }