gitxplain 0.1.3 → 0.1.8

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,41 +5,47 @@ 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
8
  import { loadEnvFile } from "./services/envLoader.js";
10
- import {
11
- saveGitConnection,
12
- isGitConnected,
13
- loadGitConnection,
14
- getGitUserInfo,
15
- verifyGitToken
16
- } from "./services/gitConnectionService.js";
17
9
  import { copyToClipboard } from "./services/clipboardService.js";
18
- import { loadConfig } from "./services/configService.js";
10
+ import {
11
+ applyConfigEnvironment,
12
+ getProviderApiKeyField,
13
+ getUserConfigPath,
14
+ loadConfig,
15
+ loadUserConfig,
16
+ updateUserConfig
17
+ } from "./services/configService.js";
19
18
  import {
20
19
  buildBranchRange,
21
20
  deletePaths,
22
21
  fetchCommitData,
23
22
  fetchWorkingTreeData,
24
23
  gitAddFiles,
24
+ gitPull,
25
25
  gitPush,
26
+ gitResetHard,
27
+ gitResetSoft,
26
28
  gitStashPop,
27
29
  gitRestoreStaged,
28
30
  getRepositoryLog,
29
31
  getRepositoryStatus,
30
32
  getDefaultBaseRef,
31
33
  isGitRepository,
34
+ listGitSubcommands,
35
+ runNativeGitPassthrough,
32
36
  resolveStashRef
33
37
  } from "./services/gitService.js";
34
38
  import { installHook } from "./services/hookService.js";
35
39
  import {
36
40
  buildReleaseMergePlan,
41
+ buildReleaseStatus,
37
42
  buildReleaseTagPlan,
38
43
  executeReleaseMerge,
39
44
  executeReleaseTagPlan,
40
45
  finalizeReleaseMergePlan,
41
46
  finalizeReleaseTagPlan,
42
47
  formatReleaseMergePlan,
48
+ formatReleaseStatus,
43
49
  formatReleaseTagPlan
44
50
  } from "./services/mergeService.js";
45
51
  import {
@@ -50,6 +56,12 @@ import {
50
56
  formatOutput,
51
57
  formatPreamble
52
58
  } from "./services/outputFormatter.js";
59
+ import {
60
+ formatPipelineRecommendations,
61
+ inspectRepositoryForPipeline,
62
+ resolvePipelineSelection,
63
+ writePipelineFiles
64
+ } from "./services/pipelineService.js";
53
65
  import { executeCommitPlan, formatCommitPlan, parseCommitPlan, reconcileCommitPlan } from "./services/commitService.js";
54
66
  import {
55
67
  executeSplit,
@@ -72,8 +84,10 @@ const MODE_FLAGS = new Map([
72
84
  ["--merge", "merge"],
73
85
  ["--tag", "tag"],
74
86
  ["--commit", "commit"],
87
+ ["--release", "release"],
75
88
  ["--log", "log"],
76
- ["--status", "status"]
89
+ ["--status", "status"],
90
+ ["--pipeline", "pipeline"]
77
91
  ]);
78
92
 
79
93
  const FORMAT_FLAGS = new Map([
@@ -82,155 +96,103 @@ const FORMAT_FLAGS = new Map([
82
96
  ["--html", "html"]
83
97
  ]);
84
98
 
99
+ const RESERVED_SUBCOMMANDS = new Set([
100
+ "help",
101
+ "config",
102
+ "install-hook",
103
+ "git",
104
+ "add",
105
+ "remove",
106
+ "del",
107
+ "bin",
108
+ "pop",
109
+ "pull",
110
+ "push"
111
+ ]);
112
+
85
113
  function printHelp() {
86
114
  console.log(`gitxplain - AI-powered Git change analysis, review, and commit workflow CLI
87
115
 
88
116
  Usage:
89
- gitxplain help
90
117
  gitxplain --help
91
- gitxplain commit
118
+ gitxplain config set provider <name>
119
+ gitxplain config set api-key <value> [--provider <name>]
120
+ gitxplain config get [key]
121
+ gitxplain config list
122
+ gitxplain <commit-id> [options]
123
+ gitxplain <start>..<end> [options]
124
+ gitxplain --branch [base-ref] [options]
125
+ gitxplain --pr [base-ref] [options]
92
126
  gitxplain --commit
93
- gitxplain merge
127
+ gitxplain --release [status]
94
128
  gitxplain --merge
95
- gitxplain tag
96
129
  gitxplain --tag
97
- gitxplain log
98
130
  gitxplain --log
99
- gitxplain status
100
131
  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]
106
- gitxplain install-hook [hook-name]
107
- gitxplain --connect-git [token]
108
- gitxplain --boot [options]
109
- gitxplain <commit-id> [options]
110
- gitxplain <start>..<end> [options]
111
- gitxplain --branch [base-ref] [options]
112
- gitxplain --pr [base-ref] [options]
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
-
123
- Modes:
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
127
- --impact Explain before-vs-after behavior changes
128
- --full Generate a full structured analysis
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)
132
+ gitxplain --pipeline
133
+
134
+ Analysis:
135
+ --summary Generate a one-line summary of a change
136
+ --issues Focus on the issue or failure being addressed
137
+ --fix Explain the fix in simple terms
138
+ --impact Explain behavior changes before vs after
139
+ --full Generate a full structured analysis
140
+ --lines Walk through the changed code file by file
141
+ --review Generate review findings, risks, and suggestions
142
+ --security Focus on security-relevant changes and concerns
143
+ --split Propose splitting a commit into smaller atomic commits
144
+ --commit Propose commits for current uncommitted changes
145
+ --execute Execute a proposed split or commit plan
146
+ --dry-run Preview the plan without executing it
147
+
148
+ Release:
149
+ --release [status] Show release branch health and next recommended action
150
+ --merge Preview or apply a merge into the release branch
151
+ --tag Preview or create release tags from version bumps
152
+
153
+ Repo:
154
+ --log Print Git log entries for the current repository
155
+ --status Print Git working tree status for the current repository
156
+ --pipeline Detect the current repository stack and create CI/CD workflow files
140
157
 
141
158
  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
159
+ config Persist provider, model, and API key settings
160
+ add Stage one or more files with git add
161
+ remove Unstage one or more files with git restore --staged
162
+ remove hard Hard reset the repository to HEAD
163
+ del Delete one or more files from the working tree
164
+ bin Soft reset HEAD~1 while keeping your changes
165
+ pop Pop a stash entry like "pop 2"
166
+ pull Run git pull, optionally with a remote and branch
167
+ push Run git push, optionally with a remote and branch
168
+ install-hook Install the gitxplain hook
169
+ git Pass through to native git commands
147
170
 
148
171
  Output:
149
- --json Print structured JSON output
150
- --markdown Print Markdown output
151
- --html Print HTML output
152
- --quiet Print only the main body without extra framing
153
- --verbose Print provider, model, cache, latency, and usage details
154
- --clipboard Copy the final output to the system clipboard
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.
158
-
159
- Providers:
160
- --provider LLM provider: openai, groq, openrouter, gemini, ollama, chutes
161
- --model Override the model name
162
-
163
- Diff Budget:
164
- --max-diff-lines <n> Limit diff lines sent to the model
172
+ --provider <name>
173
+ --model <name>
174
+ --json
175
+ --markdown
176
+ --html
177
+ --quiet
178
+ --verbose
179
+ --clipboard
180
+ --stream
181
+ --max-diff-lines <n>
165
182
 
166
183
  Comparison:
167
- --branch [base-ref] Analyze the current branch against a base branch
168
- --pr [base-ref] Alias for --branch, useful for PR-style comparisons
169
-
170
- Examples:
171
- gitxplain HEAD~1 --full
172
- gitxplain HEAD~1 --review
173
- gitxplain HEAD~5..HEAD --markdown
174
- gitxplain --branch main --review
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
195
- gitxplain HEAD~1 --provider chutes --model deepseek-ai/DeepSeek-V3-0324
196
-
197
- Provider Setup:
198
- OpenAI:
199
- export LLM_PROVIDER=openai
200
- export OPENAI_API_KEY=your_key
201
-
202
- Groq:
203
- export LLM_PROVIDER=groq
204
- export GROQ_API_KEY=your_key
205
-
206
- OpenRouter:
207
- export LLM_PROVIDER=openrouter
208
- export OPENROUTER_API_KEY=your_key
209
-
210
- Gemini:
211
- export LLM_PROVIDER=gemini
212
- export GEMINI_API_KEY=your_key
213
-
214
- Ollama:
215
- export LLM_PROVIDER=ollama
216
- export OLLAMA_MODEL=llama3.2
217
-
218
- Chutes:
219
- export LLM_PROVIDER=chutes
220
- export CHUTES_API_KEY=your_key
184
+ --branch [base-ref] Analyze the current branch against a base branch
185
+ --pr [base-ref] Alias for --branch, useful for PR-style comparisons
221
186
 
222
187
  Config:
223
188
  Project config: .gitxplainrc or .gitxplainrc.json
224
- User config: ~/.gitxplain/config.json
225
-
226
- Hook Installation:
227
- gitxplain install-hook
228
- gitxplain install-hook post-commit
189
+ User config: ~/.gitxplain/config.json (macOS/Linux) or %USERPROFILE%\\.gitxplain\\config.json (Windows)
229
190
 
230
191
  Notes:
231
192
  Run gitxplain inside a Git repository.
232
- If no mode is supplied, gitxplain will prompt you to choose one interactively.
193
+ If no command or mode is supplied, gitxplain prints this help text.
233
194
  Use --provider or --model to override your config or environment for one command.
195
+ Use gitxplain git <args...> to run any native Git subcommand with its normal flags.
234
196
  `);
235
197
  }
236
198
 
@@ -262,9 +224,120 @@ function parseNumber(value, fallback = null) {
262
224
  return parsed;
263
225
  }
264
226
 
265
- export function parseArgs(argv) {
227
+ function redactConfigValue(key, value) {
228
+ if (typeof value !== "string") {
229
+ return value;
230
+ }
231
+
232
+ if (!/api[_-]?key/i.test(key)) {
233
+ return value;
234
+ }
235
+
236
+ if (value.length <= 8) {
237
+ return "*".repeat(value.length);
238
+ }
239
+
240
+ return `${value.slice(0, 4)}...${value.slice(-4)}`;
241
+ }
242
+
243
+ function printConfigEntries(config) {
244
+ const entries = Object.entries(config).sort(([left], [right]) => left.localeCompare(right));
245
+
246
+ if (entries.length === 0) {
247
+ console.log("No user config saved yet.");
248
+ return;
249
+ }
250
+
251
+ for (const [key, value] of entries) {
252
+ console.log(`${key}: ${redactConfigValue(key, value)}`);
253
+ }
254
+ }
255
+
256
+ function resolveConfigSetUpdate(parsed, currentConfig) {
257
+ const key = parsed.configKey;
258
+ const value = parsed.configValue;
259
+
260
+ if (!key || !value) {
261
+ throw new Error('Usage: gitxplain config set <provider|model|api-key> <value> [--provider <name>]');
262
+ }
263
+
264
+ if (key === "provider") {
265
+ return { provider: value.toLowerCase() };
266
+ }
267
+
268
+ if (key === "model") {
269
+ return { model: value };
270
+ }
271
+
272
+ if (key === "api-key") {
273
+ const resolvedProvider = (parsed.provider ?? currentConfig.provider ?? currentConfig.LLM_PROVIDER ?? "").toLowerCase();
274
+ const apiKeyField = getProviderApiKeyField(resolvedProvider);
275
+
276
+ if (!apiKeyField) {
277
+ throw new Error("Set a provider first with `gitxplain config set provider <name>`, or pass `--provider <name>`.");
278
+ }
279
+
280
+ return { [apiKeyField]: value };
281
+ }
282
+
283
+ return { [key]: value };
284
+ }
285
+
286
+ function handleConfigCommand(parsed) {
287
+ const currentConfig = loadUserConfig();
288
+
289
+ if (parsed.configAction === "list" || parsed.configAction == null) {
290
+ console.log(`User config: ${getUserConfigPath()}`);
291
+ printConfigEntries(currentConfig);
292
+ return 0;
293
+ }
294
+
295
+ if (parsed.configAction === "get") {
296
+ console.log(`User config: ${getUserConfigPath()}`);
297
+
298
+ if (!parsed.configKey) {
299
+ printConfigEntries(currentConfig);
300
+ return 0;
301
+ }
302
+
303
+ const value = currentConfig[parsed.configKey];
304
+ if (value === undefined) {
305
+ console.log(`No value saved for ${parsed.configKey}.`);
306
+ return 0;
307
+ }
308
+
309
+ console.log(`${parsed.configKey}: ${redactConfigValue(parsed.configKey, value)}`);
310
+ return 0;
311
+ }
312
+
313
+ if (parsed.configAction === "set") {
314
+ const updates = resolveConfigSetUpdate(parsed, currentConfig);
315
+ const { configPath } = updateUserConfig(updates);
316
+ const [savedKey, savedValue] = Object.entries(updates)[0];
317
+ console.log(`Saved ${savedKey} to ${configPath}.`);
318
+ console.log(`${savedKey}: ${redactConfigValue(savedKey, savedValue)}`);
319
+ return 0;
320
+ }
321
+
322
+ throw new Error(`Unknown config subcommand: ${parsed.configAction}`);
323
+ }
324
+
325
+ function isDirectNativeGitSubcommand(subcommand, knownGitSubcommands) {
326
+ if (!subcommand || subcommand.startsWith("-")) {
327
+ return false;
328
+ }
329
+
330
+ if (RESERVED_SUBCOMMANDS.has(subcommand)) {
331
+ return false;
332
+ }
333
+
334
+ return knownGitSubcommands.has(subcommand);
335
+ }
336
+
337
+ export function parseArgs(argv, options = {}) {
266
338
  const args = argv.slice(2);
267
339
  const subcommand = args[0];
340
+ const knownGitSubcommands = options.gitSubcommands ?? listGitSubcommands();
268
341
  const flags = new Set(args.filter((arg) => arg.startsWith("--")));
269
342
  const valueFlags = new Set(["--provider", "--model", "--max-diff-lines", "--branch", "--pr"]);
270
343
  const positional = [];
@@ -292,54 +365,66 @@ export function parseArgs(argv) {
292
365
  const explicitMode = [...MODE_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
293
366
  const explicitFormat = [...FORMAT_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
294
367
  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";
368
+ const isConfigCommand = subcommand === "config";
369
+ const isNativeGitWrapper = subcommand === "git";
370
+ const isReleaseCommand = flags.has("--release");
302
371
  const isAddCommand = subcommand === "add";
303
372
  const isRemoveCommand = subcommand === "remove";
304
373
  const isDeleteCommand = subcommand === "del";
374
+ const isPipelineCommand = flags.has("--pipeline");
375
+ const isBinCommand = subcommand === "bin";
305
376
  const isPopCommand = subcommand === "pop";
377
+ const isPullCommand = subcommand === "pull";
306
378
  const isPushCommand = subcommand === "push";
379
+ const isRemoveHardCommand = isRemoveCommand && positional[1] === "hard" && positional.length === 2;
380
+ const isNativeGitCommand = isNativeGitWrapper || isDirectNativeGitSubcommand(subcommand, knownGitSubcommands);
307
381
 
308
382
  return {
309
383
  subcommand,
310
384
  help: flags.has("--help") || subcommand === "help",
385
+ nativeGitCommand: isNativeGitCommand,
311
386
  installHook: isInstallHook,
312
- connectGit: isConnectGit,
313
- boot: isBoot,
314
- logCommand: isLogCommand,
315
- statusCommand: isStatusCommand,
316
- commitCommand: isCommitCommand,
317
- mergeCommand: isMergeCommand,
318
- tagCommand: isTagCommand,
387
+ configCommand: isConfigCommand,
388
+ configAction: isConfigCommand ? positional[1] ?? null : null,
389
+ configKey: isConfigCommand ? positional[2] ?? null : null,
390
+ configValue: isConfigCommand ? positional.slice(3).join(" ") || null : null,
391
+ logCommand: false,
392
+ statusCommand: false,
393
+ commitCommand: false,
394
+ mergeCommand: false,
395
+ tagCommand: false,
396
+ releaseCommand: isReleaseCommand,
397
+ releaseAction: isReleaseCommand ? positional[0] ?? "status" : null,
319
398
  addCommand: isAddCommand,
320
399
  removeCommand: isRemoveCommand,
321
400
  deleteCommand: isDeleteCommand,
401
+ pipelineCommand: isPipelineCommand,
402
+ binCommand: isBinCommand,
322
403
  popCommand: isPopCommand,
404
+ pullCommand: isPullCommand,
323
405
  pushCommand: isPushCommand,
406
+ removeHardCommand: isRemoveHardCommand,
407
+ nativeGitArgs: isNativeGitWrapper ? args.slice(1) : isNativeGitCommand ? args : [],
324
408
  hookName: isInstallHook ? positional[1] ?? "post-commit" : null,
325
- actionPaths: isAddCommand || isRemoveCommand || isDeleteCommand ? positional.slice(1) : [],
326
- connectToken: isConnectGit ? positional[0] : null,
409
+ actionPaths:
410
+ isAddCommand || isDeleteCommand ? positional.slice(1) : isRemoveHardCommand ? [] : isRemoveCommand ? positional.slice(1) : [],
327
411
  stashIndex: isPopCommand ? positional[1] ?? null : null,
412
+ pullRemote: isPullCommand ? positional[1] ?? null : null,
413
+ pullBranch: isPullCommand ? positional[2] ?? null : null,
328
414
  pushRemote: isPushCommand ? positional[1] ?? null : null,
329
415
  pushBranch: isPushCommand ? positional[2] ?? null : null,
330
416
  commitRef:
331
417
  isInstallHook ||
332
- isConnectGit ||
333
- isBoot ||
334
- isLogCommand ||
335
- isStatusCommand ||
336
- isCommitCommand ||
337
- isMergeCommand ||
338
- isTagCommand ||
418
+ isConfigCommand ||
419
+ isNativeGitCommand ||
420
+ isReleaseCommand ||
339
421
  isAddCommand ||
340
422
  isRemoveCommand ||
341
423
  isDeleteCommand ||
424
+ isPipelineCommand ||
425
+ isBinCommand ||
342
426
  isPopCommand ||
427
+ isPullCommand ||
343
428
  isPushCommand ||
344
429
  subcommand === "help"
345
430
  ? null
@@ -359,6 +444,7 @@ export function parseArgs(argv) {
359
444
  quiet: flags.has("--quiet"),
360
445
  execute: flags.has("--execute"),
361
446
  dryRun: flags.has("--dry-run"),
447
+ release: flags.has("--release"),
362
448
  log: flags.has("--log"),
363
449
  status: flags.has("--status"),
364
450
  merge: flags.has("--merge"),
@@ -395,6 +481,7 @@ async function chooseModeInteractively() {
395
481
  "11. Tag Release Commits",
396
482
  "12. Repository Log",
397
483
  "13. Commit Working Tree",
484
+ "14. Create CI/CD Pipelines",
398
485
  "> "
399
486
  ].join("\n")
400
487
  );
@@ -412,7 +499,8 @@ async function chooseModeInteractively() {
412
499
  "10": "merge",
413
500
  "11": "tag",
414
501
  "12": "log",
415
- "13": "commit"
502
+ "13": "commit",
503
+ "14": "pipeline"
416
504
  };
417
505
 
418
506
  return selections[answer] ?? "full";
@@ -470,17 +558,26 @@ function renderFinalOutput({ runtimeOptions, mode, commitData, explanation, resp
470
558
 
471
559
  export async function main(argv = process.argv) {
472
560
  const cwd = process.cwd();
473
- const config = loadConfig(cwd);
474
561
  const parsed = parseArgs(argv);
475
562
  const hasNoCommandOrFlags = argv.slice(2).length === 0;
476
563
 
477
564
  loadEnvFile(cwd); // Ensure environment is loaded first
565
+ const config = loadConfig(cwd);
566
+ applyConfigEnvironment(config);
478
567
 
479
568
  if (parsed.help || hasNoCommandOrFlags) {
480
569
  printHelp();
481
570
  return 0;
482
571
  }
483
572
 
573
+ if (parsed.configCommand) {
574
+ return handleConfigCommand(parsed);
575
+ }
576
+
577
+ if (parsed.nativeGitCommand) {
578
+ return runNativeGitPassthrough(parsed.nativeGitArgs, cwd);
579
+ }
580
+
484
581
  if (!isGitRepository(cwd)) {
485
582
  console.error("gitxplain must be run inside a Git repository.");
486
583
  return 1;
@@ -492,62 +589,65 @@ export async function main(argv = process.argv) {
492
589
  return 0;
493
590
  }
494
591
 
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
- }
592
+ if (parsed.log) {
593
+ console.log(getRepositoryLog(cwd));
515
594
  return 0;
516
595
  }
517
596
 
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
- }
597
+ if (parsed.status) {
598
+ console.log(getRepositoryStatus(cwd));
536
599
  return 0;
537
600
  }
538
601
 
539
- if (parsed.logCommand || parsed.log) {
540
- console.log(getRepositoryLog(cwd));
602
+ if (parsed.releaseCommand) {
603
+ if (parsed.releaseAction !== "status") {
604
+ throw new Error(`Unknown release subcommand: ${parsed.releaseAction}`);
605
+ }
606
+
607
+ console.log(formatReleaseStatus(buildReleaseStatus(cwd)));
541
608
  return 0;
542
609
  }
543
610
 
544
- if (parsed.statusCommand || parsed.status) {
545
- console.log(getRepositoryStatus(cwd));
611
+ if (parsed.pipelineCommand) {
612
+ const analysis = inspectRepositoryForPipeline(cwd);
613
+
614
+ if (!analysis.supported) {
615
+ console.log(analysis.reason);
616
+ return 1;
617
+ }
618
+
619
+ console.log(formatPipelineRecommendations(analysis));
620
+
621
+ const answer = await askQuestion(
622
+ `\nChoose a pipeline option (1-${analysis.options.length}) or type "cancel" > `
623
+ );
624
+ const selection = resolvePipelineSelection(analysis, answer);
625
+
626
+ if (!selection) {
627
+ console.log("Aborted.");
628
+ return 0;
629
+ }
630
+
631
+ const { writtenFiles, notes } = writePipelineFiles(cwd, analysis, selection);
632
+ console.log(`\nCreated workflow files: ${writtenFiles.join(", ")}`);
633
+
634
+ if (notes.length > 0) {
635
+ console.log(`\n${notes.join("\n")}`);
636
+ }
637
+
546
638
  return 0;
547
639
  }
548
640
 
549
- if (parsed.addCommand || parsed.removeCommand || parsed.deleteCommand || parsed.popCommand || parsed.pushCommand) {
550
- if (!parsed.popCommand && parsed.actionPaths.length === 0) {
641
+ if (
642
+ parsed.addCommand ||
643
+ parsed.removeCommand ||
644
+ parsed.deleteCommand ||
645
+ parsed.binCommand ||
646
+ parsed.popCommand ||
647
+ parsed.pullCommand ||
648
+ parsed.pushCommand
649
+ ) {
650
+ if (!parsed.popCommand && !parsed.binCommand && !parsed.pullCommand && !parsed.removeHardCommand && parsed.actionPaths.length === 0) {
551
651
  if (!parsed.pushCommand) {
552
652
  throw new Error(`No paths provided for "${parsed.subcommand}".`);
553
653
  }
@@ -560,6 +660,12 @@ export async function main(argv = process.argv) {
560
660
  }
561
661
 
562
662
  if (parsed.removeCommand) {
663
+ if (parsed.removeHardCommand) {
664
+ gitResetHard("HEAD", cwd);
665
+ console.log("Hard reset to HEAD.");
666
+ return 0;
667
+ }
668
+
563
669
  gitRestoreStaged(parsed.actionPaths, cwd);
564
670
  console.log(`Unstaged ${parsed.actionPaths.join(", ")}.`);
565
671
  return 0;
@@ -571,6 +677,12 @@ export async function main(argv = process.argv) {
571
677
  return 0;
572
678
  }
573
679
 
680
+ if (parsed.binCommand) {
681
+ gitResetSoft(cwd);
682
+ console.log("Soft reset HEAD~1 and kept your changes.");
683
+ return 0;
684
+ }
685
+
574
686
  if (parsed.popCommand) {
575
687
  const stashRef = resolveStashRef(parsed.stashIndex);
576
688
  gitStashPop(parsed.stashIndex, cwd);
@@ -578,6 +690,14 @@ export async function main(argv = process.argv) {
578
690
  return 0;
579
691
  }
580
692
 
693
+ if (parsed.pullCommand) {
694
+ gitPull(cwd, parsed.pullRemote, parsed.pullBranch);
695
+ console.log(
696
+ `Pulled${parsed.pullRemote ? ` from ${parsed.pullRemote}` : ""}${parsed.pullBranch ? ` ${parsed.pullBranch}` : ""}.`
697
+ );
698
+ return 0;
699
+ }
700
+
581
701
  gitPush(cwd, parsed.pushRemote, parsed.pushBranch);
582
702
  console.log(
583
703
  `Pushed${parsed.pushRemote ? ` to ${parsed.pushRemote}` : ""}${parsed.pushBranch ? ` ${parsed.pushBranch}` : ""}.`
@@ -588,7 +708,37 @@ export async function main(argv = process.argv) {
588
708
  const runtimeOptions = resolveRuntimeOptions(parsed, config);
589
709
  const mode = parsed.mode ?? config.mode ?? (await chooseModeInteractively());
590
710
 
591
- if (mode === "commit" || parsed.commitCommand) {
711
+ if (mode === "pipeline") {
712
+ const analysis = inspectRepositoryForPipeline(cwd);
713
+
714
+ if (!analysis.supported) {
715
+ console.log(analysis.reason);
716
+ return 1;
717
+ }
718
+
719
+ console.log(formatPipelineRecommendations(analysis));
720
+
721
+ const answer = await askQuestion(
722
+ `\nChoose a pipeline option (1-${analysis.options.length}) or type "cancel" > `
723
+ );
724
+ const selection = resolvePipelineSelection(analysis, answer);
725
+
726
+ if (!selection) {
727
+ console.log("Aborted.");
728
+ return 0;
729
+ }
730
+
731
+ const { writtenFiles, notes } = writePipelineFiles(cwd, analysis, selection);
732
+ console.log(`\nCreated workflow files: ${writtenFiles.join(", ")}`);
733
+
734
+ if (notes.length > 0) {
735
+ console.log(`\n${notes.join("\n")}`);
736
+ }
737
+
738
+ return 0;
739
+ }
740
+
741
+ if (mode === "commit") {
592
742
  const commitData = fetchWorkingTreeData(cwd);
593
743
 
594
744
  if (commitData.filesChanged.length === 0 || commitData.diff === "") {
@@ -638,7 +788,7 @@ export async function main(argv = process.argv) {
638
788
  return 0;
639
789
  }
640
790
 
641
- if (mode === "merge" || parsed.mergeCommand || parsed.merge) {
791
+ if (mode === "merge" || parsed.merge) {
642
792
  if (parsed.commitRef) {
643
793
  throw new Error("--merge works from the current branch and does not accept a commit ref.");
644
794
  }
@@ -670,7 +820,7 @@ export async function main(argv = process.argv) {
670
820
  return 0;
671
821
  }
672
822
 
673
- if (mode === "tag" || parsed.tagCommand || parsed.tag) {
823
+ if (mode === "tag" || parsed.tag) {
674
824
  if (parsed.commitRef) {
675
825
  throw new Error("--tag works from the current branch and does not accept a commit ref.");
676
826
  }