4b384a7581
- Update menu.ts and selector.ts to use isStdinTTY() and function-based terminal colors - Refactor index.ts from 995-line monolith to ~270-line dispatcher that registers all commands via the CLI parser and delegates to modules - Add initTTY() call at startup for correct pipe/TTY detection - Interactive menu expanded to include new commands (explain, review, changelog, suggest, amend)
275 lines
8.7 KiB
TypeScript
275 lines
8.7 KiB
TypeScript
#!/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<number> {
|
|
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 <msg>] [--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 <key>|set <key> <value>|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 <ref>] [--to <ref>] [--count <n>]",
|
|
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);
|
|
});
|