feat: overhaul CLI with new AI commands and mole-style menu #6

Merged
Mplan merged 24 commits from v0.1.3 into main 2026-06-17 00:17:31 +08:00
10 changed files with 206 additions and 133 deletions
Showing only changes of commit 662609a78e - Show all commits
+64 -45
View File
@@ -14,7 +14,7 @@ import { handleSuggest } from "./src/commands/suggest";
import { setColorEnabled } from "./src/terminal"; 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 { VERSION } from "./src/brand";
import { SKIP_WAIT } from "./src/menu"; import { SKIP_WAIT } from "./src/menu";
// ── Interactive Menu (mole-style) ───────────────────────────────────── // ── Interactive Menu (mole-style) ─────────────────────────────────────
@@ -23,17 +23,18 @@ interface MenuItem {
key: string; key: string;
label: string; label: string;
description: string; description: string;
group: "Create" | "Inspect" | "Project";
} }
const MENU_ITEMS: MenuItem[] = [ const MENU_ITEMS: MenuItem[] = [
{ key: "commit", label: "Commit", description: "Generate AI commit message" }, { key: "commit", label: "Commit", description: "Generate AI commit message", group: "Create" },
{ key: "pr", label: "PR", description: "Create a PR with AI-generated title" }, { key: "pr", label: "PR", description: "Create a PR with AI-generated title", group: "Create" },
{ key: "explain", label: "Explain", description: "Explain staged changes in plain language" }, { key: "amend", label: "Amend", description: "Amend last commit with AI message", group: "Create" },
{ key: "review", label: "Review", description: "AI code review of staged changes" }, { key: "explain", label: "Explain", description: "Explain staged changes in plain language", group: "Inspect" },
{ key: "changelog", label: "Changelog", description: "Generate changelog from commits" }, { key: "review", label: "Review", description: "AI code review of staged changes", group: "Inspect" },
{ key: "suggest", label: "Suggest", description: "Suggest branch name or commit type" }, { key: "changelog", label: "Changelog", description: "Generate changelog from commits", group: "Inspect" },
{ key: "amend", label: "Amend", description: "Amend last commit with AI message" }, { key: "suggest", label: "Suggest", description: "Suggest branch name or commit type", group: "Inspect" },
{ key: "config", label: "Config", description: "Configure API settings" }, { key: "config", label: "Config", description: "Configure API settings", group: "Project" },
]; ];
function hideCursor() { process.stdout.write("\x1b[?25l"); } function hideCursor() { process.stdout.write("\x1b[?25l"); }
@@ -55,51 +56,70 @@ function visibleLen(s: string): number {
return s.replace(/\x1b\[[0-9;]*m/g, "").length; return s.replace(/\x1b\[[0-9;]*m/g, "").length;
} }
function renderMenu(banner: string, cursor: number): number { 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 process.stdout.write("\x1b[H"); // cursor home
let lineCount = 0; let lineCount = 0;
for (const line of banner.split("\n")) {
clearLine();
process.stdout.write(line + "\n");
lineCount++;
}
const G = GREEN();
const C = CYAN(); const C = CYAN();
const D = DIM(); const D = DIM();
const G = GREEN();
const R = RESET(); const R = RESET();
const ARROW = "➤"; const width = 72;
const separator = `${D}${"─".repeat(width)}${R}`;
// Calculate padding const write = (line = "") => {
clearLine();
process.stdout.write(`${line}\n`);
lineCount++;
};
write("");
write(` ${BOLD()}gai${R} ${D}v${VERSION}${R}`);
write(` ${D}AI-powered git helper for commits, PRs, reviews, and changelogs${R}`);
write(` ${separator}`);
write("");
const keyWidth = 3;
const labelWidth = Math.max(...MENU_ITEMS.map((m) => visibleLen(m.label))) + 2; const labelWidth = Math.max(...MENU_ITEMS.map((m) => visibleLen(m.label))) + 2;
let currentGroup: MenuItem["group"] | null = null;
for (let i = 0; i < MENU_ITEMS.length; i++) { for (let i = 0; i < MENU_ITEMS.length; i++) {
const item = MENU_ITEMS[i]!; const item = MENU_ITEMS[i]!;
const num = String(i + 1); const num = String(i + 1);
const active = i === cursor; const active = i === cursor;
clearLine(); if (item.group !== currentGroup) {
if (active) { if (currentGroup !== null) write("");
const padding = " ".repeat(Math.max(1, labelWidth - visibleLen(item.label))); write(` ${D}${item.group.toUpperCase()}${R}`);
process.stdout.write(` ${C}${ARROW} ${num}. ${item.label}${R}${padding}${D}${item.description}${R}\n`); currentGroup = item.group;
} else { }
const padding = " ".repeat(labelWidth - visibleLen(item.label) + 3);
process.stdout.write(` ${num}. ${item.label}${padding}${D}${item.description}${R}\n`); const pointer = active ? `${C}${R}` : " ";
const key = active ? `${G}${num}${R}` : `${D}${num}${R}`;
const label = active ? `${BOLD()}${item.label}${R}` : item.label;
const description = active ? item.description : `${D}${item.description}${R}`;
const row = [
pointer,
padRight(key, keyWidth),
padRight(label, labelWidth),
description,
].join(" ");
if (active) {
write(` ${C}${R} ${row}`);
} else {
write(` ${row}`);
} }
lineCount++;
} }
// Footer write("");
clearLine(); write(` ${separator}`);
process.stdout.write("\n"); write(` ${D}↑/↓ navigate enter run ${G}1-8${D} jump ${G}h${D} help ${G}v${D} version ${G}q${D} quit${R}`);
lineCount++; write("");
clearLine();
process.stdout.write(` ${D}↑↓ | Enter | ${G}H${D} Help | ${G}V${D} Version | ${G}Q${D} Quit${R}\n`);
lineCount++;
clearLine();
process.stdout.write("\n");
lineCount++;
// Clear rest of screen // Clear rest of screen
process.stdout.write("\x1b[J"); process.stdout.write("\x1b[J");
@@ -167,7 +187,6 @@ async function showMenu(): Promise<number> {
return 1; return 1;
} }
const banner = showBanner();
let cursor = 0; let cursor = 0;
const wasRaw = process.stdin.isRaw; const wasRaw = process.stdin.isRaw;
@@ -176,7 +195,7 @@ async function showMenu(): Promise<number> {
hideCursor(); hideCursor();
// Initial render // Initial render
renderMenu(banner, cursor); renderMenu(cursor);
try { try {
while (true) { while (true) {
@@ -184,11 +203,11 @@ async function showMenu(): Promise<number> {
// Escape sequences (arrows) // Escape sequences (arrows)
if (raw === "\x1b[A" || raw === "\x1bOA") { if (raw === "\x1b[A" || raw === "\x1bOA") {
if (cursor > 0) { cursor--; renderMenu(banner, cursor); } if (cursor > 0) { cursor--; renderMenu(cursor); }
continue; continue;
} }
if (raw === "\x1b[B" || raw === "\x1bOB") { if (raw === "\x1b[B" || raw === "\x1bOB") {
if (cursor < MENU_ITEMS.length - 1) { cursor++; renderMenu(banner, cursor); } if (cursor < MENU_ITEMS.length - 1) { cursor++; renderMenu(cursor); }
continue; continue;
} }
@@ -199,7 +218,7 @@ async function showMenu(): Promise<number> {
hideCursor(); hideCursor();
if (wasRaw !== true) process.stdin.setRawMode(true); if (wasRaw !== true) process.stdin.setRawMode(true);
process.stdin.resume(); process.stdin.resume();
renderMenu(banner, cursor); renderMenu(cursor);
continue; continue;
} }
@@ -217,13 +236,13 @@ async function showMenu(): Promise<number> {
const idx = parseInt(raw) - 1; const idx = parseInt(raw) - 1;
if (idx < MENU_ITEMS.length) { if (idx < MENU_ITEMS.length) {
cursor = idx; cursor = idx;
renderMenu(banner, cursor); renderMenu(cursor);
const result = await dispatchAndWait(MENU_ITEMS[idx]!, wasRaw); const result = await dispatchAndWait(MENU_ITEMS[idx]!, wasRaw);
if (result !== 0) return result; if (result !== 0) return result;
hideCursor(); hideCursor();
if (wasRaw !== true) process.stdin.setRawMode(true); if (wasRaw !== true) process.stdin.setRawMode(true);
process.stdin.resume(); process.stdin.resume();
renderMenu(banner, cursor); renderMenu(cursor);
continue; continue;
} }
} }
@@ -243,7 +262,7 @@ async function showMenu(): Promise<number> {
process.stdin.setRawMode(wasRaw === true); process.stdin.setRawMode(wasRaw === true);
process.stdin.pause(); process.stdin.pause();
process.stdout.write("\x1b[2J\x1b[H"); process.stdout.write("\x1b[2J\x1b[H");
console.log("gai v0.1.3"); console.log(`gai v${VERSION}`);
return 0; return 0;
} }
if (lower === "q") { if (lower === "q") {
+1 -1
View File
@@ -2,7 +2,7 @@
import { GREEN, CYAN, RESET } from "./terminal"; import { GREEN, CYAN, RESET } from "./terminal";
const VERSION = "0.1.3"; export const VERSION = "0.1.3";
export function showBanner(): string { export function showBanner(): string {
const G = GREEN(); const G = GREEN();
+1 -1
View File
@@ -62,7 +62,7 @@ export async function handleChangelog(args: ParsedArgs): Promise<number> {
} }
try { try {
const callbacks: StreamCallbacks = tty ? { const callbacks: StreamCallbacks | undefined = tty ? {
onToken: (token) => process.stdout.write(token), onToken: (token) => process.stdout.write(token),
} : undefined; } : undefined;
+35 -20
View File
@@ -7,6 +7,7 @@ import {
getStagedDiff, getStagedDiff,
getRecentCommits, getRecentCommits,
stageFiles, stageFiles,
applyFileSelection,
commit, commit,
} from "../git"; } from "../git";
import { selectFiles } from "../selector"; import { selectFiles } from "../selector";
@@ -50,6 +51,19 @@ function printCommitResult(result: CommitResult, msg: string) {
if (parts.length > 0) console.log(` ${parts.join(", ")}`); if (parts.length > 0) console.log(` ${parts.join(", ")}`);
} }
function printSelectionResult(result: { staged: string[]; unstaged: string[] }) {
const parts: string[] = [];
if (result.staged.length > 0) {
parts.push(`${GREEN()}staged ${result.staged.length}${RESET()}`);
}
if (result.unstaged.length > 0) {
parts.push(`${YELLOW()}unstaged ${result.unstaged.length}${RESET()}`);
}
if (parts.length > 0) {
console.log(` Updated staging area: ${parts.join(", ")}`);
}
}
async function confirmCommit(message: string): Promise<"y" | "n" | "e"> { async function confirmCommit(message: string): Promise<"y" | "n" | "e"> {
console.log(`\n ${BOLD()}Generated commit message:${RESET()}`); console.log(`\n ${BOLD()}Generated commit message:${RESET()}`);
console.log(` ${GREEN()}${message}${RESET()}\n`); console.log(` ${GREEN()}${message}${RESET()}\n`);
@@ -173,16 +187,22 @@ export async function handleCommit(args: ParsedArgs): Promise<number> {
const stagedFiles = await getStagedFiles(); const stagedFiles = await getStagedFiles();
const unstagedFiles = await getUnstagedFiles(); const unstagedFiles = await getUnstagedFiles();
if (stagedFiles.length === 0 && !amend) { if (!amend && stagedFiles.length === 0 && unstagedFiles.length === 0) {
if (autoMode && unstagedFiles.length > 0) { console.error(`\n ${RED()}Error: Nothing to commit.${RESET()}\n`);
await stageFiles(unstagedFiles.map((f) => f.path)); return 1;
console.log(` ${GREEN()}Auto-staged ${unstagedFiles.length} file(s).${RESET()}`); }
} else if (unstagedFiles.length > 0) {
if (!amend && autoMode && unstagedFiles.length > 0) {
await stageFiles(unstagedFiles.map((f) => f.path));
console.log(` ${GREEN()}Auto-staged ${unstagedFiles.length} file(s).${RESET()}`);
} else if (!amend) {
if (isStdinTTY()) {
const selected = await selectFiles(stagedFiles, unstagedFiles);
if (selected === BACK) return SKIP_WAIT as unknown as number;
printSelectionResult(await applyFileSelection(stagedFiles, unstagedFiles, selected));
} else if (stagedFiles.length === 0 && unstagedFiles.length > 0) {
console.error(`\n ${RED()}Error: No staged changes. Use -a to auto-stage, or stage files manually.${RESET()}\n`); console.error(`\n ${RED()}Error: No staged changes. Use -a to auto-stage, or stage files manually.${RESET()}\n`);
return 1; return 1;
} else {
console.error(`\n ${RED()}Error: Nothing to commit.${RESET()}\n`);
return 1;
} }
} }
@@ -215,18 +235,13 @@ export async function handleCommit(args: ParsedArgs): Promise<number> {
return 0; return 0;
} }
if (unstagedFiles.length > 0) { if (autoMode && unstagedFiles.length > 0) {
if (autoMode) { await stageFiles(unstagedFiles.map((f) => f.path));
await stageFiles(unstagedFiles.map((f) => f.path)); 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 SKIP_WAIT as unknown as number;
if (selected === BACK) return SKIP_WAIT as unknown as number; printSelectionResult(await applyFileSelection(stagedFiles, unstagedFiles, selected));
if (selected.length > 0) {
await stageFiles(selected);
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
}
}
} }
const diff = await getStagedDiff(); const diff = await getStagedDiff();
+17 -17
View File
@@ -1,5 +1,12 @@
import { loadConfig } from "../config"; import { loadConfig } from "../config";
import { isGitRepo, getStagedDiff, getUnstagedFiles, getRepoRoot, stageFiles } from "../git"; import {
isGitRepo,
getStagedFiles,
getStagedDiff,
getUnstagedFiles,
getRepoRoot,
applyFileSelection,
} from "../git";
import { selectFiles } from "../selector"; import { selectFiles } from "../selector";
import { BACK, SKIP_WAIT } from "../menu"; import { BACK, SKIP_WAIT } from "../menu";
import { collectProjectContext } from "../context"; import { collectProjectContext } from "../context";
@@ -19,7 +26,6 @@ export async function handleExplain(args: ParsedArgs): Promise<number> {
} }
const unstaged = args.flags["unstaged"] as boolean; const unstaged = args.flags["unstaged"] as boolean;
const staged = args.flags["staged"] as boolean;
const verbose = args.flags["verbose"] as boolean; const verbose = args.flags["verbose"] as boolean;
// Determine which diff to explain // Determine which diff to explain
@@ -44,22 +50,16 @@ export async function handleExplain(args: ParsedArgs): Promise<number> {
console.error(`\n ${RED()}Error: Not a git repository.${RESET()}\n`); console.error(`\n ${RED()}Error: Not a git repository.${RESET()}\n`);
return 1; return 1;
} }
diff = await getStagedDiff(); const stagedFiles = await getStagedFiles();
sourceLabel = "staged changes"; const unstagedFiles = await getUnstagedFiles();
sourceLabel = "selected changes";
// If no staged changes, offer to stage unstaged files if (stagedFiles.length > 0 || unstagedFiles.length > 0) {
if (!diff) { const selected = await selectFiles(stagedFiles, unstagedFiles);
const unstagedFiles = await getUnstagedFiles(); if (selected === BACK) return SKIP_WAIT as unknown as number;
if (unstagedFiles.length > 0) { await applyFileSelection(stagedFiles, unstagedFiles, selected);
const selected = await selectFiles([], unstagedFiles);
if (selected === BACK) return SKIP_WAIT as unknown as number;
if (selected.length > 0) {
await stageFiles(selected);
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
diff = await getStagedDiff();
}
}
} }
diff = await getStagedDiff();
} else { } else {
// Read from pipe // Read from pipe
const chunks: Buffer[] = []; const chunks: Buffer[] = [];
@@ -109,7 +109,7 @@ export async function handleExplain(args: ParsedArgs): Promise<number> {
} }
try { try {
const callbacks: StreamCallbacks = tty ? { const callbacks: StreamCallbacks | undefined = tty ? {
onToken: (token) => process.stdout.write(token), onToken: (token) => process.stdout.write(token),
} : undefined; } : undefined;
+17 -16
View File
@@ -1,5 +1,12 @@
import { loadConfig } from "../config"; import { loadConfig } from "../config";
import { isGitRepo, getStagedDiff, getUnstagedFiles, getRepoRoot, stageFiles } from "../git"; import {
isGitRepo,
getStagedFiles,
getStagedDiff,
getUnstagedFiles,
getRepoRoot,
applyFileSelection,
} from "../git";
import { selectFiles } from "../selector"; import { selectFiles } from "../selector";
import { BACK, SKIP_WAIT } from "../menu"; import { BACK, SKIP_WAIT } from "../menu";
import { collectProjectContext } from "../context"; import { collectProjectContext } from "../context";
@@ -53,22 +60,16 @@ export async function handleReview(args: ParsedArgs): Promise<number> {
console.error(`\n ${RED()}Error: Not a git repository.${RESET()}\n`); console.error(`\n ${RED()}Error: Not a git repository.${RESET()}\n`);
return 1; return 1;
} }
diff = await getStagedDiff(); const stagedFiles = await getStagedFiles();
sourceLabel = "staged changes"; const unstagedFiles = await getUnstagedFiles();
sourceLabel = "selected changes";
// If no staged changes, offer to stage unstaged files if (stagedFiles.length > 0 || unstagedFiles.length > 0) {
if (!diff) { const selected = await selectFiles(stagedFiles, unstagedFiles);
const unstagedFiles = await getUnstagedFiles(); if (selected === BACK) return SKIP_WAIT as unknown as number;
if (unstagedFiles.length > 0) { await applyFileSelection(stagedFiles, unstagedFiles, selected);
const selected = await selectFiles([], unstagedFiles);
if (selected === BACK) return SKIP_WAIT as unknown as number;
if (selected.length > 0) {
await stageFiles(selected);
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
diff = await getStagedDiff();
}
}
} }
diff = await getStagedDiff();
} }
if (!diff) { if (!diff) {
@@ -110,7 +111,7 @@ export async function handleReview(args: ParsedArgs): Promise<number> {
} }
try { try {
const callbacks: StreamCallbacks = tty ? { const callbacks: StreamCallbacks | undefined = tty ? {
onToken: (token) => process.stdout.write(token), onToken: (token) => process.stdout.write(token),
} : undefined; } : undefined;
+14 -14
View File
@@ -1,5 +1,11 @@
import { loadConfig } from "../config"; import { loadConfig } from "../config";
import { isGitRepo, getStagedDiff, getUnstagedFiles, stageFiles } from "../git"; import {
isGitRepo,
getStagedFiles,
getStagedDiff,
getUnstagedFiles,
applyFileSelection,
} from "../git";
import { selectFiles } from "../selector"; import { selectFiles } from "../selector";
import { BACK, SKIP_WAIT } from "../menu"; import { BACK, SKIP_WAIT } from "../menu";
import { import {
@@ -51,21 +57,15 @@ export async function handleSuggest(args: ParsedArgs): Promise<number> {
diff = ""; diff = "";
} }
} else { } else {
diff = await getStagedDiff(); const stagedFiles = await getStagedFiles();
const unstagedFiles = await getUnstagedFiles();
// If no staged changes, offer to stage unstaged files if (stagedFiles.length > 0 || unstagedFiles.length > 0) {
if (!diff) { const selected = await selectFiles(stagedFiles, unstagedFiles);
const unstagedFiles = await getUnstagedFiles(); if (selected === BACK) return SKIP_WAIT as unknown as number;
if (unstagedFiles.length > 0) { await applyFileSelection(stagedFiles, unstagedFiles, selected);
const selected = await selectFiles([], unstagedFiles);
if (selected === BACK) return SKIP_WAIT as unknown as number;
if (selected.length > 0) {
await stageFiles(selected);
console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`);
diff = await getStagedDiff();
}
}
} }
diff = await getStagedDiff();
} }
} }
+27
View File
@@ -98,6 +98,33 @@ export async function stageFiles(paths: string[]): Promise<void> {
await Bun.$`git add -- ${paths}`; await Bun.$`git add -- ${paths}`;
} }
export async function unstageFiles(paths: string[]): Promise<void> {
if (paths.length === 0) return;
try {
await Bun.$`git restore --staged -- ${paths}`.quiet();
} catch {
await Bun.$`git rm --cached -r -- ${paths}`.quiet();
}
}
export async function applyFileSelection(
stagedFiles: FileEntry[],
unstagedFiles: FileEntry[],
selectedPaths: string[],
): Promise<{ staged: string[]; unstaged: string[] }> {
const selected = new Set(selectedPaths);
const stagedPaths = new Set(stagedFiles.map((file) => file.path));
const unstagedPaths = new Set(unstagedFiles.map((file) => file.path));
const toUnstage = [...stagedPaths].filter((path) => !selected.has(path));
const toStage = [...selected].filter((path) => unstagedPaths.has(path));
await unstageFiles(toUnstage);
await stageFiles(toStage);
return { staged: toStage, unstaged: toUnstage };
}
export async function commit( export async function commit(
message: string, message: string,
): Promise<{ branch: string; hash: string; files: number; insertions: number; deletions: number }> { ): Promise<{ branch: string; hash: string; files: number; insertions: number; deletions: number }> {
+1
View File
@@ -225,6 +225,7 @@ export async function selectMany<T>(
if (!options.selectAllLabel) return; if (!options.selectAllLabel) return;
items[0]!.selected = items.slice(1).every((item) => item.selected); items[0]!.selected = items.slice(1).every((item) => item.selected);
}; };
syncSelectAll();
const toggle = (index: number) => { const toggle = (index: number) => {
const item = items[index]!; const item = items[index]!;
+29 -19
View File
@@ -1,44 +1,54 @@
import type { FileEntry } from "./types"; import type { FileEntry } from "./types";
import { BOLD, GREEN, YELLOW, RESET } from "./terminal";
import { isStdinTTY } from "./tty"; import { isStdinTTY } from "./tty";
import { BACK, selectMany } from "./menu"; import { BACK, selectMany } from "./menu";
import type { PromptBack } from "./menu"; import type { PromptBack } from "./menu";
function mergeFiles(stagedFiles: FileEntry[], unstagedFiles: FileEntry[]) {
const files = new Map<string, FileEntry & { staged: boolean; unstaged: boolean }>();
for (const file of stagedFiles) {
files.set(file.path, { ...file, staged: true, unstaged: false });
}
for (const file of unstagedFiles) {
const existing = files.get(file.path);
if (existing) {
existing.unstaged = true;
existing.label = existing.label === file.label
? existing.label
: `${existing.label}, ${file.label}`;
} else {
files.set(file.path, { ...file, staged: false, unstaged: true });
}
}
return [...files.values()];
}
export async function selectFiles( export async function selectFiles(
stagedFiles: FileEntry[], stagedFiles: FileEntry[],
unstagedFiles: FileEntry[], unstagedFiles: FileEntry[],
): Promise<string[] | PromptBack> { ): Promise<string[] | PromptBack> {
if (unstagedFiles.length === 0) return []; const files = mergeFiles(stagedFiles, unstagedFiles);
if (files.length === 0) return [];
if (stagedFiles.length > 0) { if (!isStdinTTY()) return stagedFiles.map((file) => file.path);
process.stdout.write(`\n ${BOLD()}Staged files (will be included):${RESET()}\n`);
for (const f of stagedFiles) {
process.stdout.write(` ${GREEN()}${RESET()} ${f.path} (${YELLOW()}${f.label}${RESET()})\n`);
}
}
if (!isStdinTTY()) return [];
const selected = await selectMany({ const selected = await selectMany({
title: "Select files to stage", title: "Select files for this action",
subtitle: `${unstagedFiles.length} unstaged file${unstagedFiles.length > 1 ? "s" : ""} available`, subtitle: `${stagedFiles.length} staged, ${unstagedFiles.length} unstaged`,
selectAllLabel: "Select all", selectAllLabel: "Select all",
cancelMessage: "Aborted.", cancelMessage: "Aborted.",
items: unstagedFiles.map((f) => ({ items: files.map((f) => ({
label: f.path, label: f.path,
value: f.path, value: f.path,
description: f.label, description: f.label,
selected: f.staged,
})), })),
}); });
if (selected === null) process.exit(1); if (selected === null) process.exit(1);
if (selected === BACK) return BACK; if (selected === BACK) return BACK;
if (selected.length > 0) {
process.stdout.write(
` ${GREEN()}Staged ${selected.length} file(s):${RESET()} ${selected.join(", ")}\n`,
);
}
return selected; return selected;
} }