import { loadConfig } from "../config"; import { isGitRepo, getStagedFiles, getStagedDiff, getUnstagedFiles, getRepoRoot, applyFileSelection, } from "../git"; import { selectFiles } from "../selector"; import { BACK, SKIP_WAIT } from "../menu"; import { collectProjectContext } from "../context"; import { REVIEW_SYSTEM_PROMPT, buildReviewPrompt } from "../prompt"; import { callAI } from "../ai"; import { BOLD, GREEN, YELLOW, RED, DIM, RESET, CYAN } from "../terminal"; import { isStdinTTY } from "../tty"; import type { StreamCallbacks } from "../types"; import type { ParsedArgs } from "../cli"; export async function handleReview(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 strictnessFlag = args.flags["strict"] as boolean ? "strict" : args.flags["lenient"] as boolean ? "lenient" : "normal"; const unstaged = args.flags["unstaged"] as boolean; const verbose = args.flags["verbose"] as boolean; let diff: string; let sourceLabel: string; if (unstaged) { if (!(await isGitRepo())) { console.error(`\n ${RED()}Error: Not a git repository.${RESET()}\n`); return 1; } try { diff = (await Bun.$`git diff`.quiet().text()).trim(); } catch { diff = ""; } sourceLabel = "unstaged changes"; } else 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(); sourceLabel = "piped input"; } else { if (!(await isGitRepo())) { console.error(`\n ${RED()}Error: Not a git repository.${RESET()}\n`); return 1; } const stagedFiles = await getStagedFiles(); const unstagedFiles = await getUnstagedFiles(); sourceLabel = "selected changes"; if (stagedFiles.length > 0 || unstagedFiles.length > 0) { const selected = await selectFiles(stagedFiles, unstagedFiles); if (selected === BACK) return SKIP_WAIT as unknown as number; await applyFileSelection(stagedFiles, unstagedFiles, selected); } diff = await getStagedDiff(); } if (!diff) { console.log(` ${DIM()}No ${sourceLabel} to review.${RESET()}`); return 0; } const MAX_DIFF_SIZE = 15000; const truncatedDiff = diff.length > MAX_DIFF_SIZE ? diff.substring(0, MAX_DIFF_SIZE) + "\n... (truncated)" : diff; let contextPrefix = ""; try { if (await isGitRepo()) { const repoRoot = await getRepoRoot(); const ctx = await collectProjectContext(repoRoot); if (ctx.packageDescription) { contextPrefix = `Project: ${ctx.packageDescription}\n\n`; } } } catch {} const userPrompt = contextPrefix + buildReviewPrompt(truncatedDiff, strictnessFlag); const strictnessLabel = strictnessFlag === "strict" ? `${RED()}strict${RESET()}` : strictnessFlag === "lenient" ? `${GREEN()}lenient${RESET()}` : `${YELLOW()}normal${RESET()}`; if (verbose) { console.log(` ${DIM()}Model: ${config.model} | API: ${config.apiBase} | Strictness: ${strictnessFlag}${RESET()}`); } const tty = isStdinTTY(); if (tty) { console.log(`\n ${BOLD()}${CYAN()}Reviewing ${sourceLabel} (${strictnessLabel})...${RESET()}\n`); } try { const callbacks: StreamCallbacks | undefined = tty ? { onToken: (token) => process.stdout.write(token), } : undefined; const result = await callAI(config, REVIEW_SYSTEM_PROMPT, userPrompt, callbacks); if (callbacks) { process.stdout.write("\n"); } else { process.stdout.write(result + "\n"); } } catch (err) { console.error(`\n ${RED()}AI request failed: ${err instanceof Error ? err.message : err}${RESET()}\n`); return 1; } return 0; }