gitxplain 0.1.8 → 0.1.9
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 +190 -4
- package/cli/index.js +430 -117
- package/cli/services/aiService.js +234 -28
- package/cli/services/cacheService.js +92 -1
- package/cli/services/clipboardService.js +6 -1
- package/cli/services/colorSupport.js +31 -0
- package/cli/services/commitService.js +105 -23
- package/cli/services/configService.js +18 -2
- package/cli/services/envLoader.js +2 -2
- package/cli/services/gitService.js +369 -23
- package/cli/services/hookService.js +36 -4
- package/cli/services/mergeService.js +33 -27
- package/cli/services/outputFormatter.js +23 -73
- package/cli/services/pipelineService.js +112 -0
- package/cli/services/promptService.js +8 -1
- package/cli/services/splitService.js +1 -21
- package/cli/services/usageService.js +158 -0
- package/package.json +2 -2
- package/prompts/blame.txt +29 -0
- package/prompts/changelog.txt +36 -0
- package/prompts/conflict.txt +33 -0
- package/prompts/pr-description.txt +40 -0
- package/prompts/refactor.txt +29 -0
- package/prompts/stash.txt +34 -0
- package/prompts/test-suggest.txt +29 -0
- package/IMPLEMENTATION.md +0 -225
- package/cli/services/chatService.js +0 -683
- package/cli/services/gitConnectionService.js +0 -267
package/cli/index.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { realpathSync } from "node:fs";
|
|
6
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
7
7
|
import { generateExplanation } from "./services/aiService.js";
|
|
8
|
+
import { clearCache, getCacheStats } from "./services/cacheService.js";
|
|
8
9
|
import { loadEnvFile } from "./services/envLoader.js";
|
|
9
10
|
import { copyToClipboard } from "./services/clipboardService.js";
|
|
11
|
+
import { getUsageStats } from "./services/usageService.js";
|
|
10
12
|
import {
|
|
11
13
|
applyConfigEnvironment,
|
|
12
14
|
getProviderApiKeyField,
|
|
@@ -18,7 +20,11 @@ import {
|
|
|
18
20
|
import {
|
|
19
21
|
buildBranchRange,
|
|
20
22
|
deletePaths,
|
|
23
|
+
fetchBlameData,
|
|
21
24
|
fetchCommitData,
|
|
25
|
+
fetchCommitDataForFile,
|
|
26
|
+
fetchConflictData,
|
|
27
|
+
fetchStashData,
|
|
22
28
|
fetchWorkingTreeData,
|
|
23
29
|
gitAddFiles,
|
|
24
30
|
gitPull,
|
|
@@ -80,6 +86,13 @@ const MODE_FLAGS = new Map([
|
|
|
80
86
|
["--lines", "lines"],
|
|
81
87
|
["--review", "review"],
|
|
82
88
|
["--security", "security"],
|
|
89
|
+
["--refactor", "refactor"],
|
|
90
|
+
["--test-suggest", "test-suggest"],
|
|
91
|
+
["--pr-description", "pr-description"],
|
|
92
|
+
["--changelog", "changelog"],
|
|
93
|
+
["--blame", "blame"],
|
|
94
|
+
["--conflict", "conflict"],
|
|
95
|
+
["--stash", "stash"],
|
|
83
96
|
["--split", "split"],
|
|
84
97
|
["--merge", "merge"],
|
|
85
98
|
["--tag", "tag"],
|
|
@@ -96,8 +109,28 @@ const FORMAT_FLAGS = new Map([
|
|
|
96
109
|
["--html", "html"]
|
|
97
110
|
]);
|
|
98
111
|
|
|
112
|
+
const ANALYSIS_MODES = new Set([
|
|
113
|
+
"summary",
|
|
114
|
+
"issues",
|
|
115
|
+
"fix",
|
|
116
|
+
"impact",
|
|
117
|
+
"full",
|
|
118
|
+
"lines",
|
|
119
|
+
"review",
|
|
120
|
+
"security",
|
|
121
|
+
"refactor",
|
|
122
|
+
"test-suggest",
|
|
123
|
+
"pr-description",
|
|
124
|
+
"changelog",
|
|
125
|
+
"blame",
|
|
126
|
+
"conflict",
|
|
127
|
+
"stash",
|
|
128
|
+
"split"
|
|
129
|
+
]);
|
|
130
|
+
|
|
99
131
|
const RESERVED_SUBCOMMANDS = new Set([
|
|
100
132
|
"help",
|
|
133
|
+
"cache",
|
|
101
134
|
"config",
|
|
102
135
|
"install-hook",
|
|
103
136
|
"git",
|
|
@@ -110,11 +143,18 @@ const RESERVED_SUBCOMMANDS = new Set([
|
|
|
110
143
|
"push"
|
|
111
144
|
]);
|
|
112
145
|
|
|
146
|
+
const CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
147
|
+
|
|
113
148
|
function printHelp() {
|
|
114
149
|
console.log(`gitxplain - AI-powered Git change analysis, review, and commit workflow CLI
|
|
115
150
|
|
|
116
151
|
Usage:
|
|
117
152
|
gitxplain --help
|
|
153
|
+
gitxplain --version
|
|
154
|
+
gitxplain cache clear
|
|
155
|
+
gitxplain cache stats
|
|
156
|
+
gitxplain --cost
|
|
157
|
+
gitxplain install-hook [post-commit|post-merge|pre-push]
|
|
118
158
|
gitxplain config set provider <name>
|
|
119
159
|
gitxplain config set api-key <value> [--provider <name>]
|
|
120
160
|
gitxplain config get [key]
|
|
@@ -127,6 +167,8 @@ Usage:
|
|
|
127
167
|
gitxplain --release [status]
|
|
128
168
|
gitxplain --merge
|
|
129
169
|
gitxplain --tag
|
|
170
|
+
gitxplain --conflict
|
|
171
|
+
gitxplain --stash [stash-ref]
|
|
130
172
|
gitxplain --log
|
|
131
173
|
gitxplain --status
|
|
132
174
|
gitxplain --pipeline
|
|
@@ -140,10 +182,19 @@ Analysis:
|
|
|
140
182
|
--lines Walk through the changed code file by file
|
|
141
183
|
--review Generate review findings, risks, and suggestions
|
|
142
184
|
--security Focus on security-relevant changes and concerns
|
|
185
|
+
--refactor Suggest refactoring opportunities in the change
|
|
186
|
+
--test-suggest Suggest tests to add or update for the change
|
|
187
|
+
--pr-description Generate a ready-to-paste PR description
|
|
188
|
+
--changelog Generate changelog-style release notes
|
|
189
|
+
--blame <file> Analyze ownership and history for one file with git blame
|
|
190
|
+
--conflict Suggest resolutions for unresolved merge conflicts in the working tree
|
|
191
|
+
--stash [ref] Explain a stash entry, defaulting to stash@{0}
|
|
143
192
|
--split Propose splitting a commit into smaller atomic commits
|
|
193
|
+
--cost Show cumulative token usage and estimated cost totals
|
|
144
194
|
--commit Propose commits for current uncommitted changes
|
|
145
195
|
--execute Execute a proposed split or commit plan
|
|
146
196
|
--dry-run Preview the plan without executing it
|
|
197
|
+
--interactive Review or edit a split plan before execution
|
|
147
198
|
|
|
148
199
|
Release:
|
|
149
200
|
--release [status] Show release branch health and next recommended action
|
|
@@ -153,7 +204,7 @@ Release:
|
|
|
153
204
|
Repo:
|
|
154
205
|
--log Print Git log entries for the current repository
|
|
155
206
|
--status Print Git working tree status for the current repository
|
|
156
|
-
--pipeline Detect the current repository stack and create
|
|
207
|
+
--pipeline Detect the current repository stack and create GitHub/GitLab/CircleCI/Bitbucket CI files
|
|
157
208
|
|
|
158
209
|
Quick Actions:
|
|
159
210
|
config Persist provider, model, and API key settings
|
|
@@ -165,7 +216,8 @@ Quick Actions:
|
|
|
165
216
|
pop Pop a stash entry like "pop 2"
|
|
166
217
|
pull Run git pull, optionally with a remote and branch
|
|
167
218
|
push Run git push, optionally with a remote and branch
|
|
168
|
-
install-hook Install
|
|
219
|
+
install-hook Install a post-commit, post-merge, or pre-push gitxplain hook
|
|
220
|
+
cache Manage gitxplain cache entries
|
|
169
221
|
git Pass through to native git commands
|
|
170
222
|
|
|
171
223
|
Output:
|
|
@@ -178,6 +230,8 @@ Output:
|
|
|
178
230
|
--verbose
|
|
179
231
|
--clipboard
|
|
180
232
|
--stream
|
|
233
|
+
--no-cache
|
|
234
|
+
--diff <file>
|
|
181
235
|
--max-diff-lines <n>
|
|
182
236
|
|
|
183
237
|
Comparison:
|
|
@@ -193,6 +247,7 @@ Notes:
|
|
|
193
247
|
If no command or mode is supplied, gitxplain prints this help text.
|
|
194
248
|
Use --provider or --model to override your config or environment for one command.
|
|
195
249
|
Use gitxplain git <args...> to run any native Git subcommand with its normal flags.
|
|
250
|
+
install-hook supports: post-commit, post-merge, pre-push.
|
|
196
251
|
`);
|
|
197
252
|
}
|
|
198
253
|
|
|
@@ -322,6 +377,34 @@ function handleConfigCommand(parsed) {
|
|
|
322
377
|
throw new Error(`Unknown config subcommand: ${parsed.configAction}`);
|
|
323
378
|
}
|
|
324
379
|
|
|
380
|
+
function handleCacheCommand(parsed) {
|
|
381
|
+
if (parsed.cacheAction == null) {
|
|
382
|
+
throw new Error('Usage: gitxplain cache <clear|stats>');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (parsed.cacheAction === "clear") {
|
|
386
|
+
const deletedCount = clearCache();
|
|
387
|
+
console.log(`Cleared ${deletedCount} cache entr${deletedCount === 1 ? "y" : "ies"}.`);
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (parsed.cacheAction === "stats") {
|
|
392
|
+
const stats = getCacheStats();
|
|
393
|
+
console.log(
|
|
394
|
+
[
|
|
395
|
+
"Cache Stats",
|
|
396
|
+
`Entries: ${stats.entryCount}`,
|
|
397
|
+
`Size: ${stats.totalSizeBytes} bytes`,
|
|
398
|
+
`Oldest: ${stats.oldestEntryIso ?? "n/a"}`,
|
|
399
|
+
`Newest: ${stats.newestEntryIso ?? "n/a"}`
|
|
400
|
+
].join("\n")
|
|
401
|
+
);
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
throw new Error(`Unknown cache subcommand: ${parsed.cacheAction}`);
|
|
406
|
+
}
|
|
407
|
+
|
|
325
408
|
function isDirectNativeGitSubcommand(subcommand, knownGitSubcommands) {
|
|
326
409
|
if (!subcommand || subcommand.startsWith("-")) {
|
|
327
410
|
return false;
|
|
@@ -339,7 +422,7 @@ export function parseArgs(argv, options = {}) {
|
|
|
339
422
|
const subcommand = args[0];
|
|
340
423
|
const knownGitSubcommands = options.gitSubcommands ?? listGitSubcommands();
|
|
341
424
|
const flags = new Set(args.filter((arg) => arg.startsWith("--")));
|
|
342
|
-
const valueFlags = new Set(["--provider", "--model", "--max-diff-lines", "--branch", "--pr"]);
|
|
425
|
+
const valueFlags = new Set(["--provider", "--model", "--max-diff-lines", "--branch", "--pr", "--blame", "--stash", "--diff"]);
|
|
343
426
|
const positional = [];
|
|
344
427
|
|
|
345
428
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -366,6 +449,7 @@ export function parseArgs(argv, options = {}) {
|
|
|
366
449
|
const explicitFormat = [...FORMAT_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
|
|
367
450
|
const isInstallHook = subcommand === "install-hook";
|
|
368
451
|
const isConfigCommand = subcommand === "config";
|
|
452
|
+
const isCacheCommand = subcommand === "cache";
|
|
369
453
|
const isNativeGitWrapper = subcommand === "git";
|
|
370
454
|
const isReleaseCommand = flags.has("--release");
|
|
371
455
|
const isAddCommand = subcommand === "add";
|
|
@@ -382,17 +466,16 @@ export function parseArgs(argv, options = {}) {
|
|
|
382
466
|
return {
|
|
383
467
|
subcommand,
|
|
384
468
|
help: flags.has("--help") || subcommand === "help",
|
|
469
|
+
version: flags.has("--version"),
|
|
470
|
+
cost: flags.has("--cost"),
|
|
385
471
|
nativeGitCommand: isNativeGitCommand,
|
|
386
472
|
installHook: isInstallHook,
|
|
387
473
|
configCommand: isConfigCommand,
|
|
474
|
+
cacheCommand: isCacheCommand,
|
|
388
475
|
configAction: isConfigCommand ? positional[1] ?? null : null,
|
|
389
476
|
configKey: isConfigCommand ? positional[2] ?? null : null,
|
|
390
477
|
configValue: isConfigCommand ? positional.slice(3).join(" ") || null : null,
|
|
391
|
-
|
|
392
|
-
statusCommand: false,
|
|
393
|
-
commitCommand: false,
|
|
394
|
-
mergeCommand: false,
|
|
395
|
-
tagCommand: false,
|
|
478
|
+
cacheAction: isCacheCommand ? positional[1] ?? null : null,
|
|
396
479
|
releaseCommand: isReleaseCommand,
|
|
397
480
|
releaseAction: isReleaseCommand ? positional[0] ?? "status" : null,
|
|
398
481
|
addCommand: isAddCommand,
|
|
@@ -416,6 +499,7 @@ export function parseArgs(argv, options = {}) {
|
|
|
416
499
|
commitRef:
|
|
417
500
|
isInstallHook ||
|
|
418
501
|
isConfigCommand ||
|
|
502
|
+
isCacheCommand ||
|
|
419
503
|
isNativeGitCommand ||
|
|
420
504
|
isReleaseCommand ||
|
|
421
505
|
isAddCommand ||
|
|
@@ -434,16 +518,21 @@ export function parseArgs(argv, options = {}) {
|
|
|
434
518
|
provider: getFlagValue(args, "--provider"),
|
|
435
519
|
model: getFlagValue(args, "--model"),
|
|
436
520
|
maxDiffLines: parseNumber(getFlagValue(args, "--max-diff-lines")),
|
|
521
|
+
blameFile: getFlagValue(args, "--blame"),
|
|
522
|
+
stashRef: flags.has("--stash") || args.some((arg) => arg.startsWith("--stash=")) ? getFlagValue(args, "--stash") : null,
|
|
523
|
+
diffFile: getFlagValue(args, "--diff"),
|
|
437
524
|
hasBranchFlag: flags.has("--branch") || args.some((arg) => arg.startsWith("--branch=")),
|
|
438
525
|
branchBase: getFlagValue(args, "--branch"),
|
|
439
526
|
hasPrFlag: flags.has("--pr") || args.some((arg) => arg.startsWith("--pr=")),
|
|
440
527
|
prBase: getFlagValue(args, "--pr"),
|
|
441
528
|
clipboard: flags.has("--clipboard"),
|
|
442
529
|
stream: flags.has("--stream"),
|
|
530
|
+
noCache: flags.has("--no-cache"),
|
|
443
531
|
verbose: flags.has("--verbose"),
|
|
444
532
|
quiet: flags.has("--quiet"),
|
|
445
533
|
execute: flags.has("--execute"),
|
|
446
534
|
dryRun: flags.has("--dry-run"),
|
|
535
|
+
interactive: flags.has("--interactive"),
|
|
447
536
|
release: flags.has("--release"),
|
|
448
537
|
log: flags.has("--log"),
|
|
449
538
|
status: flags.has("--status"),
|
|
@@ -464,62 +553,86 @@ function askQuestion(prompt) {
|
|
|
464
553
|
});
|
|
465
554
|
}
|
|
466
555
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
[
|
|
470
|
-
"What do you want to know?",
|
|
471
|
-
"1. Summary",
|
|
472
|
-
"2. Issues Fixed",
|
|
473
|
-
"3. Fix Explanation",
|
|
474
|
-
"4. Impact",
|
|
475
|
-
"5. Full Analysis",
|
|
476
|
-
"6. Line-by-Line Code Walkthrough",
|
|
477
|
-
"7. Code Review",
|
|
478
|
-
"8. Security Review",
|
|
479
|
-
"9. Split Commit",
|
|
480
|
-
"10. Merge To Release Branch",
|
|
481
|
-
"11. Tag Release Commits",
|
|
482
|
-
"12. Repository Log",
|
|
483
|
-
"13. Commit Working Tree",
|
|
484
|
-
"14. Create CI/CD Pipelines",
|
|
485
|
-
"> "
|
|
486
|
-
].join("\n")
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
const selections = {
|
|
490
|
-
"1": "summary",
|
|
491
|
-
"2": "issues",
|
|
492
|
-
"3": "fix",
|
|
493
|
-
"4": "impact",
|
|
494
|
-
"5": "full",
|
|
495
|
-
"6": "lines",
|
|
496
|
-
"7": "review",
|
|
497
|
-
"8": "security",
|
|
498
|
-
"9": "split",
|
|
499
|
-
"10": "merge",
|
|
500
|
-
"11": "tag",
|
|
501
|
-
"12": "log",
|
|
502
|
-
"13": "commit",
|
|
503
|
-
"14": "pipeline"
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
return selections[answer] ?? "full";
|
|
556
|
+
function resolveConfiguredAnalysisMode(config) {
|
|
557
|
+
return ANALYSIS_MODES.has(config.mode) ? config.mode : "full";
|
|
507
558
|
}
|
|
508
559
|
|
|
509
560
|
function resolveRuntimeOptions(parsed, config) {
|
|
510
561
|
return {
|
|
511
|
-
mode: parsed.mode ?? config
|
|
562
|
+
mode: parsed.mode ?? resolveConfiguredAnalysisMode(config),
|
|
512
563
|
format: parsed.format ?? config.format ?? "plain",
|
|
513
564
|
provider: parsed.provider ?? config.provider ?? null,
|
|
514
565
|
model: parsed.model ?? config.model ?? null,
|
|
515
566
|
maxDiffLines: parsed.maxDiffLines ?? config.maxDiffLines ?? 800,
|
|
516
567
|
clipboard: parsed.clipboard || config.clipboard === true,
|
|
517
568
|
stream: parsed.stream || config.stream === true,
|
|
569
|
+
noCache: parsed.noCache,
|
|
518
570
|
verbose: parsed.verbose || config.verbose === true,
|
|
519
571
|
quiet: parsed.quiet || config.quiet === true
|
|
520
572
|
};
|
|
521
573
|
}
|
|
522
574
|
|
|
575
|
+
function formatUsageStats(stats) {
|
|
576
|
+
return [
|
|
577
|
+
"Usage Stats",
|
|
578
|
+
`Requests: ${stats.requestCount}`,
|
|
579
|
+
`Input Tokens: ${stats.inputTokens}`,
|
|
580
|
+
`Output Tokens: ${stats.outputTokens}`,
|
|
581
|
+
`Total Tokens: ${stats.totalTokens}`,
|
|
582
|
+
`Estimated Cost: $${stats.estimatedCostUsd.toFixed(6)}`
|
|
583
|
+
].join("\n");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function reviewSplitPlanInteractively(plan) {
|
|
587
|
+
const editedCommits = [];
|
|
588
|
+
const deferredFiles = [];
|
|
589
|
+
|
|
590
|
+
for (const commit of [...plan.commits].sort((left, right) => left.order - right.order)) {
|
|
591
|
+
console.log("");
|
|
592
|
+
console.log(`${commit.order}. ${commit.message}`);
|
|
593
|
+
console.log(`Files: ${commit.files.join(", ")}`);
|
|
594
|
+
console.log(`Why: ${commit.description}`);
|
|
595
|
+
|
|
596
|
+
const action = (await askQuestion('Action [keep/edit/skip/abort] > ')).trim().toLowerCase();
|
|
597
|
+
|
|
598
|
+
if (action === "abort") {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (action === "skip") {
|
|
603
|
+
deferredFiles.push(...commit.files);
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (action === "edit") {
|
|
608
|
+
const nextMessage = await askQuestion("New commit message (leave blank to keep current) > ");
|
|
609
|
+
const nextDescription = await askQuestion("New description (leave blank to keep current) > ");
|
|
610
|
+
editedCommits.push({
|
|
611
|
+
...commit,
|
|
612
|
+
message: nextMessage.trim() === "" ? commit.message : nextMessage.trim(),
|
|
613
|
+
description: nextDescription.trim() === "" ? commit.description : nextDescription.trim()
|
|
614
|
+
});
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
editedCommits.push(commit);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (deferredFiles.length > 0) {
|
|
622
|
+
editedCommits.push({
|
|
623
|
+
order: editedCommits.length + 1,
|
|
624
|
+
message: "chore: include deferred split changes",
|
|
625
|
+
files: deferredFiles,
|
|
626
|
+
description: "Captures split groups that were skipped during interactive review."
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return {
|
|
631
|
+
...plan,
|
|
632
|
+
commits: editedCommits.map((commit, index) => ({ ...commit, order: index + 1 }))
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
523
636
|
function resolveTargetRef(parsed, cwd) {
|
|
524
637
|
if (parsed.commitRef) {
|
|
525
638
|
return parsed.commitRef;
|
|
@@ -556,6 +669,36 @@ function renderFinalOutput({ runtimeOptions, mode, commitData, explanation, resp
|
|
|
556
669
|
});
|
|
557
670
|
}
|
|
558
671
|
|
|
672
|
+
async function runPipelineCommand(cwd) {
|
|
673
|
+
const analysis = inspectRepositoryForPipeline(cwd);
|
|
674
|
+
|
|
675
|
+
if (!analysis.supported) {
|
|
676
|
+
console.log(analysis.reason);
|
|
677
|
+
return 1;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
console.log(formatPipelineRecommendations(analysis));
|
|
681
|
+
|
|
682
|
+
const answer = await askQuestion(
|
|
683
|
+
`\nChoose a pipeline option (1-${analysis.options.length}) or type "cancel" > `
|
|
684
|
+
);
|
|
685
|
+
const selection = resolvePipelineSelection(analysis, answer);
|
|
686
|
+
|
|
687
|
+
if (!selection) {
|
|
688
|
+
console.log("Aborted.");
|
|
689
|
+
return 0;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const { writtenFiles, notes } = writePipelineFiles(cwd, analysis, selection);
|
|
693
|
+
console.log(`\nCreated workflow files: ${writtenFiles.join(", ")}`);
|
|
694
|
+
|
|
695
|
+
if (notes.length > 0) {
|
|
696
|
+
console.log(`\n${notes.join("\n")}`);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return 0;
|
|
700
|
+
}
|
|
701
|
+
|
|
559
702
|
export async function main(argv = process.argv) {
|
|
560
703
|
const cwd = process.cwd();
|
|
561
704
|
const parsed = parseArgs(argv);
|
|
@@ -565,6 +708,16 @@ export async function main(argv = process.argv) {
|
|
|
565
708
|
const config = loadConfig(cwd);
|
|
566
709
|
applyConfigEnvironment(config);
|
|
567
710
|
|
|
711
|
+
if (parsed.version) {
|
|
712
|
+
console.log(CLI_VERSION);
|
|
713
|
+
return 0;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (parsed.cost) {
|
|
717
|
+
console.log(formatUsageStats(getUsageStats()));
|
|
718
|
+
return 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
568
721
|
if (parsed.help || hasNoCommandOrFlags) {
|
|
569
722
|
printHelp();
|
|
570
723
|
return 0;
|
|
@@ -574,6 +727,10 @@ export async function main(argv = process.argv) {
|
|
|
574
727
|
return handleConfigCommand(parsed);
|
|
575
728
|
}
|
|
576
729
|
|
|
730
|
+
if (parsed.cacheCommand) {
|
|
731
|
+
return handleCacheCommand(parsed);
|
|
732
|
+
}
|
|
733
|
+
|
|
577
734
|
if (parsed.nativeGitCommand) {
|
|
578
735
|
return runNativeGitPassthrough(parsed.nativeGitArgs, cwd);
|
|
579
736
|
}
|
|
@@ -608,36 +765,6 @@ export async function main(argv = process.argv) {
|
|
|
608
765
|
return 0;
|
|
609
766
|
}
|
|
610
767
|
|
|
611
|
-
if (parsed.pipelineCommand) {
|
|
612
|
-
const analysis = inspectRepositoryForPipeline(cwd);
|
|
613
|
-
|
|
614
|
-
if (!analysis.supported) {
|
|
615
|
-
console.log(analysis.reason);
|
|
616
|
-
return 1;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
console.log(formatPipelineRecommendations(analysis));
|
|
620
|
-
|
|
621
|
-
const answer = await askQuestion(
|
|
622
|
-
`\nChoose a pipeline option (1-${analysis.options.length}) or type "cancel" > `
|
|
623
|
-
);
|
|
624
|
-
const selection = resolvePipelineSelection(analysis, answer);
|
|
625
|
-
|
|
626
|
-
if (!selection) {
|
|
627
|
-
console.log("Aborted.");
|
|
628
|
-
return 0;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const { writtenFiles, notes } = writePipelineFiles(cwd, analysis, selection);
|
|
632
|
-
console.log(`\nCreated workflow files: ${writtenFiles.join(", ")}`);
|
|
633
|
-
|
|
634
|
-
if (notes.length > 0) {
|
|
635
|
-
console.log(`\n${notes.join("\n")}`);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
return 0;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
768
|
if (
|
|
642
769
|
parsed.addCommand ||
|
|
643
770
|
parsed.removeCommand ||
|
|
@@ -706,39 +833,9 @@ export async function main(argv = process.argv) {
|
|
|
706
833
|
}
|
|
707
834
|
|
|
708
835
|
const runtimeOptions = resolveRuntimeOptions(parsed, config);
|
|
709
|
-
const mode = parsed.mode
|
|
710
|
-
|
|
711
|
-
if (mode === "pipeline") {
|
|
712
|
-
const analysis = inspectRepositoryForPipeline(cwd);
|
|
713
|
-
|
|
714
|
-
if (!analysis.supported) {
|
|
715
|
-
console.log(analysis.reason);
|
|
716
|
-
return 1;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
console.log(formatPipelineRecommendations(analysis));
|
|
720
|
-
|
|
721
|
-
const answer = await askQuestion(
|
|
722
|
-
`\nChoose a pipeline option (1-${analysis.options.length}) or type "cancel" > `
|
|
723
|
-
);
|
|
724
|
-
const selection = resolvePipelineSelection(analysis, answer);
|
|
725
|
-
|
|
726
|
-
if (!selection) {
|
|
727
|
-
console.log("Aborted.");
|
|
728
|
-
return 0;
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
const { writtenFiles, notes } = writePipelineFiles(cwd, analysis, selection);
|
|
732
|
-
console.log(`\nCreated workflow files: ${writtenFiles.join(", ")}`);
|
|
733
|
-
|
|
734
|
-
if (notes.length > 0) {
|
|
735
|
-
console.log(`\n${notes.join("\n")}`);
|
|
736
|
-
}
|
|
836
|
+
const mode = ANALYSIS_MODES.has(parsed.mode) ? parsed.mode : resolveConfiguredAnalysisMode(config);
|
|
737
837
|
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
if (mode === "commit") {
|
|
838
|
+
if (parsed.mode === "commit") {
|
|
742
839
|
const commitData = fetchWorkingTreeData(cwd);
|
|
743
840
|
|
|
744
841
|
if (commitData.filesChanged.length === 0 || commitData.diff === "") {
|
|
@@ -752,6 +849,7 @@ export async function main(argv = process.argv) {
|
|
|
752
849
|
providerOverride: runtimeOptions.provider,
|
|
753
850
|
modelOverride: runtimeOptions.model,
|
|
754
851
|
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
852
|
+
noCache: runtimeOptions.noCache,
|
|
755
853
|
stream: false,
|
|
756
854
|
onChunk: null,
|
|
757
855
|
onStart: null
|
|
@@ -788,7 +886,7 @@ export async function main(argv = process.argv) {
|
|
|
788
886
|
return 0;
|
|
789
887
|
}
|
|
790
888
|
|
|
791
|
-
if (mode === "merge" || parsed.merge) {
|
|
889
|
+
if (parsed.mode === "merge" || parsed.merge) {
|
|
792
890
|
if (parsed.commitRef) {
|
|
793
891
|
throw new Error("--merge works from the current branch and does not accept a commit ref.");
|
|
794
892
|
}
|
|
@@ -820,7 +918,7 @@ export async function main(argv = process.argv) {
|
|
|
820
918
|
return 0;
|
|
821
919
|
}
|
|
822
920
|
|
|
823
|
-
if (mode === "tag" || parsed.tag) {
|
|
921
|
+
if (parsed.mode === "tag" || parsed.tag) {
|
|
824
922
|
if (parsed.commitRef) {
|
|
825
923
|
throw new Error("--tag works from the current branch and does not accept a commit ref.");
|
|
826
924
|
}
|
|
@@ -854,12 +952,210 @@ export async function main(argv = process.argv) {
|
|
|
854
952
|
|
|
855
953
|
const targetRef = resolveTargetRef(parsed, cwd);
|
|
856
954
|
|
|
955
|
+
if (parsed.mode === "blame") {
|
|
956
|
+
if (!parsed.blameFile) {
|
|
957
|
+
throw new Error("--blame requires a file path.");
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const commitData = fetchBlameData(parsed.blameFile, cwd);
|
|
961
|
+
const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
|
|
962
|
+
let streamStarted = false;
|
|
963
|
+
|
|
964
|
+
if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
|
|
965
|
+
console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
969
|
+
mode: "blame",
|
|
970
|
+
commitData,
|
|
971
|
+
providerOverride: runtimeOptions.provider,
|
|
972
|
+
modelOverride: runtimeOptions.model,
|
|
973
|
+
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
974
|
+
noCache: runtimeOptions.noCache,
|
|
975
|
+
stream: canStream,
|
|
976
|
+
onStart: canStream
|
|
977
|
+
? ({ promptMeta: streamPromptMeta }) => {
|
|
978
|
+
if (!runtimeOptions.quiet && !streamStarted) {
|
|
979
|
+
process.stdout.write(
|
|
980
|
+
formatPreamble({
|
|
981
|
+
mode: "blame",
|
|
982
|
+
commitData,
|
|
983
|
+
responseMeta: null,
|
|
984
|
+
promptMeta: streamPromptMeta,
|
|
985
|
+
options: runtimeOptions
|
|
986
|
+
})
|
|
987
|
+
);
|
|
988
|
+
streamStarted = true;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
: null,
|
|
992
|
+
onChunk: canStream ? (chunk) => process.stdout.write(chunk) : null
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
const renderedOutput = renderFinalOutput({
|
|
996
|
+
runtimeOptions,
|
|
997
|
+
mode: "blame",
|
|
998
|
+
commitData,
|
|
999
|
+
explanation,
|
|
1000
|
+
responseMeta,
|
|
1001
|
+
promptMeta
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
if (canStream) {
|
|
1005
|
+
process.stdout.write("\n");
|
|
1006
|
+
if (runtimeOptions.verbose) {
|
|
1007
|
+
process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
|
|
1008
|
+
}
|
|
1009
|
+
} else {
|
|
1010
|
+
console.log(renderedOutput);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (runtimeOptions.clipboard) {
|
|
1014
|
+
copyToClipboard(renderedOutput);
|
|
1015
|
+
if (!runtimeOptions.quiet) {
|
|
1016
|
+
console.error("Copied output to clipboard.");
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
return 0;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (parsed.mode === "conflict") {
|
|
1024
|
+
const commitData = fetchConflictData(cwd, parsed.diffFile);
|
|
1025
|
+
const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
|
|
1026
|
+
let streamStarted = false;
|
|
1027
|
+
|
|
1028
|
+
if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
|
|
1029
|
+
console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
1033
|
+
mode: "conflict",
|
|
1034
|
+
commitData,
|
|
1035
|
+
providerOverride: runtimeOptions.provider,
|
|
1036
|
+
modelOverride: runtimeOptions.model,
|
|
1037
|
+
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
1038
|
+
noCache: runtimeOptions.noCache,
|
|
1039
|
+
stream: canStream,
|
|
1040
|
+
onStart: canStream
|
|
1041
|
+
? ({ promptMeta: streamPromptMeta }) => {
|
|
1042
|
+
if (!runtimeOptions.quiet && !streamStarted) {
|
|
1043
|
+
process.stdout.write(
|
|
1044
|
+
formatPreamble({
|
|
1045
|
+
mode: "conflict",
|
|
1046
|
+
commitData,
|
|
1047
|
+
responseMeta: null,
|
|
1048
|
+
promptMeta: streamPromptMeta,
|
|
1049
|
+
options: runtimeOptions
|
|
1050
|
+
})
|
|
1051
|
+
);
|
|
1052
|
+
streamStarted = true;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
: null,
|
|
1056
|
+
onChunk: canStream ? (chunk) => process.stdout.write(chunk) : null
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
const renderedOutput = renderFinalOutput({
|
|
1060
|
+
runtimeOptions,
|
|
1061
|
+
mode: "conflict",
|
|
1062
|
+
commitData,
|
|
1063
|
+
explanation,
|
|
1064
|
+
responseMeta,
|
|
1065
|
+
promptMeta
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
if (canStream) {
|
|
1069
|
+
process.stdout.write("\n");
|
|
1070
|
+
if (runtimeOptions.verbose) {
|
|
1071
|
+
process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
|
|
1072
|
+
}
|
|
1073
|
+
} else {
|
|
1074
|
+
console.log(renderedOutput);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (runtimeOptions.clipboard) {
|
|
1078
|
+
copyToClipboard(renderedOutput);
|
|
1079
|
+
if (!runtimeOptions.quiet) {
|
|
1080
|
+
console.error("Copied output to clipboard.");
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return 0;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
if (parsed.mode === "stash") {
|
|
1088
|
+
const commitData = fetchStashData(parsed.stashRef, cwd, parsed.diffFile);
|
|
1089
|
+
const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
|
|
1090
|
+
let streamStarted = false;
|
|
1091
|
+
|
|
1092
|
+
if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
|
|
1093
|
+
console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
1097
|
+
mode: "stash",
|
|
1098
|
+
commitData,
|
|
1099
|
+
providerOverride: runtimeOptions.provider,
|
|
1100
|
+
modelOverride: runtimeOptions.model,
|
|
1101
|
+
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
1102
|
+
noCache: runtimeOptions.noCache,
|
|
1103
|
+
stream: canStream,
|
|
1104
|
+
onStart: canStream
|
|
1105
|
+
? ({ promptMeta: streamPromptMeta }) => {
|
|
1106
|
+
if (!runtimeOptions.quiet && !streamStarted) {
|
|
1107
|
+
process.stdout.write(
|
|
1108
|
+
formatPreamble({
|
|
1109
|
+
mode: "stash",
|
|
1110
|
+
commitData,
|
|
1111
|
+
responseMeta: null,
|
|
1112
|
+
promptMeta: streamPromptMeta,
|
|
1113
|
+
options: runtimeOptions
|
|
1114
|
+
})
|
|
1115
|
+
);
|
|
1116
|
+
streamStarted = true;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
: null,
|
|
1120
|
+
onChunk: canStream ? (chunk) => process.stdout.write(chunk) : null
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
const renderedOutput = renderFinalOutput({
|
|
1124
|
+
runtimeOptions,
|
|
1125
|
+
mode: "stash",
|
|
1126
|
+
commitData,
|
|
1127
|
+
explanation,
|
|
1128
|
+
responseMeta,
|
|
1129
|
+
promptMeta
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
if (canStream) {
|
|
1133
|
+
process.stdout.write("\n");
|
|
1134
|
+
if (runtimeOptions.verbose) {
|
|
1135
|
+
process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
|
|
1136
|
+
}
|
|
1137
|
+
} else {
|
|
1138
|
+
console.log(renderedOutput);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (runtimeOptions.clipboard) {
|
|
1142
|
+
copyToClipboard(renderedOutput);
|
|
1143
|
+
if (!runtimeOptions.quiet) {
|
|
1144
|
+
console.error("Copied output to clipboard.");
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
return 0;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
857
1151
|
if (!targetRef) {
|
|
858
1152
|
printHelp();
|
|
859
1153
|
return 1;
|
|
860
1154
|
}
|
|
861
1155
|
|
|
862
|
-
const commitData =
|
|
1156
|
+
const commitData = parsed.diffFile
|
|
1157
|
+
? fetchCommitDataForFile(targetRef, parsed.diffFile, cwd)
|
|
1158
|
+
: fetchCommitData(targetRef, cwd);
|
|
863
1159
|
|
|
864
1160
|
if (mode === "split") {
|
|
865
1161
|
if (commitData.analysisType !== "commit") {
|
|
@@ -872,6 +1168,7 @@ export async function main(argv = process.argv) {
|
|
|
872
1168
|
providerOverride: runtimeOptions.provider,
|
|
873
1169
|
modelOverride: runtimeOptions.model,
|
|
874
1170
|
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
1171
|
+
noCache: runtimeOptions.noCache,
|
|
875
1172
|
stream: false,
|
|
876
1173
|
onChunk: null,
|
|
877
1174
|
onStart: null
|
|
@@ -887,6 +1184,17 @@ export async function main(argv = process.argv) {
|
|
|
887
1184
|
console.log(formatSplitPlan(plan));
|
|
888
1185
|
|
|
889
1186
|
if (parsed.execute && !parsed.dryRun) {
|
|
1187
|
+
const reviewedPlan = parsed.interactive ? await reviewSplitPlanInteractively(plan) : plan;
|
|
1188
|
+
if (reviewedPlan == null) {
|
|
1189
|
+
console.log("Aborted.");
|
|
1190
|
+
return 0;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (parsed.interactive) {
|
|
1194
|
+
console.log("");
|
|
1195
|
+
console.log(formatSplitPlan(reviewedPlan));
|
|
1196
|
+
}
|
|
1197
|
+
|
|
890
1198
|
validateSplitExecutionTarget(commitData.commitId, cwd);
|
|
891
1199
|
const confirmed = await askQuestion(
|
|
892
1200
|
"\nThis will rewrite git history. Continue? (yes/no) > "
|
|
@@ -896,8 +1204,8 @@ export async function main(argv = process.argv) {
|
|
|
896
1204
|
return 0;
|
|
897
1205
|
}
|
|
898
1206
|
|
|
899
|
-
executeSplit(
|
|
900
|
-
console.log(`\nSplit complete. Created ${
|
|
1207
|
+
executeSplit(reviewedPlan, commitData.commitId, cwd);
|
|
1208
|
+
console.log(`\nSplit complete. Created ${reviewedPlan.commits.length} commits.`);
|
|
901
1209
|
} else {
|
|
902
1210
|
console.log("\nThis is a preview. Run with --execute to apply the split.");
|
|
903
1211
|
}
|
|
@@ -912,12 +1220,17 @@ export async function main(argv = process.argv) {
|
|
|
912
1220
|
const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
|
|
913
1221
|
let streamStarted = false;
|
|
914
1222
|
|
|
1223
|
+
if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
|
|
1224
|
+
console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
915
1227
|
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
916
1228
|
mode,
|
|
917
1229
|
commitData,
|
|
918
1230
|
providerOverride: runtimeOptions.provider,
|
|
919
1231
|
modelOverride: runtimeOptions.model,
|
|
920
1232
|
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
1233
|
+
noCache: runtimeOptions.noCache,
|
|
921
1234
|
stream: canStream,
|
|
922
1235
|
onStart: canStream
|
|
923
1236
|
? ({ promptMeta: streamPromptMeta }) => {
|