Fix Feynman onboarding and local install reliability

This commit is contained in:
Advait Paliwal
2026-04-15 13:46:12 -07:00
parent fa259f5cea
commit dd3c07633b
11 changed files with 512 additions and 123 deletions

View File

@@ -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;
}