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
// gai — AI-powered git commit and PR helper
// v0.1.3
import { runCLI, registerCommands, formatHelp, type CommandDef, type ParsedArgs } from "./src/cli";
import { handleCommit } from "./src/commands/commit";
@@ -11,8 +10,7 @@ import { handleExplain } from "./src/commands/explain";
import { handleReview } from "./src/commands/review";
import { handleChangelog } from "./src/commands/changelog";
import { handleSuggest } from "./src/commands/suggest";
import { setColorEnabled } from "./src/terminal";
import { BOLD, GREEN, CYAN, DIM, RESET } from "./src/terminal";
import { setColorEnabled, BOLD, GREEN, CYAN, DIM, RESET, hideCursor, showCursor, clearLine, clearScreen, visibleLength, padRight } from "./src/terminal";
import { isStdinTTY, initTTY } from "./src/tty";
import { VERSION } from "./src/brand";
import { SKIP_WAIT } from "./src/menu";
@@ -37,11 +35,6 @@ const MENU_ITEMS: MenuItem[] = [
{ 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> {
return new Promise((resolve) => {
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 {
process.stdout.write("\x1b[H"); // cursor home
@@ -84,7 +69,7 @@ function renderMenu(cursor: number): number {
write("");
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;
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
process.stdin.resume();
});
process.stdout.write("\x1b[2J\x1b[H"); // clear screen
clearScreen();
}
async function dispatchAndWait(item: MenuItem, wasRaw: boolean): Promise<number> {
showCursor();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
process.stdout.write("\x1b[2J\x1b[H"); // clear screen
clearScreen();
const result = await dispatchMenuAction(item.key);
if (result === (SKIP_WAIT as unknown as number)) {
return 0; // user explicitly backed out — skip "Press Enter" and return directly
@@ -197,6 +182,13 @@ async function showMenu(): Promise<number> {
// Initial render
renderMenu(cursor);
const exitMenu = (exitCode: number): number => {
showCursor();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
return exitCode;
};
try {
while (true) {
const raw = await readKey();
@@ -214,7 +206,7 @@ async function showMenu(): Promise<number> {
// Enter
if (raw === "\r" || raw === "\n") {
const result = await dispatchAndWait(MENU_ITEMS[cursor]!, wasRaw);
if (result !== 0) return result;
if (result !== 0) return exitMenu(result);
hideCursor();
if (wasRaw !== true) process.stdin.setRawMode(true);
process.stdin.resume();
@@ -224,11 +216,8 @@ async function showMenu(): Promise<number> {
// Ctrl+C
if (raw === "\x03") {
showCursor();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
process.stdout.write("\n");
return 0;
return exitMenu(0);
}
// Number hotkeys (1-8)
@@ -238,7 +227,7 @@ async function showMenu(): Promise<number> {
cursor = idx;
renderMenu(cursor);
const result = await dispatchAndWait(MENU_ITEMS[idx]!, wasRaw);
if (result !== 0) return result;
if (result !== 0) return exitMenu(result);
hideCursor();
if (wasRaw !== true) process.stdin.setRawMode(true);
process.stdin.resume();
@@ -250,27 +239,18 @@ async function showMenu(): Promise<number> {
// Letter hotkeys
const lower = raw.toLowerCase();
if (lower === "h") {
showCursor();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
process.stdout.write("\x1b[2J\x1b[H");
clearScreen();
console.log(formatHelp(commands));
return 0;
return exitMenu(0);
}
if (lower === "v") {
showCursor();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
process.stdout.write("\x1b[2J\x1b[H");
clearScreen();
console.log(`gai v${VERSION}`);
return 0;
return exitMenu(0);
}
if (lower === "q") {
showCursor();
process.stdin.setRawMode(wasRaw === true);
process.stdin.pause();
process.stdout.write("\n");
return 0;
return exitMenu(0);
}
}
} finally {
@@ -428,10 +408,8 @@ const commands = registerCommands(
},
);
// Keep the defs accessible for help command
const allCommandDefs = [...commands.values()].filter(
(c, i, arr) => arr.findIndex((x) => x.name === c.name) === i,
);
// Keep canonical command defs accessible for help command (deduplicate by reference)
const allCommandDefs = [...new Set(commands.values())];
// ── Main ───────────────────────────────────────────────────────────────
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "gai",
"version": "0.1.3",
"version": "0.1.4",
"description": "AI-powered git helper — commit messages, PRs, code review, changelogs, and more",
"module": "index.ts",
"type": "module",