Files
gori-agent/src/cli/prompt.ts
T

93 lines
3.7 KiB
TypeScript

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<string>;
askSecret(question: string, hasExisting?: boolean): Promise<string>;
askBoolean(question: string, defaultValue?: boolean): Promise<boolean>;
askList(question: string, defaultValues?: string[]): Promise<string[]>;
choose<T>(question: string, choices: Choice<T>[], defaultIndex?: number): Promise<T>;
close(): void;
}
export interface Choice<T> {
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<string> {
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<string> {
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<boolean> {
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<string[]> {
const answer = await ask(rl, question, defaultValues.join(","));
return answer.split(",").map((value) => value.trim()).filter(Boolean);
}
export async function choose<T>(
rl: readline.Interface,
question: string,
choices: Choice<T>[],
defaultIndex = 0
): Promise<T> {
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}.`);
}
}