gut-cli 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command11 } from "commander";
4
+ import { Command as Command10 } from "commander";
5
5
 
6
6
  // src/commands/cleanup.ts
7
7
  import { Command } from "commander";
@@ -402,40 +402,6 @@ var CodeReviewSchema = z.object({
402
402
  ),
403
403
  positives: z.array(z.string()).describe("Good practices observed")
404
404
  });
405
- var DiffSummarySchema = z.object({
406
- summary: z.string().describe("Brief one-line summary of what changed"),
407
- changes: z.array(
408
- z.object({
409
- file: z.string(),
410
- description: z.string().describe("What changed in this file")
411
- })
412
- ),
413
- impact: z.string().describe("What impact these changes have on the codebase"),
414
- notes: z.array(z.string()).optional().describe("Any important notes or considerations")
415
- });
416
- async function generateDiffSummary(diff, options) {
417
- const model = await getModel(options);
418
- const result = await generateObject({
419
- model,
420
- schema: DiffSummarySchema,
421
- prompt: `You are an expert at explaining code changes in a clear and concise way.
422
-
423
- Analyze the following git diff and provide a human-friendly summary.
424
-
425
- Focus on:
426
- - What was changed and why it might have been changed
427
- - The purpose and impact of the changes
428
- - Any notable patterns or refactoring
429
-
430
- Git diff:
431
- \`\`\`
432
- ${diff.slice(0, 1e4)}
433
- \`\`\`
434
-
435
- Explain the changes in plain language that any developer can understand.`
436
- });
437
- return result.object;
438
- }
439
405
  async function generateCodeReview(diff, options) {
440
406
  const model = await getModel(options);
441
407
  const result = await generateObject({
@@ -563,6 +529,7 @@ Explain in a way that helps someone quickly understand this file's purpose and h
563
529
  return result2.object;
564
530
  }
565
531
  let contextInfo;
532
+ let targetType;
566
533
  if (context.type === "pr") {
567
534
  contextInfo = `
568
535
  Pull Request: #${context.metadata.prNumber}
@@ -571,6 +538,7 @@ Branch: ${context.metadata.headBranch} -> ${context.metadata.baseBranch}
571
538
  Commits:
572
539
  ${context.metadata.commits?.map((c) => `- ${c}`).join("\n") || "N/A"}
573
540
  `;
541
+ targetType = "pull request";
574
542
  } else if (context.type === "file-history") {
575
543
  contextInfo = `
576
544
  File: ${context.metadata.filePath}
@@ -579,6 +547,12 @@ ${context.metadata.commits?.map((c) => `- ${c}`).join("\n") || "N/A"}
579
547
  Latest author: ${context.metadata.author}
580
548
  Latest date: ${context.metadata.date}
581
549
  `;
550
+ targetType = "file changes";
551
+ } else if (context.type === "uncommitted" || context.type === "staged") {
552
+ contextInfo = `
553
+ ${context.type === "staged" ? "Staged changes (ready to commit)" : "Uncommitted changes (work in progress)"}
554
+ `;
555
+ targetType = context.type === "staged" ? "staged changes" : "uncommitted changes";
582
556
  } else {
583
557
  contextInfo = `
584
558
  Commit: ${context.metadata.hash?.slice(0, 7)}
@@ -586,8 +560,8 @@ Message: ${context.title}
586
560
  Author: ${context.metadata.author}
587
561
  Date: ${context.metadata.date}
588
562
  `;
563
+ targetType = "commit";
589
564
  }
590
- const targetType = context.type === "pr" ? "pull request" : context.type === "file-history" ? "file changes" : "commit";
591
565
  const result = await generateObject({
592
566
  model,
593
567
  schema: ExplanationSchema,
@@ -1092,85 +1066,11 @@ function printReview(review) {
1092
1066
  console.log();
1093
1067
  }
1094
1068
 
1095
- // src/commands/ai-diff.ts
1069
+ // src/commands/ai-merge.ts
1096
1070
  import { Command as Command6 } from "commander";
1097
1071
  import chalk6 from "chalk";
1098
1072
  import ora5 from "ora";
1099
1073
  import { simpleGit as simpleGit5 } from "simple-git";
1100
- var aiDiffCommand = new Command6("ai-diff").alias("diff").description("Get an AI-powered explanation of your changes").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-s, --staged", "Explain only staged changes").option("-c, --commit <hash>", "Explain a specific commit").option("--json", "Output as JSON").action(async (options) => {
1101
- const git = simpleGit5();
1102
- const isRepo = await git.checkIsRepo();
1103
- if (!isRepo) {
1104
- console.error(chalk6.red("Error: Not a git repository"));
1105
- process.exit(1);
1106
- }
1107
- const provider = options.provider.toLowerCase();
1108
- const spinner = ora5("Getting diff...").start();
1109
- try {
1110
- let diff;
1111
- if (options.commit) {
1112
- diff = await git.diff([`${options.commit}^`, options.commit]);
1113
- spinner.text = `Analyzing commit ${options.commit.slice(0, 7)}...`;
1114
- } else if (options.staged) {
1115
- diff = await git.diff(["--cached"]);
1116
- spinner.text = "Analyzing staged changes...";
1117
- } else {
1118
- diff = await git.diff();
1119
- const stagedDiff = await git.diff(["--cached"]);
1120
- diff = stagedDiff + "\n" + diff;
1121
- spinner.text = "Analyzing uncommitted changes...";
1122
- }
1123
- if (!diff.trim()) {
1124
- spinner.info("No changes to analyze");
1125
- process.exit(0);
1126
- }
1127
- spinner.text = "AI is analyzing your changes...";
1128
- const summary = await generateDiffSummary(diff, {
1129
- provider,
1130
- model: options.model
1131
- });
1132
- spinner.stop();
1133
- if (options.json) {
1134
- console.log(JSON.stringify(summary, null, 2));
1135
- return;
1136
- }
1137
- printSummary(summary);
1138
- } catch (error) {
1139
- spinner.fail("Failed to analyze diff");
1140
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1141
- process.exit(1);
1142
- }
1143
- });
1144
- function printSummary(summary) {
1145
- console.log(chalk6.bold("\n\u{1F4DD} Change Summary\n"));
1146
- console.log(chalk6.cyan("Overview:"));
1147
- console.log(` ${summary.summary}
1148
- `);
1149
- if (summary.changes.length > 0) {
1150
- console.log(chalk6.cyan("Changes:"));
1151
- for (const change of summary.changes) {
1152
- console.log(` ${chalk6.yellow(change.file)}`);
1153
- console.log(` ${chalk6.gray(change.description)}`);
1154
- }
1155
- console.log();
1156
- }
1157
- console.log(chalk6.cyan("Impact:"));
1158
- console.log(` ${summary.impact}
1159
- `);
1160
- if (summary.notes && summary.notes.length > 0) {
1161
- console.log(chalk6.cyan("Notes:"));
1162
- for (const note of summary.notes) {
1163
- console.log(` ${chalk6.gray("\u2022")} ${note}`);
1164
- }
1165
- console.log();
1166
- }
1167
- }
1168
-
1169
- // src/commands/ai-merge.ts
1170
- import { Command as Command7 } from "commander";
1171
- import chalk7 from "chalk";
1172
- import ora6 from "ora";
1173
- import { simpleGit as simpleGit6 } from "simple-git";
1174
1074
  import * as fs from "fs";
1175
1075
  import * as path from "path";
1176
1076
  var MERGE_STRATEGY_PATHS = [
@@ -1186,57 +1086,57 @@ function findMergeStrategy(repoRoot) {
1186
1086
  }
1187
1087
  return null;
1188
1088
  }
1189
- var aiMergeCommand = new Command7("ai-merge").alias("merge").description("Merge a branch with AI-powered conflict resolution").argument("<branch>", "Branch to merge").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("--no-commit", "Do not auto-commit after resolving").action(async (branch, options) => {
1190
- const git = simpleGit6();
1089
+ var aiMergeCommand = new Command6("ai-merge").alias("merge").description("Merge a branch with AI-powered conflict resolution").argument("<branch>", "Branch to merge").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("--no-commit", "Do not auto-commit after resolving").action(async (branch, options) => {
1090
+ const git = simpleGit5();
1191
1091
  const isRepo = await git.checkIsRepo();
1192
1092
  if (!isRepo) {
1193
- console.error(chalk7.red("Error: Not a git repository"));
1093
+ console.error(chalk6.red("Error: Not a git repository"));
1194
1094
  process.exit(1);
1195
1095
  }
1196
1096
  const provider = options.provider.toLowerCase();
1197
1097
  const status = await git.status();
1198
1098
  if (status.modified.length > 0 || status.staged.length > 0) {
1199
- console.error(chalk7.red("Error: Working directory has uncommitted changes"));
1200
- console.log(chalk7.gray("Please commit or stash your changes first"));
1099
+ console.error(chalk6.red("Error: Working directory has uncommitted changes"));
1100
+ console.log(chalk6.gray("Please commit or stash your changes first"));
1201
1101
  process.exit(1);
1202
1102
  }
1203
1103
  const branchInfo = await git.branch();
1204
1104
  const currentBranch = branchInfo.current;
1205
- console.log(chalk7.bold(`
1206
- Merging ${chalk7.cyan(branch)} into ${chalk7.cyan(currentBranch)}...
1105
+ console.log(chalk6.bold(`
1106
+ Merging ${chalk6.cyan(branch)} into ${chalk6.cyan(currentBranch)}...
1207
1107
  `));
1208
1108
  try {
1209
1109
  await git.merge([branch]);
1210
- console.log(chalk7.green("\u2713 Merged successfully (no conflicts)"));
1110
+ console.log(chalk6.green("\u2713 Merged successfully (no conflicts)"));
1211
1111
  return;
1212
1112
  } catch (error) {
1213
1113
  }
1214
1114
  const conflictStatus = await git.status();
1215
1115
  const conflictedFiles = conflictStatus.conflicted;
1216
1116
  if (conflictedFiles.length === 0) {
1217
- console.error(chalk7.red("Merge failed for unknown reason"));
1117
+ console.error(chalk6.red("Merge failed for unknown reason"));
1218
1118
  await git.merge(["--abort"]);
1219
1119
  process.exit(1);
1220
1120
  }
1221
- console.log(chalk7.yellow(`\u26A0 ${conflictedFiles.length} conflict(s) detected
1121
+ console.log(chalk6.yellow(`\u26A0 ${conflictedFiles.length} conflict(s) detected
1222
1122
  `));
1223
- const spinner = ora6();
1123
+ const spinner = ora5();
1224
1124
  const rootDir = await git.revparse(["--show-toplevel"]);
1225
1125
  const strategy = findMergeStrategy(rootDir.trim());
1226
1126
  if (strategy) {
1227
- console.log(chalk7.gray("Using merge strategy from project...\n"));
1127
+ console.log(chalk6.gray("Using merge strategy from project...\n"));
1228
1128
  }
1229
1129
  for (const file of conflictedFiles) {
1230
1130
  const filePath = path.join(rootDir.trim(), file);
1231
1131
  const content = fs.readFileSync(filePath, "utf-8");
1232
- console.log(chalk7.bold(`
1132
+ console.log(chalk6.bold(`
1233
1133
  \u{1F4C4} ${file}`));
1234
1134
  const conflictMatch = content.match(/<<<<<<< HEAD[\s\S]*?>>>>>>>.+/g);
1235
1135
  if (conflictMatch) {
1236
- console.log(chalk7.gray("\u2500".repeat(50)));
1237
- console.log(chalk7.gray(conflictMatch[0].slice(0, 500)));
1238
- if (conflictMatch[0].length > 500) console.log(chalk7.gray("..."));
1239
- console.log(chalk7.gray("\u2500".repeat(50)));
1136
+ console.log(chalk6.gray("\u2500".repeat(50)));
1137
+ console.log(chalk6.gray(conflictMatch[0].slice(0, 500)));
1138
+ if (conflictMatch[0].length > 500) console.log(chalk6.gray("..."));
1139
+ console.log(chalk6.gray("\u2500".repeat(50)));
1240
1140
  }
1241
1141
  spinner.start("AI is analyzing conflict...");
1242
1142
  try {
@@ -1246,57 +1146,57 @@ Merging ${chalk7.cyan(branch)} into ${chalk7.cyan(currentBranch)}...
1246
1146
  theirsRef: branch
1247
1147
  }, { provider, model: options.model }, strategy || void 0);
1248
1148
  spinner.stop();
1249
- console.log(chalk7.cyan("\n\u{1F916} AI suggests:"));
1250
- console.log(chalk7.gray("\u2500".repeat(50)));
1149
+ console.log(chalk6.cyan("\n\u{1F916} AI suggests:"));
1150
+ console.log(chalk6.gray("\u2500".repeat(50)));
1251
1151
  const preview = resolution.resolvedContent.slice(0, 800);
1252
1152
  console.log(preview);
1253
- if (resolution.resolvedContent.length > 800) console.log(chalk7.gray("..."));
1254
- console.log(chalk7.gray("\u2500".repeat(50)));
1255
- console.log(chalk7.gray(`Strategy: ${resolution.strategy}`));
1256
- console.log(chalk7.gray(`Reason: ${resolution.explanation}`));
1153
+ if (resolution.resolvedContent.length > 800) console.log(chalk6.gray("..."));
1154
+ console.log(chalk6.gray("\u2500".repeat(50)));
1155
+ console.log(chalk6.gray(`Strategy: ${resolution.strategy}`));
1156
+ console.log(chalk6.gray(`Reason: ${resolution.explanation}`));
1257
1157
  const readline = await import("readline");
1258
1158
  const rl = readline.createInterface({
1259
1159
  input: process.stdin,
1260
1160
  output: process.stdout
1261
1161
  });
1262
1162
  const answer = await new Promise((resolve) => {
1263
- rl.question(chalk7.cyan("\nAccept this resolution? (y/n/s to skip) "), resolve);
1163
+ rl.question(chalk6.cyan("\nAccept this resolution? (y/n/s to skip) "), resolve);
1264
1164
  });
1265
1165
  rl.close();
1266
1166
  if (answer.toLowerCase() === "y") {
1267
1167
  fs.writeFileSync(filePath, resolution.resolvedContent);
1268
1168
  await git.add(file);
1269
- console.log(chalk7.green(`\u2713 Resolved ${file}`));
1169
+ console.log(chalk6.green(`\u2713 Resolved ${file}`));
1270
1170
  } else if (answer.toLowerCase() === "s") {
1271
- console.log(chalk7.yellow(`\u23ED Skipped ${file}`));
1171
+ console.log(chalk6.yellow(`\u23ED Skipped ${file}`));
1272
1172
  } else {
1273
- console.log(chalk7.yellow(`\u2717 Rejected - resolve manually: ${file}`));
1173
+ console.log(chalk6.yellow(`\u2717 Rejected - resolve manually: ${file}`));
1274
1174
  }
1275
1175
  } catch (error) {
1276
1176
  spinner.fail("AI resolution failed");
1277
- console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1278
- console.log(chalk7.yellow(`Please resolve manually: ${file}`));
1177
+ console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1178
+ console.log(chalk6.yellow(`Please resolve manually: ${file}`));
1279
1179
  }
1280
1180
  }
1281
1181
  const finalStatus = await git.status();
1282
1182
  if (finalStatus.conflicted.length > 0) {
1283
- console.log(chalk7.yellow(`
1183
+ console.log(chalk6.yellow(`
1284
1184
  \u26A0 ${finalStatus.conflicted.length} conflict(s) remaining`));
1285
- console.log(chalk7.gray("Resolve manually and run: git add <files> && git commit"));
1185
+ console.log(chalk6.gray("Resolve manually and run: git add <files> && git commit"));
1286
1186
  } else if (options.commit !== false) {
1287
1187
  await git.commit(`Merge branch '${branch}' into ${currentBranch}`);
1288
- console.log(chalk7.green("\n\u2713 All conflicts resolved and committed"));
1188
+ console.log(chalk6.green("\n\u2713 All conflicts resolved and committed"));
1289
1189
  } else {
1290
- console.log(chalk7.green("\n\u2713 All conflicts resolved"));
1291
- console.log(chalk7.gray("Run: git commit"));
1190
+ console.log(chalk6.green("\n\u2713 All conflicts resolved"));
1191
+ console.log(chalk6.gray("Run: git commit"));
1292
1192
  }
1293
1193
  });
1294
1194
 
1295
1195
  // src/commands/changelog.ts
1296
- import { Command as Command8 } from "commander";
1297
- import chalk8 from "chalk";
1298
- import ora7 from "ora";
1299
- import { simpleGit as simpleGit7 } from "simple-git";
1196
+ import { Command as Command7 } from "commander";
1197
+ import chalk7 from "chalk";
1198
+ import ora6 from "ora";
1199
+ import { simpleGit as simpleGit6 } from "simple-git";
1300
1200
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1301
1201
  import { join as join4 } from "path";
1302
1202
  var CHANGELOG_PATHS = [
@@ -1337,15 +1237,15 @@ function formatChangelog(changelog) {
1337
1237
  }
1338
1238
  return lines.join("\n");
1339
1239
  }
1340
- var changelogCommand = new Command8("changelog").description("Generate a changelog from commits between refs").argument("[from]", "Starting ref (tag, branch, commit)", "HEAD~10").argument("[to]", "Ending ref", "HEAD").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-t, --tag <tag>", "Generate changelog since this tag").option("--json", "Output as JSON").action(async (from, to, options) => {
1341
- const git = simpleGit7();
1240
+ var changelogCommand = new Command7("changelog").description("Generate a changelog from commits between refs").argument("[from]", "Starting ref (tag, branch, commit)", "HEAD~10").argument("[to]", "Ending ref", "HEAD").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-t, --tag <tag>", "Generate changelog since this tag").option("--json", "Output as JSON").action(async (from, to, options) => {
1241
+ const git = simpleGit6();
1342
1242
  const isRepo = await git.checkIsRepo();
1343
1243
  if (!isRepo) {
1344
- console.error(chalk8.red("Error: Not a git repository"));
1244
+ console.error(chalk7.red("Error: Not a git repository"));
1345
1245
  process.exit(1);
1346
1246
  }
1347
1247
  const provider = options.provider.toLowerCase();
1348
- const spinner = ora7("Analyzing commits...").start();
1248
+ const spinner = ora6("Analyzing commits...").start();
1349
1249
  try {
1350
1250
  let fromRef = from;
1351
1251
  let toRef = to;
@@ -1391,27 +1291,27 @@ var changelogCommand = new Command8("changelog").description("Generate a changel
1391
1291
  console.log(JSON.stringify(changelog, null, 2));
1392
1292
  return;
1393
1293
  }
1394
- console.log(chalk8.bold("\n\u{1F4CB} Generated Changelog\n"));
1395
- console.log(chalk8.gray("\u2500".repeat(50)));
1294
+ console.log(chalk7.bold("\n\u{1F4CB} Generated Changelog\n"));
1295
+ console.log(chalk7.gray("\u2500".repeat(50)));
1396
1296
  console.log(formatChangelog(changelog));
1397
- console.log(chalk8.gray("\u2500".repeat(50)));
1398
- console.log(chalk8.gray(`
1297
+ console.log(chalk7.gray("\u2500".repeat(50)));
1298
+ console.log(chalk7.gray(`
1399
1299
  Range: ${fromRef}..${toRef} (${commits.length} commits)`));
1400
1300
  if (existingChangelog) {
1401
- console.log(chalk8.gray("Style matched from existing CHANGELOG.md"));
1301
+ console.log(chalk7.gray("Style matched from existing CHANGELOG.md"));
1402
1302
  }
1403
1303
  } catch (error) {
1404
1304
  spinner.fail("Failed to generate changelog");
1405
- console.error(chalk8.red(error instanceof Error ? error.message : "Unknown error"));
1305
+ console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
1406
1306
  process.exit(1);
1407
1307
  }
1408
1308
  });
1409
1309
 
1410
1310
  // src/commands/ai-explain.ts
1411
- import { Command as Command9 } from "commander";
1412
- import chalk9 from "chalk";
1413
- import ora8 from "ora";
1414
- import { simpleGit as simpleGit8 } from "simple-git";
1311
+ import { Command as Command8 } from "commander";
1312
+ import chalk8 from "chalk";
1313
+ import ora7 from "ora";
1314
+ import { simpleGit as simpleGit7 } from "simple-git";
1415
1315
  import { execSync as execSync2 } from "child_process";
1416
1316
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1417
1317
  import { join as join5 } from "path";
@@ -1427,37 +1327,42 @@ function findExplainContext(repoRoot) {
1427
1327
  }
1428
1328
  return null;
1429
1329
  }
1430
- var aiExplainCommand = new Command9("ai-explain").alias("explain").description("Get an AI-powered explanation of a commit, PR, or file changes").argument("[target]", "Commit hash, PR number, PR URL, or file path").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-n, --commits <n>", "Number of commits to analyze for file history (default: 1)", "1").option("--history", "Explain file change history instead of content").option("--json", "Output as JSON").action(async (target, options) => {
1431
- const git = simpleGit8();
1330
+ var aiExplainCommand = new Command8("ai-explain").alias("explain").description("Get an AI-powered explanation of changes, commits, PRs, or files").argument("[target]", "Commit hash, PR number, PR URL, or file path (default: uncommitted changes)").option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-s, --staged", "Explain only staged changes").option("-n, --commits <n>", "Number of commits to analyze for file history (default: 1)", "1").option("--history", "Explain file change history instead of content").option("--json", "Output as JSON").action(async (target, options) => {
1331
+ const git = simpleGit7();
1432
1332
  const isRepo = await git.checkIsRepo();
1433
1333
  if (!isRepo) {
1434
- console.error(chalk9.red("Error: Not a git repository"));
1334
+ console.error(chalk8.red("Error: Not a git repository"));
1435
1335
  process.exit(1);
1436
1336
  }
1437
1337
  const provider = options.provider.toLowerCase();
1438
1338
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1439
- const spinner = ora8("Analyzing...").start();
1339
+ const spinner = ora7("Analyzing...").start();
1440
1340
  try {
1441
1341
  let context;
1442
- if (!target) {
1443
- target = "HEAD";
1444
- }
1445
- const isPR = target.match(/^#?\d+$/) || target.includes("/pull/");
1446
- const isFile = existsSync5(target);
1447
- if (isPR) {
1448
- context = await getPRContext(target, spinner);
1449
- } else if (isFile) {
1450
- if (options.history) {
1451
- context = await getFileHistoryContext(target, git, spinner, parseInt(options.commits, 10));
1342
+ if (!target || options.staged) {
1343
+ if (options.staged) {
1344
+ context = await getStagedContext(git, spinner);
1452
1345
  } else {
1453
- context = await getFileContentContext(target, spinner);
1346
+ context = await getUncommittedContext(git, spinner);
1454
1347
  }
1455
1348
  } else {
1456
- context = await getCommitContext(target, git, spinner);
1349
+ const isPR = target.match(/^#?\d+$/) || target.includes("/pull/");
1350
+ const isFile = existsSync5(target);
1351
+ if (isPR) {
1352
+ context = await getPRContext(target, spinner);
1353
+ } else if (isFile) {
1354
+ if (options.history) {
1355
+ context = await getFileHistoryContext(target, git, spinner, parseInt(options.commits, 10));
1356
+ } else {
1357
+ context = await getFileContentContext(target, spinner);
1358
+ }
1359
+ } else {
1360
+ context = await getCommitContext(target, git, spinner);
1361
+ }
1457
1362
  }
1458
1363
  const projectContext = findExplainContext(repoRoot.trim());
1459
1364
  if (projectContext) {
1460
- console.log(chalk9.gray("Using project context..."));
1365
+ console.log(chalk8.gray("Using project context..."));
1461
1366
  }
1462
1367
  spinner.text = "AI is generating explanation...";
1463
1368
  const explanation = await generateExplanation(context, {
@@ -1472,10 +1377,38 @@ var aiExplainCommand = new Command9("ai-explain").alias("explain").description("
1472
1377
  printExplanation(explanation, context.type);
1473
1378
  } catch (error) {
1474
1379
  spinner.fail("Failed to generate explanation");
1475
- console.error(chalk9.red(error instanceof Error ? error.message : "Unknown error"));
1380
+ console.error(chalk8.red(error instanceof Error ? error.message : "Unknown error"));
1476
1381
  process.exit(1);
1477
1382
  }
1478
1383
  });
1384
+ async function getUncommittedContext(git, spinner) {
1385
+ spinner.text = "Analyzing uncommitted changes...";
1386
+ const stagedDiff = await git.diff(["--cached"]);
1387
+ const unstagedDiff = await git.diff();
1388
+ const diff = (stagedDiff + "\n" + unstagedDiff).trim();
1389
+ if (!diff) {
1390
+ throw new Error("No uncommitted changes to analyze");
1391
+ }
1392
+ return {
1393
+ type: "uncommitted",
1394
+ title: "Uncommitted changes",
1395
+ diff,
1396
+ metadata: {}
1397
+ };
1398
+ }
1399
+ async function getStagedContext(git, spinner) {
1400
+ spinner.text = "Analyzing staged changes...";
1401
+ const diff = await git.diff(["--cached"]);
1402
+ if (!diff.trim()) {
1403
+ throw new Error("No staged changes to analyze");
1404
+ }
1405
+ return {
1406
+ type: "staged",
1407
+ title: "Staged changes",
1408
+ diff,
1409
+ metadata: {}
1410
+ };
1411
+ }
1479
1412
  async function getCommitContext(hash, git, spinner) {
1480
1413
  spinner.text = `Analyzing commit ${hash.slice(0, 7)}...`;
1481
1414
  const log = await git.log({ from: `${hash}^`, to: hash, maxCount: 1 });
@@ -1586,44 +1519,46 @@ function printExplanation(explanation, type) {
1586
1519
  pr: "\u{1F500}",
1587
1520
  "file-content": "\u{1F4C4}",
1588
1521
  "file-history": "\u{1F4DC}",
1589
- commit: "\u{1F4DD}"
1522
+ commit: "\u{1F4DD}",
1523
+ uncommitted: "\u270F\uFE0F",
1524
+ staged: "\u{1F4CB}"
1590
1525
  };
1591
1526
  const icon = icons[type] || "\u{1F4DD}";
1592
- console.log(chalk9.bold(`
1527
+ console.log(chalk8.bold(`
1593
1528
  ${icon} Explanation
1594
1529
  `));
1595
- console.log(chalk9.cyan("Summary:"));
1530
+ console.log(chalk8.cyan("Summary:"));
1596
1531
  console.log(` ${explanation.summary}
1597
1532
  `);
1598
- console.log(chalk9.cyan("Purpose:"));
1533
+ console.log(chalk8.cyan("Purpose:"));
1599
1534
  console.log(` ${explanation.purpose}
1600
1535
  `);
1601
1536
  if (explanation.changes.length > 0) {
1602
1537
  const header = type === "file-content" ? "Components:" : "Key Changes:";
1603
- console.log(chalk9.cyan(header));
1538
+ console.log(chalk8.cyan(header));
1604
1539
  for (const change of explanation.changes) {
1605
- console.log(` ${chalk9.yellow(change.file)}`);
1606
- console.log(` ${chalk9.gray(change.description)}`);
1540
+ console.log(` ${chalk8.yellow(change.file)}`);
1541
+ console.log(` ${chalk8.gray(change.description)}`);
1607
1542
  }
1608
1543
  console.log();
1609
1544
  }
1610
- console.log(chalk9.cyan("Impact:"));
1545
+ console.log(chalk8.cyan("Impact:"));
1611
1546
  console.log(` ${explanation.impact}
1612
1547
  `);
1613
1548
  if (explanation.notes && explanation.notes.length > 0) {
1614
- console.log(chalk9.cyan("Notes:"));
1549
+ console.log(chalk8.cyan("Notes:"));
1615
1550
  for (const note of explanation.notes) {
1616
- console.log(` ${chalk9.gray("\u2022")} ${note}`);
1551
+ console.log(` ${chalk8.gray("\u2022")} ${note}`);
1617
1552
  }
1618
1553
  console.log();
1619
1554
  }
1620
1555
  }
1621
1556
 
1622
1557
  // src/commands/ai-find.ts
1623
- import { Command as Command10 } from "commander";
1624
- import chalk10 from "chalk";
1625
- import ora9 from "ora";
1626
- import { simpleGit as simpleGit9 } from "simple-git";
1558
+ import { Command as Command9 } from "commander";
1559
+ import chalk9 from "chalk";
1560
+ import ora8 from "ora";
1561
+ import { simpleGit as simpleGit8 } from "simple-git";
1627
1562
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
1628
1563
  import { join as join6 } from "path";
1629
1564
  var CONTEXT_PATHS2 = [".gut/find.md"];
@@ -1636,16 +1571,16 @@ function findProjectContext(repoRoot) {
1636
1571
  }
1637
1572
  return null;
1638
1573
  }
1639
- var aiFindCommand = new Command10("ai-find").alias("find").description("Find commits matching a vague description using AI").argument("<query>", 'Description of the change you are looking for (e.g., "login feature added")').option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-n, --num <n>", "Number of commits to search through", "100").option("--path <path>", "Limit search to commits affecting this path").option("--author <author>", "Limit search to commits by this author").option("--since <date>", "Limit search to commits after this date").option("--until <date>", "Limit search to commits before this date").option("--max-results <n>", "Maximum number of matching commits to return", "5").option("--json", "Output as JSON").action(async (query, options) => {
1640
- const git = simpleGit9();
1574
+ var aiFindCommand = new Command9("ai-find").alias("find").description("Find commits matching a vague description using AI").argument("<query>", 'Description of the change you are looking for (e.g., "login feature added")').option("-p, --provider <provider>", "AI provider (gemini, openai, anthropic)", "gemini").option("-m, --model <model>", "Model to use (provider-specific)").option("-n, --num <n>", "Number of commits to search through", "100").option("--path <path>", "Limit search to commits affecting this path").option("--author <author>", "Limit search to commits by this author").option("--since <date>", "Limit search to commits after this date").option("--until <date>", "Limit search to commits before this date").option("--max-results <n>", "Maximum number of matching commits to return", "5").option("--json", "Output as JSON").action(async (query, options) => {
1575
+ const git = simpleGit8();
1641
1576
  const isRepo = await git.checkIsRepo();
1642
1577
  if (!isRepo) {
1643
- console.error(chalk10.red("Error: Not a git repository"));
1578
+ console.error(chalk9.red("Error: Not a git repository"));
1644
1579
  process.exit(1);
1645
1580
  }
1646
1581
  const provider = options.provider.toLowerCase();
1647
1582
  const repoRoot = await git.revparse(["--show-toplevel"]).catch(() => process.cwd());
1648
- const spinner = ora9("Searching commits...").start();
1583
+ const spinner = ora8("Searching commits...").start();
1649
1584
  try {
1650
1585
  const logOptions = [`-n`, options.num];
1651
1586
  if (options.path) {
@@ -1687,8 +1622,8 @@ var aiFindCommand = new Command10("ai-find").alias("find").description("Find com
1687
1622
  );
1688
1623
  spinner.stop();
1689
1624
  if (results.matches.length === 0) {
1690
- console.log(chalk10.yellow("\nNo matching commits found for your query."));
1691
- console.log(chalk10.gray(`Searched ${commits.length} commits.`));
1625
+ console.log(chalk9.yellow("\nNo matching commits found for your query."));
1626
+ console.log(chalk9.gray(`Searched ${commits.length} commits.`));
1692
1627
  process.exit(0);
1693
1628
  }
1694
1629
  if (options.json) {
@@ -1698,46 +1633,45 @@ var aiFindCommand = new Command10("ai-find").alias("find").description("Find com
1698
1633
  printResults(results, query);
1699
1634
  } catch (error) {
1700
1635
  spinner.fail("Failed to search commits");
1701
- console.error(chalk10.red(error instanceof Error ? error.message : "Unknown error"));
1636
+ console.error(chalk9.red(error instanceof Error ? error.message : "Unknown error"));
1702
1637
  process.exit(1);
1703
1638
  }
1704
1639
  });
1705
1640
  function printResults(results, query) {
1706
- console.log(chalk10.bold(`
1641
+ console.log(chalk9.bold(`
1707
1642
  \u{1F50D} Found ${results.matches.length} matching commit(s)
1708
1643
  `));
1709
- console.log(chalk10.gray(`Query: "${query}"
1644
+ console.log(chalk9.gray(`Query: "${query}"
1710
1645
  `));
1711
1646
  for (let i = 0; i < results.matches.length; i++) {
1712
1647
  const match = results.matches[i];
1713
1648
  const num = i + 1;
1714
- console.log(chalk10.cyan(`\u{1F4DD} Commit ${num}`));
1715
- console.log(` ${chalk10.gray("Hash:")} ${chalk10.yellow(match.hash.slice(0, 7))}`);
1716
- console.log(` ${chalk10.gray("Message:")} ${match.message.split("\n")[0]}`);
1717
- console.log(` ${chalk10.gray("Author:")} ${match.author} <${match.email}>`);
1718
- console.log(` ${chalk10.gray("Date:")} ${match.date}`);
1719
- console.log(` ${chalk10.gray("Reason:")} ${chalk10.green(match.reason)}`);
1649
+ console.log(chalk9.cyan(`\u{1F4DD} Commit ${num}`));
1650
+ console.log(` ${chalk9.gray("Hash:")} ${chalk9.yellow(match.hash.slice(0, 7))}`);
1651
+ console.log(` ${chalk9.gray("Message:")} ${match.message.split("\n")[0]}`);
1652
+ console.log(` ${chalk9.gray("Author:")} ${match.author} <${match.email}>`);
1653
+ console.log(` ${chalk9.gray("Date:")} ${match.date}`);
1654
+ console.log(` ${chalk9.gray("Reason:")} ${chalk9.green(match.reason)}`);
1720
1655
  if (match.relevance) {
1721
- const relevanceColor = match.relevance === "high" ? chalk10.green : match.relevance === "medium" ? chalk10.yellow : chalk10.gray;
1722
- console.log(` ${chalk10.gray("Match:")} ${relevanceColor(match.relevance)}`);
1656
+ const relevanceColor = match.relevance === "high" ? chalk9.green : match.relevance === "medium" ? chalk9.yellow : chalk9.gray;
1657
+ console.log(` ${chalk9.gray("Match:")} ${relevanceColor(match.relevance)}`);
1723
1658
  }
1724
1659
  console.log();
1725
1660
  }
1726
1661
  if (results.summary) {
1727
- console.log(chalk10.gray("---"));
1728
- console.log(chalk10.gray(`Summary: ${results.summary}`));
1662
+ console.log(chalk9.gray("---"));
1663
+ console.log(chalk9.gray(`Summary: ${results.summary}`));
1729
1664
  }
1730
1665
  }
1731
1666
 
1732
1667
  // src/index.ts
1733
- var program = new Command11();
1668
+ var program = new Command10();
1734
1669
  program.name("gut").description("Git Utility Tool - AI-powered git commands").version("0.1.0");
1735
1670
  program.addCommand(cleanupCommand);
1736
1671
  program.addCommand(authCommand);
1737
1672
  program.addCommand(aiCommitCommand);
1738
1673
  program.addCommand(aiPrCommand);
1739
1674
  program.addCommand(aiReviewCommand);
1740
- program.addCommand(aiDiffCommand);
1741
1675
  program.addCommand(aiMergeCommand);
1742
1676
  program.addCommand(changelogCommand);
1743
1677
  program.addCommand(aiExplainCommand);