process.ts
1 function handleEPIPE( 2 stream: NodeJS.WriteStream, 3 ): (err: NodeJS.ErrnoException) => void { 4 return (err: NodeJS.ErrnoException) => { 5 if (err.code === 'EPIPE') { 6 stream.destroy() 7 } 8 } 9 } 10 11 // Prevents memory leak when pipe is broken (e.g., `claude -p | head -1`) 12 export function registerProcessOutputErrorHandlers(): void { 13 process.stdout.on('error', handleEPIPE(process.stdout)) 14 process.stderr.on('error', handleEPIPE(process.stderr)) 15 } 16 17 function writeOut(stream: NodeJS.WriteStream, data: string): void { 18 if (stream.destroyed) { 19 return 20 } 21 22 // Note: we don't handle backpressure (write() returning false). 23 // 24 // We should consider handling the callback to ensure we wait for data to flush. 25 stream.write(data /* callback to handle here */) 26 } 27 28 export function writeToStdout(data: string): void { 29 writeOut(process.stdout, data) 30 } 31 32 export function writeToStderr(data: string): void { 33 writeOut(process.stderr, data) 34 } 35 36 // Write error to stderr and exit with code 1. Consolidates the 37 // console.error + process.exit(1) pattern used in entrypoint fast-paths. 38 export function exitWithError(message: string): never { 39 // biome-ignore lint/suspicious/noConsole:: intentional console output 40 console.error(message) 41 // eslint-disable-next-line custom-rules/no-process-exit 42 process.exit(1) 43 } 44 45 // Wait for a stdin-like stream to close, but give up after ms if no data ever 46 // arrives. First data chunk cancels the timeout — after that, wait for end 47 // unconditionally (caller's accumulator needs all chunks, not just the first). 48 // Returns true on timeout, false on end. Used by -p mode to distinguish a 49 // real pipe producer from an inherited-but-idle parent stdin. 50 export function peekForStdinData( 51 stream: NodeJS.EventEmitter, 52 ms: number, 53 ): Promise<boolean> { 54 return new Promise<boolean>(resolve => { 55 const done = (timedOut: boolean) => { 56 clearTimeout(peek) 57 stream.off('end', onEnd) 58 stream.off('data', onFirstData) 59 void resolve(timedOut) 60 } 61 const onEnd = () => done(false) 62 const onFirstData = () => clearTimeout(peek) 63 // eslint-disable-next-line no-restricted-syntax -- not a sleep: races timeout against stream end/data events 64 const peek = setTimeout(done, ms, true) 65 stream.once('end', onEnd) 66 stream.once('data', onFirstData) 67 }) 68 }