diff --git a/packages/inquirerer/README.md b/packages/inquirerer/README.md index 6f4b3a5..4ec9637 100644 --- a/packages/inquirerer/README.md +++ b/packages/inquirerer/README.md @@ -146,8 +146,10 @@ interface BaseQuestion { default?: any; // Default value defaultFrom?: string; // Dynamic default from resolver (e.g., 'git.user.name') setFrom?: string; // Auto-set value from resolver, bypassing prompt entirely + optionsFrom?: string; // Dynamic options from another answer's value useDefault?: boolean; // Skip prompt and use default required?: boolean; // Validation requirement + skipPrompt?: boolean; // Skip prompting entirely (field still in man pages / CLI flags) validate?: (input: any, answers: any) => boolean | Validation; sanitize?: (input: any, answers: any) => any; pattern?: string; // Regex pattern for validation @@ -156,6 +158,40 @@ interface BaseQuestion { } ``` +#### Skipping Prompts + +Use `skipPrompt: true` to skip interactive prompting for a question entirely. The field is omitted from the answers object unless the user explicitly passes it via a CLI flag. This is useful for fields with backend-managed defaults where the CLI should not prompt, but should still allow overrides. + +```typescript +const questions: Question[] = [ + { + type: 'text', + name: 'username', + message: 'Username', + required: true + }, + { + type: 'text', + name: 'status', + message: 'Account status', + skipPrompt: true // Won't prompt, but user can pass --status active + } +]; + +const result = await prompter.prompt({}, questions); +// { username: 'john' } — status is not included + +const result2 = await prompter.prompt({ status: 'active' }, questions); +// { username: 'john', status: 'active' } — CLI flag override works +``` + +Key behaviors: +- The question still appears in generated man pages +- CLI flag overrides (e.g. `--status active`) still work +- The field is simply left out of the answers if not provided +- Different from `when`: `skipPrompt` is unconditional, while `when` depends on other answers +- Different from `useDefault`: `skipPrompt` does not apply a default value + ### Non-Interactive Mode When running in CI/CD or without a TTY, inquirerer automatically falls back to default values: diff --git a/packages/inquirerer/__tests__/prompt.test.ts b/packages/inquirerer/__tests__/prompt.test.ts index 1a2c76a..fb268da 100644 --- a/packages/inquirerer/__tests__/prompt.test.ts +++ b/packages/inquirerer/__tests__/prompt.test.ts @@ -232,6 +232,108 @@ describe('Inquirerer', () => { snap(result); }); + describe('skipPrompt', () => { + it('should skip question with skipPrompt: true and not include it in result', async () => { + enqueueInputResponse({ type: 'read', value: 'my name' }); + + const prompter = new Inquirerer({ + input: mockInput, + output: mockOutput, + noTty: false + }); + const questions: Question[] = [ + { name: 'name', type: 'text' }, + { name: 'status', type: 'text', skipPrompt: true }, + ]; + + const result = await prompter.prompt({}, questions); + + // 'status' should not be in the result since it was skipped + expect(result).toEqual({ name: 'my name' }); + expect('status' in result).toBe(false); + }); + + it('should still allow CLI flag override when skipPrompt is true', async () => { + enqueueInputResponse({ type: 'read', value: 'my name' }); + + const prompter = new Inquirerer({ + input: mockInput, + output: mockOutput, + noTty: false + }); + const questions: Question[] = [ + { name: 'name', type: 'text' }, + { name: 'status', type: 'text', skipPrompt: true }, + ]; + + // Pass status via argv (simulating --status "active") + const result = await prompter.prompt({ status: 'active' }, questions); + + expect(result).toEqual({ name: 'my name', status: 'active' }); + }); + + it('should skip question in noTty mode with skipPrompt: true', async () => { + const prompter = new Inquirerer({ + input: mockInput, + output: mockOutput, + noTty: true + }); + const questions: Question[] = [ + { name: 'name', type: 'text', required: true }, + { name: 'status', type: 'text', skipPrompt: true }, + ]; + + const result = await prompter.prompt({ name: 'test' }, questions); + + expect(result).toEqual({ name: 'test' }); + expect('status' in result).toBe(false); + }); + + it('should include skipPrompt question in man page', () => { + const prompter = new Inquirerer({ + input: mockInput, + output: mockOutput, + noTty: false + }); + const questions: Question[] = [ + { name: 'name', type: 'text', required: true }, + { name: 'status', type: 'text', skipPrompt: true }, + ]; + + const manPage = prompter.generateManPage({ + commandName: 'test-cmd', + questions, + }); + + // Both fields should appear in the man page + expect(manPage).toContain('NAME'); + expect(manPage).toContain('STATUS'); + }); + + it('should skip multiple skipPrompt questions and only prompt remaining', async () => { + enqueueInputResponse({ type: 'read', value: 'hello' }); + + const prompter = new Inquirerer({ + input: mockInput, + output: mockOutput, + noTty: false + }); + const questions: Question[] = [ + { name: 'greeting', type: 'text' }, + { name: 'createdAt', type: 'text', skipPrompt: true }, + { name: 'updatedAt', type: 'text', skipPrompt: true }, + { name: 'internalId', type: 'text', skipPrompt: true }, + ]; + + const result = await prompter.prompt({}, questions); + + expect(result).toEqual({ greeting: 'hello' }); + expect('createdAt' in result).toBe(false); + expect('updatedAt' in result).toBe(false); + expect('internalId' in result).toBe(false); + }); + }); + it('handles readline inputs', async () => { const prompter = new Inquirerer({ diff --git a/packages/inquirerer/src/prompt.ts b/packages/inquirerer/src/prompt.ts index 0cb1a60..fb910fb 100644 --- a/packages/inquirerer/src/prompt.ts +++ b/packages/inquirerer/src/prompt.ts @@ -475,6 +475,15 @@ export class Inquirerer { continue; } + // Skip prompt entirely if skipPrompt is set. + // The question still appears in man pages and CLI flag overrides still work + // (handled by the `question.name in obj` check above), but no interactive + // prompt is shown. The field is simply left out of the answers object. + if (question.skipPrompt) { + ctx.nextQuestion(); + continue; + } + // Apply default value if applicable // this is if useDefault is set, rare! not typical defaults which happen AFTER // this is mostly to avoid a prompt for "hidden" options diff --git a/packages/inquirerer/src/question/types.ts b/packages/inquirerer/src/question/types.ts index 3786978..d68e6c7 100644 --- a/packages/inquirerer/src/question/types.ts +++ b/packages/inquirerer/src/question/types.ts @@ -32,6 +32,7 @@ export interface BaseQuestion { pattern?: string; dependsOn?: string[]; when?: (answers: any) => boolean; + skipPrompt?: boolean; } export interface ConfirmQuestion extends BaseQuestion {