feat(setup): add prompt helpers for setup wizard
Created a Prompter interface and factory function for interactive CLI prompts: - ask(): text input with optional default values - confirm(): yes/no confirmation with default - choose(): numbered menu selection with fallback - password(): text input (no echo planned in TUI) - println(): simple output helper All 9 tests pass (ask, confirm, choose, password scenarios). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createPrompter } from './prompts.js';
|
||||
import { createInterface } from 'readline/promises';
|
||||
import { Readable, Writable } from 'stream';
|
||||
|
||||
function mockReadline(inputs: string[]) {
|
||||
let idx = 0;
|
||||
const input = new Readable({
|
||||
read() {
|
||||
if (idx < inputs.length) {
|
||||
this.push(inputs[idx++] + '\n');
|
||||
} else {
|
||||
this.push(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
const output = new Writable({ write(_, __, cb) { cb(); } });
|
||||
return createInterface({ input, output });
|
||||
}
|
||||
|
||||
describe('prompts', () => {
|
||||
describe('ask', () => {
|
||||
it('returns user input', async () => {
|
||||
const rl = mockReadline(['hello']);
|
||||
const p = createPrompter(rl);
|
||||
expect(await p.ask('Name?')).toBe('hello');
|
||||
});
|
||||
|
||||
it('returns default on empty input', async () => {
|
||||
const rl = mockReadline(['']);
|
||||
const p = createPrompter(rl);
|
||||
expect(await p.ask('Name?', 'world')).toBe('world');
|
||||
});
|
||||
});
|
||||
|
||||
describe('confirm', () => {
|
||||
it('returns true for Y', async () => {
|
||||
const rl = mockReadline(['Y']);
|
||||
const p = createPrompter(rl);
|
||||
expect(await p.confirm('Ok?')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for n', async () => {
|
||||
const rl = mockReadline(['n']);
|
||||
const p = createPrompter(rl);
|
||||
expect(await p.confirm('Ok?')).toBe(false);
|
||||
});
|
||||
|
||||
it('defaults to true when defaultYes=true and input empty', async () => {
|
||||
const rl = mockReadline(['']);
|
||||
const p = createPrompter(rl);
|
||||
expect(await p.confirm('Ok?', true)).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults to false when defaultYes=false and input empty', async () => {
|
||||
const rl = mockReadline(['']);
|
||||
const p = createPrompter(rl);
|
||||
expect(await p.confirm('Ok?', false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('choose', () => {
|
||||
it('returns selected option by number', async () => {
|
||||
const rl = mockReadline(['2']);
|
||||
const p = createPrompter(rl);
|
||||
const result = await p.choose('Pick:', [
|
||||
{ label: 'A', value: 'a' },
|
||||
{ label: 'B', value: 'b' },
|
||||
{ label: 'C', value: 'c' },
|
||||
]);
|
||||
expect(result).toBe('b');
|
||||
});
|
||||
|
||||
it('returns first option on empty input', async () => {
|
||||
const rl = mockReadline(['']);
|
||||
const p = createPrompter(rl);
|
||||
const result = await p.choose('Pick:', [
|
||||
{ label: 'A', value: 'a' },
|
||||
{ label: 'B', value: 'b' },
|
||||
]);
|
||||
expect(result).toBe('a');
|
||||
});
|
||||
});
|
||||
|
||||
describe('password', () => {
|
||||
it('returns input', async () => {
|
||||
const rl = mockReadline(['secret123']);
|
||||
const p = createPrompter(rl);
|
||||
expect(await p.password('Key:')).toBe('secret123');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Interface as ReadlineInterface } from 'readline/promises';
|
||||
|
||||
export interface ChoiceOption<T = string> {
|
||||
label: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export interface Prompter {
|
||||
ask(question: string, defaultValue?: string): Promise<string>;
|
||||
confirm(question: string, defaultYes?: boolean): Promise<boolean>;
|
||||
choose<T = string>(question: string, options: ChoiceOption<T>[]): Promise<T>;
|
||||
password(question: string): Promise<string>;
|
||||
println(msg?: string): void;
|
||||
}
|
||||
|
||||
export function createPrompter(rl: ReadlineInterface): Prompter {
|
||||
return {
|
||||
async ask(question: string, defaultValue?: string): Promise<string> {
|
||||
const suffix = defaultValue !== undefined ? ` [${defaultValue}]` : '';
|
||||
const answer = await rl.question(`${question}${suffix}: `);
|
||||
return answer.trim() || defaultValue || '';
|
||||
},
|
||||
|
||||
async confirm(question: string, defaultYes = true): Promise<boolean> {
|
||||
const hint = defaultYes ? '[Y/n]' : '[y/N]';
|
||||
const answer = await rl.question(`${question} ${hint} `);
|
||||
const trimmed = answer.trim().toLowerCase();
|
||||
if (trimmed === '') return defaultYes;
|
||||
return trimmed === 'y' || trimmed === 'yes';
|
||||
},
|
||||
|
||||
async choose<T = string>(question: string, options: ChoiceOption<T>[]): Promise<T> {
|
||||
this.println(question);
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
this.println(` ${i + 1}. ${options[i].label}`);
|
||||
}
|
||||
const answer = await rl.question(`> `);
|
||||
const idx = parseInt(answer.trim(), 10) - 1;
|
||||
if (idx >= 0 && idx < options.length) return options[idx].value;
|
||||
return options[0].value;
|
||||
},
|
||||
|
||||
async password(question: string): Promise<string> {
|
||||
const answer = await rl.question(`${question}: `);
|
||||
return answer.trim();
|
||||
},
|
||||
|
||||
println(msg = ''): void {
|
||||
process.stdout.write(msg + '\n');
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user