gitxplain 0.1.3 → 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
@@ -22,24 +22,31 @@ import {
22
22
  fetchCommitData,
23
23
  fetchWorkingTreeData,
24
24
  gitAddFiles,
25
+ gitPull,
25
26
  gitPush,
27
+ gitResetHard,
28
+ gitResetSoft,
26
29
  gitStashPop,
27
30
  gitRestoreStaged,
28
31
  getRepositoryLog,
29
32
  getRepositoryStatus,
30
33
  getDefaultBaseRef,
31
34
  isGitRepository,
35
+ listGitSubcommands,
36
+ runNativeGitPassthrough,
32
37
  resolveStashRef
33
38
  } from "./services/gitService.js";
34
39
  import { installHook } from "./services/hookService.js";
35
40
  import {
36
41
  buildReleaseMergePlan,
42
+ buildReleaseStatus,
37
43
  buildReleaseTagPlan,
38
44
  executeReleaseMerge,
39
45
  executeReleaseTagPlan,
40
46
  finalizeReleaseMergePlan,
41
47
  finalizeReleaseTagPlan,
42
48
  formatReleaseMergePlan,
49
+ formatReleaseStatus,
43
50
  formatReleaseTagPlan
44
51
  } from "./services/mergeService.js";
45
52
  import {
@@ -50,6 +57,12 @@ import {
50
57
  formatOutput,
51
58
  formatPreamble
52
59
  } from "./services/outputFormatter.js";
60
+ import {
61
+ formatPipelineRecommendations,
62
+ inspectRepositoryForPipeline,
63
+ resolvePipelineSelection,
64
+ writePipelineFiles
65
+ } from "./services/pipelineService.js";
53
66
  import { executeCommitPlan, formatCommitPlan, parseCommitPlan, reconcileCommitPlan } from "./services/commitService.js";
54
67
  import {
55
68
  executeSplit,
@@ -73,7 +86,8 @@ const MODE_FLAGS = new Map([
73
86
  ["--tag", "tag"],
74
87
  ["--commit", "commit"],
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,30 +96,66 @@ const FORMAT_FLAGS = new Map([
82
96
  ["--html", "html"]
83
97
  ]);
84
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
+
85
120
  function printHelp() {
86
121
  console.log(`gitxplain - AI-powered Git change analysis, review, and commit workflow CLI
87
122
 
88
123
  Usage:
89
124
  gitxplain help
90
125
  gitxplain --help
126
+ gitxplain example
127
+ gitxplain --example
128
+ gitxplain git <native-git-args...>
129
+
130
+ Git:
91
131
  gitxplain commit
92
132
  gitxplain --commit
93
133
  gitxplain merge
94
134
  gitxplain --merge
95
135
  gitxplain tag
96
136
  gitxplain --tag
137
+ gitxplain release
138
+ gitxplain release status
97
139
  gitxplain log
98
140
  gitxplain --log
99
141
  gitxplain status
100
142
  gitxplain --status
143
+ gitxplain --pipeline
101
144
  gitxplain add <path> [more-paths...]
102
145
  gitxplain remove <path> [more-paths...]
146
+ gitxplain remove hard
103
147
  gitxplain del <path> [more-paths...]
148
+ gitxplain bin
104
149
  gitxplain pop [stash-index]
150
+ gitxplain pull [remote] [branch]
105
151
  gitxplain push [remote] [branch]
106
152
  gitxplain install-hook [hook-name]
107
- gitxplain --connect-git [token]
153
+
154
+ GitHub:
155
+ gitxplain --connect-github [token]
108
156
  gitxplain --boot [options]
157
+
158
+ Analysis:
109
159
  gitxplain <commit-id> [options]
110
160
  gitxplain <start>..<end> [options]
111
161
  gitxplain --branch [base-ref] [options]
@@ -117,8 +167,12 @@ What It Does:
117
167
  Plan commits for uncommitted work and split oversized commits into atomic steps
118
168
  Merge release-version branch changes into a dedicated release branch
119
169
  Tag release-version commit windows on the current branch
170
+ Inspect release branch health, missing tags, and drift from the source branch
120
171
  Inspect repository history and working tree status without calling the LLM
172
+ Inspect the current repository and scaffold GitHub Actions CI/CD workflows
121
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
122
176
 
123
177
  Modes:
124
178
  --summary Generate a one-line summary of a change
@@ -132,17 +186,22 @@ Modes:
132
186
  --split Propose splitting a commit into smaller atomic commits
133
187
  --merge Preview or apply a merge into the release branch based on version bumps
134
188
  --tag Preview or create release tags based on version bumps
189
+ release Show release branch health, missing tags, and next recommended action
135
190
  --commit Propose commits for current uncommitted changes
136
191
  --log Print Git log entries for the current repository
137
192
  --status Print Git working tree status for the current repository
193
+ --pipeline Detect the current repository stack and create CI/CD workflow files
138
194
  --execute Execute a proposed split or commit plan
139
195
  --dry-run Preview the plan without executing it (default for --split and --commit)
140
196
 
141
197
  Quick Actions:
142
198
  add Stage one or more files with git add
143
199
  remove Unstage one or more files with git restore --staged
200
+ remove hard Hard reset the repository to HEAD
144
201
  del Delete one or more files from the working tree
202
+ bin Soft reset HEAD~1 while keeping your changes
145
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
146
205
  push Run git push, optionally with a remote and branch
147
206
 
148
207
  Output:
@@ -153,8 +212,10 @@ Output:
153
212
  --verbose Print provider, model, cache, latency, and usage details
154
213
  --clipboard Copy the final output to the system clipboard
155
214
  --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.
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
158
219
 
159
220
  Providers:
160
221
  --provider LLM provider: openai, groq, openrouter, gemini, ollama, chutes
@@ -167,33 +228,6 @@ Comparison:
167
228
  --branch [base-ref] Analyze the current branch against a base branch
168
229
  --pr [base-ref] Alias for --branch, useful for PR-style comparisons
169
230
 
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
231
  Provider Setup:
198
232
  OpenAI:
199
233
  export LLM_PROVIDER=openai
@@ -231,6 +265,49 @@ Notes:
231
265
  Run gitxplain inside a Git repository.
232
266
  If no mode is supplied, gitxplain will prompt you to choose one interactively.
233
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
234
311
  `);
235
312
  }
236
313
 
@@ -262,9 +339,22 @@ function parseNumber(value, fallback = null) {
262
339
  return parsed;
263
340
  }
264
341
 
265
- 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 = {}) {
266
355
  const args = argv.slice(2);
267
356
  const subcommand = args[0];
357
+ const knownGitSubcommands = options.gitSubcommands ?? listGitSubcommands();
268
358
  const flags = new Set(args.filter((arg) => arg.startsWith("--")));
269
359
  const valueFlags = new Set(["--provider", "--model", "--max-diff-lines", "--branch", "--pr"]);
270
360
  const positional = [];
@@ -292,54 +382,80 @@ export function parseArgs(argv) {
292
382
  const explicitMode = [...MODE_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
293
383
  const explicitFormat = [...FORMAT_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
294
384
  const isInstallHook = subcommand === "install-hook";
295
- const isConnectGit = flags.has("--connect-git");
385
+ const isExample = flags.has("--example") || subcommand === "example";
386
+ const isNativeGitWrapper = subcommand === "git";
387
+ const isConnectGitHub = flags.has("--connect-github") || flags.has("--connect-git");
296
388
  const isBoot = flags.has("--boot");
297
389
  const isLogCommand = subcommand === "log";
298
390
  const isStatusCommand = subcommand === "status";
299
391
  const isCommitCommand = subcommand === "commit";
300
392
  const isMergeCommand = subcommand === "merge";
301
393
  const isTagCommand = subcommand === "tag";
394
+ const isReleaseCommand = subcommand === "release";
302
395
  const isAddCommand = subcommand === "add";
303
396
  const isRemoveCommand = subcommand === "remove";
304
397
  const isDeleteCommand = subcommand === "del";
398
+ const isPipelineCommand = subcommand === "pipeline" || flags.has("--pipeline");
399
+ const isBinCommand = subcommand === "bin";
305
400
  const isPopCommand = subcommand === "pop";
401
+ const isPullCommand = subcommand === "pull";
306
402
  const isPushCommand = subcommand === "push";
403
+ const isRemoveHardCommand = isRemoveCommand && positional[1] === "hard" && positional.length === 2;
404
+ const isNativeGitCommand = isNativeGitWrapper || isDirectNativeGitSubcommand(subcommand, knownGitSubcommands);
307
405
 
308
406
  return {
309
407
  subcommand,
310
408
  help: flags.has("--help") || subcommand === "help",
409
+ example: isExample,
410
+ nativeGitCommand: isNativeGitCommand,
311
411
  installHook: isInstallHook,
312
- connectGit: isConnectGit,
412
+ connectGitHub: isConnectGitHub,
313
413
  boot: isBoot,
314
414
  logCommand: isLogCommand,
315
415
  statusCommand: isStatusCommand,
316
416
  commitCommand: isCommitCommand,
317
417
  mergeCommand: isMergeCommand,
318
418
  tagCommand: isTagCommand,
419
+ releaseCommand: isReleaseCommand,
420
+ releaseAction: isReleaseCommand ? positional[1] ?? "status" : null,
319
421
  addCommand: isAddCommand,
320
422
  removeCommand: isRemoveCommand,
321
423
  deleteCommand: isDeleteCommand,
424
+ pipelineCommand: isPipelineCommand,
425
+ binCommand: isBinCommand,
322
426
  popCommand: isPopCommand,
427
+ pullCommand: isPullCommand,
323
428
  pushCommand: isPushCommand,
429
+ removeHardCommand: isRemoveHardCommand,
430
+ nativeGitArgs: isNativeGitWrapper ? args.slice(1) : isNativeGitCommand ? args : [],
324
431
  hookName: isInstallHook ? positional[1] ?? "post-commit" : null,
325
- actionPaths: isAddCommand || isRemoveCommand || isDeleteCommand ? positional.slice(1) : [],
326
- connectToken: isConnectGit ? positional[0] : null,
432
+ actionPaths:
433
+ isAddCommand || isDeleteCommand ? positional.slice(1) : isRemoveHardCommand ? [] : isRemoveCommand ? positional.slice(1) : [],
434
+ connectToken: isConnectGitHub ? positional[0] : null,
327
435
  stashIndex: isPopCommand ? positional[1] ?? null : null,
436
+ pullRemote: isPullCommand ? positional[1] ?? null : null,
437
+ pullBranch: isPullCommand ? positional[2] ?? null : null,
328
438
  pushRemote: isPushCommand ? positional[1] ?? null : null,
329
439
  pushBranch: isPushCommand ? positional[2] ?? null : null,
330
440
  commitRef:
331
441
  isInstallHook ||
332
- isConnectGit ||
442
+ isExample ||
443
+ isNativeGitCommand ||
444
+ isConnectGitHub ||
333
445
  isBoot ||
334
446
  isLogCommand ||
335
447
  isStatusCommand ||
336
448
  isCommitCommand ||
337
449
  isMergeCommand ||
338
450
  isTagCommand ||
451
+ isReleaseCommand ||
339
452
  isAddCommand ||
340
453
  isRemoveCommand ||
341
454
  isDeleteCommand ||
455
+ isPipelineCommand ||
456
+ isBinCommand ||
342
457
  isPopCommand ||
458
+ isPullCommand ||
343
459
  isPushCommand ||
344
460
  subcommand === "help"
345
461
  ? null
@@ -395,6 +511,7 @@ async function chooseModeInteractively() {
395
511
  "11. Tag Release Commits",
396
512
  "12. Repository Log",
397
513
  "13. Commit Working Tree",
514
+ "14. Create CI/CD Pipelines",
398
515
  "> "
399
516
  ].join("\n")
400
517
  );
@@ -412,7 +529,8 @@ async function chooseModeInteractively() {
412
529
  "10": "merge",
413
530
  "11": "tag",
414
531
  "12": "log",
415
- "13": "commit"
532
+ "13": "commit",
533
+ "14": "pipeline"
416
534
  };
417
535
 
418
536
  return selections[answer] ?? "full";
@@ -445,6 +563,15 @@ function resolveTargetRef(parsed, cwd) {
445
563
  return null;
446
564
  }
447
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
+
448
575
  function renderFinalOutput({ runtimeOptions, mode, commitData, explanation, responseMeta, promptMeta }) {
449
576
  if (runtimeOptions.format === "json") {
450
577
  return formatJsonOutput({ mode, commitData, explanation, responseMeta, promptMeta });
@@ -481,6 +608,15 @@ export async function main(argv = process.argv) {
481
608
  return 0;
482
609
  }
483
610
 
611
+ if (parsed.example) {
612
+ printExamples();
613
+ return 0;
614
+ }
615
+
616
+ if (parsed.nativeGitCommand) {
617
+ return runNativeGitPassthrough(parsed.nativeGitArgs, cwd);
618
+ }
619
+
484
620
  if (!isGitRepository(cwd)) {
485
621
  console.error("gitxplain must be run inside a Git repository.");
486
622
  return 1;
@@ -492,13 +628,13 @@ export async function main(argv = process.argv) {
492
628
  return 0;
493
629
  }
494
630
 
495
- if (parsed.connectGit) {
631
+ if (parsed.connectGitHub) {
496
632
  let token = parsed.connectToken;
497
633
  if (!token) {
498
634
  if (process.env.GITHUB_TOKEN) {
499
635
  token = process.env.GITHUB_TOKEN;
500
636
  } 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=...");
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=...");
502
638
  return 1;
503
639
  }
504
640
  }
@@ -517,7 +653,7 @@ export async function main(argv = process.argv) {
517
653
 
518
654
  if (parsed.boot) {
519
655
  if (!isGitConnected()) {
520
- console.error("You must connect a GitHub account first to use the interactive agent.\nCommand: gitxplain --connect-git <YOUR_TOKEN>");
656
+ console.error("You must connect a GitHub account first to use the interactive agent.\nCommand: gitxplain --connect-github <YOUR_TOKEN>");
521
657
  return 1;
522
658
  }
523
659
  const connection = loadGitConnection();
@@ -528,7 +664,13 @@ export async function main(argv = process.argv) {
528
664
  );
529
665
  const config = getProviderConfig(parsed.provider, parsed.model);
530
666
  validateProviderConfig(config);
531
- await startChatSession(connection.token, parsed.provider, parsed.model, userInfo.name || connection.user?.login);
667
+ const sessionArgs = buildBootSessionArgs(connection, userInfo, parsed);
668
+ await startChatSession(
669
+ sessionArgs.token,
670
+ sessionArgs.providerOverride,
671
+ sessionArgs.modelOverride,
672
+ sessionArgs.username
673
+ );
532
674
  } catch (configError) {
533
675
  console.error(`Missing LLM Key. Please check your .env variables or --provider flags.\n${configError.message}`);
534
676
  return 1;
@@ -546,8 +688,55 @@ export async function main(argv = process.argv) {
546
688
  return 0;
547
689
  }
548
690
 
549
- if (parsed.addCommand || parsed.removeCommand || parsed.deleteCommand || parsed.popCommand || parsed.pushCommand) {
550
- if (!parsed.popCommand && parsed.actionPaths.length === 0) {
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) {
551
740
  if (!parsed.pushCommand) {
552
741
  throw new Error(`No paths provided for "${parsed.subcommand}".`);
553
742
  }
@@ -560,6 +749,12 @@ export async function main(argv = process.argv) {
560
749
  }
561
750
 
562
751
  if (parsed.removeCommand) {
752
+ if (parsed.removeHardCommand) {
753
+ gitResetHard("HEAD", cwd);
754
+ console.log("Hard reset to HEAD.");
755
+ return 0;
756
+ }
757
+
563
758
  gitRestoreStaged(parsed.actionPaths, cwd);
564
759
  console.log(`Unstaged ${parsed.actionPaths.join(", ")}.`);
565
760
  return 0;
@@ -571,6 +766,12 @@ export async function main(argv = process.argv) {
571
766
  return 0;
572
767
  }
573
768
 
769
+ if (parsed.binCommand) {
770
+ gitResetSoft(cwd);
771
+ console.log("Soft reset HEAD~1 and kept your changes.");
772
+ return 0;
773
+ }
774
+
574
775
  if (parsed.popCommand) {
575
776
  const stashRef = resolveStashRef(parsed.stashIndex);
576
777
  gitStashPop(parsed.stashIndex, cwd);
@@ -578,6 +779,14 @@ export async function main(argv = process.argv) {
578
779
  return 0;
579
780
  }
580
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
+
581
790
  gitPush(cwd, parsed.pushRemote, parsed.pushBranch);
582
791
  console.log(
583
792
  `Pushed${parsed.pushRemote ? ` to ${parsed.pushRemote}` : ""}${parsed.pushBranch ? ` ${parsed.pushBranch}` : ""}.`
@@ -588,6 +797,36 @@ export async function main(argv = process.argv) {
588
797
  const runtimeOptions = resolveRuntimeOptions(parsed, config);
589
798
  const mode = parsed.mode ?? config.mode ?? (await chooseModeInteractively());
590
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
+
591
830
  if (mode === "commit" || parsed.commitCommand) {
592
831
  const commitData = fetchWorkingTreeData(cwd);
593
832
 
@@ -17,6 +17,19 @@ const COLORS = {
17
17
  red: "\x1b[31m"
18
18
  };
19
19
 
20
+ export function getBootHelpText() {
21
+ return [
22
+ "Available commands:",
23
+ " help Show this command list",
24
+ " repos Select a GitHub repository and commit for analysis",
25
+ " issues Summarize open issues for the selected repository",
26
+ " status Review local uncommitted git diff",
27
+ " download Download the selected repository at the selected commit",
28
+ " clear Reset the current chat history",
29
+ " exit Close the interactive session"
30
+ ].join("\n");
31
+ }
32
+
20
33
  export class ChatService {
21
34
  constructor(token, providerOverride, modelOverride, username) {
22
35
  this.token = token;
@@ -220,25 +233,32 @@ Analysis:
220
233
  });
221
234
  };
222
235
 
223
- console.log(`${COLORS.bold}${COLORS.cyan}GitHub Chat - Type 'repos' to select a repo, 'download' to clone the selected commit state, 'exit' to quit, 'clear' to reset history\n${COLORS.reset}`);
236
+ console.log(`${COLORS.bold}${COLORS.cyan}GitHub Chat${COLORS.reset}`);
237
+ console.log(`${COLORS.cyan}${getBootHelpText()}\n${COLORS.reset}`);
224
238
  console.log(`${COLORS.cyan}Model: ${this.config.model} (${this.config.provider})\n${COLORS.reset}`);
225
239
 
226
240
  while (true) {
227
241
  const userInput = await question("You: ");
242
+ const normalizedInput = userInput.trim().toLowerCase();
243
+
244
+ if (normalizedInput === "help") {
245
+ console.log(`\n${COLORS.cyan}${getBootHelpText()}\n${COLORS.reset}`);
246
+ continue;
247
+ }
228
248
 
229
- if (userInput.toLowerCase() === "exit") {
249
+ if (normalizedInput === "exit") {
230
250
  console.log(`${COLORS.green}Goodbye!${COLORS.reset}`);
231
251
  rl.close();
232
252
  break;
233
253
  }
234
254
 
235
- if (userInput.toLowerCase() === "clear") {
255
+ if (normalizedInput === "clear") {
236
256
  this.conversationHistory = [];
237
257
  console.log(`${COLORS.yellow}Conversation history cleared.\n${COLORS.reset}`);
238
258
  continue;
239
259
  }
240
260
 
241
- if (userInput.toLowerCase() === "download") {
261
+ if (normalizedInput === "download") {
242
262
  if (!this.activeRepo || !this.activeCommit) {
243
263
  console.log(`${COLORS.yellow}No repository/commit selected. Please type 'repos' first.\n${COLORS.reset}`);
244
264
  continue;
@@ -257,7 +277,7 @@ Analysis:
257
277
  continue;
258
278
  }
259
279
 
260
- if (userInput.toLowerCase() === "repos") {
280
+ if (normalizedInput === "repos") {
261
281
  const selectedRepo = await this.showRepoSelector();
262
282
  if (selectedRepo) {
263
283
  this.activeRepo = selectedRepo;
@@ -302,7 +322,7 @@ Please acknowledge this selection in a maximum of 3 sentences, giving a brief su
302
322
  continue;
303
323
  }
304
324
 
305
- if (userInput.toLowerCase() === "status") {
325
+ if (normalizedInput === "status") {
306
326
  try {
307
327
  const diff = execSync("git diff").toString();
308
328
  if (!diff) {
@@ -319,7 +339,7 @@ Please acknowledge this selection in a maximum of 3 sentences, giving a brief su
319
339
  continue;
320
340
  }
321
341
 
322
- if (userInput.toLowerCase() === "issues") {
342
+ if (normalizedInput === "issues") {
323
343
  if (!this.activeRepo) {
324
344
  console.log(`${COLORS.yellow}No active repository. Please type 'repos' first.\n${COLORS.reset}`);
325
345
  continue;
@@ -656,7 +676,7 @@ Please acknowledge this selection in a maximum of 3 sentences, giving a brief su
656
676
  }
657
677
  }
658
678
 
659
- export async function startChatSession(token, username, providerOverride, modelOverride) {
679
+ export async function startChatSession(token, providerOverride, modelOverride, username) {
660
680
  const chatService = new ChatService(token, providerOverride, modelOverride, username);
661
681
  await chatService.initializeRepoContext();
662
682
  await chatService.startInteractiveChat();