/ utils / process.ts
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  }