gitxplain 0.1.0 → 0.1.3
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/.env.example +5 -10
- package/IMPLEMENTATION.md +225 -0
- package/README.md +154 -0
- package/cli/index.js +446 -19
- package/cli/services/aiService.js +2 -2
- package/cli/services/chatService.js +663 -0
- package/cli/services/commitService.js +379 -0
- package/cli/services/envLoader.js +33 -0
- package/cli/services/gitConnectionService.js +267 -0
- package/cli/services/gitService.js +590 -0
- package/cli/services/mergeService.js +609 -0
- package/cli/services/outputFormatter.js +217 -11
- package/cli/services/promptService.js +66 -2
- package/cli/services/splitService.js +472 -0
- package/package.json +4 -3
- package/prompts/commit.txt +57 -0
- package/prompts/split.txt +44 -0
package/cli/index.js
CHANGED
|
@@ -5,15 +5,43 @@ import process from "node:process";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { realpathSync } from "node:fs";
|
|
7
7
|
import { generateExplanation } from "./services/aiService.js";
|
|
8
|
+
import { startChatSession } from "./services/chatService.js";
|
|
9
|
+
import { loadEnvFile } from "./services/envLoader.js";
|
|
10
|
+
import {
|
|
11
|
+
saveGitConnection,
|
|
12
|
+
isGitConnected,
|
|
13
|
+
loadGitConnection,
|
|
14
|
+
getGitUserInfo,
|
|
15
|
+
verifyGitToken
|
|
16
|
+
} from "./services/gitConnectionService.js";
|
|
8
17
|
import { copyToClipboard } from "./services/clipboardService.js";
|
|
9
18
|
import { loadConfig } from "./services/configService.js";
|
|
10
19
|
import {
|
|
11
20
|
buildBranchRange,
|
|
21
|
+
deletePaths,
|
|
12
22
|
fetchCommitData,
|
|
23
|
+
fetchWorkingTreeData,
|
|
24
|
+
gitAddFiles,
|
|
25
|
+
gitPush,
|
|
26
|
+
gitStashPop,
|
|
27
|
+
gitRestoreStaged,
|
|
28
|
+
getRepositoryLog,
|
|
29
|
+
getRepositoryStatus,
|
|
13
30
|
getDefaultBaseRef,
|
|
14
|
-
isGitRepository
|
|
31
|
+
isGitRepository,
|
|
32
|
+
resolveStashRef
|
|
15
33
|
} from "./services/gitService.js";
|
|
16
34
|
import { installHook } from "./services/hookService.js";
|
|
35
|
+
import {
|
|
36
|
+
buildReleaseMergePlan,
|
|
37
|
+
buildReleaseTagPlan,
|
|
38
|
+
executeReleaseMerge,
|
|
39
|
+
executeReleaseTagPlan,
|
|
40
|
+
finalizeReleaseMergePlan,
|
|
41
|
+
finalizeReleaseTagPlan,
|
|
42
|
+
formatReleaseMergePlan,
|
|
43
|
+
formatReleaseTagPlan
|
|
44
|
+
} from "./services/mergeService.js";
|
|
17
45
|
import {
|
|
18
46
|
formatFooter,
|
|
19
47
|
formatHtmlOutput,
|
|
@@ -22,6 +50,14 @@ import {
|
|
|
22
50
|
formatOutput,
|
|
23
51
|
formatPreamble
|
|
24
52
|
} from "./services/outputFormatter.js";
|
|
53
|
+
import { executeCommitPlan, formatCommitPlan, parseCommitPlan, reconcileCommitPlan } from "./services/commitService.js";
|
|
54
|
+
import {
|
|
55
|
+
executeSplit,
|
|
56
|
+
formatSplitPlan,
|
|
57
|
+
parseSplitPlan,
|
|
58
|
+
reconcileSplitPlan,
|
|
59
|
+
validateSplitExecutionTarget
|
|
60
|
+
} from "./services/splitService.js";
|
|
25
61
|
|
|
26
62
|
const MODE_FLAGS = new Map([
|
|
27
63
|
["--summary", "summary"],
|
|
@@ -31,7 +67,13 @@ const MODE_FLAGS = new Map([
|
|
|
31
67
|
["--full", "full"],
|
|
32
68
|
["--lines", "lines"],
|
|
33
69
|
["--review", "review"],
|
|
34
|
-
["--security", "security"]
|
|
70
|
+
["--security", "security"],
|
|
71
|
+
["--split", "split"],
|
|
72
|
+
["--merge", "merge"],
|
|
73
|
+
["--tag", "tag"],
|
|
74
|
+
["--commit", "commit"],
|
|
75
|
+
["--log", "log"],
|
|
76
|
+
["--status", "status"]
|
|
35
77
|
]);
|
|
36
78
|
|
|
37
79
|
const FORMAT_FLAGS = new Map([
|
|
@@ -41,35 +83,78 @@ const FORMAT_FLAGS = new Map([
|
|
|
41
83
|
]);
|
|
42
84
|
|
|
43
85
|
function printHelp() {
|
|
44
|
-
console.log(`gitxplain - AI-powered Git and
|
|
86
|
+
console.log(`gitxplain - AI-powered Git change analysis, review, and commit workflow CLI
|
|
45
87
|
|
|
46
88
|
Usage:
|
|
47
89
|
gitxplain help
|
|
48
90
|
gitxplain --help
|
|
91
|
+
gitxplain commit
|
|
92
|
+
gitxplain --commit
|
|
93
|
+
gitxplain merge
|
|
94
|
+
gitxplain --merge
|
|
95
|
+
gitxplain tag
|
|
96
|
+
gitxplain --tag
|
|
97
|
+
gitxplain log
|
|
98
|
+
gitxplain --log
|
|
99
|
+
gitxplain status
|
|
100
|
+
gitxplain --status
|
|
101
|
+
gitxplain add <path> [more-paths...]
|
|
102
|
+
gitxplain remove <path> [more-paths...]
|
|
103
|
+
gitxplain del <path> [more-paths...]
|
|
104
|
+
gitxplain pop [stash-index]
|
|
105
|
+
gitxplain push [remote] [branch]
|
|
49
106
|
gitxplain install-hook [hook-name]
|
|
107
|
+
gitxplain --connect-git [token]
|
|
108
|
+
gitxplain --boot [options]
|
|
50
109
|
gitxplain <commit-id> [options]
|
|
51
110
|
gitxplain <start>..<end> [options]
|
|
52
111
|
gitxplain --branch [base-ref] [options]
|
|
53
112
|
gitxplain --pr [base-ref] [options]
|
|
54
113
|
|
|
114
|
+
What It Does:
|
|
115
|
+
Analyze commits, ranges, branches, and working tree changes
|
|
116
|
+
Generate summaries, reviews, security checks, and line-by-line walkthroughs
|
|
117
|
+
Plan commits for uncommitted work and split oversized commits into atomic steps
|
|
118
|
+
Merge release-version branch changes into a dedicated release branch
|
|
119
|
+
Tag release-version commit windows on the current branch
|
|
120
|
+
Inspect repository history and working tree status without calling the LLM
|
|
121
|
+
Run quick local actions to stage, unstage, delete files, pop stashes, or push
|
|
122
|
+
|
|
55
123
|
Modes:
|
|
56
|
-
--summary Generate a one-line summary
|
|
57
|
-
--issues Focus on bug or
|
|
58
|
-
--fix Explain the fix in simple terms
|
|
124
|
+
--summary Generate a one-line summary of a change
|
|
125
|
+
--issues Focus on the bug, issue, or failure being addressed
|
|
126
|
+
--fix Explain the fix in simple, junior-friendly terms
|
|
59
127
|
--impact Explain before-vs-after behavior changes
|
|
60
128
|
--full Generate a full structured analysis
|
|
61
|
-
--lines
|
|
62
|
-
--review Generate
|
|
63
|
-
--security Focus on security
|
|
129
|
+
--lines Walk through the changed code file by file
|
|
130
|
+
--review Generate review findings, risks, and suggestions
|
|
131
|
+
--security Focus on security-relevant changes and concerns
|
|
132
|
+
--split Propose splitting a commit into smaller atomic commits
|
|
133
|
+
--merge Preview or apply a merge into the release branch based on version bumps
|
|
134
|
+
--tag Preview or create release tags based on version bumps
|
|
135
|
+
--commit Propose commits for current uncommitted changes
|
|
136
|
+
--log Print Git log entries for the current repository
|
|
137
|
+
--status Print Git working tree status for the current repository
|
|
138
|
+
--execute Execute a proposed split or commit plan
|
|
139
|
+
--dry-run Preview the plan without executing it (default for --split and --commit)
|
|
140
|
+
|
|
141
|
+
Quick Actions:
|
|
142
|
+
add Stage one or more files with git add
|
|
143
|
+
remove Unstage one or more files with git restore --staged
|
|
144
|
+
del Delete one or more files from the working tree
|
|
145
|
+
pop Pop a stash entry with a plain numeric index like "pop 2"
|
|
146
|
+
push Run git push, optionally with a remote and branch
|
|
64
147
|
|
|
65
148
|
Output:
|
|
66
|
-
--json Print JSON output
|
|
149
|
+
--json Print structured JSON output
|
|
67
150
|
--markdown Print Markdown output
|
|
68
151
|
--html Print HTML output
|
|
69
|
-
--quiet Print only the
|
|
152
|
+
--quiet Print only the main body without extra framing
|
|
70
153
|
--verbose Print provider, model, cache, latency, and usage details
|
|
71
154
|
--clipboard Copy the final output to the system clipboard
|
|
72
|
-
--stream Stream
|
|
155
|
+
--stream Stream model output as it is generated when supported
|
|
156
|
+
--boot Launch an interactive chat session for dynamic querying, PR creation, and cloning.
|
|
157
|
+
--connect-git Save your GitHub Personal Access Token to act autonomously inside Chat.
|
|
73
158
|
|
|
74
159
|
Providers:
|
|
75
160
|
--provider LLM provider: openai, groq, openrouter, gemini, ollama, chutes
|
|
@@ -79,14 +164,34 @@ Diff Budget:
|
|
|
79
164
|
--max-diff-lines <n> Limit diff lines sent to the model
|
|
80
165
|
|
|
81
166
|
Comparison:
|
|
82
|
-
--branch [base-ref] Analyze current branch against base branch
|
|
83
|
-
--pr [base-ref] Alias for --branch, useful for PR-style
|
|
167
|
+
--branch [base-ref] Analyze the current branch against a base branch
|
|
168
|
+
--pr [base-ref] Alias for --branch, useful for PR-style comparisons
|
|
84
169
|
|
|
85
170
|
Examples:
|
|
86
171
|
gitxplain HEAD~1 --full
|
|
172
|
+
gitxplain HEAD~1 --review
|
|
87
173
|
gitxplain HEAD~5..HEAD --markdown
|
|
88
174
|
gitxplain --branch main --review
|
|
89
175
|
gitxplain --pr origin/main --security --stream
|
|
176
|
+
gitxplain commit
|
|
177
|
+
gitxplain --commit --execute
|
|
178
|
+
gitxplain merge
|
|
179
|
+
gitxplain --merge --execute
|
|
180
|
+
gitxplain tag
|
|
181
|
+
gitxplain --tag --execute
|
|
182
|
+
gitxplain log
|
|
183
|
+
gitxplain --log
|
|
184
|
+
gitxplain status
|
|
185
|
+
gitxplain --status
|
|
186
|
+
gitxplain add README.md
|
|
187
|
+
gitxplain remove README.md
|
|
188
|
+
gitxplain del scratch.txt
|
|
189
|
+
gitxplain pop
|
|
190
|
+
gitxplain pop 2
|
|
191
|
+
gitxplain push
|
|
192
|
+
gitxplain push origin main
|
|
193
|
+
gitxplain HEAD~1 --split
|
|
194
|
+
gitxplain HEAD --split --execute
|
|
90
195
|
gitxplain HEAD~1 --provider chutes --model deepseek-ai/DeepSeek-V3-0324
|
|
91
196
|
|
|
92
197
|
Provider Setup:
|
|
@@ -124,6 +229,7 @@ Hook Installation:
|
|
|
124
229
|
|
|
125
230
|
Notes:
|
|
126
231
|
Run gitxplain inside a Git repository.
|
|
232
|
+
If no mode is supplied, gitxplain will prompt you to choose one interactively.
|
|
127
233
|
Use --provider or --model to override your config or environment for one command.
|
|
128
234
|
`);
|
|
129
235
|
}
|
|
@@ -186,13 +292,58 @@ export function parseArgs(argv) {
|
|
|
186
292
|
const explicitMode = [...MODE_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
|
|
187
293
|
const explicitFormat = [...FORMAT_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
|
|
188
294
|
const isInstallHook = subcommand === "install-hook";
|
|
295
|
+
const isConnectGit = flags.has("--connect-git");
|
|
296
|
+
const isBoot = flags.has("--boot");
|
|
297
|
+
const isLogCommand = subcommand === "log";
|
|
298
|
+
const isStatusCommand = subcommand === "status";
|
|
299
|
+
const isCommitCommand = subcommand === "commit";
|
|
300
|
+
const isMergeCommand = subcommand === "merge";
|
|
301
|
+
const isTagCommand = subcommand === "tag";
|
|
302
|
+
const isAddCommand = subcommand === "add";
|
|
303
|
+
const isRemoveCommand = subcommand === "remove";
|
|
304
|
+
const isDeleteCommand = subcommand === "del";
|
|
305
|
+
const isPopCommand = subcommand === "pop";
|
|
306
|
+
const isPushCommand = subcommand === "push";
|
|
189
307
|
|
|
190
308
|
return {
|
|
191
309
|
subcommand,
|
|
192
310
|
help: flags.has("--help") || subcommand === "help",
|
|
193
311
|
installHook: isInstallHook,
|
|
312
|
+
connectGit: isConnectGit,
|
|
313
|
+
boot: isBoot,
|
|
314
|
+
logCommand: isLogCommand,
|
|
315
|
+
statusCommand: isStatusCommand,
|
|
316
|
+
commitCommand: isCommitCommand,
|
|
317
|
+
mergeCommand: isMergeCommand,
|
|
318
|
+
tagCommand: isTagCommand,
|
|
319
|
+
addCommand: isAddCommand,
|
|
320
|
+
removeCommand: isRemoveCommand,
|
|
321
|
+
deleteCommand: isDeleteCommand,
|
|
322
|
+
popCommand: isPopCommand,
|
|
323
|
+
pushCommand: isPushCommand,
|
|
194
324
|
hookName: isInstallHook ? positional[1] ?? "post-commit" : null,
|
|
195
|
-
|
|
325
|
+
actionPaths: isAddCommand || isRemoveCommand || isDeleteCommand ? positional.slice(1) : [],
|
|
326
|
+
connectToken: isConnectGit ? positional[0] : null,
|
|
327
|
+
stashIndex: isPopCommand ? positional[1] ?? null : null,
|
|
328
|
+
pushRemote: isPushCommand ? positional[1] ?? null : null,
|
|
329
|
+
pushBranch: isPushCommand ? positional[2] ?? null : null,
|
|
330
|
+
commitRef:
|
|
331
|
+
isInstallHook ||
|
|
332
|
+
isConnectGit ||
|
|
333
|
+
isBoot ||
|
|
334
|
+
isLogCommand ||
|
|
335
|
+
isStatusCommand ||
|
|
336
|
+
isCommitCommand ||
|
|
337
|
+
isMergeCommand ||
|
|
338
|
+
isTagCommand ||
|
|
339
|
+
isAddCommand ||
|
|
340
|
+
isRemoveCommand ||
|
|
341
|
+
isDeleteCommand ||
|
|
342
|
+
isPopCommand ||
|
|
343
|
+
isPushCommand ||
|
|
344
|
+
subcommand === "help"
|
|
345
|
+
? null
|
|
346
|
+
: positional[0] ?? null,
|
|
196
347
|
mode: explicitMode,
|
|
197
348
|
format: explicitFormat,
|
|
198
349
|
provider: getFlagValue(args, "--provider"),
|
|
@@ -205,7 +356,13 @@ export function parseArgs(argv) {
|
|
|
205
356
|
clipboard: flags.has("--clipboard"),
|
|
206
357
|
stream: flags.has("--stream"),
|
|
207
358
|
verbose: flags.has("--verbose"),
|
|
208
|
-
quiet: flags.has("--quiet")
|
|
359
|
+
quiet: flags.has("--quiet"),
|
|
360
|
+
execute: flags.has("--execute"),
|
|
361
|
+
dryRun: flags.has("--dry-run"),
|
|
362
|
+
log: flags.has("--log"),
|
|
363
|
+
status: flags.has("--status"),
|
|
364
|
+
merge: flags.has("--merge"),
|
|
365
|
+
tag: flags.has("--tag")
|
|
209
366
|
};
|
|
210
367
|
}
|
|
211
368
|
|
|
@@ -233,6 +390,11 @@ async function chooseModeInteractively() {
|
|
|
233
390
|
"6. Line-by-Line Code Walkthrough",
|
|
234
391
|
"7. Code Review",
|
|
235
392
|
"8. Security Review",
|
|
393
|
+
"9. Split Commit",
|
|
394
|
+
"10. Merge To Release Branch",
|
|
395
|
+
"11. Tag Release Commits",
|
|
396
|
+
"12. Repository Log",
|
|
397
|
+
"13. Commit Working Tree",
|
|
236
398
|
"> "
|
|
237
399
|
].join("\n")
|
|
238
400
|
);
|
|
@@ -245,7 +407,12 @@ async function chooseModeInteractively() {
|
|
|
245
407
|
"5": "full",
|
|
246
408
|
"6": "lines",
|
|
247
409
|
"7": "review",
|
|
248
|
-
"8": "security"
|
|
410
|
+
"8": "security",
|
|
411
|
+
"9": "split",
|
|
412
|
+
"10": "merge",
|
|
413
|
+
"11": "tag",
|
|
414
|
+
"12": "log",
|
|
415
|
+
"13": "commit"
|
|
249
416
|
};
|
|
250
417
|
|
|
251
418
|
return selections[answer] ?? "full";
|
|
@@ -305,8 +472,11 @@ export async function main(argv = process.argv) {
|
|
|
305
472
|
const cwd = process.cwd();
|
|
306
473
|
const config = loadConfig(cwd);
|
|
307
474
|
const parsed = parseArgs(argv);
|
|
475
|
+
const hasNoCommandOrFlags = argv.slice(2).length === 0;
|
|
308
476
|
|
|
309
|
-
|
|
477
|
+
loadEnvFile(cwd); // Ensure environment is loaded first
|
|
478
|
+
|
|
479
|
+
if (parsed.help || hasNoCommandOrFlags) {
|
|
310
480
|
printHelp();
|
|
311
481
|
return 0;
|
|
312
482
|
}
|
|
@@ -322,7 +492,216 @@ export async function main(argv = process.argv) {
|
|
|
322
492
|
return 0;
|
|
323
493
|
}
|
|
324
494
|
|
|
495
|
+
if (parsed.connectGit) {
|
|
496
|
+
let token = parsed.connectToken;
|
|
497
|
+
if (!token) {
|
|
498
|
+
if (process.env.GITHUB_TOKEN) {
|
|
499
|
+
token = process.env.GITHUB_TOKEN;
|
|
500
|
+
} else {
|
|
501
|
+
console.error("Please provide your GitHub Personal Access Token.\nRun: gitxplain --connect-git <YOUR_TOKEN>\nOr set it in your .env as GITHUB_TOKEN=...");
|
|
502
|
+
return 1;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
console.log("Verifying token with GitHub API...");
|
|
507
|
+
const userInfo = await verifyGitToken(token);
|
|
508
|
+
await saveGitConnection(token, "github", userInfo);
|
|
509
|
+
console.log(`\nSuccessfully connected to GitHub as: \x1b[36m${userInfo.login}\x1b[0m`);
|
|
510
|
+
console.log(`Token saved securely to your local configuration.\n`);
|
|
511
|
+
} catch (e) {
|
|
512
|
+
console.error(`Token verification failed: ${e.message}`);
|
|
513
|
+
return 1;
|
|
514
|
+
}
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (parsed.boot) {
|
|
519
|
+
if (!isGitConnected()) {
|
|
520
|
+
console.error("You must connect a GitHub account first to use the interactive agent.\nCommand: gitxplain --connect-git <YOUR_TOKEN>");
|
|
521
|
+
return 1;
|
|
522
|
+
}
|
|
523
|
+
const connection = loadGitConnection();
|
|
524
|
+
const userInfo = getGitUserInfo();
|
|
525
|
+
try {
|
|
526
|
+
const { getProviderConfig, validateProviderConfig } = await import(
|
|
527
|
+
"./services/aiService.js"
|
|
528
|
+
);
|
|
529
|
+
const config = getProviderConfig(parsed.provider, parsed.model);
|
|
530
|
+
validateProviderConfig(config);
|
|
531
|
+
await startChatSession(connection.token, parsed.provider, parsed.model, userInfo.name || connection.user?.login);
|
|
532
|
+
} catch (configError) {
|
|
533
|
+
console.error(`Missing LLM Key. Please check your .env variables or --provider flags.\n${configError.message}`);
|
|
534
|
+
return 1;
|
|
535
|
+
}
|
|
536
|
+
return 0;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (parsed.logCommand || parsed.log) {
|
|
540
|
+
console.log(getRepositoryLog(cwd));
|
|
541
|
+
return 0;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (parsed.statusCommand || parsed.status) {
|
|
545
|
+
console.log(getRepositoryStatus(cwd));
|
|
546
|
+
return 0;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (parsed.addCommand || parsed.removeCommand || parsed.deleteCommand || parsed.popCommand || parsed.pushCommand) {
|
|
550
|
+
if (!parsed.popCommand && parsed.actionPaths.length === 0) {
|
|
551
|
+
if (!parsed.pushCommand) {
|
|
552
|
+
throw new Error(`No paths provided for "${parsed.subcommand}".`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (parsed.addCommand) {
|
|
557
|
+
gitAddFiles(parsed.actionPaths, cwd);
|
|
558
|
+
console.log(`Staged ${parsed.actionPaths.join(", ")}.`);
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (parsed.removeCommand) {
|
|
563
|
+
gitRestoreStaged(parsed.actionPaths, cwd);
|
|
564
|
+
console.log(`Unstaged ${parsed.actionPaths.join(", ")}.`);
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (parsed.deleteCommand) {
|
|
569
|
+
deletePaths(parsed.actionPaths, cwd);
|
|
570
|
+
console.log(`Deleted ${parsed.actionPaths.join(", ")}.`);
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (parsed.popCommand) {
|
|
575
|
+
const stashRef = resolveStashRef(parsed.stashIndex);
|
|
576
|
+
gitStashPop(parsed.stashIndex, cwd);
|
|
577
|
+
console.log(`Popped ${stashRef}.`);
|
|
578
|
+
return 0;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
gitPush(cwd, parsed.pushRemote, parsed.pushBranch);
|
|
582
|
+
console.log(
|
|
583
|
+
`Pushed${parsed.pushRemote ? ` to ${parsed.pushRemote}` : ""}${parsed.pushBranch ? ` ${parsed.pushBranch}` : ""}.`
|
|
584
|
+
);
|
|
585
|
+
return 0;
|
|
586
|
+
}
|
|
587
|
+
|
|
325
588
|
const runtimeOptions = resolveRuntimeOptions(parsed, config);
|
|
589
|
+
const mode = parsed.mode ?? config.mode ?? (await chooseModeInteractively());
|
|
590
|
+
|
|
591
|
+
if (mode === "commit" || parsed.commitCommand) {
|
|
592
|
+
const commitData = fetchWorkingTreeData(cwd);
|
|
593
|
+
|
|
594
|
+
if (commitData.filesChanged.length === 0 || commitData.diff === "") {
|
|
595
|
+
console.log("Working tree is clean. Nothing to commit.");
|
|
596
|
+
return 0;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
600
|
+
mode: "commit",
|
|
601
|
+
commitData,
|
|
602
|
+
providerOverride: runtimeOptions.provider,
|
|
603
|
+
modelOverride: runtimeOptions.model,
|
|
604
|
+
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
605
|
+
stream: false,
|
|
606
|
+
onChunk: null,
|
|
607
|
+
onStart: null
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const plan = reconcileCommitPlan(parseCommitPlan(explanation), cwd);
|
|
611
|
+
|
|
612
|
+
if (!plan.reason_to_commit || plan.commits.length === 0) {
|
|
613
|
+
console.log("No meaningful commit grouping recommended.");
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
console.log(formatCommitPlan(plan));
|
|
618
|
+
|
|
619
|
+
if (parsed.execute && !parsed.dryRun) {
|
|
620
|
+
const confirmed = await askQuestion(
|
|
621
|
+
"\nThis will create new commits from your working tree changes. Continue? (yes/no) > "
|
|
622
|
+
);
|
|
623
|
+
if (confirmed.toLowerCase() !== "yes") {
|
|
624
|
+
console.log("Aborted.");
|
|
625
|
+
return 0;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
executeCommitPlan(plan, cwd);
|
|
629
|
+
console.log(`\nCommit complete. Created ${plan.commits.length} commits.`);
|
|
630
|
+
} else {
|
|
631
|
+
console.log("\nThis is a preview. Run with --execute to apply the commit plan.");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (runtimeOptions.verbose) {
|
|
635
|
+
process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return 0;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
if (mode === "merge" || parsed.mergeCommand || parsed.merge) {
|
|
642
|
+
if (parsed.commitRef) {
|
|
643
|
+
throw new Error("--merge works from the current branch and does not accept a commit ref.");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const plan = finalizeReleaseMergePlan(buildReleaseMergePlan(cwd));
|
|
647
|
+
|
|
648
|
+
if (plan.windows.length === 0) {
|
|
649
|
+
console.log("No unreleased release commits detected. Nothing to merge.");
|
|
650
|
+
return 0;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
console.log(formatReleaseMergePlan(plan));
|
|
654
|
+
|
|
655
|
+
if (parsed.execute && !parsed.dryRun) {
|
|
656
|
+
const confirmed = await askQuestion(
|
|
657
|
+
`\nThis will create ${plan.windows.length} release commit(s) on ${plan.releaseBranch}. Continue? (yes/no) > `
|
|
658
|
+
);
|
|
659
|
+
if (confirmed.toLowerCase() !== "yes") {
|
|
660
|
+
console.log("Aborted.");
|
|
661
|
+
return 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
executeReleaseMerge(plan, cwd);
|
|
665
|
+
console.log(`\nRelease promotion complete. Created ${plan.windows.length} release commit(s) on ${plan.releaseBranch}.`);
|
|
666
|
+
} else {
|
|
667
|
+
console.log(`\nThis is a preview. Run with --execute to create release commits on ${plan.releaseBranch}.`);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (mode === "tag" || parsed.tagCommand || parsed.tag) {
|
|
674
|
+
if (parsed.commitRef) {
|
|
675
|
+
throw new Error("--tag works from the current branch and does not accept a commit ref.");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const plan = finalizeReleaseTagPlan(buildReleaseTagPlan(cwd));
|
|
679
|
+
|
|
680
|
+
if (plan.tags.length === 0) {
|
|
681
|
+
console.log("No unreleased release tags detected. Nothing to tag.");
|
|
682
|
+
return 0;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
console.log(formatReleaseTagPlan(plan));
|
|
686
|
+
|
|
687
|
+
if (parsed.execute && !parsed.dryRun) {
|
|
688
|
+
const confirmed = await askQuestion(
|
|
689
|
+
`\nThis will create ${plan.tags.length} release tag(s). Continue? (yes/no) > `
|
|
690
|
+
);
|
|
691
|
+
if (confirmed.toLowerCase() !== "yes") {
|
|
692
|
+
console.log("Aborted.");
|
|
693
|
+
return 0;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
executeReleaseTagPlan(plan, cwd);
|
|
697
|
+
console.log(`\nRelease tagging complete. Created ${plan.tags.length} release tag(s).`);
|
|
698
|
+
} else {
|
|
699
|
+
console.log("\nThis is a preview. Run with --execute to create release tags.");
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return 0;
|
|
703
|
+
}
|
|
704
|
+
|
|
326
705
|
const targetRef = resolveTargetRef(parsed, cwd);
|
|
327
706
|
|
|
328
707
|
if (!targetRef) {
|
|
@@ -330,8 +709,56 @@ export async function main(argv = process.argv) {
|
|
|
330
709
|
return 1;
|
|
331
710
|
}
|
|
332
711
|
|
|
333
|
-
const mode = parsed.mode ?? config.mode ?? (await chooseModeInteractively());
|
|
334
712
|
const commitData = fetchCommitData(targetRef, cwd);
|
|
713
|
+
|
|
714
|
+
if (mode === "split") {
|
|
715
|
+
if (commitData.analysisType !== "commit") {
|
|
716
|
+
throw new Error("--split only supports analyzing a single commit.");
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
720
|
+
mode: "split",
|
|
721
|
+
commitData,
|
|
722
|
+
providerOverride: runtimeOptions.provider,
|
|
723
|
+
modelOverride: runtimeOptions.model,
|
|
724
|
+
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
725
|
+
stream: false,
|
|
726
|
+
onChunk: null,
|
|
727
|
+
onStart: null
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
const plan = reconcileSplitPlan(parseSplitPlan(explanation), commitData.filesChanged);
|
|
731
|
+
|
|
732
|
+
if (!plan.reason_to_split || plan.commits.length === 0) {
|
|
733
|
+
console.log("This commit is already atomic. No split recommended.");
|
|
734
|
+
return 0;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
console.log(formatSplitPlan(plan));
|
|
738
|
+
|
|
739
|
+
if (parsed.execute && !parsed.dryRun) {
|
|
740
|
+
validateSplitExecutionTarget(commitData.commitId, cwd);
|
|
741
|
+
const confirmed = await askQuestion(
|
|
742
|
+
"\nThis will rewrite git history. Continue? (yes/no) > "
|
|
743
|
+
);
|
|
744
|
+
if (confirmed.toLowerCase() !== "yes") {
|
|
745
|
+
console.log("Aborted.");
|
|
746
|
+
return 0;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
executeSplit(plan, commitData.commitId, cwd);
|
|
750
|
+
console.log(`\nSplit complete. Created ${plan.commits.length} commits.`);
|
|
751
|
+
} else {
|
|
752
|
+
console.log("\nThis is a preview. Run with --execute to apply the split.");
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (runtimeOptions.verbose) {
|
|
756
|
+
process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return 0;
|
|
760
|
+
}
|
|
761
|
+
|
|
335
762
|
const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
|
|
336
763
|
let streamStarted = false;
|
|
337
764
|
|
|
@@ -12,7 +12,7 @@ const SUPPORTED_PROVIDERS = new Set([
|
|
|
12
12
|
]);
|
|
13
13
|
const SYSTEM_PROMPT = "You explain Git commits clearly and accurately for developers.";
|
|
14
14
|
|
|
15
|
-
function getProviderConfig(providerOverride, modelOverride) {
|
|
15
|
+
export function getProviderConfig(providerOverride, modelOverride) {
|
|
16
16
|
const provider = (providerOverride ?? process.env.LLM_PROVIDER ?? "openai").toLowerCase();
|
|
17
17
|
|
|
18
18
|
if (!SUPPORTED_PROVIDERS.has(provider)) {
|
|
@@ -79,7 +79,7 @@ function getProviderConfig(providerOverride, modelOverride) {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function validateProviderConfig(config) {
|
|
82
|
+
export function validateProviderConfig(config) {
|
|
83
83
|
if (!config.model) {
|
|
84
84
|
throw new Error(`No model configured for provider "${config.provider}".`);
|
|
85
85
|
}
|