feat: overhaul CLI with new AI commands and mole-style menu #6
@@ -15,6 +15,7 @@ import { setColorEnabled } from "./src/terminal";
|
|||||||
import { BOLD, GREEN, CYAN, DIM, RESET } 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 { showBanner } from "./src/brand";
|
import { showBanner } from "./src/brand";
|
||||||
|
import { SKIP_WAIT } from "./src/menu";
|
||||||
|
|
||||||
// ── Interactive Menu (mole-style) ─────────────────────────────────────
|
// ── Interactive Menu (mole-style) ─────────────────────────────────────
|
||||||
|
|
||||||
@@ -153,6 +154,9 @@ async function dispatchAndWait(item: MenuItem, wasRaw: boolean): Promise<number>
|
|||||||
process.stdin.pause();
|
process.stdin.pause();
|
||||||
process.stdout.write("\x1b[2J\x1b[H"); // clear screen
|
process.stdout.write("\x1b[2J\x1b[H"); // clear screen
|
||||||
const result = await dispatchMenuAction(item.key);
|
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
|
||||||
|
}
|
||||||
await waitForEnter();
|
await waitForEnter();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
commit,
|
commit,
|
||||||
} from "../git";
|
} from "../git";
|
||||||
import { selectFiles } from "../selector";
|
import { selectFiles } from "../selector";
|
||||||
import { BACK } from "../menu";
|
import { BACK, SKIP_WAIT } from "../menu";
|
||||||
import { collectProjectContext } from "../context";
|
import { collectProjectContext } from "../context";
|
||||||
import { buildPrompt, SYSTEM_PROMPT } from "../prompt";
|
import { buildPrompt, SYSTEM_PROMPT } from "../prompt";
|
||||||
import { generateCommitMessage } from "../ai";
|
import { generateCommitMessage } from "../ai";
|
||||||
@@ -221,7 +221,7 @@ export async function handleCommit(args: ParsedArgs): Promise<number> {
|
|||||||
console.log(` ${GREEN()}Auto-staged ${unstagedFiles.length} file(s).${RESET()}`);
|
console.log(` ${GREEN()}Auto-staged ${unstagedFiles.length} file(s).${RESET()}`);
|
||||||
} else {
|
} else {
|
||||||
const selected = await selectFiles(stagedFiles, unstagedFiles);
|
const selected = await selectFiles(stagedFiles, unstagedFiles);
|
||||||
if (selected === BACK) return 0;
|
if (selected === BACK) return SKIP_WAIT as unknown as number;
|
||||||
if (selected.length > 0) {
|
if (selected.length > 0) {
|
||||||
await stageFiles(selected);
|
await stageFiles(selected);
|
||||||
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
|
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { loadConfig, saveConfig } from "../config";
|
import { loadConfig, saveConfig } from "../config";
|
||||||
import { BOLD, GREEN, YELLOW, CYAN, RED, DIM, RESET } from "../terminal";
|
import { BOLD, GREEN, YELLOW, CYAN, RED, DIM, RESET } from "../terminal";
|
||||||
import { isStdinTTY } from "../tty";
|
import { isStdinTTY } from "../tty";
|
||||||
|
import { SKIP_WAIT } from "../menu";
|
||||||
import type { Config } from "../types";
|
import type { Config } from "../types";
|
||||||
import type { ParsedArgs } from "../cli";
|
import type { ParsedArgs } from "../cli";
|
||||||
|
|
||||||
@@ -323,7 +324,7 @@ export async function handleConfig(args: ParsedArgs): Promise<number> {
|
|||||||
// gai config (no args) → interactive
|
// gai config (no args) → interactive
|
||||||
if (positional.length === 0) {
|
if (positional.length === 0) {
|
||||||
const result = await interactiveConfig();
|
const result = await interactiveConfig();
|
||||||
return result === "back" ? 0 : 0;
|
return result === "back" ? (SKIP_WAIT as unknown as number) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`\n ${RED()}Error: Unknown config subcommand: ${positional[0]}${RESET()}`);
|
console.error(`\n ${RED()}Error: Unknown config subcommand: ${positional[0]}${RESET()}`);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { loadConfig } from "../config";
|
import { loadConfig } from "../config";
|
||||||
import { isGitRepo, getStagedDiff, getUnstagedFiles, getRepoRoot, stageFiles } from "../git";
|
import { isGitRepo, getStagedDiff, getUnstagedFiles, getRepoRoot, stageFiles } from "../git";
|
||||||
import { selectFiles } from "../selector";
|
import { selectFiles } from "../selector";
|
||||||
import { BACK } from "../menu";
|
import { BACK, SKIP_WAIT } from "../menu";
|
||||||
import { collectProjectContext } from "../context";
|
import { collectProjectContext } from "../context";
|
||||||
import { EXPLAIN_SYSTEM_PROMPT, buildExplainPrompt } from "../prompt";
|
import { EXPLAIN_SYSTEM_PROMPT, buildExplainPrompt } from "../prompt";
|
||||||
import { callAI } from "../ai";
|
import { callAI } from "../ai";
|
||||||
@@ -52,7 +52,7 @@ export async function handleExplain(args: ParsedArgs): Promise<number> {
|
|||||||
const unstagedFiles = await getUnstagedFiles();
|
const unstagedFiles = await getUnstagedFiles();
|
||||||
if (unstagedFiles.length > 0) {
|
if (unstagedFiles.length > 0) {
|
||||||
const selected = await selectFiles([], unstagedFiles);
|
const selected = await selectFiles([], unstagedFiles);
|
||||||
if (selected === BACK) return 0;
|
if (selected === BACK) return SKIP_WAIT as unknown as number;
|
||||||
if (selected.length > 0) {
|
if (selected.length > 0) {
|
||||||
await stageFiles(selected);
|
await stageFiles(selected);
|
||||||
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
|
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ import { isGitRepo, getRepoRoot } from "../git";
|
|||||||
import { collectProjectContext } from "../context";
|
import { collectProjectContext } from "../context";
|
||||||
import { PR_SYSTEM_PROMPT, buildPRPrompt } from "../prompt";
|
import { PR_SYSTEM_PROMPT, buildPRPrompt } from "../prompt";
|
||||||
import { generatePRMessage } from "../ai";
|
import { generatePRMessage } from "../ai";
|
||||||
import { BACK, selectOne } from "../menu";
|
import { BACK, SKIP_WAIT, selectOne } from "../menu";
|
||||||
import {
|
import {
|
||||||
getDefaultBranch,
|
getDefaultBranch,
|
||||||
getBranchName,
|
getBranchName,
|
||||||
@@ -68,7 +68,7 @@ export async function handlePR(args: ParsedArgs): Promise<number> {
|
|||||||
if (!platform) {
|
if (!platform) {
|
||||||
const hostname = (await getRemoteHostname()) || "unknown";
|
const hostname = (await getRemoteHostname()) || "unknown";
|
||||||
const chosen = await selectPlatform(hostname);
|
const chosen = await selectPlatform(hostname);
|
||||||
if (chosen === BACK) return 0;
|
if (chosen === BACK) return SKIP_WAIT as unknown as number;
|
||||||
if (!chosen) {
|
if (!chosen) {
|
||||||
console.log(" Aborted.");
|
console.log(" Aborted.");
|
||||||
return 0;
|
return 0;
|
||||||
@@ -98,7 +98,7 @@ export async function handlePR(args: ParsedArgs): Promise<number> {
|
|||||||
items: [{ label: "Back", value: "back" as const }],
|
items: [{ label: "Back", value: "back" as const }],
|
||||||
});
|
});
|
||||||
if (choice === null) process.exit(0);
|
if (choice === null) process.exit(0);
|
||||||
return 0;
|
return SKIP_WAIT as unknown as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(` ${commits.length} commit${commits.length > 1 ? "s" : ""} on this branch`);
|
console.log(` ${commits.length} commit${commits.length > 1 ? "s" : ""} on this branch`);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { loadConfig } from "../config";
|
import { loadConfig } from "../config";
|
||||||
import { isGitRepo, getStagedDiff, getUnstagedFiles, getRepoRoot, stageFiles } from "../git";
|
import { isGitRepo, getStagedDiff, getUnstagedFiles, getRepoRoot, stageFiles } from "../git";
|
||||||
import { selectFiles } from "../selector";
|
import { selectFiles } from "../selector";
|
||||||
import { BACK } from "../menu";
|
import { BACK, SKIP_WAIT } from "../menu";
|
||||||
import { collectProjectContext } from "../context";
|
import { collectProjectContext } from "../context";
|
||||||
import { REVIEW_SYSTEM_PROMPT, buildReviewPrompt } from "../prompt";
|
import { REVIEW_SYSTEM_PROMPT, buildReviewPrompt } from "../prompt";
|
||||||
import { callAI } from "../ai";
|
import { callAI } from "../ai";
|
||||||
@@ -61,7 +61,7 @@ export async function handleReview(args: ParsedArgs): Promise<number> {
|
|||||||
const unstagedFiles = await getUnstagedFiles();
|
const unstagedFiles = await getUnstagedFiles();
|
||||||
if (unstagedFiles.length > 0) {
|
if (unstagedFiles.length > 0) {
|
||||||
const selected = await selectFiles([], unstagedFiles);
|
const selected = await selectFiles([], unstagedFiles);
|
||||||
if (selected === BACK) return 0;
|
if (selected === BACK) return SKIP_WAIT as unknown as number;
|
||||||
if (selected.length > 0) {
|
if (selected.length > 0) {
|
||||||
await stageFiles(selected);
|
await stageFiles(selected);
|
||||||
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
|
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { loadConfig } from "../config";
|
import { loadConfig } from "../config";
|
||||||
import { isGitRepo, getStagedDiff, getUnstagedFiles, stageFiles } from "../git";
|
import { isGitRepo, getStagedDiff, getUnstagedFiles, stageFiles } from "../git";
|
||||||
import { selectFiles } from "../selector";
|
import { selectFiles } from "../selector";
|
||||||
import { BACK } from "../menu";
|
import { BACK, SKIP_WAIT } from "../menu";
|
||||||
import {
|
import {
|
||||||
SUGGEST_SYSTEM_PROMPT,
|
SUGGEST_SYSTEM_PROMPT,
|
||||||
buildSuggestBranchPrompt,
|
buildSuggestBranchPrompt,
|
||||||
@@ -58,7 +58,7 @@ export async function handleSuggest(args: ParsedArgs): Promise<number> {
|
|||||||
const unstagedFiles = await getUnstagedFiles();
|
const unstagedFiles = await getUnstagedFiles();
|
||||||
if (unstagedFiles.length > 0) {
|
if (unstagedFiles.length > 0) {
|
||||||
const selected = await selectFiles([], unstagedFiles);
|
const selected = await selectFiles([], unstagedFiles);
|
||||||
if (selected === BACK) return 0;
|
if (selected === BACK) return SKIP_WAIT as unknown as number;
|
||||||
if (selected.length > 0) {
|
if (selected.length > 0) {
|
||||||
await stageFiles(selected);
|
await stageFiles(selected);
|
||||||
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
|
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const BACKSPACE = "\x7f";
|
|||||||
export const BACK = Symbol("prompt-back");
|
export const BACK = Symbol("prompt-back");
|
||||||
export type PromptBack = typeof BACK;
|
export type PromptBack = typeof BACK;
|
||||||
|
|
||||||
|
// Sent by command handlers to skip the "Press Enter to return" wait in the
|
||||||
|
// interactive menu when the user explicitly backed out of a sub-menu.
|
||||||
|
export const SKIP_WAIT = Symbol("skip-wait");
|
||||||
|
|
||||||
export interface Choice<T> {
|
export interface Choice<T> {
|
||||||
label: string;
|
label: string;
|
||||||
value: T;
|
value: T;
|
||||||
|
|||||||
Reference in New Issue
Block a user