#!/usr/bin/env bun // gai — AI-powered git commit and PR helper // v0.2.0 import { runCLI, registerCommands, formatHelp, type CommandDef, type ParsedArgs } from "./src/cli"; import { handleCommit } from "./src/commands/commit"; import { handlePR } from "./src/commands/pr"; import { handleConfig } from "./src/commands/config"; 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 { isStdinTTY, initTTY } from "./src/tty"; import { BACK, selectOne } from "./src/menu"; // ── Interactive Menu (default, no subcommand) ───────────────────────── async function showMenu(): Promise { if (!isStdinTTY()) { console.error("Error: Interactive menu requires a TTY. Use --help for usage."); return 1; } while (true) { const selected = await selectOne({ title: "gai", subtitle: "AI-powered git helper — choose a workflow", allowBack: false, items: [ { label: "commit", value: "commit", description: "Generate AI commit message" }, { label: "pr", value: "pr", description: "Create a PR with AI-generated title" }, { label: "explain", value: "explain", description: "Explain staged changes in plain language" }, { label: "review", value: "review", description: "AI code review of staged changes" }, { label: "changelog", value: "changelog", description: "Generate changelog from commits" }, { label: "suggest", value: "suggest", description: "Suggest branch name or commit type" }, { label: "amend", value: "amend", description: "Amend last commit with AI message" }, { label: "config", value: "config", description: "Configure API settings" }, ], }); if (selected === null || selected === BACK) return 0; // Build synthetic args for subcommand handlers const fakeArgs: ParsedArgs = { command: selected, flags: {}, positional: [], raw: [], subcommand: { name: selected, description: "", handler: async () => 0, }, }; let result: number; switch (selected) { case "commit": result = await handleCommit(fakeArgs); break; case "pr": result = await handlePR(fakeArgs); break; case "config": result = await handleConfig(fakeArgs); break; case "explain": result = await handleExplain(fakeArgs); break; case "review": result = await handleReview(fakeArgs); break; case "changelog": result = await handleChangelog(fakeArgs); break; case "suggest": result = await handleSuggest(fakeArgs); break; case "amend": fakeArgs.flags["amend"] = true; result = await handleCommit(fakeArgs); break; default: result = 0; } // Return to menu unless they explicitly chose "back" if (result !== 0) return result; // Loop back to menu for another action } } // ── Command Definitions ──────────────────────────────────────────────── const commands = registerCommands( { name: "", description: "Open interactive menu", usage: "gai", handler: async () => showMenu(), } as CommandDef, { name: "help", aliases: ["h"], description: "Show help for gai or a specific command", usage: "gai help [command]", handler: async (args: ParsedArgs) => { const commandsMap = registerCommands(...allCommandDefs); const cmdName = args.positional[0] || undefined; console.log(formatHelp(commandsMap, cmdName)); return 0; }, } as CommandDef, { name: "commit", aliases: ["c", "ci"], description: "Generate AI commit message for staged changes", usage: "gai commit [-a|--all] [-d|--dry-run] [-m|--message ] [--amend]", flags: [ { long: "all", short: "a", type: "boolean", description: "Auto-stage all changed files" }, { long: "auto", type: "boolean", description: "Alias for --all" }, { long: "dry-run", short: "d", type: "boolean", description: "Generate message without committing" }, { long: "message", short: "m", type: "string", description: "Use provided message (skip AI)" }, { long: "amend", type: "boolean", description: "Amend the last commit with AI-generated message" }, ], examples: [ "gai commit", "gai commit -a", "gai commit -d", "gai commit -m 'fix: typo in README'", "gai commit --amend", "git diff --staged | gai commit", ], handler: handleCommit, }, { name: "pr", aliases: ["p"], description: "Create a PR with AI-generated title and body", usage: "gai pr [--draft]", flags: [ { long: "draft", type: "boolean", description: "Create as draft PR" }, ], examples: [ "gai pr", "gai pr --draft", ], handler: handlePR, }, { name: "config", aliases: ["cfg"], description: "Configure API settings", usage: "gai config [get |set |list]", flags: [], examples: [ "gai config", "gai config list", "gai config get model", "gai config set model gpt-4o", ], handler: handleConfig, }, { name: "explain", aliases: ["x"], description: "Explain staged changes in plain language", usage: "gai explain [--unstaged]", flags: [ { long: "unstaged", short: "u", type: "boolean", description: "Explain unstaged changes instead of staged" }, { long: "staged", short: "s", type: "boolean", description: "Explain staged changes (default)" }, ], examples: [ "gai explain", "gai explain --unstaged", "git diff main..feature | gai explain", ], handler: handleExplain, }, { name: "review", aliases: ["r", "rv"], description: "AI code review of staged changes", usage: "gai review [--strict|--lenient] [--unstaged]", flags: [ { long: "strict", type: "boolean", description: "Thorough review, flag minor issues" }, { long: "lenient", type: "boolean", description: "Focus only on major issues" }, { long: "unstaged", short: "u", type: "boolean", description: "Review unstaged changes instead of staged" }, ], examples: [ "gai review", "gai review --strict", "gai review --unstaged", "git diff main..feature | gai review", ], handler: handleReview, }, { name: "changelog", aliases: ["cl", "log"], description: "Generate changelog from recent commits", usage: "gai changelog [--from ] [--to ] [--count ]", flags: [ { long: "from", short: "f", type: "string", description: "Starting ref (tag or commit)" }, { long: "to", short: "t", type: "string", description: "Ending ref (default: HEAD)" }, { long: "count", short: "n", type: "string", description: "Number of recent commits (default: 20)" }, ], examples: [ "gai changelog", "gai changelog --from v1.0.0", "gai changelog --from v1.0.0 --to v1.1.0", "gai changelog -n 50", ], handler: handleChangelog, }, { name: "suggest", aliases: ["sg"], description: "Suggest branch name or commit type based on changes", usage: "gai suggest [branch|type]", flags: [ { long: "unstaged", short: "u", type: "boolean", description: "Use unstaged changes" }, ], examples: [ "gai suggest branch", "gai suggest type", "git diff | gai suggest branch", ], handler: handleSuggest, }, ); // Keep the defs accessible for help command const allCommandDefs = [...commands.values()].filter( (c, i, arr) => arr.findIndex((x) => x.name === c.name) === i, ); // ── Main ─────────────────────────────────────────────────────────────── process.on("SIGINT", () => { process.stdout.write("\x1b[?25h"); // ensure cursor is shown process.stdout.write("\n"); process.exit(130); }); const args = process.argv.slice(2); // Initialize TTY detection early (before any command handlers run) initTTY(); // Apply --no-color early if (args.includes("--no-color")) { setColorEnabled(false); } runCLI(args, commands) .then((exitCode) => { process.exit(exitCode); }) .catch((err) => { console.error(`\n Unexpected error: ${err.message ?? err}\n`); process.exit(1); });