/ utils / renderOptions.ts
renderOptions.ts
 1  import { openSync } from 'fs'
 2  import { ReadStream } from 'tty'
 3  import type { RenderOptions } from '../ink.js'
 4  import { isEnvTruthy } from './envUtils.js'
 5  import { logError } from './log.js'
 6  
 7  // Cached stdin override - computed once per process
 8  let cachedStdinOverride: ReadStream | undefined | null = null
 9  
10  /**
11   * Gets a ReadStream for /dev/tty when stdin is piped.
12   * This allows interactive Ink rendering even when stdin is a pipe.
13   * Result is cached for the lifetime of the process.
14   */
15  function getStdinOverride(): ReadStream | undefined {
16    // Return cached result if already computed
17    if (cachedStdinOverride !== null) {
18      return cachedStdinOverride
19    }
20  
21    // No override needed if stdin is already a TTY
22    if (process.stdin.isTTY) {
23      cachedStdinOverride = undefined
24      return undefined
25    }
26  
27    // Skip in CI environments
28    if (isEnvTruthy(process.env.CI)) {
29      cachedStdinOverride = undefined
30      return undefined
31    }
32  
33    // Skip if running MCP (input hijacking breaks MCP)
34    if (process.argv.includes('mcp')) {
35      cachedStdinOverride = undefined
36      return undefined
37    }
38  
39    // No /dev/tty on Windows
40    if (process.platform === 'win32') {
41      cachedStdinOverride = undefined
42      return undefined
43    }
44  
45    // Try to open /dev/tty as an alternative input source
46    try {
47      const ttyFd = openSync('/dev/tty', 'r')
48      const ttyStream = new ReadStream(ttyFd)
49      // Explicitly set isTTY to true since we know /dev/tty is a TTY.
50      // This is needed because some runtimes (like Bun's compiled binaries)
51      // may not correctly detect isTTY on ReadStream created from a file descriptor.
52      ttyStream.isTTY = true
53      cachedStdinOverride = ttyStream
54      return cachedStdinOverride
55    } catch (err) {
56      logError(err as Error)
57      cachedStdinOverride = undefined
58      return undefined
59    }
60  }
61  
62  /**
63   * Returns base render options for Ink, including stdin override when needed.
64   * Use this for all render() calls to ensure piped input works correctly.
65   *
66   * @param exitOnCtrlC - Whether to exit on Ctrl+C (usually false for dialogs)
67   */
68  export function getBaseRenderOptions(
69    exitOnCtrlC: boolean = false,
70  ): RenderOptions {
71    const stdin = getStdinOverride()
72    const options: RenderOptions = { exitOnCtrlC }
73    if (stdin) {
74      options.stdin = stdin
75    }
76    return options
77  }