gitxplain 0.1.0 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/index.js CHANGED
@@ -5,15 +5,50 @@ 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
+ gitPull,
26
+ gitPush,
27
+ gitResetHard,
28
+ gitResetSoft,
29
+ gitStashPop,
30
+ gitRestoreStaged,
31
+ getRepositoryLog,
32
+ getRepositoryStatus,
13
33
  getDefaultBaseRef,
14
- isGitRepository
34
+ isGitRepository,
35
+ listGitSubcommands,
36
+ runNativeGitPassthrough,
37
+ resolveStashRef
15
38
  } from "./services/gitService.js";
16
39
  import { installHook } from "./services/hookService.js";
40
+ import {
41
+ buildReleaseMergePlan,
42
+ buildReleaseStatus,
43
+ buildReleaseTagPlan,
44
+ executeReleaseMerge,
45
+ executeReleaseTagPlan,
46
+ finalizeReleaseMergePlan,
47
+ finalizeReleaseTagPlan,
48
+ formatReleaseMergePlan,
49
+ formatReleaseStatus,
50
+ formatReleaseTagPlan
51
+ } from "./services/mergeService.js";
17
52
  import {
18
53
  formatFooter,
19
54
  formatHtmlOutput,
@@ -22,6 +57,20 @@ import {
22
57
  formatOutput,
23
58
  formatPreamble
24
59
  } from "./services/outputFormatter.js";
60
+ import {
61
+ formatPipelineRecommendations,
62
+ inspectRepositoryForPipeline,
63
+ resolvePipelineSelection,
64
+ writePipelineFiles
65
+ } from "./services/pipelineService.js";
66
+ import { executeCommitPlan, formatCommitPlan, parseCommitPlan, reconcileCommitPlan } from "./services/commitService.js";
67
+ import {
68
+ executeSplit,
69
+ formatSplitPlan,
70
+ parseSplitPlan,
71
+ reconcileSplitPlan,
72
+ validateSplitExecutionTarget
73
+ } from "./services/splitService.js";
25
74
 
26
75
  const MODE_FLAGS = new Map([
27
76
  ["--summary", "summary"],
@@ -31,7 +80,14 @@ const MODE_FLAGS = new Map([
31
80
  ["--full", "full"],
32
81
  ["--lines", "lines"],
33
82
  ["--review", "review"],
34
- ["--security", "security"]
83
+ ["--security", "security"],
84
+ ["--split", "split"],
85
+ ["--merge", "merge"],
86
+ ["--tag", "tag"],
87
+ ["--commit", "commit"],
88
+ ["--log", "log"],
89
+ ["--status", "status"],
90
+ ["--pipeline", "pipeline"]
35
91
  ]);
36
92
 
37
93
  const FORMAT_FLAGS = new Map([
@@ -40,36 +96,126 @@ const FORMAT_FLAGS = new Map([
40
96
  ["--html", "html"]
41
97
  ]);
42
98
 
99
+ const RESERVED_SUBCOMMANDS = new Set([
100
+ "help",
101
+ "example",
102
+ "install-hook",
103
+ "git",
104
+ "add",
105
+ "remove",
106
+ "del",
107
+ "bin",
108
+ "pop",
109
+ "pull",
110
+ "push",
111
+ "commit",
112
+ "merge",
113
+ "tag",
114
+ "release",
115
+ "log",
116
+ "status",
117
+ "pipeline"
118
+ ]);
119
+
43
120
  function printHelp() {
44
- console.log(`gitxplain - AI-powered Git and branch explainer
121
+ console.log(`gitxplain - AI-powered Git change analysis, review, and commit workflow CLI
45
122
 
46
123
  Usage:
47
124
  gitxplain help
48
125
  gitxplain --help
126
+ gitxplain example
127
+ gitxplain --example
128
+ gitxplain git <native-git-args...>
129
+
130
+ Git:
131
+ gitxplain commit
132
+ gitxplain --commit
133
+ gitxplain merge
134
+ gitxplain --merge
135
+ gitxplain tag
136
+ gitxplain --tag
137
+ gitxplain release
138
+ gitxplain release status
139
+ gitxplain log
140
+ gitxplain --log
141
+ gitxplain status
142
+ gitxplain --status
143
+ gitxplain --pipeline
144
+ gitxplain add <path> [more-paths...]
145
+ gitxplain remove <path> [more-paths...]
146
+ gitxplain remove hard
147
+ gitxplain del <path> [more-paths...]
148
+ gitxplain bin
149
+ gitxplain pop [stash-index]
150
+ gitxplain pull [remote] [branch]
151
+ gitxplain push [remote] [branch]
49
152
  gitxplain install-hook [hook-name]
153
+
154
+ GitHub:
155
+ gitxplain --connect-github [token]
156
+ gitxplain --boot [options]
157
+
158
+ Analysis:
50
159
  gitxplain <commit-id> [options]
51
160
  gitxplain <start>..<end> [options]
52
161
  gitxplain --branch [base-ref] [options]
53
162
  gitxplain --pr [base-ref] [options]
54
163
 
164
+ What It Does:
165
+ Analyze commits, ranges, branches, and working tree changes
166
+ Generate summaries, reviews, security checks, and line-by-line walkthroughs
167
+ Plan commits for uncommitted work and split oversized commits into atomic steps
168
+ Merge release-version branch changes into a dedicated release branch
169
+ Tag release-version commit windows on the current branch
170
+ Inspect release branch health, missing tags, and drift from the source branch
171
+ Inspect repository history and working tree status without calling the LLM
172
+ Inspect the current repository and scaffold GitHub Actions CI/CD workflows
173
+ Run quick local actions to stage, unstage, delete files, pop stashes, or push
174
+ Pull from a remote or soft-reset the latest commit without leaving the CLI
175
+ Pass through any native Git command and flags when you need the full Git surface
176
+
55
177
  Modes:
56
- --summary Generate a one-line summary
57
- --issues Focus on bug or issue analysis
58
- --fix Explain the fix in simple terms
178
+ --summary Generate a one-line summary of a change
179
+ --issues Focus on the bug, issue, or failure being addressed
180
+ --fix Explain the fix in simple, junior-friendly terms
59
181
  --impact Explain before-vs-after behavior changes
60
182
  --full Generate a full structured analysis
61
- --lines Explain the changed code line by line
62
- --review Generate a code review with risks and suggestions
63
- --security Focus on security risks introduced by the change
183
+ --lines Walk through the changed code file by file
184
+ --review Generate review findings, risks, and suggestions
185
+ --security Focus on security-relevant changes and concerns
186
+ --split Propose splitting a commit into smaller atomic commits
187
+ --merge Preview or apply a merge into the release branch based on version bumps
188
+ --tag Preview or create release tags based on version bumps
189
+ release Show release branch health, missing tags, and next recommended action
190
+ --commit Propose commits for current uncommitted changes
191
+ --log Print Git log entries for the current repository
192
+ --status Print Git working tree status for the current repository
193
+ --pipeline Detect the current repository stack and create CI/CD workflow files
194
+ --execute Execute a proposed split or commit plan
195
+ --dry-run Preview the plan without executing it (default for --split and --commit)
196
+
197
+ Quick Actions:
198
+ add Stage one or more files with git add
199
+ remove Unstage one or more files with git restore --staged
200
+ remove hard Hard reset the repository to HEAD
201
+ del Delete one or more files from the working tree
202
+ bin Soft reset HEAD~1 while keeping your changes
203
+ pop Pop a stash entry with a plain numeric index like "pop 2"
204
+ pull Run git pull, optionally with a remote and branch
205
+ push Run git push, optionally with a remote and branch
64
206
 
65
207
  Output:
66
- --json Print JSON output
208
+ --json Print structured JSON output
67
209
  --markdown Print Markdown output
68
210
  --html Print HTML output
69
- --quiet Print only the explanation body
211
+ --quiet Print only the main body without extra framing
70
212
  --verbose Print provider, model, cache, latency, and usage details
71
213
  --clipboard Copy the final output to the system clipboard
72
- --stream Stream the explanation as it is generated when supported
214
+ --stream Stream model output as it is generated when supported
215
+
216
+ GitHub:
217
+ --connect-github Save your GitHub Personal Access Token to act autonomously inside Chat
218
+ --boot Launch an interactive chat session for dynamic querying, PR creation, and cloning
73
219
 
74
220
  Providers:
75
221
  --provider LLM provider: openai, groq, openrouter, gemini, ollama, chutes
@@ -79,15 +225,8 @@ Diff Budget:
79
225
  --max-diff-lines <n> Limit diff lines sent to the model
80
226
 
81
227
  Comparison:
82
- --branch [base-ref] Analyze current branch against base branch
83
- --pr [base-ref] Alias for --branch, useful for PR-style summaries
84
-
85
- Examples:
86
- gitxplain HEAD~1 --full
87
- gitxplain HEAD~5..HEAD --markdown
88
- gitxplain --branch main --review
89
- gitxplain --pr origin/main --security --stream
90
- gitxplain HEAD~1 --provider chutes --model deepseek-ai/DeepSeek-V3-0324
228
+ --branch [base-ref] Analyze the current branch against a base branch
229
+ --pr [base-ref] Alias for --branch, useful for PR-style comparisons
91
230
 
92
231
  Provider Setup:
93
232
  OpenAI:
@@ -124,7 +263,51 @@ Hook Installation:
124
263
 
125
264
  Notes:
126
265
  Run gitxplain inside a Git repository.
266
+ If no mode is supplied, gitxplain will prompt you to choose one interactively.
127
267
  Use --provider or --model to override your config or environment for one command.
268
+ Use gitxplain git <args...> to run any native Git subcommand with its normal flags.
269
+ `);
270
+ }
271
+
272
+ function printExamples() {
273
+ console.log(`gitxplain examples
274
+
275
+ Examples:
276
+ gitxplain --connect-github <token>
277
+ gitxplain --boot
278
+ gitxplain HEAD~1 --full
279
+ gitxplain HEAD~1 --review
280
+ gitxplain HEAD~5..HEAD --markdown
281
+ gitxplain --branch main --review
282
+ gitxplain --pr origin/main --security --stream
283
+ gitxplain commit
284
+ gitxplain --commit --execute
285
+ gitxplain merge
286
+ gitxplain --merge --execute
287
+ gitxplain tag
288
+ gitxplain --tag --execute
289
+ gitxplain release
290
+ gitxplain release status
291
+ gitxplain log
292
+ gitxplain --log
293
+ gitxplain status
294
+ gitxplain --status
295
+ gitxplain pipeline
296
+ gitxplain --pipeline
297
+ gitxplain add README.md
298
+ gitxplain remove README.md
299
+ gitxplain remove hard
300
+ gitxplain del scratch.txt
301
+ gitxplain bin
302
+ gitxplain pop
303
+ gitxplain pop 2
304
+ gitxplain pull
305
+ gitxplain pull origin main
306
+ gitxplain push
307
+ gitxplain push origin main
308
+ gitxplain HEAD~1 --split
309
+ gitxplain HEAD --split --execute
310
+ gitxplain HEAD~1 --provider chutes --model deepseek-ai/DeepSeek-V3-0324
128
311
  `);
129
312
  }
130
313
 
@@ -156,9 +339,22 @@ function parseNumber(value, fallback = null) {
156
339
  return parsed;
157
340
  }
158
341
 
159
- export function parseArgs(argv) {
342
+ function isDirectNativeGitSubcommand(subcommand, knownGitSubcommands) {
343
+ if (!subcommand || subcommand.startsWith("-")) {
344
+ return false;
345
+ }
346
+
347
+ if (RESERVED_SUBCOMMANDS.has(subcommand)) {
348
+ return false;
349
+ }
350
+
351
+ return knownGitSubcommands.has(subcommand);
352
+ }
353
+
354
+ export function parseArgs(argv, options = {}) {
160
355
  const args = argv.slice(2);
161
356
  const subcommand = args[0];
357
+ const knownGitSubcommands = options.gitSubcommands ?? listGitSubcommands();
162
358
  const flags = new Set(args.filter((arg) => arg.startsWith("--")));
163
359
  const valueFlags = new Set(["--provider", "--model", "--max-diff-lines", "--branch", "--pr"]);
164
360
  const positional = [];
@@ -186,13 +382,84 @@ export function parseArgs(argv) {
186
382
  const explicitMode = [...MODE_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
187
383
  const explicitFormat = [...FORMAT_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
188
384
  const isInstallHook = subcommand === "install-hook";
385
+ const isExample = flags.has("--example") || subcommand === "example";
386
+ const isNativeGitWrapper = subcommand === "git";
387
+ const isConnectGitHub = flags.has("--connect-github") || flags.has("--connect-git");
388
+ const isBoot = flags.has("--boot");
389
+ const isLogCommand = subcommand === "log";
390
+ const isStatusCommand = subcommand === "status";
391
+ const isCommitCommand = subcommand === "commit";
392
+ const isMergeCommand = subcommand === "merge";
393
+ const isTagCommand = subcommand === "tag";
394
+ const isReleaseCommand = subcommand === "release";
395
+ const isAddCommand = subcommand === "add";
396
+ const isRemoveCommand = subcommand === "remove";
397
+ const isDeleteCommand = subcommand === "del";
398
+ const isPipelineCommand = subcommand === "pipeline" || flags.has("--pipeline");
399
+ const isBinCommand = subcommand === "bin";
400
+ const isPopCommand = subcommand === "pop";
401
+ const isPullCommand = subcommand === "pull";
402
+ const isPushCommand = subcommand === "push";
403
+ const isRemoveHardCommand = isRemoveCommand && positional[1] === "hard" && positional.length === 2;
404
+ const isNativeGitCommand = isNativeGitWrapper || isDirectNativeGitSubcommand(subcommand, knownGitSubcommands);
189
405
 
190
406
  return {
191
407
  subcommand,
192
408
  help: flags.has("--help") || subcommand === "help",
409
+ example: isExample,
410
+ nativeGitCommand: isNativeGitCommand,
193
411
  installHook: isInstallHook,
412
+ connectGitHub: isConnectGitHub,
413
+ boot: isBoot,
414
+ logCommand: isLogCommand,
415
+ statusCommand: isStatusCommand,
416
+ commitCommand: isCommitCommand,
417
+ mergeCommand: isMergeCommand,
418
+ tagCommand: isTagCommand,
419
+ releaseCommand: isReleaseCommand,
420
+ releaseAction: isReleaseCommand ? positional[1] ?? "status" : null,
421
+ addCommand: isAddCommand,
422
+ removeCommand: isRemoveCommand,
423
+ deleteCommand: isDeleteCommand,
424
+ pipelineCommand: isPipelineCommand,
425
+ binCommand: isBinCommand,
426
+ popCommand: isPopCommand,
427
+ pullCommand: isPullCommand,
428
+ pushCommand: isPushCommand,
429
+ removeHardCommand: isRemoveHardCommand,
430
+ nativeGitArgs: isNativeGitWrapper ? args.slice(1) : isNativeGitCommand ? args : [],
194
431
  hookName: isInstallHook ? positional[1] ?? "post-commit" : null,
195
- commitRef: isInstallHook || subcommand === "help" ? null : positional[0] ?? null,
432
+ actionPaths:
433
+ isAddCommand || isDeleteCommand ? positional.slice(1) : isRemoveHardCommand ? [] : isRemoveCommand ? positional.slice(1) : [],
434
+ connectToken: isConnectGitHub ? positional[0] : null,
435
+ stashIndex: isPopCommand ? positional[1] ?? null : null,
436
+ pullRemote: isPullCommand ? positional[1] ?? null : null,
437
+ pullBranch: isPullCommand ? positional[2] ?? null : null,
438
+ pushRemote: isPushCommand ? positional[1] ?? null : null,
439
+ pushBranch: isPushCommand ? positional[2] ?? null : null,
440
+ commitRef:
441
+ isInstallHook ||
442
+ isExample ||
443
+ isNativeGitCommand ||
444
+ isConnectGitHub ||
445
+ isBoot ||
446
+ isLogCommand ||
447
+ isStatusCommand ||
448
+ isCommitCommand ||
449
+ isMergeCommand ||
450
+ isTagCommand ||
451
+ isReleaseCommand ||
452
+ isAddCommand ||
453
+ isRemoveCommand ||
454
+ isDeleteCommand ||
455
+ isPipelineCommand ||
456
+ isBinCommand ||
457
+ isPopCommand ||
458
+ isPullCommand ||
459
+ isPushCommand ||
460
+ subcommand === "help"
461
+ ? null
462
+ : positional[0] ?? null,
196
463
  mode: explicitMode,
197
464
  format: explicitFormat,
198
465
  provider: getFlagValue(args, "--provider"),
@@ -205,7 +472,13 @@ export function parseArgs(argv) {
205
472
  clipboard: flags.has("--clipboard"),
206
473
  stream: flags.has("--stream"),
207
474
  verbose: flags.has("--verbose"),
208
- quiet: flags.has("--quiet")
475
+ quiet: flags.has("--quiet"),
476
+ execute: flags.has("--execute"),
477
+ dryRun: flags.has("--dry-run"),
478
+ log: flags.has("--log"),
479
+ status: flags.has("--status"),
480
+ merge: flags.has("--merge"),
481
+ tag: flags.has("--tag")
209
482
  };
210
483
  }
211
484
 
@@ -233,6 +506,12 @@ async function chooseModeInteractively() {
233
506
  "6. Line-by-Line Code Walkthrough",
234
507
  "7. Code Review",
235
508
  "8. Security Review",
509
+ "9. Split Commit",
510
+ "10. Merge To Release Branch",
511
+ "11. Tag Release Commits",
512
+ "12. Repository Log",
513
+ "13. Commit Working Tree",
514
+ "14. Create CI/CD Pipelines",
236
515
  "> "
237
516
  ].join("\n")
238
517
  );
@@ -245,7 +524,13 @@ async function chooseModeInteractively() {
245
524
  "5": "full",
246
525
  "6": "lines",
247
526
  "7": "review",
248
- "8": "security"
527
+ "8": "security",
528
+ "9": "split",
529
+ "10": "merge",
530
+ "11": "tag",
531
+ "12": "log",
532
+ "13": "commit",
533
+ "14": "pipeline"
249
534
  };
250
535
 
251
536
  return selections[answer] ?? "full";
@@ -278,6 +563,15 @@ function resolveTargetRef(parsed, cwd) {
278
563
  return null;
279
564
  }
280
565
 
566
+ export function buildBootSessionArgs(connection, userInfo, parsed) {
567
+ return {
568
+ token: connection.token,
569
+ providerOverride: parsed.provider,
570
+ modelOverride: parsed.model,
571
+ username: userInfo.name || connection.user?.login || null
572
+ };
573
+ }
574
+
281
575
  function renderFinalOutput({ runtimeOptions, mode, commitData, explanation, responseMeta, promptMeta }) {
282
576
  if (runtimeOptions.format === "json") {
283
577
  return formatJsonOutput({ mode, commitData, explanation, responseMeta, promptMeta });
@@ -305,12 +599,24 @@ export async function main(argv = process.argv) {
305
599
  const cwd = process.cwd();
306
600
  const config = loadConfig(cwd);
307
601
  const parsed = parseArgs(argv);
602
+ const hasNoCommandOrFlags = argv.slice(2).length === 0;
603
+
604
+ loadEnvFile(cwd); // Ensure environment is loaded first
308
605
 
309
- if (parsed.help) {
606
+ if (parsed.help || hasNoCommandOrFlags) {
310
607
  printHelp();
311
608
  return 0;
312
609
  }
313
610
 
611
+ if (parsed.example) {
612
+ printExamples();
613
+ return 0;
614
+ }
615
+
616
+ if (parsed.nativeGitCommand) {
617
+ return runNativeGitPassthrough(parsed.nativeGitArgs, cwd);
618
+ }
619
+
314
620
  if (!isGitRepository(cwd)) {
315
621
  console.error("gitxplain must be run inside a Git repository.");
316
622
  return 1;
@@ -322,7 +628,319 @@ export async function main(argv = process.argv) {
322
628
  return 0;
323
629
  }
324
630
 
631
+ if (parsed.connectGitHub) {
632
+ let token = parsed.connectToken;
633
+ if (!token) {
634
+ if (process.env.GITHUB_TOKEN) {
635
+ token = process.env.GITHUB_TOKEN;
636
+ } else {
637
+ console.error("Please provide your GitHub Personal Access Token.\nRun: gitxplain --connect-github <YOUR_TOKEN>\nOr set it in your .env as GITHUB_TOKEN=...");
638
+ return 1;
639
+ }
640
+ }
641
+ try {
642
+ console.log("Verifying token with GitHub API...");
643
+ const userInfo = await verifyGitToken(token);
644
+ await saveGitConnection(token, "github", userInfo);
645
+ console.log(`\nSuccessfully connected to GitHub as: \x1b[36m${userInfo.login}\x1b[0m`);
646
+ console.log(`Token saved securely to your local configuration.\n`);
647
+ } catch (e) {
648
+ console.error(`Token verification failed: ${e.message}`);
649
+ return 1;
650
+ }
651
+ return 0;
652
+ }
653
+
654
+ if (parsed.boot) {
655
+ if (!isGitConnected()) {
656
+ console.error("You must connect a GitHub account first to use the interactive agent.\nCommand: gitxplain --connect-github <YOUR_TOKEN>");
657
+ return 1;
658
+ }
659
+ const connection = loadGitConnection();
660
+ const userInfo = getGitUserInfo();
661
+ try {
662
+ const { getProviderConfig, validateProviderConfig } = await import(
663
+ "./services/aiService.js"
664
+ );
665
+ const config = getProviderConfig(parsed.provider, parsed.model);
666
+ validateProviderConfig(config);
667
+ const sessionArgs = buildBootSessionArgs(connection, userInfo, parsed);
668
+ await startChatSession(
669
+ sessionArgs.token,
670
+ sessionArgs.providerOverride,
671
+ sessionArgs.modelOverride,
672
+ sessionArgs.username
673
+ );
674
+ } catch (configError) {
675
+ console.error(`Missing LLM Key. Please check your .env variables or --provider flags.\n${configError.message}`);
676
+ return 1;
677
+ }
678
+ return 0;
679
+ }
680
+
681
+ if (parsed.logCommand || parsed.log) {
682
+ console.log(getRepositoryLog(cwd));
683
+ return 0;
684
+ }
685
+
686
+ if (parsed.statusCommand || parsed.status) {
687
+ console.log(getRepositoryStatus(cwd));
688
+ return 0;
689
+ }
690
+
691
+ if (parsed.releaseCommand) {
692
+ if (parsed.releaseAction !== "status") {
693
+ throw new Error(`Unknown release subcommand: ${parsed.releaseAction}`);
694
+ }
695
+
696
+ console.log(formatReleaseStatus(buildReleaseStatus(cwd)));
697
+ return 0;
698
+ }
699
+
700
+ if (parsed.pipelineCommand) {
701
+ const analysis = inspectRepositoryForPipeline(cwd);
702
+
703
+ if (!analysis.supported) {
704
+ console.log(analysis.reason);
705
+ return 1;
706
+ }
707
+
708
+ console.log(formatPipelineRecommendations(analysis));
709
+
710
+ const answer = await askQuestion(
711
+ `\nChoose a pipeline option (1-${analysis.options.length}) or type "cancel" > `
712
+ );
713
+ const selection = resolvePipelineSelection(analysis, answer);
714
+
715
+ if (!selection) {
716
+ console.log("Aborted.");
717
+ return 0;
718
+ }
719
+
720
+ const { writtenFiles, notes } = writePipelineFiles(cwd, analysis, selection);
721
+ console.log(`\nCreated workflow files: ${writtenFiles.join(", ")}`);
722
+
723
+ if (notes.length > 0) {
724
+ console.log(`\n${notes.join("\n")}`);
725
+ }
726
+
727
+ return 0;
728
+ }
729
+
730
+ if (
731
+ parsed.addCommand ||
732
+ parsed.removeCommand ||
733
+ parsed.deleteCommand ||
734
+ parsed.binCommand ||
735
+ parsed.popCommand ||
736
+ parsed.pullCommand ||
737
+ parsed.pushCommand
738
+ ) {
739
+ if (!parsed.popCommand && !parsed.binCommand && !parsed.pullCommand && !parsed.removeHardCommand && parsed.actionPaths.length === 0) {
740
+ if (!parsed.pushCommand) {
741
+ throw new Error(`No paths provided for "${parsed.subcommand}".`);
742
+ }
743
+ }
744
+
745
+ if (parsed.addCommand) {
746
+ gitAddFiles(parsed.actionPaths, cwd);
747
+ console.log(`Staged ${parsed.actionPaths.join(", ")}.`);
748
+ return 0;
749
+ }
750
+
751
+ if (parsed.removeCommand) {
752
+ if (parsed.removeHardCommand) {
753
+ gitResetHard("HEAD", cwd);
754
+ console.log("Hard reset to HEAD.");
755
+ return 0;
756
+ }
757
+
758
+ gitRestoreStaged(parsed.actionPaths, cwd);
759
+ console.log(`Unstaged ${parsed.actionPaths.join(", ")}.`);
760
+ return 0;
761
+ }
762
+
763
+ if (parsed.deleteCommand) {
764
+ deletePaths(parsed.actionPaths, cwd);
765
+ console.log(`Deleted ${parsed.actionPaths.join(", ")}.`);
766
+ return 0;
767
+ }
768
+
769
+ if (parsed.binCommand) {
770
+ gitResetSoft(cwd);
771
+ console.log("Soft reset HEAD~1 and kept your changes.");
772
+ return 0;
773
+ }
774
+
775
+ if (parsed.popCommand) {
776
+ const stashRef = resolveStashRef(parsed.stashIndex);
777
+ gitStashPop(parsed.stashIndex, cwd);
778
+ console.log(`Popped ${stashRef}.`);
779
+ return 0;
780
+ }
781
+
782
+ if (parsed.pullCommand) {
783
+ gitPull(cwd, parsed.pullRemote, parsed.pullBranch);
784
+ console.log(
785
+ `Pulled${parsed.pullRemote ? ` from ${parsed.pullRemote}` : ""}${parsed.pullBranch ? ` ${parsed.pullBranch}` : ""}.`
786
+ );
787
+ return 0;
788
+ }
789
+
790
+ gitPush(cwd, parsed.pushRemote, parsed.pushBranch);
791
+ console.log(
792
+ `Pushed${parsed.pushRemote ? ` to ${parsed.pushRemote}` : ""}${parsed.pushBranch ? ` ${parsed.pushBranch}` : ""}.`
793
+ );
794
+ return 0;
795
+ }
796
+
325
797
  const runtimeOptions = resolveRuntimeOptions(parsed, config);
798
+ const mode = parsed.mode ?? config.mode ?? (await chooseModeInteractively());
799
+
800
+ if (mode === "pipeline") {
801
+ const analysis = inspectRepositoryForPipeline(cwd);
802
+
803
+ if (!analysis.supported) {
804
+ console.log(analysis.reason);
805
+ return 1;
806
+ }
807
+
808
+ console.log(formatPipelineRecommendations(analysis));
809
+
810
+ const answer = await askQuestion(
811
+ `\nChoose a pipeline option (1-${analysis.options.length}) or type "cancel" > `
812
+ );
813
+ const selection = resolvePipelineSelection(analysis, answer);
814
+
815
+ if (!selection) {
816
+ console.log("Aborted.");
817
+ return 0;
818
+ }
819
+
820
+ const { writtenFiles, notes } = writePipelineFiles(cwd, analysis, selection);
821
+ console.log(`\nCreated workflow files: ${writtenFiles.join(", ")}`);
822
+
823
+ if (notes.length > 0) {
824
+ console.log(`\n${notes.join("\n")}`);
825
+ }
826
+
827
+ return 0;
828
+ }
829
+
830
+ if (mode === "commit" || parsed.commitCommand) {
831
+ const commitData = fetchWorkingTreeData(cwd);
832
+
833
+ if (commitData.filesChanged.length === 0 || commitData.diff === "") {
834
+ console.log("Working tree is clean. Nothing to commit.");
835
+ return 0;
836
+ }
837
+
838
+ const { explanation, responseMeta, promptMeta } = await generateExplanation({
839
+ mode: "commit",
840
+ commitData,
841
+ providerOverride: runtimeOptions.provider,
842
+ modelOverride: runtimeOptions.model,
843
+ maxDiffLines: runtimeOptions.maxDiffLines,
844
+ stream: false,
845
+ onChunk: null,
846
+ onStart: null
847
+ });
848
+
849
+ const plan = reconcileCommitPlan(parseCommitPlan(explanation), cwd);
850
+
851
+ if (!plan.reason_to_commit || plan.commits.length === 0) {
852
+ console.log("No meaningful commit grouping recommended.");
853
+ return 0;
854
+ }
855
+
856
+ console.log(formatCommitPlan(plan));
857
+
858
+ if (parsed.execute && !parsed.dryRun) {
859
+ const confirmed = await askQuestion(
860
+ "\nThis will create new commits from your working tree changes. Continue? (yes/no) > "
861
+ );
862
+ if (confirmed.toLowerCase() !== "yes") {
863
+ console.log("Aborted.");
864
+ return 0;
865
+ }
866
+
867
+ executeCommitPlan(plan, cwd);
868
+ console.log(`\nCommit complete. Created ${plan.commits.length} commits.`);
869
+ } else {
870
+ console.log("\nThis is a preview. Run with --execute to apply the commit plan.");
871
+ }
872
+
873
+ if (runtimeOptions.verbose) {
874
+ process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
875
+ }
876
+
877
+ return 0;
878
+ }
879
+
880
+ if (mode === "merge" || parsed.mergeCommand || parsed.merge) {
881
+ if (parsed.commitRef) {
882
+ throw new Error("--merge works from the current branch and does not accept a commit ref.");
883
+ }
884
+
885
+ const plan = finalizeReleaseMergePlan(buildReleaseMergePlan(cwd));
886
+
887
+ if (plan.windows.length === 0) {
888
+ console.log("No unreleased release commits detected. Nothing to merge.");
889
+ return 0;
890
+ }
891
+
892
+ console.log(formatReleaseMergePlan(plan));
893
+
894
+ if (parsed.execute && !parsed.dryRun) {
895
+ const confirmed = await askQuestion(
896
+ `\nThis will create ${plan.windows.length} release commit(s) on ${plan.releaseBranch}. Continue? (yes/no) > `
897
+ );
898
+ if (confirmed.toLowerCase() !== "yes") {
899
+ console.log("Aborted.");
900
+ return 0;
901
+ }
902
+
903
+ executeReleaseMerge(plan, cwd);
904
+ console.log(`\nRelease promotion complete. Created ${plan.windows.length} release commit(s) on ${plan.releaseBranch}.`);
905
+ } else {
906
+ console.log(`\nThis is a preview. Run with --execute to create release commits on ${plan.releaseBranch}.`);
907
+ }
908
+
909
+ return 0;
910
+ }
911
+
912
+ if (mode === "tag" || parsed.tagCommand || parsed.tag) {
913
+ if (parsed.commitRef) {
914
+ throw new Error("--tag works from the current branch and does not accept a commit ref.");
915
+ }
916
+
917
+ const plan = finalizeReleaseTagPlan(buildReleaseTagPlan(cwd));
918
+
919
+ if (plan.tags.length === 0) {
920
+ console.log("No unreleased release tags detected. Nothing to tag.");
921
+ return 0;
922
+ }
923
+
924
+ console.log(formatReleaseTagPlan(plan));
925
+
926
+ if (parsed.execute && !parsed.dryRun) {
927
+ const confirmed = await askQuestion(
928
+ `\nThis will create ${plan.tags.length} release tag(s). Continue? (yes/no) > `
929
+ );
930
+ if (confirmed.toLowerCase() !== "yes") {
931
+ console.log("Aborted.");
932
+ return 0;
933
+ }
934
+
935
+ executeReleaseTagPlan(plan, cwd);
936
+ console.log(`\nRelease tagging complete. Created ${plan.tags.length} release tag(s).`);
937
+ } else {
938
+ console.log("\nThis is a preview. Run with --execute to create release tags.");
939
+ }
940
+
941
+ return 0;
942
+ }
943
+
326
944
  const targetRef = resolveTargetRef(parsed, cwd);
327
945
 
328
946
  if (!targetRef) {
@@ -330,8 +948,56 @@ export async function main(argv = process.argv) {
330
948
  return 1;
331
949
  }
332
950
 
333
- const mode = parsed.mode ?? config.mode ?? (await chooseModeInteractively());
334
951
  const commitData = fetchCommitData(targetRef, cwd);
952
+
953
+ if (mode === "split") {
954
+ if (commitData.analysisType !== "commit") {
955
+ throw new Error("--split only supports analyzing a single commit.");
956
+ }
957
+
958
+ const { explanation, responseMeta, promptMeta } = await generateExplanation({
959
+ mode: "split",
960
+ commitData,
961
+ providerOverride: runtimeOptions.provider,
962
+ modelOverride: runtimeOptions.model,
963
+ maxDiffLines: runtimeOptions.maxDiffLines,
964
+ stream: false,
965
+ onChunk: null,
966
+ onStart: null
967
+ });
968
+
969
+ const plan = reconcileSplitPlan(parseSplitPlan(explanation), commitData.filesChanged);
970
+
971
+ if (!plan.reason_to_split || plan.commits.length === 0) {
972
+ console.log("This commit is already atomic. No split recommended.");
973
+ return 0;
974
+ }
975
+
976
+ console.log(formatSplitPlan(plan));
977
+
978
+ if (parsed.execute && !parsed.dryRun) {
979
+ validateSplitExecutionTarget(commitData.commitId, cwd);
980
+ const confirmed = await askQuestion(
981
+ "\nThis will rewrite git history. Continue? (yes/no) > "
982
+ );
983
+ if (confirmed.toLowerCase() !== "yes") {
984
+ console.log("Aborted.");
985
+ return 0;
986
+ }
987
+
988
+ executeSplit(plan, commitData.commitId, cwd);
989
+ console.log(`\nSplit complete. Created ${plan.commits.length} commits.`);
990
+ } else {
991
+ console.log("\nThis is a preview. Run with --execute to apply the split.");
992
+ }
993
+
994
+ if (runtimeOptions.verbose) {
995
+ process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
996
+ }
997
+
998
+ return 0;
999
+ }
1000
+
335
1001
  const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
336
1002
  let streamStarted = false;
337
1003