import readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; import { Writable } from "node:stream"; export interface PromptSession { ask(question: string, defaultValue?: string): Promise; askSecret(question: string, hasExisting?: boolean): Promise; askBoolean(question: string, defaultValue?: boolean): Promise; askList(question: string, defaultValues?: string[]): Promise; choose(question: string, choices: Choice[], defaultIndex?: number): Promise; close(): void; } export interface Choice { label: string; value: T; hint?: string; } export function createPromptSession(): PromptSession { let rl = readline.createInterface({ input, output }); return { ask: (question, defaultValue) => ask(rl, question, defaultValue), askSecret: async (question, hasExisting) => { rl.close(); try { return await askSecret(question, hasExisting); } finally { rl = readline.createInterface({ input, output }); } }, askBoolean: (question, defaultValue) => askBoolean(rl, question, defaultValue), askList: (question, defaultValues) => askList(rl, question, defaultValues), choose: (question, choices, defaultIndex) => choose(rl, question, choices, defaultIndex), close: () => rl.close() }; } async function askSecret(question: string, hasExisting = false): Promise { if (!input.isTTY || !output.isTTY) throw new Error("Secret input requires an interactive terminal"); const muted = new Writable({ write(_chunk, _encoding, callback) { callback(); } }); const secretRl = readline.createInterface({ input, output: muted, terminal: true }); output.write(`${question}${hasExisting ? " (leave blank to keep existing)" : ""}: `); try { const answer = (await secretRl.question("")).trim(); output.write("\n"); return answer; } finally { secretRl.close(); } } export async function ask(rl: readline.Interface, question: string, defaultValue?: string): Promise { const suffix = defaultValue !== undefined && defaultValue !== "" ? ` [${defaultValue}]` : ""; const answer = (await rl.question(`${question}${suffix}: `)).trim(); return answer || defaultValue || ""; } export async function askBoolean(rl: readline.Interface, question: string, defaultValue = false): Promise { const suffix = defaultValue ? " [Y/n]" : " [y/N]"; for (;;) { const answer = (await rl.question(`${question}${suffix}: `)).trim().toLowerCase(); if (!answer) return defaultValue; if (["y", "yes"].includes(answer)) return true; if (["n", "no"].includes(answer)) return false; console.log("Please answer yes or no."); } } export async function askList(rl: readline.Interface, question: string, defaultValues: string[] = []): Promise { const answer = await ask(rl, question, defaultValues.join(",")); return answer.split(",").map((value) => value.trim()).filter(Boolean); } export async function choose( rl: readline.Interface, question: string, choices: Choice[], defaultIndex = 0 ): Promise { if (choices.length === 0) throw new Error("No choices available"); console.log(question); choices.forEach((choice, index) => { const marker = index === defaultIndex ? "*" : " "; const hint = choice.hint ? ` - ${choice.hint}` : ""; console.log(` ${marker} ${index + 1}) ${choice.label}${hint}`); }); for (;;) { const answer = (await rl.question(`Choose 1-${choices.length} [${defaultIndex + 1}]: `)).trim(); if (!answer) return choices[defaultIndex]?.value ?? choices[0].value; const index = Number(answer) - 1; if (Number.isInteger(index) && choices[index]) return choices[index].value; console.log(`Please enter a number from 1 to ${choices.length}.`); } }