@zigrivers/scaffold 3.11.0 → 3.13.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.
- package/README.md +6 -5
- package/content/knowledge/core/automated-review-tooling.md +137 -140
- package/content/knowledge/core/multi-model-research-dispatch.md +27 -16
- package/content/knowledge/core/multi-model-review-dispatch.md +47 -6
- package/content/skills/multi-model-dispatch/SKILL.md +20 -22
- package/content/tools/post-implementation-review.md +71 -26
- package/content/tools/review-code.md +37 -11
- package/content/tools/review-pr.md +65 -23
- package/dist/cli/commands/build.test.js +1 -0
- package/dist/cli/commands/build.test.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +88 -77
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/init.test.js +30 -0
- package/dist/cli/commands/init.test.js.map +1 -1
- package/dist/cli/commands/knowledge.test.js +1 -0
- package/dist/cli/commands/knowledge.test.js.map +1 -1
- package/dist/cli/commands/run.test.js +1 -0
- package/dist/cli/commands/run.test.js.map +1 -1
- package/dist/cli/commands/skip.test.js +1 -0
- package/dist/cli/commands/skip.test.js.map +1 -1
- package/dist/cli/output/auto.d.ts +19 -6
- package/dist/cli/output/auto.d.ts.map +1 -1
- package/dist/cli/output/auto.js +10 -6
- package/dist/cli/output/auto.js.map +1 -1
- package/dist/cli/output/context.d.ts +23 -5
- package/dist/cli/output/context.d.ts.map +1 -1
- package/dist/cli/output/context.js.map +1 -1
- package/dist/cli/output/context.test.js +585 -0
- package/dist/cli/output/context.test.js.map +1 -1
- package/dist/cli/output/error-display.test.js +1 -0
- package/dist/cli/output/error-display.test.js.map +1 -1
- package/dist/cli/output/interactive.d.ts +19 -6
- package/dist/cli/output/interactive.d.ts.map +1 -1
- package/dist/cli/output/interactive.js +165 -59
- package/dist/cli/output/interactive.js.map +1 -1
- package/dist/cli/output/json.d.ts +19 -6
- package/dist/cli/output/json.d.ts.map +1 -1
- package/dist/cli/output/json.js +10 -6
- package/dist/cli/output/json.js.map +1 -1
- package/dist/core/assembly/overlay-state-resolver.test.js +1 -0
- package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
- package/dist/e2e/game-pipeline.test.js +1 -0
- package/dist/e2e/game-pipeline.test.js.map +1 -1
- package/dist/e2e/init.test.js +1 -0
- package/dist/e2e/init.test.js.map +1 -1
- package/dist/e2e/project-type-overlays.test.js +1 -0
- package/dist/e2e/project-type-overlays.test.js.map +1 -1
- package/dist/wizard/copy/backend.d.ts +3 -0
- package/dist/wizard/copy/backend.d.ts.map +1 -0
- package/dist/wizard/copy/backend.js +49 -0
- package/dist/wizard/copy/backend.js.map +1 -0
- package/dist/wizard/copy/browser-extension.d.ts +3 -0
- package/dist/wizard/copy/browser-extension.d.ts.map +1 -0
- package/dist/wizard/copy/browser-extension.js +35 -0
- package/dist/wizard/copy/browser-extension.js.map +1 -0
- package/dist/wizard/copy/cli.d.ts +3 -0
- package/dist/wizard/copy/cli.d.ts.map +1 -0
- package/dist/wizard/copy/cli.js +40 -0
- package/dist/wizard/copy/cli.js.map +1 -0
- package/dist/wizard/copy/core.d.ts +3 -0
- package/dist/wizard/copy/core.d.ts.map +1 -0
- package/dist/wizard/copy/core.js +52 -0
- package/dist/wizard/copy/core.js.map +1 -0
- package/dist/wizard/copy/data-pipeline.d.ts +3 -0
- package/dist/wizard/copy/data-pipeline.d.ts.map +1 -0
- package/dist/wizard/copy/data-pipeline.js +66 -0
- package/dist/wizard/copy/data-pipeline.js.map +1 -0
- package/dist/wizard/copy/game.d.ts +3 -0
- package/dist/wizard/copy/game.d.ts.map +1 -0
- package/dist/wizard/copy/game.js +115 -0
- package/dist/wizard/copy/game.js.map +1 -0
- package/dist/wizard/copy/index.d.ts +8 -0
- package/dist/wizard/copy/index.d.ts.map +1 -0
- package/dist/wizard/copy/index.js +32 -0
- package/dist/wizard/copy/index.js.map +1 -0
- package/dist/wizard/copy/library.d.ts +3 -0
- package/dist/wizard/copy/library.d.ts.map +1 -0
- package/dist/wizard/copy/library.js +44 -0
- package/dist/wizard/copy/library.js.map +1 -0
- package/dist/wizard/copy/ml.d.ts +3 -0
- package/dist/wizard/copy/ml.d.ts.map +1 -0
- package/dist/wizard/copy/ml.js +45 -0
- package/dist/wizard/copy/ml.js.map +1 -0
- package/dist/wizard/copy/mobile-app.d.ts +3 -0
- package/dist/wizard/copy/mobile-app.d.ts.map +1 -0
- package/dist/wizard/copy/mobile-app.js +45 -0
- package/dist/wizard/copy/mobile-app.js.map +1 -0
- package/dist/wizard/copy/types.d.ts +60 -0
- package/dist/wizard/copy/types.d.ts.map +1 -0
- package/dist/wizard/copy/types.js +2 -0
- package/dist/wizard/copy/types.js.map +1 -0
- package/dist/wizard/copy/types.test-d.d.ts +2 -0
- package/dist/wizard/copy/types.test-d.d.ts.map +1 -0
- package/dist/wizard/copy/types.test-d.js +36 -0
- package/dist/wizard/copy/types.test-d.js.map +1 -0
- package/dist/wizard/copy/web-app.d.ts +3 -0
- package/dist/wizard/copy/web-app.d.ts.map +1 -0
- package/dist/wizard/copy/web-app.js +46 -0
- package/dist/wizard/copy/web-app.js.map +1 -0
- package/dist/wizard/questions.d.ts.map +1 -1
- package/dist/wizard/questions.js +87 -53
- package/dist/wizard/questions.js.map +1 -1
- package/dist/wizard/questions.test.js +3 -2
- package/dist/wizard/questions.test.js.map +1 -1
- package/dist/wizard/wizard.test.js +70 -0
- package/dist/wizard/wizard.test.js.map +1 -1
- package/package.json +1 -1
- package/skills/multi-model-dispatch/SKILL.md +20 -22
|
@@ -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
|