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/.github/workflows/ci.yml +28 -0
- package/.github/workflows/release.yml +27 -0
- package/IMPLEMENTATION.md +10 -10
- package/README.md +386 -110
- package/cli/index.js +359 -209
- package/cli/services/chatService.js +28 -8
- package/cli/services/configService.js +75 -3
- package/cli/services/gitService.js +45 -3
- package/cli/services/mergeService.js +303 -69
- package/cli/services/outputFormatter.js +2 -36
- package/cli/services/pipelineService.js +721 -0
- package/package.json +2 -2
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 {
|
|
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
|
|
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
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
--
|
|
125
|
-
--
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
--
|
|
150
|
-
--
|
|
151
|
-
--
|
|
152
|
-
--
|
|
153
|
-
--
|
|
154
|
-
--
|
|
155
|
-
--
|
|
156
|
-
--
|
|
157
|
-
--
|
|
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]
|
|
168
|
-
--pr [base-ref]
|
|
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
|
|
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
|
-
|
|
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
|
|
296
|
-
const
|
|
297
|
-
const
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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:
|
|
326
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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.
|
|
496
|
-
|
|
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.
|
|
519
|
-
|
|
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.
|
|
540
|
-
|
|
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.
|
|
545
|
-
|
|
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 (
|
|
550
|
-
|
|
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 === "
|
|
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.
|
|
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.
|
|
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
|
}
|