import { loadConfig } from "../config"; import { isGitRepo, getStagedDiff, getUnstagedFiles, stageFiles } from "../git"; import { selectFiles } from "../selector"; import { BACK } from "../menu"; import { SUGGEST_SYSTEM_PROMPT, buildSuggestBranchPrompt, buildSuggestTypePrompt, } from "../prompt"; import { callAI } from "../ai"; import { BOLD, GREEN, YELLOW, RED, DIM, RESET, CYAN } from "../terminal"; import { isStdinTTY } from "../tty"; import type { Config } from "../types"; import type { ParsedArgs } from "../cli"; export async function handleSuggest(args: ParsedArgs): Promise { const config = await loadConfig(); if (!config.apiKey) { console.error(`\n ${RED()}Error: API key not set. Run ${BOLD()}gai config${RESET()}${RED()} to configure.${RESET()}\n`); return 1; } const mode = args.positional[0] || "branch"; const verbose = args.flags["verbose"] as boolean; if (mode !== "branch" && mode !== "type") { console.error(`\n ${RED()}Error: Unknown suggest mode: ${mode}${RESET()}`); console.error(` Try: gai suggest branch | gai suggest type\n`); return 1; } // Get diff (staged, or unstaged if --unstaged, or piped) let diff: string; if (!isStdinTTY()) { const chunks: Buffer[] = []; for await (const chunk of process.stdin) { chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk); } diff = Buffer.concat(chunks).toString("utf-8").trim(); } else { if (!(await isGitRepo())) { console.error(`\n ${RED()}Error: Not a git repository.${RESET()}\n`); return 1; } if (args.flags["unstaged"] as boolean) { try { diff = (await Bun.$`git diff`.quiet().text()).trim(); } catch { diff = ""; } } else { diff = await getStagedDiff(); // If no staged changes, offer to stage unstaged files if (!diff) { const unstagedFiles = await getUnstagedFiles(); if (unstagedFiles.length > 0) { const selected = await selectFiles([], unstagedFiles); if (selected === BACK) return 0; if (selected.length > 0) { await stageFiles(selected); console.log(` ${GREEN()}Staged ${selected.length} file(s).${RESET()}`); diff = await getStagedDiff(); } } } } } if (!diff) { console.log(` ${DIM()}No changes to suggest from.${RESET()}`); return 0; } const MAX_DIFF_SIZE = 15000; const truncatedDiff = diff.length > MAX_DIFF_SIZE ? diff.substring(0, MAX_DIFF_SIZE) + "\n... (truncated)" : diff; if (verbose) { console.log(` ${DIM()}Model: ${config.model} | API: ${config.apiBase}${RESET()}`); } if (mode === "branch") { return handleSuggestBranch(config, truncatedDiff); } else { return handleSuggestType(config, truncatedDiff); } } async function handleSuggestBranch(config: Config, diff: string): Promise { const tty = isStdinTTY(); if (tty) { console.log(`\n ${BOLD()}${CYAN()}Suggesting branch names...${RESET()}\n`); } try { const raw = await callAI(config, SUGGEST_SYSTEM_PROMPT, buildSuggestBranchPrompt(diff)); const suggestions = raw .split("\n") .map((line) => line.replace(/^[\d.\s\-*]+/, "").trim()) .filter(Boolean); if (suggestions.length === 0) { console.log(` ${DIM()}No suggestions generated.${RESET()}`); return 0; } for (const s of suggestions) { console.log(` ${GREEN()}${s}${RESET()}`); } console.log(""); } catch (err) { console.error(`\n ${RED()}AI request failed: ${err instanceof Error ? err.message : err}${RESET()}\n`); return 1; } return 0; } async function handleSuggestType(config: Config, diff: string): Promise { const tty = isStdinTTY(); if (tty) { console.log(`\n ${BOLD()}${CYAN()}Suggesting commit type...${RESET()}\n`); } try { const raw = await callAI(config, SUGGEST_SYSTEM_PROMPT, buildSuggestTypePrompt(diff)); const type = raw.trim().toLowerCase(); const validTypes = ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore", "revert"]; if (validTypes.includes(type)) { console.log(` Suggested type: ${GREEN()}${BOLD()}${type}${RESET()}\n`); } else { console.log(` Suggested type: ${YELLOW()}${raw.trim()}${RESET()}`); console.log(` ${DIM()}(Not a standard Conventional Commit type)${RESET()}\n`); } } catch (err) { console.error(`\n ${RED()}AI request failed: ${err instanceof Error ? err.message : err}${RESET()}\n`); return 1; } return 0; }