gitxplain 0.1.0
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/.env.example +28 -0
- package/README.md +268 -0
- package/cli/index.js +413 -0
- package/cli/services/aiService.js +362 -0
- package/cli/services/cacheService.js +37 -0
- package/cli/services/clipboardService.js +28 -0
- package/cli/services/configService.js +28 -0
- package/cli/services/gitService.js +132 -0
- package/cli/services/hookService.js +21 -0
- package/cli/services/outputFormatter.js +197 -0
- package/cli/services/promptService.js +83 -0
- package/package.json +21 -0
- package/prompts/impact.txt +15 -0
- package/prompts/issue.txt +18 -0
- package/prompts/junior.txt +15 -0
- package/prompts/lines.txt +22 -0
- package/prompts/master.txt +38 -0
- package/prompts/review.txt +26 -0
- package/prompts/security.txt +33 -0
- package/prompts/summary.txt +10 -0
package/.env.example
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
LLM_PROVIDER=openai
|
|
2
|
+
LLM_MODEL=
|
|
3
|
+
|
|
4
|
+
OPENAI_API_KEY=
|
|
5
|
+
OPENAI_MODEL=gpt-4.1-mini
|
|
6
|
+
OPENAI_BASE_URL=https://api.openai.com/v1
|
|
7
|
+
|
|
8
|
+
GROQ_API_KEY=
|
|
9
|
+
GROQ_MODEL=llama-3.3-70b-versatile
|
|
10
|
+
GROQ_BASE_URL=https://api.groq.com/openai/v1
|
|
11
|
+
|
|
12
|
+
OPENROUTER_API_KEY=
|
|
13
|
+
OPENROUTER_MODEL=openai/gpt-4.1-mini
|
|
14
|
+
OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
|
15
|
+
OPENROUTER_SITE_URL=https://github.com
|
|
16
|
+
OPENROUTER_APP_NAME=gitxplain
|
|
17
|
+
|
|
18
|
+
GEMINI_API_KEY=
|
|
19
|
+
GEMINI_MODEL=gemini-2.5-flash
|
|
20
|
+
GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta
|
|
21
|
+
|
|
22
|
+
OLLAMA_API_KEY=ollama
|
|
23
|
+
OLLAMA_MODEL=llama3.2
|
|
24
|
+
OLLAMA_BASE_URL=http://127.0.0.1:11434/v1
|
|
25
|
+
|
|
26
|
+
CHUTES_API_KEY=
|
|
27
|
+
CHUTES_MODEL=deepseek-ai/DeepSeek-V3-0324
|
|
28
|
+
CHUTES_BASE_URL=https://llm.chutes.ai/v1
|
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# gitxplain
|
|
2
|
+
|
|
3
|
+
`gitxplain` is a Node.js CLI that analyzes Git commits, commit ranges, and branch diffs to generate structured, human-readable explanations with AI.
|
|
4
|
+
|
|
5
|
+
Supported providers:
|
|
6
|
+
|
|
7
|
+
- OpenAI
|
|
8
|
+
- Groq
|
|
9
|
+
- OpenRouter
|
|
10
|
+
- Gemini
|
|
11
|
+
- Ollama
|
|
12
|
+
- Chutes AI
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Explains what a commit does, why it exists, and how the fix works
|
|
17
|
+
- Supports focused output modes like summary, issue, fix, impact, review, security, and line-by-line walkthroughs
|
|
18
|
+
- Supports single commits, commit ranges, and branch-vs-base comparisons
|
|
19
|
+
- Truncates oversized diffs before sending them to the model and reports that truncation
|
|
20
|
+
- Streams output for supported providers
|
|
21
|
+
- Caches responses locally to reduce repeat API costs
|
|
22
|
+
- Supports plain, JSON, Markdown, and HTML output
|
|
23
|
+
- Supports clipboard copy, verbosity controls, and hook installation
|
|
24
|
+
- Supports project-level and user-level config files
|
|
25
|
+
- Falls back to an interactive prompt when no analysis flag is supplied
|
|
26
|
+
- Returns plain text or JSON output
|
|
27
|
+
- Uses native Node APIs only, so the MVP has no runtime dependencies
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Node.js 18+
|
|
32
|
+
- A Git repository in your current working directory
|
|
33
|
+
- An API key for your chosen provider, or a local Ollama instance
|
|
34
|
+
|
|
35
|
+
Optional environment variables:
|
|
36
|
+
|
|
37
|
+
- `LLM_PROVIDER` default: `openai`
|
|
38
|
+
- `LLM_MODEL` optional shared model override
|
|
39
|
+
- `OPENAI_API_KEY`, `OPENAI_MODEL`, `OPENAI_BASE_URL`
|
|
40
|
+
- `GROQ_API_KEY`, `GROQ_MODEL`, `GROQ_BASE_URL`
|
|
41
|
+
- `OPENROUTER_API_KEY`, `OPENROUTER_MODEL`, `OPENROUTER_BASE_URL`
|
|
42
|
+
- `OPENROUTER_SITE_URL`, `OPENROUTER_APP_NAME`
|
|
43
|
+
- `GEMINI_API_KEY`, `GEMINI_MODEL`, `GEMINI_BASE_URL`
|
|
44
|
+
- `OLLAMA_API_KEY` optional, default: `ollama`
|
|
45
|
+
- `OLLAMA_MODEL`, `OLLAMA_BASE_URL` default: `http://127.0.0.1:11434/v1`
|
|
46
|
+
- `CHUTES_API_KEY`, `CHUTES_MODEL`, `CHUTES_BASE_URL`
|
|
47
|
+
|
|
48
|
+
Optional config files:
|
|
49
|
+
|
|
50
|
+
- Project: `.gitxplainrc` or `.gitxplainrc.json`
|
|
51
|
+
- User: `~/.gitxplain/config.json`
|
|
52
|
+
|
|
53
|
+
You can start from:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cp .env.example .env
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
gitxplain help
|
|
63
|
+
gitxplain <commit-id>
|
|
64
|
+
gitxplain <commit-id> --summary
|
|
65
|
+
gitxplain <commit-id> --issues
|
|
66
|
+
gitxplain <commit-id> --fix
|
|
67
|
+
gitxplain <commit-id> --impact
|
|
68
|
+
gitxplain <commit-id> --full
|
|
69
|
+
gitxplain <commit-id> --lines
|
|
70
|
+
gitxplain <commit-id> --review
|
|
71
|
+
gitxplain <commit-id> --security
|
|
72
|
+
gitxplain <commit-id> --json
|
|
73
|
+
gitxplain <commit-id> --markdown
|
|
74
|
+
gitxplain <commit-id> --html
|
|
75
|
+
gitxplain <commit-id> --stream
|
|
76
|
+
gitxplain <commit-id> --clipboard
|
|
77
|
+
gitxplain <commit-id> --verbose
|
|
78
|
+
gitxplain <commit-id> --quiet
|
|
79
|
+
gitxplain <start>..<end> --markdown
|
|
80
|
+
gitxplain --branch main --review
|
|
81
|
+
gitxplain --pr origin/main --security
|
|
82
|
+
gitxplain install-hook
|
|
83
|
+
gitxplain <commit-id> --provider openrouter --model anthropic/claude-3.7-sonnet
|
|
84
|
+
gitxplain <commit-id> --provider chutes --model deepseek-ai/DeepSeek-V3-0324
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Examples:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm start -- HEAD~1 --summary
|
|
91
|
+
npm start -- a1b2c3d --full
|
|
92
|
+
npm start -- HEAD~1 --lines
|
|
93
|
+
npm start -- HEAD~5..HEAD --markdown
|
|
94
|
+
npm start -- --branch main --review
|
|
95
|
+
npm start -- HEAD~1 --provider groq --model llama-3.3-70b-versatile
|
|
96
|
+
npm start -- HEAD~1 --provider gemini --model gemini-2.5-flash
|
|
97
|
+
npm start -- HEAD~1 --provider chutes --model deepseek-ai/DeepSeek-V3-0324
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Running The CLI
|
|
101
|
+
|
|
102
|
+
To use the actual `gitxplain` command directly:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
cd /home/guru/Dev/gitxplain
|
|
106
|
+
npm link
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then from any Git repository:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
gitxplain help
|
|
113
|
+
gitxplain HEAD~1 --full
|
|
114
|
+
gitxplain a1b2c3d --summary
|
|
115
|
+
gitxplain HEAD~1 --lines
|
|
116
|
+
gitxplain HEAD~5..HEAD --markdown
|
|
117
|
+
gitxplain --branch main --review
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The `gitxplain help` command also prints quick API-key setup examples for:
|
|
121
|
+
|
|
122
|
+
- OpenAI
|
|
123
|
+
- Groq
|
|
124
|
+
- OpenRouter
|
|
125
|
+
- Gemini
|
|
126
|
+
- Ollama
|
|
127
|
+
- Chutes AI
|
|
128
|
+
|
|
129
|
+
If you do not want to link it globally, you can still run it locally:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
node /home/guru/Dev/gitxplain/cli/index.js HEAD~1 --full
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Output Modes
|
|
136
|
+
|
|
137
|
+
- `--summary`: one-sentence commit summary
|
|
138
|
+
- `--issues`: bug or issue-oriented analysis
|
|
139
|
+
- `--fix`: junior-friendly explanation of the fix
|
|
140
|
+
- `--impact`: before-vs-after explanation focused on behavior changes
|
|
141
|
+
- `--full`: full structured analysis
|
|
142
|
+
- `--lines`: file-by-file, line-by-line walkthrough of the changed code
|
|
143
|
+
- `--review`: code review findings with actionable suggestions
|
|
144
|
+
- `--security`: security-focused analysis of the change
|
|
145
|
+
- `--json`: return structured JSON instead of formatted text
|
|
146
|
+
- `--markdown`: return Markdown output
|
|
147
|
+
- `--html`: return HTML output
|
|
148
|
+
|
|
149
|
+
If no analysis flag is supplied, the CLI asks what kind of explanation you want.
|
|
150
|
+
|
|
151
|
+
## Comparison Modes
|
|
152
|
+
|
|
153
|
+
Single commit:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
gitxplain HEAD~1 --full
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Commit range:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
gitxplain HEAD~5..HEAD --markdown
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Branch or PR-style comparison:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
gitxplain --branch main --review
|
|
169
|
+
gitxplain --pr origin/main --security
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`--branch` and `--pr` compare the current branch to a base ref using the merge base with `HEAD`.
|
|
173
|
+
|
|
174
|
+
## Config File
|
|
175
|
+
|
|
176
|
+
Example `.gitxplainrc`:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
{
|
|
180
|
+
"provider": "groq",
|
|
181
|
+
"model": "llama-3.3-70b-versatile",
|
|
182
|
+
"mode": "full",
|
|
183
|
+
"format": "markdown",
|
|
184
|
+
"maxDiffLines": 600,
|
|
185
|
+
"stream": true,
|
|
186
|
+
"verbose": false
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
CLI flags still override config values for a single command.
|
|
191
|
+
|
|
192
|
+
## Clipboard, Streaming, And Hooks
|
|
193
|
+
|
|
194
|
+
Copy the final output to your clipboard:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
gitxplain HEAD~1 --markdown --clipboard
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Stream long responses as they arrive:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
gitxplain HEAD~1 --full --stream
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Install a post-commit hook that saves a Markdown explanation under `.git/gitxplain/last-explanation.md`:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
gitxplain install-hook
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Provider Setup
|
|
213
|
+
|
|
214
|
+
OpenAI:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
export LLM_PROVIDER=openai
|
|
218
|
+
export OPENAI_API_KEY=your_key
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Groq:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
export LLM_PROVIDER=groq
|
|
225
|
+
export GROQ_API_KEY=your_key
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
OpenRouter:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
export LLM_PROVIDER=openrouter
|
|
232
|
+
export OPENROUTER_API_KEY=your_key
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Gemini:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
export LLM_PROVIDER=gemini
|
|
239
|
+
export GEMINI_API_KEY=your_key
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Ollama:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
export LLM_PROVIDER=ollama
|
|
246
|
+
export OLLAMA_MODEL=llama3.2
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Chutes AI:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
export LLM_PROVIDER=chutes
|
|
253
|
+
export CHUTES_API_KEY=your_key
|
|
254
|
+
export CHUTES_MODEL=deepseek-ai/DeepSeek-V3-0324
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Development
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
npm run lint
|
|
261
|
+
npm test
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
To make the command globally available during local development:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
npm link
|
|
268
|
+
```
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { realpathSync } from "node:fs";
|
|
7
|
+
import { generateExplanation } from "./services/aiService.js";
|
|
8
|
+
import { copyToClipboard } from "./services/clipboardService.js";
|
|
9
|
+
import { loadConfig } from "./services/configService.js";
|
|
10
|
+
import {
|
|
11
|
+
buildBranchRange,
|
|
12
|
+
fetchCommitData,
|
|
13
|
+
getDefaultBaseRef,
|
|
14
|
+
isGitRepository
|
|
15
|
+
} from "./services/gitService.js";
|
|
16
|
+
import { installHook } from "./services/hookService.js";
|
|
17
|
+
import {
|
|
18
|
+
formatFooter,
|
|
19
|
+
formatHtmlOutput,
|
|
20
|
+
formatJsonOutput,
|
|
21
|
+
formatMarkdownOutput,
|
|
22
|
+
formatOutput,
|
|
23
|
+
formatPreamble
|
|
24
|
+
} from "./services/outputFormatter.js";
|
|
25
|
+
|
|
26
|
+
const MODE_FLAGS = new Map([
|
|
27
|
+
["--summary", "summary"],
|
|
28
|
+
["--issues", "issues"],
|
|
29
|
+
["--fix", "fix"],
|
|
30
|
+
["--impact", "impact"],
|
|
31
|
+
["--full", "full"],
|
|
32
|
+
["--lines", "lines"],
|
|
33
|
+
["--review", "review"],
|
|
34
|
+
["--security", "security"]
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const FORMAT_FLAGS = new Map([
|
|
38
|
+
["--json", "json"],
|
|
39
|
+
["--markdown", "markdown"],
|
|
40
|
+
["--html", "html"]
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
function printHelp() {
|
|
44
|
+
console.log(`gitxplain - AI-powered Git and branch explainer
|
|
45
|
+
|
|
46
|
+
Usage:
|
|
47
|
+
gitxplain help
|
|
48
|
+
gitxplain --help
|
|
49
|
+
gitxplain install-hook [hook-name]
|
|
50
|
+
gitxplain <commit-id> [options]
|
|
51
|
+
gitxplain <start>..<end> [options]
|
|
52
|
+
gitxplain --branch [base-ref] [options]
|
|
53
|
+
gitxplain --pr [base-ref] [options]
|
|
54
|
+
|
|
55
|
+
Modes:
|
|
56
|
+
--summary Generate a one-line summary
|
|
57
|
+
--issues Focus on bug or issue analysis
|
|
58
|
+
--fix Explain the fix in simple terms
|
|
59
|
+
--impact Explain before-vs-after behavior changes
|
|
60
|
+
--full Generate a full structured analysis
|
|
61
|
+
--lines Explain the changed code line by line
|
|
62
|
+
--review Generate a code review with risks and suggestions
|
|
63
|
+
--security Focus on security risks introduced by the change
|
|
64
|
+
|
|
65
|
+
Output:
|
|
66
|
+
--json Print JSON output
|
|
67
|
+
--markdown Print Markdown output
|
|
68
|
+
--html Print HTML output
|
|
69
|
+
--quiet Print only the explanation body
|
|
70
|
+
--verbose Print provider, model, cache, latency, and usage details
|
|
71
|
+
--clipboard Copy the final output to the system clipboard
|
|
72
|
+
--stream Stream the explanation as it is generated when supported
|
|
73
|
+
|
|
74
|
+
Providers:
|
|
75
|
+
--provider LLM provider: openai, groq, openrouter, gemini, ollama, chutes
|
|
76
|
+
--model Override the model name
|
|
77
|
+
|
|
78
|
+
Diff Budget:
|
|
79
|
+
--max-diff-lines <n> Limit diff lines sent to the model
|
|
80
|
+
|
|
81
|
+
Comparison:
|
|
82
|
+
--branch [base-ref] Analyze current branch against base branch
|
|
83
|
+
--pr [base-ref] Alias for --branch, useful for PR-style summaries
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
gitxplain HEAD~1 --full
|
|
87
|
+
gitxplain HEAD~5..HEAD --markdown
|
|
88
|
+
gitxplain --branch main --review
|
|
89
|
+
gitxplain --pr origin/main --security --stream
|
|
90
|
+
gitxplain HEAD~1 --provider chutes --model deepseek-ai/DeepSeek-V3-0324
|
|
91
|
+
|
|
92
|
+
Provider Setup:
|
|
93
|
+
OpenAI:
|
|
94
|
+
export LLM_PROVIDER=openai
|
|
95
|
+
export OPENAI_API_KEY=your_key
|
|
96
|
+
|
|
97
|
+
Groq:
|
|
98
|
+
export LLM_PROVIDER=groq
|
|
99
|
+
export GROQ_API_KEY=your_key
|
|
100
|
+
|
|
101
|
+
OpenRouter:
|
|
102
|
+
export LLM_PROVIDER=openrouter
|
|
103
|
+
export OPENROUTER_API_KEY=your_key
|
|
104
|
+
|
|
105
|
+
Gemini:
|
|
106
|
+
export LLM_PROVIDER=gemini
|
|
107
|
+
export GEMINI_API_KEY=your_key
|
|
108
|
+
|
|
109
|
+
Ollama:
|
|
110
|
+
export LLM_PROVIDER=ollama
|
|
111
|
+
export OLLAMA_MODEL=llama3.2
|
|
112
|
+
|
|
113
|
+
Chutes:
|
|
114
|
+
export LLM_PROVIDER=chutes
|
|
115
|
+
export CHUTES_API_KEY=your_key
|
|
116
|
+
|
|
117
|
+
Config:
|
|
118
|
+
Project config: .gitxplainrc or .gitxplainrc.json
|
|
119
|
+
User config: ~/.gitxplain/config.json
|
|
120
|
+
|
|
121
|
+
Hook Installation:
|
|
122
|
+
gitxplain install-hook
|
|
123
|
+
gitxplain install-hook post-commit
|
|
124
|
+
|
|
125
|
+
Notes:
|
|
126
|
+
Run gitxplain inside a Git repository.
|
|
127
|
+
Use --provider or --model to override your config or environment for one command.
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getFlagValue(args, flagName) {
|
|
132
|
+
const directIndex = args.findIndex((arg) => arg === flagName);
|
|
133
|
+
if (directIndex >= 0) {
|
|
134
|
+
const nextArg = args[directIndex + 1];
|
|
135
|
+
if (nextArg && !nextArg.startsWith("--")) {
|
|
136
|
+
return nextArg;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const inline = args.find((arg) => arg.startsWith(`${flagName}=`));
|
|
143
|
+
return inline ? inline.slice(flagName.length + 1) : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseNumber(value, fallback = null) {
|
|
147
|
+
if (value == null || value === "") {
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const parsed = Number.parseInt(value, 10);
|
|
152
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
153
|
+
throw new Error(`Invalid numeric value: ${value}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function parseArgs(argv) {
|
|
160
|
+
const args = argv.slice(2);
|
|
161
|
+
const subcommand = args[0];
|
|
162
|
+
const flags = new Set(args.filter((arg) => arg.startsWith("--")));
|
|
163
|
+
const valueFlags = new Set(["--provider", "--model", "--max-diff-lines", "--branch", "--pr"]);
|
|
164
|
+
const positional = [];
|
|
165
|
+
|
|
166
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
167
|
+
const arg = args[index];
|
|
168
|
+
|
|
169
|
+
if (!arg.startsWith("--")) {
|
|
170
|
+
positional.push(arg);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (arg.includes("=")) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (valueFlags.has(arg)) {
|
|
179
|
+
const nextArg = args[index + 1];
|
|
180
|
+
if (nextArg && !nextArg.startsWith("--")) {
|
|
181
|
+
index += 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const explicitMode = [...MODE_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
|
|
187
|
+
const explicitFormat = [...FORMAT_FLAGS.entries()].find(([flag]) => flags.has(flag))?.[1] ?? null;
|
|
188
|
+
const isInstallHook = subcommand === "install-hook";
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
subcommand,
|
|
192
|
+
help: flags.has("--help") || subcommand === "help",
|
|
193
|
+
installHook: isInstallHook,
|
|
194
|
+
hookName: isInstallHook ? positional[1] ?? "post-commit" : null,
|
|
195
|
+
commitRef: isInstallHook || subcommand === "help" ? null : positional[0] ?? null,
|
|
196
|
+
mode: explicitMode,
|
|
197
|
+
format: explicitFormat,
|
|
198
|
+
provider: getFlagValue(args, "--provider"),
|
|
199
|
+
model: getFlagValue(args, "--model"),
|
|
200
|
+
maxDiffLines: parseNumber(getFlagValue(args, "--max-diff-lines")),
|
|
201
|
+
hasBranchFlag: flags.has("--branch") || args.some((arg) => arg.startsWith("--branch=")),
|
|
202
|
+
branchBase: getFlagValue(args, "--branch"),
|
|
203
|
+
hasPrFlag: flags.has("--pr") || args.some((arg) => arg.startsWith("--pr=")),
|
|
204
|
+
prBase: getFlagValue(args, "--pr"),
|
|
205
|
+
clipboard: flags.has("--clipboard"),
|
|
206
|
+
stream: flags.has("--stream"),
|
|
207
|
+
verbose: flags.has("--verbose"),
|
|
208
|
+
quiet: flags.has("--quiet")
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function askQuestion(prompt) {
|
|
213
|
+
return new Promise((resolve) => {
|
|
214
|
+
process.stdout.write(prompt);
|
|
215
|
+
process.stdin.resume();
|
|
216
|
+
process.stdin.setEncoding("utf8");
|
|
217
|
+
process.stdin.once("data", (input) => {
|
|
218
|
+
process.stdin.pause();
|
|
219
|
+
resolve(input.trim());
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function chooseModeInteractively() {
|
|
225
|
+
const answer = await askQuestion(
|
|
226
|
+
[
|
|
227
|
+
"What do you want to know?",
|
|
228
|
+
"1. Summary",
|
|
229
|
+
"2. Issues Fixed",
|
|
230
|
+
"3. Fix Explanation",
|
|
231
|
+
"4. Impact",
|
|
232
|
+
"5. Full Analysis",
|
|
233
|
+
"6. Line-by-Line Code Walkthrough",
|
|
234
|
+
"7. Code Review",
|
|
235
|
+
"8. Security Review",
|
|
236
|
+
"> "
|
|
237
|
+
].join("\n")
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const selections = {
|
|
241
|
+
"1": "summary",
|
|
242
|
+
"2": "issues",
|
|
243
|
+
"3": "fix",
|
|
244
|
+
"4": "impact",
|
|
245
|
+
"5": "full",
|
|
246
|
+
"6": "lines",
|
|
247
|
+
"7": "review",
|
|
248
|
+
"8": "security"
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return selections[answer] ?? "full";
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function resolveRuntimeOptions(parsed, config) {
|
|
255
|
+
return {
|
|
256
|
+
mode: parsed.mode ?? config.mode ?? "full",
|
|
257
|
+
format: parsed.format ?? config.format ?? "plain",
|
|
258
|
+
provider: parsed.provider ?? config.provider ?? null,
|
|
259
|
+
model: parsed.model ?? config.model ?? null,
|
|
260
|
+
maxDiffLines: parsed.maxDiffLines ?? config.maxDiffLines ?? 800,
|
|
261
|
+
clipboard: parsed.clipboard || config.clipboard === true,
|
|
262
|
+
stream: parsed.stream || config.stream === true,
|
|
263
|
+
verbose: parsed.verbose || config.verbose === true,
|
|
264
|
+
quiet: parsed.quiet || config.quiet === true
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function resolveTargetRef(parsed, cwd) {
|
|
269
|
+
if (parsed.commitRef) {
|
|
270
|
+
return parsed.commitRef;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (parsed.hasBranchFlag || parsed.hasPrFlag) {
|
|
274
|
+
const baseRef = parsed.branchBase || parsed.prBase || getDefaultBaseRef(cwd);
|
|
275
|
+
return buildBranchRange(baseRef, cwd);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function renderFinalOutput({ runtimeOptions, mode, commitData, explanation, responseMeta, promptMeta }) {
|
|
282
|
+
if (runtimeOptions.format === "json") {
|
|
283
|
+
return formatJsonOutput({ mode, commitData, explanation, responseMeta, promptMeta });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (runtimeOptions.format === "markdown") {
|
|
287
|
+
return formatMarkdownOutput({ mode, commitData, explanation, responseMeta, promptMeta });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (runtimeOptions.format === "html") {
|
|
291
|
+
return formatHtmlOutput({ mode, commitData, explanation, responseMeta, promptMeta });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return formatOutput({
|
|
295
|
+
mode,
|
|
296
|
+
commitData,
|
|
297
|
+
explanation,
|
|
298
|
+
responseMeta,
|
|
299
|
+
promptMeta,
|
|
300
|
+
options: runtimeOptions
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function main(argv = process.argv) {
|
|
305
|
+
const cwd = process.cwd();
|
|
306
|
+
const config = loadConfig(cwd);
|
|
307
|
+
const parsed = parseArgs(argv);
|
|
308
|
+
|
|
309
|
+
if (parsed.help) {
|
|
310
|
+
printHelp();
|
|
311
|
+
return 0;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!isGitRepository(cwd)) {
|
|
315
|
+
console.error("gitxplain must be run inside a Git repository.");
|
|
316
|
+
return 1;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (parsed.installHook) {
|
|
320
|
+
const hookPath = installHook({ cwd, hookName: parsed.hookName });
|
|
321
|
+
console.log(`Installed ${parsed.hookName} hook at ${hookPath}`);
|
|
322
|
+
return 0;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const runtimeOptions = resolveRuntimeOptions(parsed, config);
|
|
326
|
+
const targetRef = resolveTargetRef(parsed, cwd);
|
|
327
|
+
|
|
328
|
+
if (!targetRef) {
|
|
329
|
+
printHelp();
|
|
330
|
+
return 1;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const mode = parsed.mode ?? config.mode ?? (await chooseModeInteractively());
|
|
334
|
+
const commitData = fetchCommitData(targetRef, cwd);
|
|
335
|
+
const canStream = runtimeOptions.stream && runtimeOptions.format === "plain";
|
|
336
|
+
let streamStarted = false;
|
|
337
|
+
|
|
338
|
+
const { explanation, responseMeta, promptMeta } = await generateExplanation({
|
|
339
|
+
mode,
|
|
340
|
+
commitData,
|
|
341
|
+
providerOverride: runtimeOptions.provider,
|
|
342
|
+
modelOverride: runtimeOptions.model,
|
|
343
|
+
maxDiffLines: runtimeOptions.maxDiffLines,
|
|
344
|
+
stream: canStream,
|
|
345
|
+
onStart: canStream
|
|
346
|
+
? ({ promptMeta: streamPromptMeta }) => {
|
|
347
|
+
if (!runtimeOptions.quiet && !streamStarted) {
|
|
348
|
+
process.stdout.write(
|
|
349
|
+
formatPreamble({
|
|
350
|
+
mode,
|
|
351
|
+
commitData,
|
|
352
|
+
responseMeta: null,
|
|
353
|
+
promptMeta: streamPromptMeta,
|
|
354
|
+
options: runtimeOptions
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
streamStarted = true;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
: null,
|
|
361
|
+
onChunk: canStream ? (chunk) => process.stdout.write(chunk) : null
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
let renderedOutput;
|
|
365
|
+
|
|
366
|
+
if (canStream) {
|
|
367
|
+
process.stdout.write("\n");
|
|
368
|
+
if (runtimeOptions.verbose) {
|
|
369
|
+
process.stdout.write(formatFooter({ responseMeta, promptMeta, options: runtimeOptions }));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
renderedOutput = renderFinalOutput({
|
|
373
|
+
runtimeOptions,
|
|
374
|
+
mode,
|
|
375
|
+
commitData,
|
|
376
|
+
explanation,
|
|
377
|
+
responseMeta,
|
|
378
|
+
promptMeta
|
|
379
|
+
});
|
|
380
|
+
} else {
|
|
381
|
+
renderedOutput = renderFinalOutput({
|
|
382
|
+
runtimeOptions,
|
|
383
|
+
mode,
|
|
384
|
+
commitData,
|
|
385
|
+
explanation,
|
|
386
|
+
responseMeta,
|
|
387
|
+
promptMeta
|
|
388
|
+
});
|
|
389
|
+
console.log(renderedOutput);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (runtimeOptions.clipboard) {
|
|
393
|
+
copyToClipboard(renderedOutput);
|
|
394
|
+
if (!runtimeOptions.quiet) {
|
|
395
|
+
console.error("Copied output to clipboard.");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const entryFile = fileURLToPath(import.meta.url);
|
|
403
|
+
const executedFile = process.argv[1] ? realpathSync(path.resolve(process.argv[1])) : "";
|
|
404
|
+
|
|
405
|
+
if (executedFile === entryFile) {
|
|
406
|
+
main().then(
|
|
407
|
+
(code) => process.exit(code),
|
|
408
|
+
(error) => {
|
|
409
|
+
console.error(error.message);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
}
|