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/README.md +16 -29
- package/dist/index.js +150 -216
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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-
|
|
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
|
|
1190
|
-
const git =
|
|
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(
|
|
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(
|
|
1200
|
-
console.log(
|
|
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(
|
|
1206
|
-
Merging ${
|
|
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(
|
|
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(
|
|
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(
|
|
1121
|
+
console.log(chalk6.yellow(`\u26A0 ${conflictedFiles.length} conflict(s) detected
|
|
1222
1122
|
`));
|
|
1223
|
-
const spinner =
|
|
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(
|
|
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(
|
|
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(
|
|
1237
|
-
console.log(
|
|
1238
|
-
if (conflictMatch[0].length > 500) console.log(
|
|
1239
|
-
console.log(
|
|
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(
|
|
1250
|
-
console.log(
|
|
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(
|
|
1254
|
-
console.log(
|
|
1255
|
-
console.log(
|
|
1256
|
-
console.log(
|
|
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(
|
|
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(
|
|
1169
|
+
console.log(chalk6.green(`\u2713 Resolved ${file}`));
|
|
1270
1170
|
} else if (answer.toLowerCase() === "s") {
|
|
1271
|
-
console.log(
|
|
1171
|
+
console.log(chalk6.yellow(`\u23ED Skipped ${file}`));
|
|
1272
1172
|
} else {
|
|
1273
|
-
console.log(
|
|
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(
|
|
1278
|
-
console.log(
|
|
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(
|
|
1183
|
+
console.log(chalk6.yellow(`
|
|
1284
1184
|
\u26A0 ${finalStatus.conflicted.length} conflict(s) remaining`));
|
|
1285
|
-
console.log(
|
|
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(
|
|
1188
|
+
console.log(chalk6.green("\n\u2713 All conflicts resolved and committed"));
|
|
1289
1189
|
} else {
|
|
1290
|
-
console.log(
|
|
1291
|
-
console.log(
|
|
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
|
|
1297
|
-
import
|
|
1298
|
-
import
|
|
1299
|
-
import { simpleGit as
|
|
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
|
|
1341
|
-
const git =
|
|
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(
|
|
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 =
|
|
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(
|
|
1395
|
-
console.log(
|
|
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(
|
|
1398
|
-
console.log(
|
|
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(
|
|
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(
|
|
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
|
|
1412
|
-
import
|
|
1413
|
-
import
|
|
1414
|
-
import { simpleGit as
|
|
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
|
|
1431
|
-
const git =
|
|
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(
|
|
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 =
|
|
1339
|
+
const spinner = ora7("Analyzing...").start();
|
|
1440
1340
|
try {
|
|
1441
1341
|
let context;
|
|
1442
|
-
if (!target) {
|
|
1443
|
-
|
|
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
|
|
1346
|
+
context = await getUncommittedContext(git, spinner);
|
|
1454
1347
|
}
|
|
1455
1348
|
} else {
|
|
1456
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
1527
|
+
console.log(chalk8.bold(`
|
|
1593
1528
|
${icon} Explanation
|
|
1594
1529
|
`));
|
|
1595
|
-
console.log(
|
|
1530
|
+
console.log(chalk8.cyan("Summary:"));
|
|
1596
1531
|
console.log(` ${explanation.summary}
|
|
1597
1532
|
`);
|
|
1598
|
-
console.log(
|
|
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(
|
|
1538
|
+
console.log(chalk8.cyan(header));
|
|
1604
1539
|
for (const change of explanation.changes) {
|
|
1605
|
-
console.log(` ${
|
|
1606
|
-
console.log(` ${
|
|
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(
|
|
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(
|
|
1549
|
+
console.log(chalk8.cyan("Notes:"));
|
|
1615
1550
|
for (const note of explanation.notes) {
|
|
1616
|
-
console.log(` ${
|
|
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
|
|
1624
|
-
import
|
|
1625
|
-
import
|
|
1626
|
-
import { simpleGit as
|
|
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
|
|
1640
|
-
const git =
|
|
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(
|
|
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 =
|
|
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(
|
|
1691
|
-
console.log(
|
|
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(
|
|
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(
|
|
1641
|
+
console.log(chalk9.bold(`
|
|
1707
1642
|
\u{1F50D} Found ${results.matches.length} matching commit(s)
|
|
1708
1643
|
`));
|
|
1709
|
-
console.log(
|
|
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(
|
|
1715
|
-
console.log(` ${
|
|
1716
|
-
console.log(` ${
|
|
1717
|
-
console.log(` ${
|
|
1718
|
-
console.log(` ${
|
|
1719
|
-
console.log(` ${
|
|
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" ?
|
|
1722
|
-
console.log(` ${
|
|
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(
|
|
1728
|
-
console.log(
|
|
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
|
|
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);
|