From 311d059e5267d64b47e0791b2452e1ad4f3b45e4 Mon Sep 17 00:00:00 2001 From: Mplan Date: Thu, 18 Jun 2026 01:57:01 +0800 Subject: [PATCH] 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 --- index.ts | 64 +++++++++++++++++----------------------------------- package.json | 2 +- 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/index.ts b/index.ts index b8169f0..5929b79 100644 --- a/index.ts +++ b/index.ts @@ -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 { return new Promise((resolve) => { const onData = (data: Buffer) => { @@ -52,14 +45,6 @@ async function readKey(): Promise { }); } -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 { // 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 { 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 { // 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 { // 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 { // 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 { 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 { // 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 ─────────────────────────────────────────────────────────────── diff --git a/package.json b/package.json index 849414d..b62fdba 100644 --- a/package.json +++ b/package.json @@ -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",