refactor: clean up main entry point

- Use shared terminal helpers (hideCursor, showCursor, clearLine,
  clearScreen, visibleLength, padRight) from terminal.ts
- Simplify allCommandDefs dedup with new Set()
- Centralize terminal state restore into exitMenu() closure + finally

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-18 01:57:01 +08:00
parent f3b5c631de
commit 311d059e52
2 changed files with 22 additions and 44 deletions
+21 -43
View File
@@ -1,7 +1,6 @@
#!/usr/bin/env bun #!/usr/bin/env bun
// gai — AI-powered git commit and PR helper // gai — AI-powered git commit and PR helper
// v0.1.3
import { runCLI, registerCommands, formatHelp, type CommandDef, type ParsedArgs } from "./src/cli"; import { runCLI, registerCommands, formatHelp, type CommandDef, type ParsedArgs } from "./src/cli";
import { handleCommit } from "./src/commands/commit"; import { handleCommit } from "./src/commands/commit";
@@ -11,8 +10,7 @@ import { handleExplain } from "./src/commands/explain";
import { handleReview } from "./src/commands/review"; import { handleReview } from "./src/commands/review";
import { handleChangelog } from "./src/commands/changelog"; import { handleChangelog } from "./src/commands/changelog";
import { handleSuggest } from "./src/commands/suggest"; import { handleSuggest } from "./src/commands/suggest";
import { setColorEnabled } from "./src/terminal"; import { setColorEnabled, BOLD, GREEN, CYAN, DIM, RESET, hideCursor, showCursor, clearLine, clearScreen, visibleLength, padRight } from "./src/terminal";
import { BOLD, GREEN, CYAN, DIM, RESET } from "./src/terminal";
import { isStdinTTY, initTTY } from "./src/tty"; import { isStdinTTY, initTTY } from "./src/tty";
import { VERSION } from "./src/brand"; import { VERSION } from "./src/brand";
import { SKIP_WAIT } from "./src/menu"; import { SKIP_WAIT } from "./src/menu";
@@ -37,11 +35,6 @@ const MENU_ITEMS: MenuItem[] = [
{ key: "config", label: "Config", description: "Configure API settings", group: "Project" }, { key: "config", label: "Config", description: "Configure API settings", group: "Project" },
]; ];
function hideCursor() { process.stdout.write("\x1b[?25l"); }
function showCursor() { process.stdout.write("\x1b[?25h"); }
function clearLine() { process.stdout.write("\r\x1b[2K"); }
async function readKey(): Promise<string> { async function readKey(): Promise<string> {
return new Promise((resolve) => { return new Promise((resolve) => {
const onData = (data: Buffer) => { const onData = (data: Buffer) => {
@@ -52,14 +45,6 @@ async function readKey(): Promise<string> {
}); });
} }
function visibleLen(s: string): number {
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
}
function padRight(value: string, width: number): string {
return value + " ".repeat(Math.max(0, width - visibleLen(value)));
}
function renderMenu(cursor: number): number { function renderMenu(cursor: number): number {
process.stdout.write("\x1b[H"); // cursor home process.stdout.write("\x1b[H"); // cursor home
@@ -84,7 +69,7 @@ function renderMenu(cursor: number): number {
write(""); write("");
const keyWidth = 3; const keyWidth = 3;
const labelWidth = Math.max(...MENU_ITEMS.map((m) => visibleLen(m.label))) + 2; const labelWidth = Math.max(...MENU_ITEMS.map((m) => visibleLength(m.label))) + 2;
let currentGroup: MenuItem["group"] | null = null; let currentGroup: MenuItem["group"] | null = null;
for (let i = 0; i < MENU_ITEMS.length; i++) { for (let i = 0; i < MENU_ITEMS.length; i++) {
@@ -165,14 +150,14 @@ async function waitForEnter(): Promise<void> {
// Resume stdin in case it was paused // Resume stdin in case it was paused
process.stdin.resume(); process.stdin.resume();
}); });
process.stdout.write("\x1b[2J\x1b[H"); // clear screen clearScreen();
} }
async function dispatchAndWait(item: MenuItem, wasRaw: boolean): Promise<number> { async function dispatchAndWait(item: MenuItem, wasRaw: boolean): Promise<number> {
showCursor(); showCursor();
process.stdin.setRawMode(wasRaw === true); process.stdin.setRawMode(wasRaw === true);
process.stdin.pause(); process.stdin.pause();
process.stdout.write("\x1b[2J\x1b[H"); // clear screen clearScreen();
const result = await dispatchMenuAction(item.key); const result = await dispatchMenuAction(item.key);
if (result === (SKIP_WAIT as unknown as number)) { if (result === (SKIP_WAIT as unknown as number)) {
return 0; // user explicitly backed out — skip "Press Enter" and return directly return 0; // user explicitly backed out — skip "Press Enter" and return directly
@@ -197,6 +182,13 @@ async function showMenu(): Promise<number> {
// Initial render // Initial render
renderMenu(cursor); renderMenu(cursor);
const exitMenu = (exitCode: number): number => {
showCursor();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
return exitCode;
};
try { try {
while (true) { while (true) {
const raw = await readKey(); const raw = await readKey();
@@ -214,7 +206,7 @@ async function showMenu(): Promise<number> {
// Enter // Enter
if (raw === "\r" || raw === "\n") { if (raw === "\r" || raw === "\n") {
const result = await dispatchAndWait(MENU_ITEMS[cursor]!, wasRaw); const result = await dispatchAndWait(MENU_ITEMS[cursor]!, wasRaw);
if (result !== 0) return result; if (result !== 0) return exitMenu(result);
hideCursor(); hideCursor();
if (wasRaw !== true) process.stdin.setRawMode(true); if (wasRaw !== true) process.stdin.setRawMode(true);
process.stdin.resume(); process.stdin.resume();
@@ -224,11 +216,8 @@ async function showMenu(): Promise<number> {
// Ctrl+C // Ctrl+C
if (raw === "\x03") { if (raw === "\x03") {
showCursor();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
process.stdout.write("\n"); process.stdout.write("\n");
return 0; return exitMenu(0);
} }
// Number hotkeys (1-8) // Number hotkeys (1-8)
@@ -238,7 +227,7 @@ async function showMenu(): Promise<number> {
cursor = idx; cursor = idx;
renderMenu(cursor); renderMenu(cursor);
const result = await dispatchAndWait(MENU_ITEMS[idx]!, wasRaw); const result = await dispatchAndWait(MENU_ITEMS[idx]!, wasRaw);
if (result !== 0) return result; if (result !== 0) return exitMenu(result);
hideCursor(); hideCursor();
if (wasRaw !== true) process.stdin.setRawMode(true); if (wasRaw !== true) process.stdin.setRawMode(true);
process.stdin.resume(); process.stdin.resume();
@@ -250,27 +239,18 @@ async function showMenu(): Promise<number> {
// Letter hotkeys // Letter hotkeys
const lower = raw.toLowerCase(); const lower = raw.toLowerCase();
if (lower === "h") { if (lower === "h") {
showCursor(); clearScreen();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
process.stdout.write("\x1b[2J\x1b[H");
console.log(formatHelp(commands)); console.log(formatHelp(commands));
return 0; return exitMenu(0);
} }
if (lower === "v") { if (lower === "v") {
showCursor(); clearScreen();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
process.stdout.write("\x1b[2J\x1b[H");
console.log(`gai v${VERSION}`); console.log(`gai v${VERSION}`);
return 0; return exitMenu(0);
} }
if (lower === "q") { if (lower === "q") {
showCursor();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
process.stdout.write("\n"); process.stdout.write("\n");
return 0; return exitMenu(0);
} }
} }
} finally { } finally {
@@ -428,10 +408,8 @@ const commands = registerCommands(
}, },
); );
// Keep the defs accessible for help command // Keep canonical command defs accessible for help command (deduplicate by reference)
const allCommandDefs = [...commands.values()].filter( const allCommandDefs = [...new Set(commands.values())];
(c, i, arr) => arr.findIndex((x) => x.name === c.name) === i,
);
// ── Main ─────────────────────────────────────────────────────────────── // ── Main ───────────────────────────────────────────────────────────────
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "gai", "name": "gai",
"version": "0.1.3", "version": "0.1.4",
"description": "AI-powered git helper — commit messages, PRs, code review, changelogs, and more", "description": "AI-powered git helper — commit messages, PRs, code review, changelogs, and more",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",