Fix Feynman onboarding and local install reliability
This commit is contained in:
@@ -1,30 +1,130 @@
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import {
|
||||
confirm as clackConfirm,
|
||||
intro as clackIntro,
|
||||
isCancel,
|
||||
multiselect as clackMultiselect,
|
||||
outro as clackOutro,
|
||||
select as clackSelect,
|
||||
text as clackText,
|
||||
type Option,
|
||||
} from "@clack/prompts";
|
||||
|
||||
export async function promptText(question: string, defaultValue = ""): Promise<string> {
|
||||
if (!input.isTTY || !output.isTTY) {
|
||||
export class SetupCancelledError extends Error {
|
||||
constructor(message = "setup cancelled") {
|
||||
super(message);
|
||||
this.name = "SetupCancelledError";
|
||||
}
|
||||
}
|
||||
|
||||
export type PromptSelectOption<T = string> = {
|
||||
value: T;
|
||||
label: string;
|
||||
hint?: string;
|
||||
};
|
||||
|
||||
function ensureInteractiveTerminal(): void {
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
throw new Error("feynman setup requires an interactive terminal.");
|
||||
}
|
||||
const rl = createInterface({ input, output });
|
||||
try {
|
||||
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
||||
const value = (await rl.question(`${question}${suffix}: `)).trim();
|
||||
return value || defaultValue;
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
|
||||
function guardCancelled<T>(value: T | symbol): T {
|
||||
if (isCancel(value)) {
|
||||
throw new SetupCancelledError();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function isInteractiveTerminal(): boolean {
|
||||
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
||||
}
|
||||
|
||||
export async function promptIntro(title: string): Promise<void> {
|
||||
ensureInteractiveTerminal();
|
||||
clackIntro(title);
|
||||
}
|
||||
|
||||
export async function promptOutro(message: string): Promise<void> {
|
||||
ensureInteractiveTerminal();
|
||||
clackOutro(message);
|
||||
}
|
||||
|
||||
export async function promptText(question: string, defaultValue = "", placeholder?: string): Promise<string> {
|
||||
ensureInteractiveTerminal();
|
||||
|
||||
const value = guardCancelled(
|
||||
await clackText({
|
||||
message: question,
|
||||
initialValue: defaultValue || undefined,
|
||||
placeholder: placeholder ?? (defaultValue || undefined),
|
||||
}),
|
||||
);
|
||||
|
||||
const normalized = String(value ?? "").trim();
|
||||
return normalized || defaultValue;
|
||||
}
|
||||
|
||||
export async function promptSelect<T>(
|
||||
question: string,
|
||||
options: PromptSelectOption<T>[],
|
||||
initialValue?: T,
|
||||
): Promise<T> {
|
||||
ensureInteractiveTerminal();
|
||||
|
||||
const selection = guardCancelled(
|
||||
await clackSelect({
|
||||
message: question,
|
||||
options: options.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
hint: option.hint,
|
||||
})) as Option<T>[],
|
||||
initialValue,
|
||||
}),
|
||||
);
|
||||
|
||||
return selection;
|
||||
}
|
||||
|
||||
export async function promptChoice(question: string, choices: string[], defaultIndex = 0): Promise<number> {
|
||||
console.log(question);
|
||||
for (const [index, choice] of choices.entries()) {
|
||||
const marker = index === defaultIndex ? "*" : " ";
|
||||
console.log(` ${marker} ${index + 1}. ${choice}`);
|
||||
}
|
||||
const answer = await promptText("Select", String(defaultIndex + 1));
|
||||
const parsed = Number(answer);
|
||||
if (!Number.isFinite(parsed) || parsed < 1 || parsed > choices.length) {
|
||||
return defaultIndex;
|
||||
}
|
||||
return parsed - 1;
|
||||
const options = choices.map((choice, index) => ({
|
||||
value: index,
|
||||
label: choice,
|
||||
}));
|
||||
return promptSelect(question, options, Math.max(0, Math.min(defaultIndex, choices.length - 1)));
|
||||
}
|
||||
|
||||
export async function promptConfirm(question: string, initialValue = true): Promise<boolean> {
|
||||
ensureInteractiveTerminal();
|
||||
|
||||
return guardCancelled(
|
||||
await clackConfirm({
|
||||
message: question,
|
||||
initialValue,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function promptMultiSelect<T>(
|
||||
question: string,
|
||||
options: PromptSelectOption<T>[],
|
||||
initialValues: T[] = [],
|
||||
): Promise<T[]> {
|
||||
ensureInteractiveTerminal();
|
||||
|
||||
const selection = guardCancelled(
|
||||
await clackMultiselect({
|
||||
message: question,
|
||||
options: options.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
hint: option.hint,
|
||||
})) as Option<T>[],
|
||||
initialValues,
|
||||
required: false,
|
||||
}),
|
||||
);
|
||||
|
||||
return selection;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user