Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/inquirerer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
102 changes: 102 additions & 0 deletions packages/inquirerer/__tests__/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
9 changes: 9 additions & 0 deletions packages/inquirerer/src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/inquirerer/src/question/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface BaseQuestion {
pattern?: string;
dependsOn?: string[];
when?: (answers: any) => boolean;
skipPrompt?: boolean;
}

export interface ConfirmQuestion extends BaseQuestion {
Expand Down