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