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