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/.github/workflows/ci.yml +28 -0
- package/.github/workflows/release.yml +27 -0
- package/IMPLEMENTATION.md +10 -10
- package/README.md +47 -0
- package/cli/index.js +283 -44
- package/cli/services/chatService.js +28 -8
- package/cli/services/gitService.js +45 -3
- package/cli/services/mergeService.js +273 -56
- package/cli/services/outputFormatter.js +2 -36
- package/cli/services/pipelineService.js +721 -0
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
326
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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.
|
|
550
|
-
if (
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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,
|
|
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();
|