gitxplain 0.1.6 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/index.js CHANGED
@@ -3,23 +3,28 @@
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 { startChatSession } from "./services/chatService.js";
8
+ import { clearCache, getCacheStats } from "./services/cacheService.js";
9
9
  import { loadEnvFile } from "./services/envLoader.js";
10
- import {
11
- saveGitConnection,
12
- isGitConnected,
13
- loadGitConnection,
14
- getGitUserInfo,
15
- verifyGitToken
16
- } from "./services/gitConnectionService.js";
17
10
  import { copyToClipboard } from "./services/clipboardService.js";
18
- import { loadConfig } from "./services/configService.js";
11
+ import { getUsageStats } from "./services/usageService.js";
12
+ import {
13
+ applyConfigEnvironment,
14
+ getProviderApiKeyField,
15
+ getUserConfigPath,
16
+ loadConfig,
17
+ loadUserConfig,
18
+ updateUserConfig
19
+ } from "./services/configService.js";
19
20
  import {
20
21
  buildBranchRange,
21
22
  deletePaths,
23
+ fetchBlameData,
22
24
  fetchCommitData,
25
+ fetchCommitDataForFile,
26
+ fetchConflictData,
27
+ fetchStashData,
23
28
  fetchWorkingTreeData,
24
29
  gitAddFiles,
25
30
  gitPull,
@@ -81,10 +86,18 @@ const MODE_FLAGS = new Map([
81
86
  ["--lines", "lines"],
82
87
  ["--review", "review"],
83
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"],
84
96
  ["--split", "split"],
85
97
  ["--merge", "merge"],
86
98
  ["--tag", "tag"],
87
99
  ["--commit", "commit"],
100
+ ["--release", "release"],
88
101
  ["--log", "log"],
89
102
  ["--status", "status"],
90
103
  ["--pipeline", "pipeline"]
@@ -96,9 +109,29 @@ 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",
101
- "example",
133
+ "cache",
134
+ "config",
102
135
  "install-hook",
103
136
  "git",
104
137
  "add",
@@ -107,207 +140,114 @@ const RESERVED_SUBCOMMANDS = new Set([
107
140
  "bin",
108
141
  "pop",
109
142
  "pull",
110
- "push",
111
- "commit",
112
- "merge",
113
- "tag",
114
- "release",
115
- "log",
116
- "status",
117
- "pipeline"
143
+ "push"
118
144
  ]);
119
145
 
146
+ const CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
147
+
120
148
  function printHelp() {
121
149
  console.log(`gitxplain - AI-powered Git change analysis, review, and commit workflow CLI
122
150
 
123
151
  Usage:
124
- gitxplain help
125
152
  gitxplain --help
126
- gitxplain example
127
- gitxplain --example
128
- gitxplain git <native-git-args...>
129
-
130
- Git:
131
- gitxplain commit
153
+ gitxplain --version
154
+ gitxplain cache clear
155
+ gitxplain cache stats
156
+ gitxplain --cost
157
+ gitxplain install-hook [post-commit|post-merge|pre-push]
158
+ gitxplain config set provider <name>
159
+ gitxplain config set api-key <value> [--provider <name>]
160
+ gitxplain config get [key]
161
+ gitxplain config list
162
+ gitxplain <commit-id> [options]
163
+ gitxplain <start>..<end> [options]
164
+ gitxplain --branch [base-ref] [options]
165
+ gitxplain --pr [base-ref] [options]
132
166
  gitxplain --commit
133
- gitxplain merge
167
+ gitxplain --release [status]
134
168
  gitxplain --merge
135
- gitxplain tag
136
169
  gitxplain --tag
137
- gitxplain release
138
- gitxplain release status
139
- gitxplain log
170
+ gitxplain --conflict
171
+ gitxplain --stash [stash-ref]
140
172
  gitxplain --log
141
- gitxplain status
142
173
  gitxplain --status
143
174
  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]
152
- gitxplain install-hook [hook-name]
153
-
154
- GitHub:
155
- gitxplain --connect-github [token]
156
- gitxplain --boot [options]
157
175
 
158
176
  Analysis:
159
- gitxplain <commit-id> [options]
160
- gitxplain <start>..<end> [options]
161
- gitxplain --branch [base-ref] [options]
162
- gitxplain --pr [base-ref] [options]
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
-
177
- Modes:
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
181
- --impact Explain before-vs-after behavior changes
182
- --full Generate a full structured analysis
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)
177
+ --summary Generate a one-line summary of a change
178
+ --issues Focus on the issue or failure being addressed
179
+ --fix Explain the fix in simple terms
180
+ --impact Explain behavior changes before vs after
181
+ --full Generate a full structured analysis
182
+ --lines Walk through the changed code file by file
183
+ --review Generate review findings, risks, and suggestions
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}
192
+ --split Propose splitting a commit into smaller atomic commits
193
+ --cost Show cumulative token usage and estimated cost totals
194
+ --commit Propose commits for current uncommitted changes
195
+ --execute Execute a proposed split or commit plan
196
+ --dry-run Preview the plan without executing it
197
+ --interactive Review or edit a split plan before execution
198
+
199
+ Release:
200
+ --release [status] Show release branch health and next recommended action
201
+ --merge Preview or apply a merge into the release branch
202
+ --tag Preview or create release tags from version bumps
203
+
204
+ Repo:
205
+ --log Print Git log entries for the current repository
206
+ --status Print Git working tree status for the current repository
207
+ --pipeline Detect the current repository stack and create GitHub/GitLab/CircleCI/Bitbucket CI files
196
208
 
197
209
  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
210
+ config Persist provider, model, and API key settings
211
+ add Stage one or more files with git add
212
+ remove Unstage one or more files with git restore --staged
213
+ remove hard Hard reset the repository to HEAD
214
+ del Delete one or more files from the working tree
215
+ bin Soft reset HEAD~1 while keeping your changes
216
+ pop Pop a stash entry like "pop 2"
217
+ pull Run git pull, optionally with a remote and branch
218
+ push Run git push, optionally with a remote and branch
219
+ install-hook Install a post-commit, post-merge, or pre-push gitxplain hook
220
+ cache Manage gitxplain cache entries
221
+ git Pass through to native git commands
206
222
 
207
223
  Output:
208
- --json Print structured JSON output
209
- --markdown Print Markdown output
210
- --html Print HTML output
211
- --quiet Print only the main body without extra framing
212
- --verbose Print provider, model, cache, latency, and usage details
213
- --clipboard Copy the final output to the system clipboard
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
219
-
220
- Providers:
221
- --provider LLM provider: openai, groq, openrouter, gemini, ollama, chutes
222
- --model Override the model name
223
-
224
- Diff Budget:
225
- --max-diff-lines <n> Limit diff lines sent to the model
224
+ --provider <name>
225
+ --model <name>
226
+ --json
227
+ --markdown
228
+ --html
229
+ --quiet
230
+ --verbose
231
+ --clipboard
232
+ --stream
233
+ --no-cache
234
+ --diff <file>
235
+ --max-diff-lines <n>
226
236
 
227
237
  Comparison:
228
- --branch [base-ref] Analyze the current branch against a base branch
229
- --pr [base-ref] Alias for --branch, useful for PR-style comparisons
230
-
231
- Provider Setup:
232
- OpenAI:
233
- export LLM_PROVIDER=openai
234
- export OPENAI_API_KEY=your_key
235
-
236
- Groq:
237
- export LLM_PROVIDER=groq
238
- export GROQ_API_KEY=your_key
239
-
240
- OpenRouter:
241
- export LLM_PROVIDER=openrouter
242
- export OPENROUTER_API_KEY=your_key
243
-
244
- Gemini:
245
- export LLM_PROVIDER=gemini
246
- export GEMINI_API_KEY=your_key
247
-
248
- Ollama:
249
- export LLM_PROVIDER=ollama
250
- export OLLAMA_MODEL=llama3.2
251
-
252
- Chutes:
253
- export LLM_PROVIDER=chutes
254
- export CHUTES_API_KEY=your_key
238
+ --branch [base-ref] Analyze the current branch against a base branch
239
+ --pr [base-ref] Alias for --branch, useful for PR-style comparisons
255
240
 
256
241
  Config:
257
242
  Project config: .gitxplainrc or .gitxplainrc.json
258
- User config: ~/.gitxplain/config.json
259
-
260
- Hook Installation:
261
- gitxplain install-hook
262
- gitxplain install-hook post-commit
243
+ User config: ~/.gitxplain/config.json (macOS/Linux) or %USERPROFILE%\\.gitxplain\\config.json (Windows)
263
244
 
264
245
  Notes:
265
246
  Run gitxplain inside a Git repository.
266
- If no mode is supplied, gitxplain will prompt you to choose one interactively.
247
+ If no command or mode is supplied, gitxplain prints this help text.
267
248
  Use --provider or --model to override your config or environment for one command.
268
249
  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
250
+ install-hook supports: post-commit, post-merge, pre-push.
311
251
  `);
312
252
  }
313
253
 
@@ -339,6 +279,132 @@ function parseNumber(value, fallback = null) {
339
279
  return parsed;
340
280
  }
341
281
 
282
+ function redactConfigValue(key, value) {
283
+ if (typeof value !== "string") {
284
+ return value;
285
+ }
286
+
287
+ if (!/api[_-]?key/i.test(key)) {
288
+ return value;
289
+ }
290
+
291
+ if (value.length <= 8) {
292
+ return "*".repeat(value.length);
293
+ }
294
+
295
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
296
+ }
297
+
298
+ function printConfigEntries(config) {
299
+ const entries = Object.entries(config).sort(([left], [right]) => left.localeCompare(right));
300
+
301
+ if (entries.length === 0) {
302
+ console.log("No user config saved yet.");
303
+ return;
304
+ }
305
+
306
+ for (const [key, value] of entries) {
307
+ console.log(`${key}: ${redactConfigValue(key, value)}`);
308
+ }
309
+ }
310
+
311
+ function resolveConfigSetUpdate(parsed, currentConfig) {
312
+ const key = parsed.configKey;
313
+ const value = parsed.configValue;
314
+
315
+ if (!key || !value) {
316
+ throw new Error('Usage: gitxplain config set <provider|model|api-key> <value> [--provider <name>]');
317
+ }
318
+
319
+ if (key === "provider") {
320
+ return { provider: value.toLowerCase() };
321
+ }
322
+
323
+ if (key === "model") {
324
+ return { model: value };
325
+ }
326
+
327
+ if (key === "api-key") {
328
+ const resolvedProvider = (parsed.provider ?? currentConfig.provider ?? currentConfig.LLM_PROVIDER ?? "").toLowerCase();
329
+ const apiKeyField = getProviderApiKeyField(resolvedProvider);
330
+
331
+ if (!apiKeyField) {
332
+ throw new Error("Set a provider first with `gitxplain config set provider <name>`, or pass `--provider <name>`.");
333
+ }
334
+
335
+ return { [apiKeyField]: value };
336
+ }
337
+
338
+ return { [key]: value };
339
+ }
340
+
341
+ function handleConfigCommand(parsed) {
342
+ const currentConfig = loadUserConfig();
343
+
344
+ if (parsed.configAction === "list" || parsed.configAction == null) {
345
+ console.log(`User config: ${getUserConfigPath()}`);
346
+ printConfigEntries(currentConfig);
347
+ return 0;
348
+ }
349
+
350
+ if (parsed.configAction === "get") {
351
+ console.log(`User config: ${getUserConfigPath()}`);
352
+
353
+ if (!parsed.configKey) {
354
+ printConfigEntries(currentConfig);
355
+ return 0;
356
+ }
357
+
358
+ const value = currentConfig[parsed.configKey];
359
+ if (value === undefined) {
360
+ console.log(`No value saved for ${parsed.configKey}.`);
361
+ return 0;
362
+ }
363
+
364
+ console.log(`${parsed.configKey}: ${redactConfigValue(parsed.configKey, value)}`);
365
+ return 0;
366
+ }
367
+
368
+ if (parsed.configAction === "set") {
369
+ const updates = resolveConfigSetUpdate(parsed, currentConfig);
370
+ const { configPath } = updateUserConfig(updates);
371
+ const [savedKey, savedValue] = Object.entries(updates)[0];
372
+ console.log(`Saved ${savedKey} to ${configPath}.`);
373
+ console.log(`${savedKey}: ${redactConfigValue(savedKey, savedValue)}`);
374
+ return 0;
375
+ }
376
+
377
+ throw new Error(`Unknown config subcommand: ${parsed.configAction}`);
378
+ }
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
+
342
408
  function isDirectNativeGitSubcommand(subcommand, knownGitSubcommands) {
343
409
  if (!subcommand || subcommand.startsWith("-")) {
344
410
  return false;
@@ -356,7 +422,7 @@ export function parseArgs(argv, options = {}) {
356
422
  const subcommand = args[0];
357
423
  const knownGitSubcommands = options.gitSubcommands ?? listGitSubcommands();
358
424
  const flags = new Set(args.filter((arg) => arg.startsWith("--")));
359
- 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"]);
360
426
  const positional = [];
361
427
 
362
428
  for (let index = 0; index < args.length; index += 1) {
@@ -382,20 +448,14 @@ export function parseArgs(argv, options = {}) {
382
448
  const explicitMode = [...MODE_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
383
449
  const explicitFormat = [...FORMAT_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
384
450
  const isInstallHook = subcommand === "install-hook";
385
- const isExample = flags.has("--example") || subcommand === "example";
451
+ const isConfigCommand = subcommand === "config";
452
+ const isCacheCommand = subcommand === "cache";
386
453
  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";
454
+ const isReleaseCommand = flags.has("--release");
395
455
  const isAddCommand = subcommand === "add";
396
456
  const isRemoveCommand = subcommand === "remove";
397
457
  const isDeleteCommand = subcommand === "del";
398
- const isPipelineCommand = subcommand === "pipeline" || flags.has("--pipeline");
458
+ const isPipelineCommand = flags.has("--pipeline");
399
459
  const isBinCommand = subcommand === "bin";
400
460
  const isPopCommand = subcommand === "pop";
401
461
  const isPullCommand = subcommand === "pull";
@@ -406,18 +466,18 @@ export function parseArgs(argv, options = {}) {
406
466
  return {
407
467
  subcommand,
408
468
  help: flags.has("--help") || subcommand === "help",
409
- example: isExample,
469
+ version: flags.has("--version"),
470
+ cost: flags.has("--cost"),
410
471
  nativeGitCommand: isNativeGitCommand,
411
472
  installHook: isInstallHook,
412
- connectGitHub: isConnectGitHub,
413
- boot: isBoot,
414
- logCommand: isLogCommand,
415
- statusCommand: isStatusCommand,
416
- commitCommand: isCommitCommand,
417
- mergeCommand: isMergeCommand,
418
- tagCommand: isTagCommand,
473
+ configCommand: isConfigCommand,
474
+ cacheCommand: isCacheCommand,
475
+ configAction: isConfigCommand ? positional[1] ?? null : null,
476
+ configKey: isConfigCommand ? positional[2] ?? null : null,
477
+ configValue: isConfigCommand ? positional.slice(3).join(" ") || null : null,
478
+ cacheAction: isCacheCommand ? positional[1] ?? null : null,
419
479
  releaseCommand: isReleaseCommand,
420
- releaseAction: isReleaseCommand ? positional[1] ?? "status" : null,
480
+ releaseAction: isReleaseCommand ? positional[0] ?? "status" : null,
421
481
  addCommand: isAddCommand,
422
482
  removeCommand: isRemoveCommand,
423
483
  deleteCommand: isDeleteCommand,
@@ -431,7 +491,6 @@ export function parseArgs(argv, options = {}) {
431
491
  hookName: isInstallHook ? positional[1] ?? "post-commit" : null,
432
492
  actionPaths:
433
493
  isAddCommand || isDeleteCommand ? positional.slice(1) : isRemoveHardCommand ? [] : isRemoveCommand ? positional.slice(1) : [],
434
- connectToken: isConnectGitHub ? positional[0] : null,
435
494
  stashIndex: isPopCommand ? positional[1] ?? null : null,
436
495
  pullRemote: isPullCommand ? positional[1] ?? null : null,
437
496
  pullBranch: isPullCommand ? positional[2] ?? null : null,
@@ -439,15 +498,9 @@ export function parseArgs(argv, options = {}) {
439
498
  pushBranch: isPushCommand ? positional[2] ?? null : null,
440
499
  commitRef:
441
500
  isInstallHook ||
442
- isExample ||
501
+ isConfigCommand ||
502
+ isCacheCommand ||
443
503
  isNativeGitCommand ||
444
- isConnectGitHub ||
445
- isBoot ||
446
- isLogCommand ||
447
- isStatusCommand ||
448
- isCommitCommand ||
449
- isMergeCommand ||
450
- isTagCommand ||
451
504
  isReleaseCommand ||
452
505
  isAddCommand ||
453
506
  isRemoveCommand ||
@@ -465,16 +518,22 @@ export function parseArgs(argv, options = {}) {
465
518
  provider: getFlagValue(args, "--provider"),
466
519
  model: getFlagValue(args, "--model"),
467
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"),
468
524
  hasBranchFlag: flags.has("--branch") || args.some((arg) => arg.startsWith("--branch=")),
469
525
  branchBase: getFlagValue(args, "--branch"),
470
526
  hasPrFlag: flags.has("--pr") || args.some((arg) => arg.startsWith("--pr=")),
471
527
  prBase: getFlagValue(args, "--pr"),
472
528
  clipboard: flags.has("--clipboard"),
473
529
  stream: flags.has("--stream"),
530
+ noCache: flags.has("--no-cache"),
474
531
  verbose: flags.has("--verbose"),
475
532
  quiet: flags.has("--quiet"),
476
533
  execute: flags.has("--execute"),
477
534
  dryRun: flags.has("--dry-run"),
535
+ interactive: flags.has("--interactive"),
536
+ release: flags.has("--release"),
478
537
  log: flags.has("--log"),
479
538
  status: flags.has("--status"),
480
539
  merge: flags.has("--merge"),
@@ -494,62 +553,86 @@ function askQuestion(prompt) {
494
553
  });
495
554
  }
496
555
 
497
- async function chooseModeInteractively() {
498
- const answer = await askQuestion(
499
- [
500
- "What do you want to know?",
501
- "1. Summary",
502
- "2. Issues Fixed",
503
- "3. Fix Explanation",
504
- "4. Impact",
505
- "5. Full Analysis",
506
- "6. Line-by-Line Code Walkthrough",
507
- "7. Code Review",
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",
515
- "> "
516
- ].join("\n")
517
- );
518
-
519
- const selections = {
520
- "1": "summary",
521
- "2": "issues",
522
- "3": "fix",
523
- "4": "impact",
524
- "5": "full",
525
- "6": "lines",
526
- "7": "review",
527
- "8": "security",
528
- "9": "split",
529
- "10": "merge",
530
- "11": "tag",
531
- "12": "log",
532
- "13": "commit",
533
- "14": "pipeline"
534
- };
535
-
536
- return selections[answer] ?? "full";
556
+ function resolveConfiguredAnalysisMode(config) {
557
+ return ANALYSIS_MODES.has(config.mode) ? config.mode : "full";
537
558
  }
538
559
 
539
560
  function resolveRuntimeOptions(parsed, config) {
540
561
  return {
541
- mode: parsed.mode ?? config.mode ?? "full",
562
+ mode: parsed.mode ?? resolveConfiguredAnalysisMode(config),
542
563
  format: parsed.format ?? config.format ?? "plain",
543
564
  provider: parsed.provider ?? config.provider ?? null,
544
565
  model: parsed.model ?? config.model ?? null,
545
566
  maxDiffLines: parsed.maxDiffLines ?? config.maxDiffLines ?? 800,
546
567
  clipboard: parsed.clipboard || config.clipboard === true,
547
568
  stream: parsed.stream || config.stream === true,
569
+ noCache: parsed.noCache,
548
570
  verbose: parsed.verbose || config.verbose === true,
549
571
  quiet: parsed.quiet || config.quiet === true
550
572
  };
551
573
  }
552
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
+
553
636
  function resolveTargetRef(parsed, cwd) {
554
637
  if (parsed.commitRef) {
555
638
  return parsed.commitRef;
@@ -563,15 +646,6 @@ function resolveTargetRef(parsed, cwd) {
563
646
  return null;
564
647
  }
565
648
 
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
-
575
649
  function renderFinalOutput({ runtimeOptions, mode, commitData, explanation, responseMeta, promptMeta }) {
576
650
  if (runtimeOptions.format === "json") {
577
651
  return formatJsonOutput({ mode, commitData, explanation, responseMeta, promptMeta });
@@ -595,22 +669,66 @@ function renderFinalOutput({ runtimeOptions, mode, commitData, explanation, resp
595
669
  });
596
670
  }
597
671
 
672
+ async function runPipelineCommand(cwd) {
673
+ const analysis = inspectRepositoryForPipeline(cwd);
674
+
675
+ if (!analysis.supported) {
676
+ console.log(analysis.reason);
677
+ return 1;
678
+ }
679
+
680
+ console.log(formatPipelineRecommendations(analysis));
681
+
682
+ const answer = await askQuestion(
683
+ `\nChoose a pipeline option (1-${analysis.options.length}) or type "cancel" > `
684
+ );
685
+ const selection = resolvePipelineSelection(analysis, answer);
686
+
687
+ if (!selection) {
688
+ console.log("Aborted.");
689
+ return 0;
690
+ }
691
+
692
+ const { writtenFiles, notes } = writePipelineFiles(cwd, analysis, selection);
693
+ console.log(`\nCreated workflow files: ${writtenFiles.join(", ")}`);
694
+
695
+ if (notes.length > 0) {
696
+ console.log(`\n${notes.join("\n")}`);
697
+ }
698
+
699
+ return 0;
700
+ }
701
+
598
702
  export async function main(argv = process.argv) {
599
703
  const cwd = process.cwd();
600
- const config = loadConfig(cwd);
601
704
  const parsed = parseArgs(argv);
602
705
  const hasNoCommandOrFlags = argv.slice(2).length === 0;
603
706
 
604
707
  loadEnvFile(cwd); // Ensure environment is loaded first
708
+ const config = loadConfig(cwd);
709
+ applyConfigEnvironment(config);
710
+
711
+ if (parsed.version) {
712
+ console.log(CLI_VERSION);
713
+ return 0;
714
+ }
715
+
716
+ if (parsed.cost) {
717
+ console.log(formatUsageStats(getUsageStats()));
718
+ return 0;
719
+ }
605
720
 
606
721
  if (parsed.help || hasNoCommandOrFlags) {
607
722
  printHelp();
608
723
  return 0;
609
724
  }
610
725
 
611
- if (parsed.example) {
612
- printExamples();
613
- return 0;
726
+ if (parsed.configCommand) {
727
+ return handleConfigCommand(parsed);
728
+ }
729
+
730
+ if (parsed.cacheCommand) {
731
+ return handleCacheCommand(parsed);
614
732
  }
615
733
 
616
734
  if (parsed.nativeGitCommand) {
@@ -628,62 +746,12 @@ export async function main(argv = process.argv) {
628
746
  return 0;
629
747
  }
630
748
 
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) {
749
+ if (parsed.log) {
682
750
  console.log(getRepositoryLog(cwd));
683
751
  return 0;
684
752
  }
685
753
 
686
- if (parsed.statusCommand || parsed.status) {
754
+ if (parsed.status) {
687
755
  console.log(getRepositoryStatus(cwd));
688
756
  return 0;
689
757
  }
@@ -697,36 +765,6 @@ export async function main(argv = process.argv) {
697
765
  return 0;
698
766
  }
699
767
 
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
768
  if (
731
769
  parsed.addCommand ||
732
770
  parsed.removeCommand ||
@@ -795,39 +833,9 @@ export async function main(argv = process.argv) {
795
833
  }
796
834
 
797
835
  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
- }
836
+ const mode = ANALYSIS_MODES.has(parsed.mode) ? parsed.mode : resolveConfiguredAnalysisMode(config);
826
837
 
827
- return 0;
828
- }
829
-
830
- if (mode === "commit" || parsed.commitCommand) {
838
+ if (parsed.mode === "commit") {
831
839
  const commitData = fetchWorkingTreeData(cwd);
832
840
 
833
841
  if (commitData.filesChanged.length === 0 || commitData.diff === "") {
@@ -841,6 +849,7 @@ export async function main(argv = process.argv) {
841
849
  providerOverride: runtimeOptions.provider,
842
850
  modelOverride: runtimeOptions.model,
843
851
  maxDiffLines: runtimeOptions.maxDiffLines,
852
+ noCache: runtimeOptions.noCache,
844
853
  stream: false,
845
854
  onChunk: null,
846
855
  onStart: null
@@ -877,7 +886,7 @@ export async function main(argv = process.argv) {
877
886
  return 0;
878
887
  }
879
888
 
880
- if (mode === "merge" || parsed.mergeCommand || parsed.merge) {
889
+ if (parsed.mode === "merge" || parsed.merge) {
881
890
  if (parsed.commitRef) {
882
891
  throw new Error("--merge works from the current branch and does not accept a commit ref.");
883
892
  }
@@ -909,7 +918,7 @@ export async function main(argv = process.argv) {
909
918
  return 0;
910
919
  }
911
920
 
912
- if (mode === "tag" || parsed.tagCommand || parsed.tag) {
921
+ if (parsed.mode === "tag" || parsed.tag) {
913
922
  if (parsed.commitRef) {
914
923
  throw new Error("--tag works from the current branch and does not accept a commit ref.");
915
924
  }
@@ -943,12 +952,210 @@ export async function main(argv = process.argv) {
943
952
 
944
953
  const targetRef = resolveTargetRef(parsed, cwd);
945
954
 
955
+ if (parsed.mode === "blame") {
956
+ if (!parsed.blameFile) {
957
+ throw new Error("--blame requires a file path.");
958
+ }
959
+
960
+ const commitData = fetchBlameData(parsed.blameFile, cwd);
961
+ const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
962
+ let streamStarted = false;
963
+
964
+ if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
965
+ console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
966
+ }
967
+
968
+ const { explanation, responseMeta, promptMeta } = await generateExplanation({
969
+ mode: "blame",
970
+ commitData,
971
+ providerOverride: runtimeOptions.provider,
972
+ modelOverride: runtimeOptions.model,
973
+ maxDiffLines: runtimeOptions.maxDiffLines,
974
+ noCache: runtimeOptions.noCache,
975
+ stream: canStream,
976
+ onStart: canStream
977
+ ? ({ promptMeta: streamPromptMeta }) => {
978
+ if (!runtimeOptions.quiet && !streamStarted) {
979
+ process.stdout.write(
980
+ formatPreamble({
981
+ mode: "blame",
982
+ commitData,
983
+ responseMeta: null,
984
+ promptMeta: streamPromptMeta,
985
+ options: runtimeOptions
986
+ })
987
+ );
988
+ streamStarted = true;
989
+ }
990
+ }
991
+ : null,
992
+ onChunk: canStream ? (chunk) => process.stdout.write(chunk) : null
993
+ });
994
+
995
+ const renderedOutput = renderFinalOutput({
996
+ runtimeOptions,
997
+ mode: "blame",
998
+ commitData,
999
+ explanation,
1000
+ responseMeta,
1001
+ promptMeta
1002
+ });
1003
+
1004
+ if (canStream) {
1005
+ process.stdout.write("\n");
1006
+ if (runtimeOptions.verbose) {
1007
+ process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
1008
+ }
1009
+ } else {
1010
+ console.log(renderedOutput);
1011
+ }
1012
+
1013
+ if (runtimeOptions.clipboard) {
1014
+ copyToClipboard(renderedOutput);
1015
+ if (!runtimeOptions.quiet) {
1016
+ console.error("Copied output to clipboard.");
1017
+ }
1018
+ }
1019
+
1020
+ return 0;
1021
+ }
1022
+
1023
+ if (parsed.mode === "conflict") {
1024
+ const commitData = fetchConflictData(cwd, parsed.diffFile);
1025
+ const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
1026
+ let streamStarted = false;
1027
+
1028
+ if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
1029
+ console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
1030
+ }
1031
+
1032
+ const { explanation, responseMeta, promptMeta } = await generateExplanation({
1033
+ mode: "conflict",
1034
+ commitData,
1035
+ providerOverride: runtimeOptions.provider,
1036
+ modelOverride: runtimeOptions.model,
1037
+ maxDiffLines: runtimeOptions.maxDiffLines,
1038
+ noCache: runtimeOptions.noCache,
1039
+ stream: canStream,
1040
+ onStart: canStream
1041
+ ? ({ promptMeta: streamPromptMeta }) => {
1042
+ if (!runtimeOptions.quiet && !streamStarted) {
1043
+ process.stdout.write(
1044
+ formatPreamble({
1045
+ mode: "conflict",
1046
+ commitData,
1047
+ responseMeta: null,
1048
+ promptMeta: streamPromptMeta,
1049
+ options: runtimeOptions
1050
+ })
1051
+ );
1052
+ streamStarted = true;
1053
+ }
1054
+ }
1055
+ : null,
1056
+ onChunk: canStream ? (chunk) => process.stdout.write(chunk) : null
1057
+ });
1058
+
1059
+ const renderedOutput = renderFinalOutput({
1060
+ runtimeOptions,
1061
+ mode: "conflict",
1062
+ commitData,
1063
+ explanation,
1064
+ responseMeta,
1065
+ promptMeta
1066
+ });
1067
+
1068
+ if (canStream) {
1069
+ process.stdout.write("\n");
1070
+ if (runtimeOptions.verbose) {
1071
+ process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
1072
+ }
1073
+ } else {
1074
+ console.log(renderedOutput);
1075
+ }
1076
+
1077
+ if (runtimeOptions.clipboard) {
1078
+ copyToClipboard(renderedOutput);
1079
+ if (!runtimeOptions.quiet) {
1080
+ console.error("Copied output to clipboard.");
1081
+ }
1082
+ }
1083
+
1084
+ return 0;
1085
+ }
1086
+
1087
+ if (parsed.mode === "stash") {
1088
+ const commitData = fetchStashData(parsed.stashRef, cwd, parsed.diffFile);
1089
+ const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
1090
+ let streamStarted = false;
1091
+
1092
+ if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
1093
+ console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
1094
+ }
1095
+
1096
+ const { explanation, responseMeta, promptMeta } = await generateExplanation({
1097
+ mode: "stash",
1098
+ commitData,
1099
+ providerOverride: runtimeOptions.provider,
1100
+ modelOverride: runtimeOptions.model,
1101
+ maxDiffLines: runtimeOptions.maxDiffLines,
1102
+ noCache: runtimeOptions.noCache,
1103
+ stream: canStream,
1104
+ onStart: canStream
1105
+ ? ({ promptMeta: streamPromptMeta }) => {
1106
+ if (!runtimeOptions.quiet && !streamStarted) {
1107
+ process.stdout.write(
1108
+ formatPreamble({
1109
+ mode: "stash",
1110
+ commitData,
1111
+ responseMeta: null,
1112
+ promptMeta: streamPromptMeta,
1113
+ options: runtimeOptions
1114
+ })
1115
+ );
1116
+ streamStarted = true;
1117
+ }
1118
+ }
1119
+ : null,
1120
+ onChunk: canStream ? (chunk) => process.stdout.write(chunk) : null
1121
+ });
1122
+
1123
+ const renderedOutput = renderFinalOutput({
1124
+ runtimeOptions,
1125
+ mode: "stash",
1126
+ commitData,
1127
+ explanation,
1128
+ responseMeta,
1129
+ promptMeta
1130
+ });
1131
+
1132
+ if (canStream) {
1133
+ process.stdout.write("\n");
1134
+ if (runtimeOptions.verbose) {
1135
+ process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
1136
+ }
1137
+ } else {
1138
+ console.log(renderedOutput);
1139
+ }
1140
+
1141
+ if (runtimeOptions.clipboard) {
1142
+ copyToClipboard(renderedOutput);
1143
+ if (!runtimeOptions.quiet) {
1144
+ console.error("Copied output to clipboard.");
1145
+ }
1146
+ }
1147
+
1148
+ return 0;
1149
+ }
1150
+
946
1151
  if (!targetRef) {
947
1152
  printHelp();
948
1153
  return 1;
949
1154
  }
950
1155
 
951
- const commitData = fetchCommitData(targetRef, cwd);
1156
+ const commitData = parsed.diffFile
1157
+ ? fetchCommitDataForFile(targetRef, parsed.diffFile, cwd)
1158
+ : fetchCommitData(targetRef, cwd);
952
1159
 
953
1160
  if (mode === "split") {
954
1161
  if (commitData.analysisType !== "commit") {
@@ -961,6 +1168,7 @@ export async function main(argv = process.argv) {
961
1168
  providerOverride: runtimeOptions.provider,
962
1169
  modelOverride: runtimeOptions.model,
963
1170
  maxDiffLines: runtimeOptions.maxDiffLines,
1171
+ noCache: runtimeOptions.noCache,
964
1172
  stream: false,
965
1173
  onChunk: null,
966
1174
  onStart: null
@@ -976,6 +1184,17 @@ export async function main(argv = process.argv) {
976
1184
  console.log(formatSplitPlan(plan));
977
1185
 
978
1186
  if (parsed.execute && !parsed.dryRun) {
1187
+ const reviewedPlan = parsed.interactive ? await reviewSplitPlanInteractively(plan) : plan;
1188
+ if (reviewedPlan == null) {
1189
+ console.log("Aborted.");
1190
+ return 0;
1191
+ }
1192
+
1193
+ if (parsed.interactive) {
1194
+ console.log("");
1195
+ console.log(formatSplitPlan(reviewedPlan));
1196
+ }
1197
+
979
1198
  validateSplitExecutionTarget(commitData.commitId, cwd);
980
1199
  const confirmed = await askQuestion(
981
1200
  "\nThis will rewrite git history. Continue? (yes/no) > "
@@ -985,8 +1204,8 @@ export async function main(argv = process.argv) {
985
1204
  return 0;
986
1205
  }
987
1206
 
988
- executeSplit(plan, commitData.commitId, cwd);
989
- console.log(`\nSplit complete. Created ${plan.commits.length} commits.`);
1207
+ executeSplit(reviewedPlan, commitData.commitId, cwd);
1208
+ console.log(`\nSplit complete. Created ${reviewedPlan.commits.length} commits.`);
990
1209
  } else {
991
1210
  console.log("\nThis is a preview. Run with --execute to apply the split.");
992
1211
  }
@@ -1001,12 +1220,17 @@ export async function main(argv = process.argv) {
1001
1220
  const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
1002
1221
  let streamStarted = false;
1003
1222
 
1223
+ if (runtimeOptions.stream && !canStream && !runtimeOptions.quiet) {
1224
+ console.error(`Streaming is only supported with plain output. Ignoring --stream for ${runtimeOptions.format} format.`);
1225
+ }
1226
+
1004
1227
  const { explanation, responseMeta, promptMeta } = await generateExplanation({
1005
1228
  mode,
1006
1229
  commitData,
1007
1230
  providerOverride: runtimeOptions.provider,
1008
1231
  modelOverride: runtimeOptions.model,
1009
1232
  maxDiffLines: runtimeOptions.maxDiffLines,
1233
+ noCache: runtimeOptions.noCache,
1010
1234
  stream: canStream,
1011
1235
  onStart: canStream
1012
1236
  ? ({ promptMeta: streamPromptMeta }) => {