diff --git a/src/commands/commit.ts b/src/commands/commit.ts index d8ff3e6..9cc0575 100644 --- a/src/commands/commit.ts +++ b/src/commands/commit.ts @@ -1,4 +1,3 @@ -import * as readline from "node:readline"; import { isGitRepo, getRepoRoot, @@ -18,20 +17,11 @@ import { generateCommitMessage } from "../ai"; import { copyToClipboard } from "../clipboard"; import { BOLD, GREEN, YELLOW, CYAN, RED, DIM, RESET } from "../terminal"; import { isStdinTTY } from "../tty"; +import { ask, editLine } from "../tty-input"; import type { Config, CommitResult, StreamCallbacks } from "../types"; import { loadConfig } from "../config"; import type { ParsedArgs } from "../cli"; -function ask(question: string): Promise { - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - return new Promise((resolve) => { - rl.question(question, (answer) => { - rl.close(); - resolve(answer.trim()); - }); - }); -} - function printCommitResult(result: CommitResult, msg: string) { console.log(`\n ${GREEN()}${BOLD()}✔ Committed successfully!${RESET()}`); const id = result.branch && result.hash @@ -75,99 +65,8 @@ async function confirmCommit(message: string): Promise<"y" | "n" | "e"> { } async function editMessage(current: string): Promise { - if (!isStdinTTY()) return null; process.stdout.write(` ${DIM()}Edit message (Enter to confirm, Esc to abort):${RESET()}\n`); - - const savedRaw = process.stdin.isRaw; - process.stdin.setRawMode(true); - process.stdin.resume(); - - let buffer = current; - let cursor = current.length; - - function render() { - process.stdout.write("\x1b[2K\r > " + buffer); - if (cursor < buffer.length) { - process.stdout.write(`\x1b[${buffer.length - cursor}D`); - } - } - - process.stdout.write(" > "); - process.stdout.write(buffer); - - return new Promise((resolve) => { - let escapeBuf = ""; - - process.stdin.on("data", (data: Buffer) => { - const key = data.toString(); - - if (key === "\x03") { - process.stdin.setRawMode(savedRaw === true); - process.stdin.pause(); - process.stdin.removeAllListeners("data"); - process.stdout.write("\n"); - resolve(null); - return; - } - - if (key === "\x1b" || key.startsWith("\x1b[")) { - escapeBuf = key; - if (key.length >= 3) { handleSeq(key); escapeBuf = ""; } - return; - } - - if (escapeBuf) { - escapeBuf += key; - if (/^[A-Za-z~]$/.test(key)) { handleSeq(escapeBuf); escapeBuf = ""; } - else if (escapeBuf.length > 8) escapeBuf = ""; - return; - } - - if (key === "\r") { - process.stdin.setRawMode(savedRaw === true); - process.stdin.pause(); - process.stdin.removeAllListeners("data"); - process.stdout.write("\n"); - const result = buffer.trim(); - resolve(result || null); - return; - } - - if (key === "\x7f") { - if (cursor > 0) { - buffer = buffer.slice(0, cursor - 1) + buffer.slice(cursor); - cursor--; - render(); - } - return; - } - - if (key === "\x01") { if (cursor > 0) { process.stdout.write(`\x1b[${cursor}D`); cursor = 0; } return; } - if (key === "\x05") { if (cursor < buffer.length) { process.stdout.write(`\x1b[${buffer.length - cursor}C`); cursor = buffer.length; } return; } - if (key === "\x0b") { buffer = buffer.slice(0, cursor); render(); return; } - if (key === "\x15") { buffer = buffer.slice(cursor); cursor = 0; render(); return; } - - if (key.charCodeAt(0) >= 32 && key.charCodeAt(0) < 127) { - buffer = buffer.slice(0, cursor) + key + buffer.slice(cursor); - cursor++; - render(); - } - }); - - function handleSeq(seq: string) { - if (seq === "\x1b[D" || seq === "\x1bOD") { - if (cursor > 0) { cursor--; process.stdout.write("\x1b[D"); } - } else if (seq === "\x1b[C" || seq === "\x1bOC") { - if (cursor < buffer.length) { cursor++; process.stdout.write("\x1b[C"); } - } else if (seq === "\x1b[H" || seq === "\x1b[1~" || seq === "\x1bOH") { - if (cursor > 0) { process.stdout.write(`\x1b[${cursor}D`); cursor = 0; } - } else if (seq === "\x1b[F" || seq === "\x1b[4~" || seq === "\x1bOF") { - if (cursor < buffer.length) { process.stdout.write(`\x1b[${buffer.length - cursor}C`); cursor = buffer.length; } - } else if (seq === "\x1b[3~") { - if (cursor < buffer.length) { buffer = buffer.slice(0, cursor) + buffer.slice(cursor + 1); render(); } - } - } - }); + return editLine(current); } export async function handleCommit(args: ParsedArgs): Promise { diff --git a/src/commands/config.ts b/src/commands/config.ts index b692fab..91d24ff 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,7 +1,8 @@ import { loadConfig, saveConfig } from "../config"; -import { BOLD, GREEN, YELLOW, CYAN, RED, DIM, RESET } from "../terminal"; +import { BOLD, GREEN, YELLOW, CYAN, RED, DIM, RESET, hideCursor, showCursor, clearLine, moveUp, visibleLength } from "../terminal"; import { isStdinTTY } from "../tty"; import { SKIP_WAIT } from "../menu"; +import { editLine } from "../tty-input"; import type { Config } from "../types"; import type { ParsedArgs } from "../cli"; @@ -66,18 +67,6 @@ const CONFIG_FIELDS: ConfigField[] = [ }, ]; -function visibleLength(value: string) { - return value.replace(/\x1b\[[0-9;]*m/g, "").length; -} - -function clearLine() { - process.stdout.write("\r\x1b[2K"); -} - -function moveUp(lines: number) { - if (lines > 0) process.stdout.write(`\x1b[${lines}A`); -} - function renderConfigPage( config: Config, cursor: number, @@ -151,29 +140,29 @@ async function interactiveConfig(): Promise<"done" | "back"> { if (wasRaw !== true) process.stdin.setRawMode(true); process.stdin.resume(); - process.stdout.write("\x1b[?25l"); + hideCursor(); const render = () => { moveUp(renderedCursorRow); renderedLines = renderConfigPage(config, cursor, renderedLines, status, editState); renderedCursorRow = editState ? 4 + cursor : 0; - process.stdout.write(editState ? "\x1b[?25h" : "\x1b[?25l"); + editState ? showCursor() : hideCursor(); }; render(); - return new Promise((resolve) => { + return new Promise((resolve, reject) => { const finish = (value: "done" | "back") => { + process.stdin.removeListener("data", onData); process.stdin.setRawMode(wasRaw === true); process.stdin.pause(); - process.stdin.removeListener("data", onData); moveUp(renderedCursorRow); for (let i = 0; i < renderedLines; i++) { clearLine(); process.stdout.write("\n"); } moveUp(renderedLines); - process.stdout.write("\x1b[?25h"); + showCursor(); resolve(value); }; diff --git a/src/commands/explain.ts b/src/commands/explain.ts index 15eb9ac..88ac103 100644 --- a/src/commands/explain.ts +++ b/src/commands/explain.ts @@ -1,15 +1,6 @@ import { loadConfig } from "../config"; -import { - isGitRepo, - getStagedFiles, - getStagedDiff, - getUnstagedFiles, - getRepoRoot, - applyFileSelection, -} from "../git"; -import { selectFiles } from "../selector"; +import { collectDiff } from "../diff-source"; import { BACK, SKIP_WAIT } from "../menu"; -import { collectProjectContext } from "../context"; import { EXPLAIN_SYSTEM_PROMPT, buildExplainPrompt } from "../prompt"; import { callAI } from "../ai"; import { BOLD, GREEN, RED, DIM, RESET, CYAN } from "../terminal"; @@ -18,112 +9,66 @@ import type { StreamCallbacks } from "../types"; import type { ParsedArgs } from "../cli"; export async function handleExplain(args: ParsedArgs): Promise { - const config = await loadConfig(); + 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; - } + 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 unstaged = args.flags["unstaged"] as boolean; - const verbose = args.flags["verbose"] as boolean; + const unstaged = args.flags["unstaged"] as boolean; + const verbose = args.flags["verbose"] as boolean; - // Determine which diff to explain - let diff: string; - let sourceLabel: string; + let diff: string; + let sourceLabel: string; + let contextPrefix: 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 { - // Default: staged changes (or piped) - if (isStdinTTY()) { - 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"; + try { + const result = await collectDiff({ unstaged, includeProjectContext: true }); + if (result.back) return SKIP_WAIT as unknown as number; + diff = result.diff; + sourceLabel = result.sourceLabel; + contextPrefix = result.contextPrefix; + } catch (err) { + console.error(`\n ${RED()}Error: ${err instanceof Error ? err.message : err}${RESET()}\n`); + return 1; + } - 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(); - } else { - // Read from pipe - 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"; - } - } + if (!diff) { + console.log(` ${DIM()}No ${sourceLabel} to explain.${RESET()}`); + return 0; + } - if (!diff) { - console.log(` ${DIM()}No ${sourceLabel} to explain.${RESET()}`); - return 0; - } + if (verbose) { + console.log(` ${DIM()}Explaining ${sourceLabel} (${diff.length} bytes)${RESET()}`); + } - if (args.flags["verbose"]) { - console.log(` ${DIM()}Explaining ${sourceLabel} (${diff.length} bytes)${RESET()}`); - } + const userPrompt = contextPrefix + buildExplainPrompt(diff); - 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()}`); + } - // Collect project context for better explanations - 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 tty = isStdinTTY(); + if (tty) { + console.log(`\n ${BOLD()}${CYAN()}Analyzing ${sourceLabel}...${RESET()}\n`); + } - const userPrompt = contextPrefix + buildExplainPrompt(truncatedDiff); + try { + const callbacks: StreamCallbacks | undefined = tty ? { + onToken: (token) => process.stdout.write(token), + } : undefined; - if (verbose) { - console.log(` ${DIM()}Model: ${config.model} | API: ${config.apiBase}${RESET()}`); - } + const explanation = await callAI(config, EXPLAIN_SYSTEM_PROMPT, userPrompt, callbacks); + if (callbacks) { + process.stdout.write("\n"); + } else { + process.stdout.write(explanation + "\n"); + } + } catch (err) { + console.error(`\n ${RED()}AI request failed: ${err instanceof Error ? err.message : err}${RESET()}\n`); + return 1; + } - const tty = isStdinTTY(); - if (tty) { - console.log(`\n ${BOLD()}${CYAN()}Analyzing ${sourceLabel}...${RESET()}\n`); - } - - try { - const callbacks: StreamCallbacks | undefined = tty ? { - onToken: (token) => process.stdout.write(token), - } : undefined; - - const explanation = await callAI(config, EXPLAIN_SYSTEM_PROMPT, userPrompt, callbacks); - if (callbacks) { - process.stdout.write("\n"); - } else { - // Non-TTY: print the result directly - process.stdout.write(explanation + "\n"); - } - } catch (err) { - console.error(`\n ${RED()}AI request failed: ${err instanceof Error ? err.message : err}${RESET()}\n`); - return 1; - } - - return 0; + return 0; } diff --git a/src/commands/pr.ts b/src/commands/pr.ts index 304ac83..af17075 100644 --- a/src/commands/pr.ts +++ b/src/commands/pr.ts @@ -1,4 +1,3 @@ -import * as readline from "node:readline"; import { loadConfig } from "../config"; import { isGitRepo, getRepoRoot } from "../git"; import { collectProjectContext } from "../context"; @@ -19,19 +18,10 @@ import { import type { Platform } from "../pr"; import { BOLD, GREEN, YELLOW, CYAN, RED, DIM, RESET } from "../terminal"; import { isStdinTTY } from "../tty"; +import { ask } from "../tty-input"; import { copyToClipboard } from "../clipboard"; import type { ParsedArgs } from "../cli"; -function ask(question: string): Promise { - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - return new Promise((resolve) => { - rl.question(question, (answer) => { - rl.close(); - resolve(answer.trim()); - }); - }); -} - async function selectPlatform(hostname: string): Promise { if (!isStdinTTY()) { console.error(`\n ${RED()}Error: Platform selection requires a TTY.${RESET()}\n`); diff --git a/src/commands/review.ts b/src/commands/review.ts index 754459e..aa8a02a 100644 --- a/src/commands/review.ts +++ b/src/commands/review.ts @@ -1,15 +1,6 @@ import { loadConfig } from "../config"; -import { - isGitRepo, - getStagedFiles, - getStagedDiff, - getUnstagedFiles, - getRepoRoot, - applyFileSelection, -} from "../git"; -import { selectFiles } from "../selector"; +import { collectDiff } from "../diff-source"; 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"; @@ -18,113 +9,74 @@ import type { StreamCallbacks } from "../types"; import type { ParsedArgs } from "../cli"; export async function handleReview(args: ParsedArgs): Promise { - const config = await loadConfig(); + 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; - } + 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 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; + const unstaged = args.flags["unstaged"] as boolean; + const verbose = args.flags["verbose"] as boolean; - let diff: string; - let sourceLabel: string; + let diff: string; + let sourceLabel: string; + let contextPrefix: 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"; + try { + const result = await collectDiff({ unstaged, includeProjectContext: true }); + if (result.back) return SKIP_WAIT as unknown as number; + diff = result.diff; + sourceLabel = result.sourceLabel; + contextPrefix = result.contextPrefix; + } catch (err) { + console.error(`\n ${RED()}Error: ${err instanceof Error ? err.message : err}${RESET()}\n`); + return 1; + } - 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; + } - if (!diff) { - console.log(` ${DIM()}No ${sourceLabel} to review.${RESET()}`); - return 0; - } + const userPrompt = contextPrefix + buildReviewPrompt(diff, strictnessFlag); - const MAX_DIFF_SIZE = 15000; - const truncatedDiff = diff.length > MAX_DIFF_SIZE - ? diff.substring(0, MAX_DIFF_SIZE) + "\n... (truncated)" - : diff; + const strictnessLabel = strictnessFlag === "strict" + ? `${RED()}strict${RESET()}` + : strictnessFlag === "lenient" + ? `${GREEN()}lenient${RESET()}` + : `${YELLOW()}normal${RESET()}`; - 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 {} + if (verbose) { + console.log(` ${DIM()}Model: ${config.model} | API: ${config.apiBase} | Strictness: ${strictnessFlag}${RESET()}`); + } - const userPrompt = contextPrefix + buildReviewPrompt(truncatedDiff, strictnessFlag); + const tty = isStdinTTY(); + if (tty) { + console.log(`\n ${BOLD()}${CYAN()}Reviewing ${sourceLabel} (${strictnessLabel})...${RESET()}\n`); + } - const strictnessLabel = strictnessFlag === "strict" - ? `${RED()}strict${RESET()}` - : strictnessFlag === "lenient" - ? `${GREEN()}lenient${RESET()}` - : `${YELLOW()}normal${RESET()}`; + try { + const callbacks: StreamCallbacks | undefined = tty ? { + onToken: (token) => process.stdout.write(token), + } : undefined; - if (verbose) { - console.log(` ${DIM()}Model: ${config.model} | API: ${config.apiBase} | Strictness: ${strictnessFlag}${RESET()}`); - } + 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; + } - 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; + return 0; } diff --git a/src/commands/suggest.ts b/src/commands/suggest.ts index 676051c..35059bc 100644 --- a/src/commands/suggest.ts +++ b/src/commands/suggest.ts @@ -1,17 +1,10 @@ import { loadConfig } from "../config"; -import { - isGitRepo, - getStagedFiles, - getStagedDiff, - getUnstagedFiles, - applyFileSelection, -} from "../git"; -import { selectFiles } from "../selector"; +import { collectDiff } from "../diff-source"; import { BACK, SKIP_WAIT } from "../menu"; import { - SUGGEST_SYSTEM_PROMPT, - buildSuggestBranchPrompt, - buildSuggestTypePrompt, + SUGGEST_SYSTEM_PROMPT, + buildSuggestBranchPrompt, + buildSuggestTypePrompt, } from "../prompt"; import { callAI } from "../ai"; import { BOLD, GREEN, YELLOW, RED, DIM, RESET, CYAN } from "../terminal"; @@ -20,127 +13,101 @@ import type { Config } from "../types"; import type { ParsedArgs } from "../cli"; export async function handleSuggest(args: ParsedArgs): Promise { - const config = await loadConfig(); + 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; - } + 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; + 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; - } + 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; - } + const unstaged = args.flags["unstaged"] as boolean; - if (args.flags["unstaged"] as boolean) { - try { - diff = (await Bun.$`git diff`.quiet().text()).trim(); - } catch { - diff = ""; - } - } else { - const stagedFiles = await getStagedFiles(); - const unstagedFiles = await getUnstagedFiles(); + let diff: string; - 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(); - } - } + try { + const result = await collectDiff({ unstaged, includeProjectContext: false }); + if (result.back) return SKIP_WAIT as unknown as number; + diff = result.diff; + } catch (err) { + console.error(`\n ${RED()}Error: ${err instanceof Error ? err.message : err}${RESET()}\n`); + return 1; + } - if (!diff) { - console.log(` ${DIM()}No changes to suggest from.${RESET()}`); - return 0; - } + 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 (verbose) { - console.log(` ${DIM()}Model: ${config.model} | API: ${config.apiBase}${RESET()}`); - } - - if (mode === "branch") { - return handleSuggestBranch(config, truncatedDiff); - } else { - return handleSuggestType(config, truncatedDiff); - } + if (mode === "branch") { + return handleSuggestBranch(config, diff); + } + return handleSuggestType(config, diff); } async function handleSuggestBranch(config: Config, diff: string): Promise { - const tty = isStdinTTY(); - if (tty) { - console.log(`\n ${BOLD()}${CYAN()}Suggesting branch names...${RESET()}\n`); - } + 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); + 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; - } + 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; - } + 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; + 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`); - } + 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"]; + 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; - } + 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; + return 0; }