@zigrivers/scaffold 3.12.0 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/commands/adopt.d.ts.map +1 -1
  3. package/dist/cli/commands/adopt.js +8 -7
  4. package/dist/cli/commands/adopt.js.map +1 -1
  5. package/dist/cli/commands/adopt.serialization.test.js +8 -0
  6. package/dist/cli/commands/adopt.serialization.test.js.map +1 -1
  7. package/dist/cli/commands/adopt.test.js +8 -0
  8. package/dist/cli/commands/adopt.test.js.map +1 -1
  9. package/dist/cli/commands/build.d.ts.map +1 -1
  10. package/dist/cli/commands/build.js +191 -180
  11. package/dist/cli/commands/build.js.map +1 -1
  12. package/dist/cli/commands/build.test.js +1 -0
  13. package/dist/cli/commands/build.test.js.map +1 -1
  14. package/dist/cli/commands/complete.d.ts.map +1 -1
  15. package/dist/cli/commands/complete.js +16 -12
  16. package/dist/cli/commands/complete.js.map +1 -1
  17. package/dist/cli/commands/complete.test.js +14 -5
  18. package/dist/cli/commands/complete.test.js.map +1 -1
  19. package/dist/cli/commands/init.d.ts.map +1 -1
  20. package/dist/cli/commands/init.js +120 -115
  21. package/dist/cli/commands/init.js.map +1 -1
  22. package/dist/cli/commands/init.test.js +40 -4
  23. package/dist/cli/commands/init.test.js.map +1 -1
  24. package/dist/cli/commands/knowledge.test.js +1 -0
  25. package/dist/cli/commands/knowledge.test.js.map +1 -1
  26. package/dist/cli/commands/reset.d.ts.map +1 -1
  27. package/dist/cli/commands/reset.js +44 -40
  28. package/dist/cli/commands/reset.js.map +1 -1
  29. package/dist/cli/commands/reset.test.js +42 -20
  30. package/dist/cli/commands/reset.test.js.map +1 -1
  31. package/dist/cli/commands/rework.d.ts.map +1 -1
  32. package/dist/cli/commands/rework.js +16 -12
  33. package/dist/cli/commands/rework.js.map +1 -1
  34. package/dist/cli/commands/rework.test.js +12 -3
  35. package/dist/cli/commands/rework.test.js.map +1 -1
  36. package/dist/cli/commands/run.d.ts.map +1 -1
  37. package/dist/cli/commands/run.js +318 -298
  38. package/dist/cli/commands/run.js.map +1 -1
  39. package/dist/cli/commands/run.test.js +93 -120
  40. package/dist/cli/commands/run.test.js.map +1 -1
  41. package/dist/cli/commands/skip.d.ts.map +1 -1
  42. package/dist/cli/commands/skip.js +19 -15
  43. package/dist/cli/commands/skip.js.map +1 -1
  44. package/dist/cli/commands/skip.test.js +23 -11
  45. package/dist/cli/commands/skip.test.js.map +1 -1
  46. package/dist/cli/commands/update.d.ts.map +1 -1
  47. package/dist/cli/commands/update.js +3 -1
  48. package/dist/cli/commands/update.js.map +1 -1
  49. package/dist/cli/commands/update.test.js +8 -4
  50. package/dist/cli/commands/update.test.js.map +1 -1
  51. package/dist/cli/commands/version.d.ts.map +1 -1
  52. package/dist/cli/commands/version.js +3 -1
  53. package/dist/cli/commands/version.js.map +1 -1
  54. package/dist/cli/commands/version.test.js +9 -5
  55. package/dist/cli/commands/version.test.js.map +1 -1
  56. package/dist/cli/index.d.ts.map +1 -1
  57. package/dist/cli/index.js +2 -0
  58. package/dist/cli/index.js.map +1 -1
  59. package/dist/cli/output/auto.d.ts +19 -6
  60. package/dist/cli/output/auto.d.ts.map +1 -1
  61. package/dist/cli/output/auto.js +10 -6
  62. package/dist/cli/output/auto.js.map +1 -1
  63. package/dist/cli/output/context.d.ts +23 -5
  64. package/dist/cli/output/context.d.ts.map +1 -1
  65. package/dist/cli/output/context.js.map +1 -1
  66. package/dist/cli/output/context.test.js +585 -0
  67. package/dist/cli/output/context.test.js.map +1 -1
  68. package/dist/cli/output/error-display.test.js +1 -0
  69. package/dist/cli/output/error-display.test.js.map +1 -1
  70. package/dist/cli/output/interactive.d.ts +20 -6
  71. package/dist/cli/output/interactive.d.ts.map +1 -1
  72. package/dist/cli/output/interactive.js +170 -59
  73. package/dist/cli/output/interactive.js.map +1 -1
  74. package/dist/cli/output/json.d.ts +19 -6
  75. package/dist/cli/output/json.d.ts.map +1 -1
  76. package/dist/cli/output/json.js +10 -6
  77. package/dist/cli/output/json.js.map +1 -1
  78. package/dist/cli/shutdown.d.ts +51 -0
  79. package/dist/cli/shutdown.d.ts.map +1 -0
  80. package/dist/cli/shutdown.js +199 -0
  81. package/dist/cli/shutdown.js.map +1 -0
  82. package/dist/cli/shutdown.test.d.ts +2 -0
  83. package/dist/cli/shutdown.test.d.ts.map +1 -0
  84. package/dist/cli/shutdown.test.js +316 -0
  85. package/dist/cli/shutdown.test.js.map +1 -0
  86. package/dist/core/assembly/overlay-state-resolver.test.js +1 -0
  87. package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
  88. package/dist/e2e/game-pipeline.test.js +1 -0
  89. package/dist/e2e/game-pipeline.test.js.map +1 -1
  90. package/dist/e2e/init.test.js +6 -4
  91. package/dist/e2e/init.test.js.map +1 -1
  92. package/dist/e2e/project-type-overlays.test.js +1 -0
  93. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  94. package/dist/state/lock-manager.d.ts +1 -0
  95. package/dist/state/lock-manager.d.ts.map +1 -1
  96. package/dist/state/lock-manager.js +1 -1
  97. package/dist/state/lock-manager.js.map +1 -1
  98. package/dist/wizard/copy/backend.d.ts +3 -0
  99. package/dist/wizard/copy/backend.d.ts.map +1 -0
  100. package/dist/wizard/copy/backend.js +49 -0
  101. package/dist/wizard/copy/backend.js.map +1 -0
  102. package/dist/wizard/copy/browser-extension.d.ts +3 -0
  103. package/dist/wizard/copy/browser-extension.d.ts.map +1 -0
  104. package/dist/wizard/copy/browser-extension.js +35 -0
  105. package/dist/wizard/copy/browser-extension.js.map +1 -0
  106. package/dist/wizard/copy/cli.d.ts +3 -0
  107. package/dist/wizard/copy/cli.d.ts.map +1 -0
  108. package/dist/wizard/copy/cli.js +40 -0
  109. package/dist/wizard/copy/cli.js.map +1 -0
  110. package/dist/wizard/copy/core.d.ts +3 -0
  111. package/dist/wizard/copy/core.d.ts.map +1 -0
  112. package/dist/wizard/copy/core.js +52 -0
  113. package/dist/wizard/copy/core.js.map +1 -0
  114. package/dist/wizard/copy/data-pipeline.d.ts +3 -0
  115. package/dist/wizard/copy/data-pipeline.d.ts.map +1 -0
  116. package/dist/wizard/copy/data-pipeline.js +66 -0
  117. package/dist/wizard/copy/data-pipeline.js.map +1 -0
  118. package/dist/wizard/copy/game.d.ts +3 -0
  119. package/dist/wizard/copy/game.d.ts.map +1 -0
  120. package/dist/wizard/copy/game.js +115 -0
  121. package/dist/wizard/copy/game.js.map +1 -0
  122. package/dist/wizard/copy/index.d.ts +8 -0
  123. package/dist/wizard/copy/index.d.ts.map +1 -0
  124. package/dist/wizard/copy/index.js +32 -0
  125. package/dist/wizard/copy/index.js.map +1 -0
  126. package/dist/wizard/copy/library.d.ts +3 -0
  127. package/dist/wizard/copy/library.d.ts.map +1 -0
  128. package/dist/wizard/copy/library.js +44 -0
  129. package/dist/wizard/copy/library.js.map +1 -0
  130. package/dist/wizard/copy/ml.d.ts +3 -0
  131. package/dist/wizard/copy/ml.d.ts.map +1 -0
  132. package/dist/wizard/copy/ml.js +45 -0
  133. package/dist/wizard/copy/ml.js.map +1 -0
  134. package/dist/wizard/copy/mobile-app.d.ts +3 -0
  135. package/dist/wizard/copy/mobile-app.d.ts.map +1 -0
  136. package/dist/wizard/copy/mobile-app.js +45 -0
  137. package/dist/wizard/copy/mobile-app.js.map +1 -0
  138. package/dist/wizard/copy/types.d.ts +60 -0
  139. package/dist/wizard/copy/types.d.ts.map +1 -0
  140. package/dist/wizard/copy/types.js +2 -0
  141. package/dist/wizard/copy/types.js.map +1 -0
  142. package/dist/wizard/copy/types.test-d.d.ts +2 -0
  143. package/dist/wizard/copy/types.test-d.d.ts.map +1 -0
  144. package/dist/wizard/copy/types.test-d.js +36 -0
  145. package/dist/wizard/copy/types.test-d.js.map +1 -0
  146. package/dist/wizard/copy/web-app.d.ts +3 -0
  147. package/dist/wizard/copy/web-app.d.ts.map +1 -0
  148. package/dist/wizard/copy/web-app.js +46 -0
  149. package/dist/wizard/copy/web-app.js.map +1 -0
  150. package/dist/wizard/questions.d.ts.map +1 -1
  151. package/dist/wizard/questions.js +87 -53
  152. package/dist/wizard/questions.js.map +1 -1
  153. package/dist/wizard/questions.test.js +3 -2
  154. package/dist/wizard/questions.test.js.map +1 -1
  155. package/dist/wizard/wizard.test.js +70 -0
  156. package/dist/wizard/wizard.test.js.map +1 -1
  157. package/package.json +1 -1
@@ -333,6 +333,179 @@ describe('AutoOutput', () => {
333
333
  expect(allWritten).toContain('"key"');
334
334
  });
335
335
  });
336
+ // Module-level mock for @inquirer/prompts (ES module namespace is immutable, so vi.spyOn won't work)
337
+ vi.mock('@inquirer/prompts', () => ({ input: vi.fn(), confirm: vi.fn() }));
338
+ describe('InteractiveOutput — bug fixes', () => {
339
+ let _stdoutWrite;
340
+ beforeEach(() => {
341
+ _stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
342
+ vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
343
+ });
344
+ afterEach(() => {
345
+ vi.restoreAllMocks();
346
+ });
347
+ // Bug A2: canPrompt() should check both stdin and stdout
348
+ describe('canPrompt checks both stdin and stdout', () => {
349
+ it('prompt() returns default when stdin is not a TTY (piped input)', async () => {
350
+ const originalStdinIsTTY = process.stdin.isTTY;
351
+ const originalStdoutIsTTY = process.stdout.isTTY;
352
+ try {
353
+ // Simulate `scaffold init < answers.txt`: stdout is TTY but stdin is not
354
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
355
+ Object.defineProperty(process.stdin, 'isTTY', { value: undefined, configurable: true });
356
+ const out = new InteractiveOutput();
357
+ const result = await out.prompt('Name:', 'fallback');
358
+ expect(result).toBe('fallback');
359
+ }
360
+ finally {
361
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
362
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
363
+ }
364
+ });
365
+ it('confirm() returns default when stdin is not a TTY', async () => {
366
+ const originalStdinIsTTY = process.stdin.isTTY;
367
+ const originalStdoutIsTTY = process.stdout.isTTY;
368
+ try {
369
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
370
+ Object.defineProperty(process.stdin, 'isTTY', { value: undefined, configurable: true });
371
+ const out = new InteractiveOutput();
372
+ const result = await out.confirm('Continue?', true);
373
+ expect(result).toBe(true);
374
+ }
375
+ finally {
376
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
377
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
378
+ }
379
+ });
380
+ it('select() returns default when stdin is not a TTY', async () => {
381
+ const originalStdinIsTTY = process.stdin.isTTY;
382
+ const originalStdoutIsTTY = process.stdout.isTTY;
383
+ try {
384
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
385
+ Object.defineProperty(process.stdin, 'isTTY', { value: undefined, configurable: true });
386
+ const out = new InteractiveOutput();
387
+ const result = await out.select('Pick:', ['a', 'b'], 'b');
388
+ expect(result).toBe('b');
389
+ }
390
+ finally {
391
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
392
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
393
+ }
394
+ });
395
+ it('prompt() returns default when stdout is not a TTY (piped output)', async () => {
396
+ const originalStdinIsTTY = process.stdin.isTTY;
397
+ const originalStdoutIsTTY = process.stdout.isTTY;
398
+ try {
399
+ // Simulate `scaffold init | less`: stdin is TTY but stdout is not
400
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
401
+ Object.defineProperty(process.stdout, 'isTTY', { value: undefined, configurable: true });
402
+ const out = new InteractiveOutput();
403
+ const result = await out.prompt('Name:', 'fallback');
404
+ expect(result).toBe('fallback');
405
+ }
406
+ finally {
407
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
408
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
409
+ }
410
+ });
411
+ });
412
+ // Bug A1: NO_COLOR should NOT disable interactivity
413
+ describe('NO_COLOR does not disable interactivity', () => {
414
+ it('prompt() calls inquirer when TTY even with NO_COLOR set', async () => {
415
+ const originalStdinIsTTY = process.stdin.isTTY;
416
+ const originalStdoutIsTTY = process.stdout.isTTY;
417
+ const originalNoColor = process.env['NO_COLOR'];
418
+ try {
419
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
420
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
421
+ process.env['NO_COLOR'] = '1';
422
+ const { input } = await import('@inquirer/prompts');
423
+ const inputMock = vi.mocked(input);
424
+ inputMock.mockResolvedValueOnce('user-typed');
425
+ const out = new InteractiveOutput();
426
+ const result = await out.prompt('Name:', 'default');
427
+ expect(result).toBe('user-typed');
428
+ expect(inputMock).toHaveBeenCalled();
429
+ }
430
+ finally {
431
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
432
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
433
+ if (originalNoColor === undefined) {
434
+ delete process.env['NO_COLOR'];
435
+ }
436
+ else {
437
+ process.env['NO_COLOR'] = originalNoColor;
438
+ }
439
+ }
440
+ });
441
+ it('confirm() calls inquirer when TTY even with NO_COLOR set', async () => {
442
+ const originalStdinIsTTY = process.stdin.isTTY;
443
+ const originalStdoutIsTTY = process.stdout.isTTY;
444
+ const originalNoColor = process.env['NO_COLOR'];
445
+ try {
446
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
447
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
448
+ process.env['NO_COLOR'] = '1';
449
+ const { confirm } = await import('@inquirer/prompts');
450
+ const confirmMock = vi.mocked(confirm);
451
+ confirmMock.mockResolvedValueOnce(true);
452
+ const out = new InteractiveOutput();
453
+ const result = await out.confirm('Continue?', false);
454
+ expect(result).toBe(true);
455
+ expect(confirmMock).toHaveBeenCalled();
456
+ }
457
+ finally {
458
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
459
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
460
+ if (originalNoColor === undefined) {
461
+ delete process.env['NO_COLOR'];
462
+ }
463
+ else {
464
+ process.env['NO_COLOR'] = originalNoColor;
465
+ }
466
+ }
467
+ });
468
+ });
469
+ // Bug A4: select() should trim input before exact text match
470
+ describe('select() trims input before exact text match', () => {
471
+ it('select() matches option when input has trailing whitespace', async () => {
472
+ const originalStdinIsTTY = process.stdin.isTTY;
473
+ const originalStdoutIsTTY = process.stdout.isTTY;
474
+ try {
475
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
476
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
477
+ const { input } = await import('@inquirer/prompts');
478
+ const inputMock = vi.mocked(input);
479
+ inputMock.mockResolvedValueOnce('spa '); // trailing space
480
+ const out = new InteractiveOutput();
481
+ const result = await out.select('Pick:', ['spa', 'web', 'api'], 'web');
482
+ expect(result).toBe('spa');
483
+ }
484
+ finally {
485
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
486
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
487
+ }
488
+ });
489
+ it('select() returns trimmed value (not raw) for exact text match', async () => {
490
+ const originalStdinIsTTY = process.stdin.isTTY;
491
+ const originalStdoutIsTTY = process.stdout.isTTY;
492
+ try {
493
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
494
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
495
+ const { input } = await import('@inquirer/prompts');
496
+ const inputMock = vi.mocked(input);
497
+ inputMock.mockResolvedValueOnce(' api '); // leading + trailing spaces
498
+ const out = new InteractiveOutput();
499
+ const result = await out.select('Pick:', ['spa', 'web', 'api'], 'web');
500
+ expect(result).toBe('api'); // trimmed, not ' api '
501
+ }
502
+ finally {
503
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
504
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
505
+ }
506
+ });
507
+ });
508
+ });
336
509
  describe('InteractiveOutput — wizard primitives', () => {
337
510
  beforeEach(() => {
338
511
  vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
@@ -420,4 +593,416 @@ describe('AutoOutput — wizard primitives', () => {
420
593
  expect(result).toEqual(['x', 'y']);
421
594
  });
422
595
  });
596
+ describe('InteractiveOutput — re-prompt on invalid input (A3)', () => {
597
+ let stdoutWrite;
598
+ beforeEach(() => {
599
+ stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
600
+ vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
601
+ });
602
+ afterEach(() => {
603
+ vi.restoreAllMocks();
604
+ });
605
+ it('select() re-prompts on invalid input then accepts valid input', async () => {
606
+ const originalStdinIsTTY = process.stdin.isTTY;
607
+ const originalStdoutIsTTY = process.stdout.isTTY;
608
+ try {
609
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
610
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
611
+ const { input } = await import('@inquirer/prompts');
612
+ const inputMock = vi.mocked(input);
613
+ // First call returns invalid input, second returns valid input
614
+ inputMock.mockResolvedValueOnce('banana');
615
+ inputMock.mockResolvedValueOnce('spa');
616
+ const out = new InteractiveOutput();
617
+ const result = await out.select('Pick:', ['spa', 'web', 'api'], 'web');
618
+ expect(result).toBe('spa');
619
+ // Should have been called twice (first invalid, second valid)
620
+ expect(inputMock).toHaveBeenCalledTimes(2);
621
+ // Error message should have been printed
622
+ const allWritten = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
623
+ expect(allWritten).toContain('Invalid');
624
+ }
625
+ finally {
626
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
627
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
628
+ }
629
+ });
630
+ it('select() re-prompts on invalid number then accepts valid number', async () => {
631
+ const originalStdinIsTTY = process.stdin.isTTY;
632
+ const originalStdoutIsTTY = process.stdout.isTTY;
633
+ try {
634
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
635
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
636
+ const { input } = await import('@inquirer/prompts');
637
+ const inputMock = vi.mocked(input);
638
+ // First call: out-of-range number; second: valid number
639
+ inputMock.mockResolvedValueOnce('99');
640
+ inputMock.mockResolvedValueOnce('2');
641
+ const out = new InteractiveOutput();
642
+ const result = await out.select('Pick:', ['spa', 'web', 'api'], 'spa');
643
+ expect(result).toBe('web');
644
+ expect(inputMock).toHaveBeenCalledTimes(2);
645
+ }
646
+ finally {
647
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
648
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
649
+ }
650
+ });
651
+ it('select() does NOT re-print options on re-prompt', async () => {
652
+ const originalStdinIsTTY = process.stdin.isTTY;
653
+ const originalStdoutIsTTY = process.stdout.isTTY;
654
+ try {
655
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
656
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
657
+ const { input } = await import('@inquirer/prompts');
658
+ const inputMock = vi.mocked(input);
659
+ inputMock.mockResolvedValueOnce('invalid');
660
+ inputMock.mockResolvedValueOnce('spa');
661
+ const out = new InteractiveOutput();
662
+ await out.select('Pick:', ['spa', 'web', 'api'], 'web');
663
+ // Count how many times "1. spa" appears — should be exactly once
664
+ const allWritten = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
665
+ const optionMatches = allWritten.match(/1\. spa/g);
666
+ expect(optionMatches).toHaveLength(1);
667
+ }
668
+ finally {
669
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
670
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
671
+ }
672
+ });
673
+ it('multiSelect() re-prompts when no valid selections from non-empty input', async () => {
674
+ const originalStdinIsTTY = process.stdin.isTTY;
675
+ const originalStdoutIsTTY = process.stdout.isTTY;
676
+ try {
677
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
678
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
679
+ const { input } = await import('@inquirer/prompts');
680
+ const inputMock = vi.mocked(input);
681
+ // First call: all invalid; second call: valid
682
+ inputMock.mockResolvedValueOnce('banana, grape');
683
+ inputMock.mockResolvedValueOnce('1, 3');
684
+ const out = new InteractiveOutput();
685
+ const result = await out.multiSelect('Pick:', ['spa', 'web', 'api'], ['spa']);
686
+ expect(result).toEqual(['spa', 'api']);
687
+ expect(inputMock).toHaveBeenCalledTimes(2);
688
+ // Error message should have been printed
689
+ const allWritten = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
690
+ expect(allWritten).toContain('Invalid');
691
+ }
692
+ finally {
693
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
694
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
695
+ }
696
+ });
697
+ it('multiSelect() warns about partial invalid entries and returns valid ones', async () => {
698
+ const originalStdinIsTTY = process.stdin.isTTY;
699
+ const originalStdoutIsTTY = process.stdout.isTTY;
700
+ try {
701
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
702
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
703
+ const { input } = await import('@inquirer/prompts');
704
+ const inputMock = vi.mocked(input);
705
+ inputMock.mockResolvedValueOnce('1, banana, 2');
706
+ const out = new InteractiveOutput();
707
+ const result = await out.multiSelect('Pick:', ['a', 'b', 'c']);
708
+ expect(result).toEqual(['a', 'b']);
709
+ // Should NOT re-prompt — only called once
710
+ expect(inputMock).toHaveBeenCalledTimes(1);
711
+ // Warning about unrecognized entries
712
+ const allWritten = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
713
+ expect(allWritten).toContain('Ignored unrecognized: banana');
714
+ }
715
+ finally {
716
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
717
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
718
+ }
719
+ });
720
+ it('multiSelect() returns defaults on empty input (pressing Enter)', async () => {
721
+ const originalStdinIsTTY = process.stdin.isTTY;
722
+ const originalStdoutIsTTY = process.stdout.isTTY;
723
+ try {
724
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
725
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
726
+ const { input } = await import('@inquirer/prompts');
727
+ const inputMock = vi.mocked(input);
728
+ // inquirer returns the default value string when user presses Enter
729
+ inputMock.mockResolvedValueOnce('spa');
730
+ const out = new InteractiveOutput();
731
+ const result = await out.multiSelect('Pick:', ['spa', 'web', 'api'], ['spa']);
732
+ expect(result).toEqual(['spa']);
733
+ // Should NOT re-prompt — only called once
734
+ expect(inputMock).toHaveBeenCalledTimes(1);
735
+ }
736
+ finally {
737
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
738
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
739
+ }
740
+ });
741
+ });
742
+ describe('InteractiveOutput — rich rendering + ? help', () => {
743
+ let stdoutWrite;
744
+ beforeEach(() => {
745
+ stdoutWrite = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
746
+ vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
747
+ });
748
+ afterEach(() => {
749
+ vi.restoreAllMocks();
750
+ });
751
+ it('select renders friendly labels from rich options', async () => {
752
+ const originalStdinIsTTY = process.stdin.isTTY;
753
+ const originalStdoutIsTTY = process.stdout.isTTY;
754
+ try {
755
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
756
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
757
+ const { input } = await import('@inquirer/prompts');
758
+ const inputMock = vi.mocked(input);
759
+ inputMock.mockResolvedValueOnce('1');
760
+ const out = new InteractiveOutput();
761
+ await out.select('Pick:', [
762
+ { value: 'spa', label: 'SPA App', short: 'Client side.' },
763
+ { value: 'ssr', label: 'SSR App' },
764
+ ], 'spa');
765
+ const allWritten = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
766
+ expect(allWritten).toContain('SPA App');
767
+ expect(allWritten).toContain('Client side.');
768
+ // Should NOT contain raw value as the display name (label takes precedence)
769
+ expect(allWritten).not.toContain('1. spa');
770
+ }
771
+ finally {
772
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
773
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
774
+ }
775
+ });
776
+ it('select shows (? for help) suffix only when help.long is set', async () => {
777
+ const originalStdinIsTTY = process.stdin.isTTY;
778
+ const originalStdoutIsTTY = process.stdout.isTTY;
779
+ try {
780
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
781
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
782
+ const { input } = await import('@inquirer/prompts');
783
+ const inputMock = vi.mocked(input);
784
+ // With help.long — should show suffix
785
+ inputMock.mockResolvedValueOnce('1');
786
+ const out1 = new InteractiveOutput();
787
+ await out1.select('Pick:', ['a', 'b'], 'a', { long: 'Detailed help text.' });
788
+ const written1 = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
789
+ expect(written1).toContain('(? for help)');
790
+ // Reset
791
+ stdoutWrite.mockClear();
792
+ // Without help — should NOT show suffix
793
+ inputMock.mockResolvedValueOnce('1');
794
+ const out2 = new InteractiveOutput();
795
+ await out2.select('Pick:', ['a', 'b'], 'a');
796
+ const written2 = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
797
+ expect(written2).not.toContain('(? for help)');
798
+ }
799
+ finally {
800
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
801
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
802
+ }
803
+ });
804
+ it('typing ? prints long help and re-prompts', async () => {
805
+ const originalStdinIsTTY = process.stdin.isTTY;
806
+ const originalStdoutIsTTY = process.stdout.isTTY;
807
+ try {
808
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
809
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
810
+ const { input } = await import('@inquirer/prompts');
811
+ const inputMock = vi.mocked(input);
812
+ // First call returns ?, second returns valid input
813
+ inputMock.mockResolvedValueOnce('?');
814
+ inputMock.mockResolvedValueOnce('1');
815
+ const out = new InteractiveOutput();
816
+ const result = await out.select('Pick:', ['a', 'b'], 'a', { long: 'Here is detailed help.' });
817
+ expect(result).toBe('a');
818
+ const allWritten = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
819
+ expect(allWritten).toContain('Here is detailed help.');
820
+ // input should have been called twice (? then valid)
821
+ expect(inputMock).toHaveBeenCalledTimes(2);
822
+ }
823
+ finally {
824
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
825
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
826
+ }
827
+ });
828
+ it('typing ? with no long help prints "no additional help"', async () => {
829
+ const originalStdinIsTTY = process.stdin.isTTY;
830
+ const originalStdoutIsTTY = process.stdout.isTTY;
831
+ try {
832
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
833
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
834
+ const { input } = await import('@inquirer/prompts');
835
+ const inputMock = vi.mocked(input);
836
+ inputMock.mockResolvedValueOnce('?');
837
+ inputMock.mockResolvedValueOnce('1');
838
+ const out = new InteractiveOutput();
839
+ const result = await out.select('Pick:', ['a', 'b'], 'a');
840
+ expect(result).toBe('a');
841
+ const allWritten = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
842
+ expect(allWritten).toContain('No additional help');
843
+ }
844
+ finally {
845
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
846
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
847
+ }
848
+ });
849
+ it('multiSelect ? handling triggers help', async () => {
850
+ const originalStdinIsTTY = process.stdin.isTTY;
851
+ const originalStdoutIsTTY = process.stdout.isTTY;
852
+ try {
853
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
854
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
855
+ const { input } = await import('@inquirer/prompts');
856
+ const inputMock = vi.mocked(input);
857
+ inputMock.mockResolvedValueOnce('?');
858
+ inputMock.mockResolvedValueOnce('1');
859
+ const out = new InteractiveOutput();
860
+ const result = await out.multiSelect('Pick:', ['a', 'b'], ['a'], { long: 'Multi help text.' });
861
+ expect(result).toEqual(['a']);
862
+ const allWritten = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
863
+ expect(allWritten).toContain('Multi help text.');
864
+ expect(inputMock).toHaveBeenCalledTimes(2);
865
+ }
866
+ finally {
867
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
868
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
869
+ }
870
+ });
871
+ it('prompt prints dim short hint', async () => {
872
+ const originalStdinIsTTY = process.stdin.isTTY;
873
+ const originalStdoutIsTTY = process.stdout.isTTY;
874
+ try {
875
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
876
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
877
+ const { input } = await import('@inquirer/prompts');
878
+ const inputMock = vi.mocked(input);
879
+ inputMock.mockResolvedValueOnce('typed');
880
+ const out = new InteractiveOutput();
881
+ await out.prompt('Name:', 'default', { short: 'A hint.' });
882
+ const allWritten = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
883
+ expect(allWritten).toContain('A hint.');
884
+ }
885
+ finally {
886
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
887
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
888
+ }
889
+ });
890
+ it('confirm prints dim short hint', async () => {
891
+ const originalStdinIsTTY = process.stdin.isTTY;
892
+ const originalStdoutIsTTY = process.stdout.isTTY;
893
+ try {
894
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
895
+ Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true });
896
+ const { confirm } = await import('@inquirer/prompts');
897
+ const confirmMock = vi.mocked(confirm);
898
+ confirmMock.mockResolvedValueOnce(true);
899
+ const out = new InteractiveOutput();
900
+ await out.confirm('Continue?', false, { short: 'Confirm hint.' });
901
+ const allWritten = stdoutWrite.mock.calls.map(c => String(c[0])).join('');
902
+ expect(allWritten).toContain('Confirm hint.');
903
+ }
904
+ finally {
905
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalStdoutIsTTY, configurable: true });
906
+ Object.defineProperty(process.stdin, 'isTTY', { value: originalStdinIsTTY, configurable: true });
907
+ }
908
+ });
909
+ });
910
+ describe('InteractiveOutput — label text matching', () => {
911
+ beforeEach(() => {
912
+ vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
913
+ vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
914
+ });
915
+ afterEach(() => {
916
+ vi.restoreAllMocks();
917
+ });
918
+ it('select accepts label text input (case-insensitive)', async () => {
919
+ const originalStdinIsTTY = process.stdin.isTTY;
920
+ const originalStdoutIsTTY = process.stdout.isTTY;
921
+ try {
922
+ Object.defineProperty(process.stdout, 'isTTY', {
923
+ value: true, configurable: true,
924
+ });
925
+ Object.defineProperty(process.stdin, 'isTTY', {
926
+ value: true, configurable: true,
927
+ });
928
+ const { input } = await import('@inquirer/prompts');
929
+ const inputMock = vi.mocked(input);
930
+ inputMock.mockResolvedValueOnce('Backend Service');
931
+ const out = new InteractiveOutput();
932
+ const result = await out.select('Pick:', [
933
+ { value: 'backend', label: 'Backend service' },
934
+ { value: 'frontend', label: 'Frontend app' },
935
+ ], 'frontend');
936
+ expect(result).toBe('backend');
937
+ }
938
+ finally {
939
+ Object.defineProperty(process.stdout, 'isTTY', {
940
+ value: originalStdoutIsTTY, configurable: true,
941
+ });
942
+ Object.defineProperty(process.stdin, 'isTTY', {
943
+ value: originalStdinIsTTY, configurable: true,
944
+ });
945
+ }
946
+ });
947
+ it('select prefers exact value match over label match', async () => {
948
+ const originalStdinIsTTY = process.stdin.isTTY;
949
+ const originalStdoutIsTTY = process.stdout.isTTY;
950
+ try {
951
+ Object.defineProperty(process.stdout, 'isTTY', {
952
+ value: true, configurable: true,
953
+ });
954
+ Object.defineProperty(process.stdin, 'isTTY', {
955
+ value: true, configurable: true,
956
+ });
957
+ const { input } = await import('@inquirer/prompts');
958
+ const inputMock = vi.mocked(input);
959
+ inputMock.mockResolvedValueOnce('spa');
960
+ const out = new InteractiveOutput();
961
+ const result = await out.select('Pick:', [
962
+ { value: 'spa', label: 'SPA App' },
963
+ { value: 'ssr', label: 'SSR App' },
964
+ ], 'ssr');
965
+ // Should match as exact value, not fall through to label
966
+ expect(result).toBe('spa');
967
+ }
968
+ finally {
969
+ Object.defineProperty(process.stdout, 'isTTY', {
970
+ value: originalStdoutIsTTY, configurable: true,
971
+ });
972
+ Object.defineProperty(process.stdin, 'isTTY', {
973
+ value: originalStdinIsTTY, configurable: true,
974
+ });
975
+ }
976
+ });
977
+ it('multiSelect accepts label text input (case-insensitive)', async () => {
978
+ const originalStdinIsTTY = process.stdin.isTTY;
979
+ const originalStdoutIsTTY = process.stdout.isTTY;
980
+ try {
981
+ Object.defineProperty(process.stdout, 'isTTY', {
982
+ value: true, configurable: true,
983
+ });
984
+ Object.defineProperty(process.stdin, 'isTTY', {
985
+ value: true, configurable: true,
986
+ });
987
+ const { input } = await import('@inquirer/prompts');
988
+ const inputMock = vi.mocked(input);
989
+ inputMock.mockResolvedValueOnce('Backend Service, Frontend app');
990
+ const out = new InteractiveOutput();
991
+ const result = await out.multiSelect('Pick:', [
992
+ { value: 'backend', label: 'Backend service' },
993
+ { value: 'frontend', label: 'Frontend app' },
994
+ { value: 'cli', label: 'CLI tool' },
995
+ ]);
996
+ expect(result).toEqual(['backend', 'frontend']);
997
+ }
998
+ finally {
999
+ Object.defineProperty(process.stdout, 'isTTY', {
1000
+ value: originalStdoutIsTTY, configurable: true,
1001
+ });
1002
+ Object.defineProperty(process.stdin, 'isTTY', {
1003
+ value: originalStdinIsTTY, configurable: true,
1004
+ });
1005
+ }
1006
+ });
1007
+ });
423
1008
  //# sourceMappingURL=context.test.js.map