gitxplain 0.1.8 → 0.2.0
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/.github/workflows/ci.yml +2 -0
- package/.github/workflows/release.yml +92 -5
- package/README.md +227 -4
- package/cli/index.js +439 -114
- 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 +43 -30
- package/cli/services/outputFormatter.js +23 -73
- package/cli/services/pipelineService.js +344 -9
- package/cli/services/promptService.js +8 -1
- package/cli/services/splitService.js +1 -21
- package/cli/services/usageService.js +158 -0
- package/package.json +4 -4
- package/packaging/README.md +60 -0
- package/packaging/aur/.SRCINFO +12 -0
- package/packaging/aur/PKGBUILD +22 -0
- package/packaging/homebrew-tap/Formula/gitxplain.rb +19 -0
- 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/scripts/build-deb.sh +64 -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,44 @@ 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, updatedFiles, unchangedFiles, notes } = writePipelineFiles(cwd, analysis, selection);
|
|
693
|
+
|
|
694
|
+
if (updatedFiles.length === 0 && unchangedFiles.length > 0) {
|
|
695
|
+
console.log(`\nWorkflow files already matched the current template: ${unchangedFiles.join(", ")}`);
|
|
696
|
+
} else if (updatedFiles.length > 0 && unchangedFiles.length === 0) {
|
|
697
|
+
console.log(`\nUpdated workflow files: ${updatedFiles.join(", ")}`);
|
|
698
|
+
} else {
|
|
699
|
+
console.log(`\nUpdated workflow files: ${updatedFiles.join(", ")}`);
|
|
700
|
+
console.log(`Unchanged workflow files: ${unchangedFiles.join(", ")}`);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (notes.length > 0) {
|
|
704
|
+
console.log(`\n${notes.join("\n")}`);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return 0;
|
|
708
|
+
}
|
|
709
|
+
|
|
559
710
|
export async function main(argv = process.argv) {
|
|
560
711
|
const cwd = process.cwd();
|
|
561
712
|
const parsed = parseArgs(argv);
|
|
@@ -565,6 +716,16 @@ export async function main(argv = process.argv) {
|
|
|
565
716
|
const config = loadConfig(cwd);
|
|
566
717
|
applyConfigEnvironment(config);
|
|
567
718
|
|
|
719
|
+
if (parsed.version) {
|
|
720
|
+
console.log(CLI_VERSION);
|
|
721
|
+
return 0;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (parsed.cost) {
|
|
725
|
+
console.log(formatUsageStats(getUsageStats()));
|
|
726
|
+
return 0;
|
|
727
|
+
}
|
|
728
|
+
|
|
568
729
|
if (parsed.help || hasNoCommandOrFlags) {
|
|
569
730
|
printHelp();
|
|
570
731
|
return 0;
|
|
@@ -574,6 +735,10 @@ export async function main(argv = process.argv) {
|
|
|
574
735
|
return handleConfigCommand(parsed);
|
|
575
736
|
}
|
|
576
737
|
|
|
738
|
+
if (parsed.cacheCommand) {
|
|
739
|
+
return handleCacheCommand(parsed);
|
|
740
|
+
}
|
|
741
|
+
|
|
577
742
|
if (parsed.nativeGitCommand) {
|
|
578
743
|
return runNativeGitPassthrough(parsed.nativeGitArgs, cwd);
|
|
579
744
|
}
|
|
@@ -609,33 +774,7 @@ export async function main(argv = process.argv) {
|
|
|
609
774
|
}
|
|
610
775
|
|
|
611
776
|
if (parsed.pipelineCommand) {
|
|
612
|
-
|
|
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;
|
|
777
|
+
return runPipelineCommand(cwd);
|
|
639
778
|
}
|
|
640
779
|
|
|
641
780
|
if (
|
|
@@ -706,39 +845,9 @@ export async function main(argv = process.argv) {
|
|
|
706
845
|
}
|
|
707
846
|
|
|
708
847
|
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
|
-
}
|
|
737
|
-
|
|
738
|
-
return 0;
|
|
739
|
-
}
|
|
848
|
+
const mode = ANALYSIS_MODES.has(parsed.mode) ? parsed.mode : resolveConfiguredAnalysisMode(config);
|
|
740
849
|
|
|
741
|
-
if (mode === "commit") {
|
|
850
|
+
if (parsed.mode === "commit") {
|
|
742
851
|
const commitData = fetchWorkingTreeData(cwd);
|
|
743
852
|
|
|
744
853
|
if (commitData.filesChanged.length === 0 || commitData.diff === "") {
|
|
@@ -752,6 +861,7 @@ export async function main(argv = process.argv) {
|
|
|
752
861
|
providerOverride: runtimeOptions.provider,
|
|
753
862
|
modelOverride: runtimeOptions.model,
|
|
754
863
|
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
864
|
+
noCache: runtimeOptions.noCache,
|
|
755
865
|
stream: false,
|
|
756
866
|
onChunk: null,
|
|
757
867
|
onStart: null
|
|
@@ -788,7 +898,7 @@ export async function main(argv = process.argv) {
|
|
|
788
898
|
return 0;
|
|
789
899
|
}
|
|
790
900
|
|
|
791
|
-
if (mode === "merge" || parsed.merge) {
|
|
901
|
+
if (parsed.mode === "merge" || parsed.merge) {
|
|
792
902
|
if (parsed.commitRef) {
|
|
793
903
|
throw new Error("--merge works from the current branch and does not accept a commit ref.");
|
|
794
904
|
}
|
|
@@ -820,7 +930,7 @@ export async function main(argv = process.argv) {
|
|
|
820
930
|
return 0;
|
|
821
931
|
}
|
|
822
932
|
|
|
823
|
-
if (mode === "tag" || parsed.tag) {
|
|
933
|
+
if (parsed.mode === "tag" || parsed.tag) {
|
|
824
934
|
if (parsed.commitRef) {
|
|
825
935
|
throw new Error("--tag works from the current branch and does not accept a commit ref.");
|
|
826
936
|
}
|
|
@@ -854,12 +964,210 @@ export async function main(argv = process.argv) {
|
|
|
854
964
|
|
|
855
965
|
const targetRef = resolveTargetRef(parsed, cwd);
|
|
856
966
|
|
|
967
|
+
if (parsed.mode === "blame") {
|
|
968
|
+
if (!parsed.blameFile) {
|
|
969
|
+
throw new Error("--blame requires a file path.");
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const commitData = fetchBlameData(parsed.blameFile, cwd);
|
|
973
|
+
const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
|
|
974
|
+
let streamStarted = false;
|
|
975
|
+
|
|
976
|
+
if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
|
|
977
|
+
console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
981
|
+
mode: "blame",
|
|
982
|
+
commitData,
|
|
983
|
+
providerOverride: runtimeOptions.provider,
|
|
984
|
+
modelOverride: runtimeOptions.model,
|
|
985
|
+
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
986
|
+
noCache: runtimeOptions.noCache,
|
|
987
|
+
stream: canStream,
|
|
988
|
+
onStart: canStream
|
|
989
|
+
? ({ promptMeta: streamPromptMeta }) => {
|
|
990
|
+
if (!runtimeOptions.quiet && !streamStarted) {
|
|
991
|
+
process.stdout.write(
|
|
992
|
+
formatPreamble({
|
|
993
|
+
mode: "blame",
|
|
994
|
+
commitData,
|
|
995
|
+
responseMeta: null,
|
|
996
|
+
promptMeta: streamPromptMeta,
|
|
997
|
+
options: runtimeOptions
|
|
998
|
+
})
|
|
999
|
+
);
|
|
1000
|
+
streamStarted = true;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
: null,
|
|
1004
|
+
onChunk: canStream ? (chunk) => process.stdout.write(chunk) : null
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
const renderedOutput = renderFinalOutput({
|
|
1008
|
+
runtimeOptions,
|
|
1009
|
+
mode: "blame",
|
|
1010
|
+
commitData,
|
|
1011
|
+
explanation,
|
|
1012
|
+
responseMeta,
|
|
1013
|
+
promptMeta
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
if (canStream) {
|
|
1017
|
+
process.stdout.write("\n");
|
|
1018
|
+
if (runtimeOptions.verbose) {
|
|
1019
|
+
process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
|
|
1020
|
+
}
|
|
1021
|
+
} else {
|
|
1022
|
+
console.log(renderedOutput);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (runtimeOptions.clipboard) {
|
|
1026
|
+
copyToClipboard(renderedOutput);
|
|
1027
|
+
if (!runtimeOptions.quiet) {
|
|
1028
|
+
console.error("Copied output to clipboard.");
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return 0;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (parsed.mode === "conflict") {
|
|
1036
|
+
const commitData = fetchConflictData(cwd, parsed.diffFile);
|
|
1037
|
+
const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
|
|
1038
|
+
let streamStarted = false;
|
|
1039
|
+
|
|
1040
|
+
if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
|
|
1041
|
+
console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
1045
|
+
mode: "conflict",
|
|
1046
|
+
commitData,
|
|
1047
|
+
providerOverride: runtimeOptions.provider,
|
|
1048
|
+
modelOverride: runtimeOptions.model,
|
|
1049
|
+
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
1050
|
+
noCache: runtimeOptions.noCache,
|
|
1051
|
+
stream: canStream,
|
|
1052
|
+
onStart: canStream
|
|
1053
|
+
? ({ promptMeta: streamPromptMeta }) => {
|
|
1054
|
+
if (!runtimeOptions.quiet && !streamStarted) {
|
|
1055
|
+
process.stdout.write(
|
|
1056
|
+
formatPreamble({
|
|
1057
|
+
mode: "conflict",
|
|
1058
|
+
commitData,
|
|
1059
|
+
responseMeta: null,
|
|
1060
|
+
promptMeta: streamPromptMeta,
|
|
1061
|
+
options: runtimeOptions
|
|
1062
|
+
})
|
|
1063
|
+
);
|
|
1064
|
+
streamStarted = true;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
: null,
|
|
1068
|
+
onChunk: canStream ? (chunk) => process.stdout.write(chunk) : null
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
const renderedOutput = renderFinalOutput({
|
|
1072
|
+
runtimeOptions,
|
|
1073
|
+
mode: "conflict",
|
|
1074
|
+
commitData,
|
|
1075
|
+
explanation,
|
|
1076
|
+
responseMeta,
|
|
1077
|
+
promptMeta
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
if (canStream) {
|
|
1081
|
+
process.stdout.write("\n");
|
|
1082
|
+
if (runtimeOptions.verbose) {
|
|
1083
|
+
process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
|
|
1084
|
+
}
|
|
1085
|
+
} else {
|
|
1086
|
+
console.log(renderedOutput);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (runtimeOptions.clipboard) {
|
|
1090
|
+
copyToClipboard(renderedOutput);
|
|
1091
|
+
if (!runtimeOptions.quiet) {
|
|
1092
|
+
console.error("Copied output to clipboard.");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return 0;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
if (parsed.mode === "stash") {
|
|
1100
|
+
const commitData = fetchStashData(parsed.stashRef, cwd, parsed.diffFile);
|
|
1101
|
+
const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
|
|
1102
|
+
let streamStarted = false;
|
|
1103
|
+
|
|
1104
|
+
if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
|
|
1105
|
+
console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
1109
|
+
mode: "stash",
|
|
1110
|
+
commitData,
|
|
1111
|
+
providerOverride: runtimeOptions.provider,
|
|
1112
|
+
modelOverride: runtimeOptions.model,
|
|
1113
|
+
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
1114
|
+
noCache: runtimeOptions.noCache,
|
|
1115
|
+
stream: canStream,
|
|
1116
|
+
onStart: canStream
|
|
1117
|
+
? ({ promptMeta: streamPromptMeta }) => {
|
|
1118
|
+
if (!runtimeOptions.quiet && !streamStarted) {
|
|
1119
|
+
process.stdout.write(
|
|
1120
|
+
formatPreamble({
|
|
1121
|
+
mode: "stash",
|
|
1122
|
+
commitData,
|
|
1123
|
+
responseMeta: null,
|
|
1124
|
+
promptMeta: streamPromptMeta,
|
|
1125
|
+
options: runtimeOptions
|
|
1126
|
+
})
|
|
1127
|
+
);
|
|
1128
|
+
streamStarted = true;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
: null,
|
|
1132
|
+
onChunk: canStream ? (chunk) => process.stdout.write(chunk) : null
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
const renderedOutput = renderFinalOutput({
|
|
1136
|
+
runtimeOptions,
|
|
1137
|
+
mode: "stash",
|
|
1138
|
+
commitData,
|
|
1139
|
+
explanation,
|
|
1140
|
+
responseMeta,
|
|
1141
|
+
promptMeta
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
if (canStream) {
|
|
1145
|
+
process.stdout.write("\n");
|
|
1146
|
+
if (runtimeOptions.verbose) {
|
|
1147
|
+
process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
|
|
1148
|
+
}
|
|
1149
|
+
} else {
|
|
1150
|
+
console.log(renderedOutput);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (runtimeOptions.clipboard) {
|
|
1154
|
+
copyToClipboard(renderedOutput);
|
|
1155
|
+
if (!runtimeOptions.quiet) {
|
|
1156
|
+
console.error("Copied output to clipboard.");
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return 0;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
857
1163
|
if (!targetRef) {
|
|
858
1164
|
printHelp();
|
|
859
1165
|
return 1;
|
|
860
1166
|
}
|
|
861
1167
|
|
|
862
|
-
const commitData =
|
|
1168
|
+
const commitData = parsed.diffFile
|
|
1169
|
+
? fetchCommitDataForFile(targetRef, parsed.diffFile, cwd)
|
|
1170
|
+
: fetchCommitData(targetRef, cwd);
|
|
863
1171
|
|
|
864
1172
|
if (mode === "split") {
|
|
865
1173
|
if (commitData.analysisType !== "commit") {
|
|
@@ -872,6 +1180,7 @@ export async function main(argv = process.argv) {
|
|
|
872
1180
|
providerOverride: runtimeOptions.provider,
|
|
873
1181
|
modelOverride: runtimeOptions.model,
|
|
874
1182
|
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
1183
|
+
noCache: runtimeOptions.noCache,
|
|
875
1184
|
stream: false,
|
|
876
1185
|
onChunk: null,
|
|
877
1186
|
onStart: null
|
|
@@ -887,6 +1196,17 @@ export async function main(argv = process.argv) {
|
|
|
887
1196
|
console.log(formatSplitPlan(plan));
|
|
888
1197
|
|
|
889
1198
|
if (parsed.execute && !parsed.dryRun) {
|
|
1199
|
+
const reviewedPlan = parsed.interactive ? await reviewSplitPlanInteractively(plan) : plan;
|
|
1200
|
+
if (reviewedPlan == null) {
|
|
1201
|
+
console.log("Aborted.");
|
|
1202
|
+
return 0;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (parsed.interactive) {
|
|
1206
|
+
console.log("");
|
|
1207
|
+
console.log(formatSplitPlan(reviewedPlan));
|
|
1208
|
+
}
|
|
1209
|
+
|
|
890
1210
|
validateSplitExecutionTarget(commitData.commitId, cwd);
|
|
891
1211
|
const confirmed = await askQuestion(
|
|
892
1212
|
"\nThis will rewrite git history. Continue? (yes/no) > "
|
|
@@ -896,8 +1216,8 @@ export async function main(argv = process.argv) {
|
|
|
896
1216
|
return 0;
|
|
897
1217
|
}
|
|
898
1218
|
|
|
899
|
-
executeSplit(
|
|
900
|
-
console.log(`\nSplit complete. Created ${
|
|
1219
|
+
executeSplit(reviewedPlan, commitData.commitId, cwd);
|
|
1220
|
+
console.log(`\nSplit complete. Created ${reviewedPlan.commits.length} commits.`);
|
|
901
1221
|
} else {
|
|
902
1222
|
console.log("\nThis is a preview. Run with --execute to apply the split.");
|
|
903
1223
|
}
|
|
@@ -912,12 +1232,17 @@ export async function main(argv = process.argv) {
|
|
|
912
1232
|
const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
|
|
913
1233
|
let streamStarted = false;
|
|
914
1234
|
|
|
1235
|
+
if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
|
|
1236
|
+
console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
915
1239
|
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
916
1240
|
mode,
|
|
917
1241
|
commitData,
|
|
918
1242
|
providerOverride: runtimeOptions.provider,
|
|
919
1243
|
modelOverride: runtimeOptions.model,
|
|
920
1244
|
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
1245
|
+
noCache: runtimeOptions.noCache,
|
|
921
1246
|
stream: canStream,
|
|
922
1247
|
onStart: canStream
|
|
923
1248
|
? ({ promptMeta: streamPromptMeta }) => {
|