@@ -184,6 +184,20 @@ function generatePromptMessage(question: Question, ctx: PromptContext): string {
184184 return lines . join ( '\n' ) + '\n' ;
185185}
186186
187+ export class PromptTimeoutError extends Error {
188+ public readonly currentQuestion : Question ;
189+ public readonly allQuestions : Question [ ] ;
190+
191+ constructor ( message : string , currentQuestion : Question , allQuestions : Question [ ] ) {
192+ super ( message ) ;
193+ this . name = 'PromptTimeoutError' ;
194+ this . currentQuestion = currentQuestion ;
195+ this . allQuestions = allQuestions ;
196+ }
197+ }
198+
199+ const DEFAULT_NON_TTY_TIMEOUT = 30_000 ;
200+
187201export interface InquirererOptions {
188202 noTty ?: boolean ;
189203 input ?: Readable ;
@@ -192,6 +206,7 @@ export interface InquirererOptions {
192206 globalMaxLines ?: number ;
193207 mutateArgs ?: boolean ;
194208 resolverRegistry ?: DefaultResolverRegistry ;
209+ timeout ?: number ;
195210}
196211export class Inquirerer {
197212 private rl : readline . Interface | null ;
@@ -203,6 +218,7 @@ export class Inquirerer {
203218 private globalMaxLines : number ;
204219 private mutateArgs : boolean ;
205220 private resolverRegistry : DefaultResolverRegistry ;
221+ private timeout : number | undefined ;
206222
207223 private handledKeys : Set < string > = new Set ( ) ;
208224
@@ -216,7 +232,8 @@ export class Inquirerer {
216232 useDefaults = false ,
217233 globalMaxLines = 10 ,
218234 mutateArgs = true ,
219- resolverRegistry = globalResolverRegistry
235+ resolverRegistry = globalResolverRegistry ,
236+ timeout
220237 } = options ?? { }
221238
222239 this . useDefaults = useDefaults ;
@@ -227,6 +244,12 @@ export class Inquirerer {
227244 this . globalMaxLines = globalMaxLines ;
228245 this . resolverRegistry = resolverRegistry ;
229246
247+ if ( timeout !== undefined ) {
248+ this . timeout = timeout ;
249+ } else if ( ! noTty && ! ( input as any ) . isTTY ) {
250+ this . timeout = DEFAULT_NON_TTY_TIMEOUT ;
251+ }
252+
230253 if ( ! noTty ) {
231254 this . rl = readline . createInterface ( {
232255 input,
@@ -501,7 +524,11 @@ export class Inquirerer {
501524 }
502525
503526 while ( ctx . needsInput ) {
504- obj [ question . name ] = await this . handleQuestionType ( question , ctx ) ;
527+ obj [ question . name ] = await this . withTimeout (
528+ this . handleQuestionType ( question , ctx ) ,
529+ question ,
530+ ordered
531+ ) ;
505532
506533 if ( ! this . isValid ( question , obj , ctx ) ) {
507534 if ( this . noTty ) {
@@ -1375,6 +1402,83 @@ export class Inquirerer {
13751402 return Math . min ( this . globalMaxLines , defaultLength ) ;
13761403 }
13771404
1405+ private withTimeout < T > ( promise : Promise < T > , currentQuestion : Question , allQuestions : Question [ ] ) : Promise < T > {
1406+ if ( this . timeout === undefined ) {
1407+ return promise ;
1408+ }
1409+
1410+ const ms = this . timeout ;
1411+
1412+ return new Promise < T > ( ( resolve , reject ) => {
1413+ const timer = setTimeout ( ( ) => {
1414+ reject ( new PromptTimeoutError (
1415+ this . formatTimeoutError ( currentQuestion , allQuestions , ms ) ,
1416+ currentQuestion ,
1417+ allQuestions
1418+ ) ) ;
1419+ } , ms ) ;
1420+
1421+ promise . then (
1422+ value => { clearTimeout ( timer ) ; resolve ( value ) ; } ,
1423+ err => { clearTimeout ( timer ) ; reject ( err ) ; }
1424+ ) ;
1425+ } ) ;
1426+ }
1427+
1428+ private formatTimeoutError ( currentQuestion : Question , allQuestions : Question [ ] , ms : number ) : string {
1429+ const seconds = Math . round ( ms / 1000 ) ;
1430+ const lines : string [ ] = [ ] ;
1431+
1432+ lines . push ( '' ) ;
1433+ lines . push ( `PROMPT TIMEOUT: No input received for "${ currentQuestion . name } " after ${ seconds } s.` ) ;
1434+ lines . push ( '' ) ;
1435+ lines . push ( 'This usually happens when running in a non-interactive environment' ) ;
1436+ lines . push ( '(CI, scripts, AI agents) without the proper flags.' ) ;
1437+ lines . push ( '' ) ;
1438+ lines . push ( 'REQUIRED ARGUMENTS:' ) ;
1439+
1440+ for ( const q of allQuestions ) {
1441+ const flag = `--${ q . name } ` ;
1442+ const aliases = q . alias
1443+ ? ( Array . isArray ( q . alias ) ? q . alias : [ q . alias ] )
1444+ . map ( a => a . length === 1 ? `-${ a } ` : `--${ a } ` )
1445+ . join ( ', ' )
1446+ : '' ;
1447+ const aliasStr = aliases ? ` (${ aliases } )` : '' ;
1448+ const label = q . message || q . name ;
1449+ const req = q . required ? ' [REQUIRED]' : '' ;
1450+ const def = 'default' in q && q . default !== undefined ? ` (default: ${ JSON . stringify ( q . default ) } )` : '' ;
1451+ lines . push ( ` ${ flag } ${ aliasStr } ${ label } ${ req } ${ def } ` ) ;
1452+ }
1453+
1454+ lines . push ( '' ) ;
1455+ lines . push ( 'HOW TO FIX:' ) ;
1456+ lines . push ( ' 1. Pass all required arguments as CLI flags:' ) ;
1457+
1458+ const requiredFlags = allQuestions
1459+ . filter ( q => q . required )
1460+ . map ( q => `--${ q . name } <value>` ) ;
1461+ if ( requiredFlags . length > 0 ) {
1462+ lines . push ( ` $ command ${ requiredFlags . join ( ' ' ) } ` ) ;
1463+ } else {
1464+ lines . push ( ' $ command --flag1 <value> --flag2 <value>' ) ;
1465+ }
1466+
1467+ lines . push ( '' ) ;
1468+ lines . push ( ' 2. Or enable non-interactive mode:' ) ;
1469+ lines . push ( ' new Inquirerer({ noTty: true, useDefaults: true })' ) ;
1470+ lines . push ( '' ) ;
1471+ lines . push ( ' 3. Or pass --no-tty if the CLI supports it.' ) ;
1472+ lines . push ( '' ) ;
1473+ lines . push ( 'WHY THIS HAPPENED:' ) ;
1474+ lines . push ( ' The prompt expected interactive TTY input (keyboard), but no input' ) ;
1475+ lines . push ( ' was received. AI agents and CI pipelines must pass arguments via' ) ;
1476+ lines . push ( ' CLI flags instead of interactive prompts.' ) ;
1477+ lines . push ( '' ) ;
1478+
1479+ return lines . join ( '\n' ) ;
1480+ }
1481+
13781482 // Method to cleanly close the readline interface
13791483 // NOTE: use exit() to close!
13801484 public close ( ) {
0 commit comments