Skip to content

Commit c299691

Browse files
committed
feat: switch setup wizard to @clack/prompts for styled terminal UI
Replaces hand-rolled readline prompts with clack's styled components: spinners, select menus, confirm prompts, notes, intro/outro banners. Handles TTY detection internally — no more /dev/tty workarounds.
1 parent 27de363 commit c299691

4 files changed

Lines changed: 102 additions & 172 deletions

File tree

bun.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"prepublishOnly": "bun run build"
4242
},
4343
"dependencies": {
44+
"@clack/prompts": "^1.1.0",
4445
"@huggingface/transformers": "^3.5.1",
4546
"@opencode-ai/plugin": "^1.2.26",
4647
"ai-tokenizer": "^1.0.6",

src/cli/prompts.ts

Lines changed: 33 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,44 @@
1-
import { createReadStream } from "node:fs";
2-
import { createInterface } from "node:readline";
3-
4-
function getInput(): NodeJS.ReadableStream {
5-
// When piped (curl | bash), stdin is consumed by the pipe.
6-
// Open /dev/tty directly to read from the actual terminal.
7-
if (!process.stdin.isTTY) {
8-
try {
9-
return createReadStream("/dev/tty");
10-
} catch {
11-
// Windows or no TTY available — fall back to stdin
12-
}
1+
import {
2+
confirm as clackConfirm,
3+
intro,
4+
isCancel,
5+
log,
6+
note,
7+
outro,
8+
select,
9+
spinner,
10+
} from "@clack/prompts";
11+
12+
export { intro, log, note, outro, spinner };
13+
14+
function handleCancel(value: unknown): void {
15+
if (isCancel(value)) {
16+
log.warn("Setup cancelled.");
17+
process.exit(0);
1318
}
14-
return process.stdin;
15-
}
16-
17-
const rl = createInterface({ input: getInput(), output: process.stdout });
18-
19-
function ask(question: string): Promise<string> {
20-
return new Promise((resolve) => {
21-
rl.question(question, (answer) => resolve(answer.trim()));
22-
});
23-
}
24-
25-
export function closePrompts(): void {
26-
rl.close();
2719
}
2820

2921
export async function confirm(message: string, defaultYes = true): Promise<boolean> {
30-
const hint = defaultYes ? "Y/n" : "y/N";
31-
const answer = await ask(` ${message} [${hint}] `);
32-
if (answer === "") return defaultYes;
33-
return answer.toLowerCase().startsWith("y");
22+
const result = await clackConfirm({
23+
message,
24+
initialValue: defaultYes,
25+
});
26+
handleCancel(result);
27+
return result as boolean;
3428
}
3529

3630
export async function selectOne(
3731
message: string,
3832
options: { label: string; value: string; recommended?: boolean }[],
3933
): Promise<string> {
40-
console.log(` ${message}`);
41-
console.log("");
42-
for (let i = 0; i < options.length; i++) {
43-
const opt = options[i];
44-
const rec = opt.recommended ? " (recommended)" : "";
45-
const num = `${i + 1}`.padStart(3);
46-
console.log(` ${num}) ${opt.label}${rec}`);
47-
}
48-
console.log("");
49-
50-
while (true) {
51-
const answer = await ask(" Enter number: ");
52-
const idx = Number.parseInt(answer, 10) - 1;
53-
if (idx >= 0 && idx < options.length) {
54-
return options[idx].value;
55-
}
56-
console.log(" Invalid selection, try again.");
57-
}
58-
}
59-
60-
export async function selectMultiple(
61-
message: string,
62-
options: { label: string; value: string }[],
63-
): Promise<string[]> {
64-
console.log(` ${message}`);
65-
console.log("");
66-
for (let i = 0; i < options.length; i++) {
67-
const num = `${i + 1}`.padStart(3);
68-
console.log(` ${num}) ${options[i].label}`);
69-
}
70-
console.log("");
71-
72-
const answer = await ask(" Enter numbers (comma-separated, or 'none'): ");
73-
if (answer.toLowerCase() === "none" || answer === "") return [];
74-
75-
const indices = answer.split(",").map((s) => Number.parseInt(s.trim(), 10) - 1);
76-
return indices.filter((i) => i >= 0 && i < options.length).map((i) => options[i].value);
34+
const result = await select({
35+
message,
36+
options: options.map((opt) => ({
37+
label: opt.recommended ? `${opt.label} (recommended)` : opt.label,
38+
value: opt.value,
39+
hint: opt.recommended ? "recommended" : undefined,
40+
})),
41+
});
42+
handleCancel(result);
43+
return result as string;
7744
}

0 commit comments

Comments
 (0)