@@ -19,6 +19,8 @@ let earlyInputBuffer = ''
1919let isCapturing = false
2020// Reference to the readable handler so we can remove it later
2121let readableHandler : ( ( ) => void ) | null = null
22+ // Safety valve: auto-cleanup after timeout so stdin.ref() never leaks
23+ let safetyTimer : ReturnType < typeof setTimeout > | null = null
2224
2325/**
2426 * Start capturing stdin data early, before the REPL is initialized.
@@ -60,6 +62,20 @@ export function startCapturingEarlyInput(): void {
6062 }
6163
6264 process . stdin . on ( 'readable' , readableHandler )
65+
66+ // Safety valve: if Ink never takes over within 10s (e.g. setup dialog
67+ // stalls, or an error prevents Ink mount on Windows), unref stdin so
68+ // the process doesn't hang forever. The REPL's Ink App normally calls
69+ // consumeEarlyInput() → stopCapturingEarlyInput() long before this.
70+ safetyTimer = setTimeout ( ( ) => {
71+ if ( isCapturing ) {
72+ stopCapturingEarlyInput ( )
73+ }
74+ } , 10_000 )
75+ // Don't let the timer itself keep the event loop alive
76+ if ( safetyTimer && typeof safetyTimer === 'object' && 'unref' in safetyTimer ) {
77+ safetyTimer . unref ( )
78+ }
6379 } catch {
6480 // If we can't set raw mode, just silently continue without early capture
6581 isCapturing = false
@@ -172,14 +188,34 @@ export function stopCapturingEarlyInput(): void {
172188
173189 isCapturing = false
174190
191+ // Clear safety timer
192+ if ( safetyTimer ) {
193+ clearTimeout ( safetyTimer )
194+ safetyTimer = null
195+ }
196+
175197 if ( readableHandler ) {
176198 process . stdin . removeListener ( 'readable' , readableHandler )
177199 readableHandler = null
178200 }
179201
180- // Don't reset stdin state - the REPL's Ink App will manage stdin state.
181- // If we call setRawMode(false) here, it can interfere with the REPL's
182- // own stdin setup which happens around the same time.
202+ // Undo the ref() from startCapturingEarlyInput so the event loop isn't
203+ // kept alive if Ink never takes over (e.g. raw mode unsupported on
204+ // Windows Node.js, or an error during setup). Ink's own
205+ // handleSetRawMode(true) calls stdin.ref() again, and its
206+ // handleSetRawMode(false) / unmount path calls stdin.unref(), so this
207+ // unref is safe even when Ink does take over — the two ref/unref calls
208+ // balance out.
209+ try {
210+ process . stdin . unref ( )
211+ } catch {
212+ // stdin may already be destroyed
213+ }
214+
215+ // Don't reset setRawMode here — Ink's App.handleSetRawMode(true)
216+ // calls stopCapturingEarlyInput() synchronously and then immediately
217+ // calls setRawMode(true) + ref() on the same stdin, so toggling it
218+ // off here would add a visible flicker on Windows.
183219}
184220
185221/**
0 commit comments