(engine: ResolvedEngine, prompt: string, opts: InvokeOptions = {})
| 424 | * `spawn` we get direct control and can `child.stdin.end()` immediately. |
| 425 | */ |
| 426 | export function invokeEngineAsync(engine: ResolvedEngine, prompt: string, opts: InvokeOptions = {}): Promise<string> { |
| 427 | const { bin, args } = engine.config; |
| 428 | const timeout = opts.timeout ?? DEFAULT_TIMEOUT; |
| 429 | const maxBuffer = opts.maxBuffer ?? DEFAULT_MAXBUF; |
| 430 | |
| 431 | return new Promise((resolve, reject) => { |
| 432 | const child = spawn(bin, args(prompt, engine), { |
| 433 | stdio: ['pipe', 'pipe', 'pipe'], |
| 434 | }); |
| 435 | |
| 436 | // Close stdin immediately with EOF so `claude -p` doesn't wait on it. |
| 437 | // If spawn itself failed (ENOENT etc) `child.stdin` may be null — guard. |
| 438 | try { child.stdin?.end(); } catch { /* spawn error will surface below */ } |
| 439 | |
| 440 | const stdoutChunks: Buffer[] = []; |
| 441 | const stderrChunks: Buffer[] = []; |
| 442 | let stdoutBytes = 0; |
| 443 | let stderrBytes = 0; |
| 444 | let settled = false; |
| 445 | let timer: ReturnType<typeof setTimeout> | undefined; |
| 446 | |
| 447 | /** Compute the redacted tail of buffered stderr for error reporting. */ |
| 448 | const stderrTail = () => |
| 449 | redactSecrets(tailString(Buffer.concat(stderrChunks), STDERR_TAIL_BYTES)); |
| 450 | |
| 451 | /** Send SIGTERM, then escalate to SIGKILL after a grace period in case |
| 452 | * the child traps SIGTERM. `.unref()` so the escalation timer does not |
| 453 | * keep the event loop alive past shutdown. */ |
| 454 | const killChild = () => { |
| 455 | try { child.kill('SIGTERM'); } catch { /* already dead */ } |
| 456 | const escalate = setTimeout(() => { |
| 457 | try { child.kill('SIGKILL'); } catch { /* already dead */ } |
| 458 | }, SIGKILL_GRACE_MS); |
| 459 | escalate.unref(); |
| 460 | }; |
| 461 | |
| 462 | const fail = (err: EngineInvocationError) => { |
| 463 | if (settled) return; |
| 464 | settled = true; |
| 465 | if (timer !== undefined) clearTimeout(timer); |
| 466 | killChild(); |
| 467 | reject(err); |
| 468 | }; |
| 469 | |
| 470 | const succeed = (out: string) => { |
| 471 | if (settled) return; |
| 472 | settled = true; |
| 473 | if (timer !== undefined) clearTimeout(timer); |
| 474 | resolve(out); |
| 475 | }; |
| 476 | |
| 477 | child.stdout?.on('data', (d: Buffer) => { |
| 478 | stdoutBytes += d.length; |
| 479 | if (stdoutBytes > maxBuffer) { |
| 480 | const stderr = stderrTail(); |
| 481 | fail(new EngineInvocationError({ |
| 482 | engine: engine.name, bin, stderr, |
| 483 | killed: true, code: null, signal: null, reason: 'maxbuffer', |
no test coverage detected