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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
const ANSI = {
|
|
4
|
+
reset: "\u001b[0m",
|
|
5
|
+
bold: "\u001b[1m",
|
|
6
|
+
cyan: "\u001b[36m",
|
|
7
|
+
yellow: "\u001b[33m",
|
|
8
|
+
green: "\u001b[32m",
|
|
9
|
+
red: "\u001b[31m",
|
|
10
|
+
gray: "\u001b[90m"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function supportsColor() {
|
|
14
|
+
return Boolean(process.stdout?.isTTY) && process.env.NO_COLOR == null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function colorize(text, color) {
|
|
18
|
+
if (!supportsColor()) {
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `${color}${text}${ANSI.reset}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatTargetLabel(commitData) {
|
|
26
|
+
return commitData.analysisType === "range" ? "Range" : "Commit";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function highlightLine(line) {
|
|
30
|
+
if (/^([0-9]+\.)?\s*(Summary|Issue|Root Cause|Fix|Impact|Risk Level|Technical Breakdown|Security Findings|Suggestions|Review Findings):/i.test(line)) {
|
|
31
|
+
return colorize(line, ANSI.bold + ANSI.cyan);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (/risk/i.test(line) && /\blow\b/i.test(line)) {
|
|
35
|
+
return colorize(line, ANSI.green);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (/risk/i.test(line) && /\bmedium\b/i.test(line)) {
|
|
39
|
+
return colorize(line, ANSI.yellow);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (/risk/i.test(line) && /\bhigh\b/i.test(line)) {
|
|
43
|
+
return colorize(line, ANSI.red);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return line;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function formatExplanation(explanation) {
|
|
50
|
+
return explanation
|
|
51
|
+
.split("\n")
|
|
52
|
+
.map((line) => highlightLine(line))
|
|
53
|
+
.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatPreamble({ mode, commitData, options, promptMeta }) {
|
|
57
|
+
if (options.quiet) {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const header = [
|
|
62
|
+
`${colorize(formatTargetLabel(commitData), ANSI.bold + ANSI.cyan)}: ${commitData.displayRef}`,
|
|
63
|
+
`Files Changed: ${commitData.filesChanged.length}`,
|
|
64
|
+
`Stats: ${commitData.stats}`,
|
|
65
|
+
`Mode: ${mode}`
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
if (commitData.analysisType === "range") {
|
|
69
|
+
header.splice(1, 0, `Commits: ${commitData.commitCount}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (promptMeta?.warnings?.length) {
|
|
73
|
+
header.push(...promptMeta.warnings.map((warning) => colorize(`Warning: ${warning}`, ANSI.yellow)));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return `${header.join("\n")}\n\n`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatFooter({ responseMeta, promptMeta, options }) {
|
|
80
|
+
if (!options.verbose || !responseMeta) {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const lines = [
|
|
85
|
+
"",
|
|
86
|
+
colorize("Meta:", ANSI.bold + ANSI.gray),
|
|
87
|
+
`Provider: ${responseMeta.provider}`,
|
|
88
|
+
`Model: ${responseMeta.model}`,
|
|
89
|
+
`Cache: ${responseMeta.cacheHit ? "hit" : "miss"}`,
|
|
90
|
+
`Latency: ${responseMeta.latencyMs}ms`
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
if (responseMeta.usage) {
|
|
94
|
+
lines.push(`Usage: ${JSON.stringify(responseMeta.usage)}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (promptMeta?.warnings?.length) {
|
|
98
|
+
lines.push(...promptMeta.warnings);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `${lines.join("\n")}\n`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function formatOutput({ mode, commitData, explanation, responseMeta, promptMeta, options }) {
|
|
105
|
+
return `${formatPreamble({ mode, commitData, options, promptMeta })}${formatExplanation(explanation)}${formatFooter({ responseMeta, promptMeta, options })}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function formatMarkdownOutput({ mode, commitData, explanation, responseMeta, promptMeta }) {
|
|
109
|
+
const lines = [
|
|
110
|
+
`# gitxplain`,
|
|
111
|
+
``,
|
|
112
|
+
`- Target: ${commitData.displayRef}`,
|
|
113
|
+
`- Type: ${commitData.analysisType}`,
|
|
114
|
+
`- Files Changed: ${commitData.filesChanged.length}`,
|
|
115
|
+
`- Stats: ${commitData.stats}`,
|
|
116
|
+
`- Mode: ${mode}`
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
if (commitData.analysisType === "range") {
|
|
120
|
+
lines.push(`- Commits: ${commitData.commitCount}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (responseMeta) {
|
|
124
|
+
lines.push(`- Provider: ${responseMeta.provider}`);
|
|
125
|
+
lines.push(`- Model: ${responseMeta.model}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (promptMeta?.warnings?.length) {
|
|
129
|
+
lines.push(...promptMeta.warnings.map((warning) => `- Warning: ${warning}`));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
lines.push("", explanation);
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function escapeHtml(text) {
|
|
137
|
+
return text
|
|
138
|
+
.replaceAll("&", "&")
|
|
139
|
+
.replaceAll("<", "<")
|
|
140
|
+
.replaceAll(">", ">");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function formatHtmlOutput({ mode, commitData, explanation, responseMeta, promptMeta }) {
|
|
144
|
+
const metaItems = [
|
|
145
|
+
`<li><strong>Target:</strong> ${escapeHtml(commitData.displayRef)}</li>`,
|
|
146
|
+
`<li><strong>Type:</strong> ${escapeHtml(commitData.analysisType)}</li>`,
|
|
147
|
+
`<li><strong>Files Changed:</strong> ${commitData.filesChanged.length}</li>`,
|
|
148
|
+
`<li><strong>Stats:</strong> ${escapeHtml(commitData.stats)}</li>`,
|
|
149
|
+
`<li><strong>Mode:</strong> ${escapeHtml(mode)}</li>`
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
if (commitData.analysisType === "range") {
|
|
153
|
+
metaItems.push(`<li><strong>Commits:</strong> ${commitData.commitCount}</li>`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (responseMeta) {
|
|
157
|
+
metaItems.push(`<li><strong>Provider:</strong> ${escapeHtml(responseMeta.provider)}</li>`);
|
|
158
|
+
metaItems.push(`<li><strong>Model:</strong> ${escapeHtml(responseMeta.model)}</li>`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (promptMeta?.warnings?.length) {
|
|
162
|
+
metaItems.push(
|
|
163
|
+
...promptMeta.warnings.map((warning) => `<li><strong>Warning:</strong> ${escapeHtml(warning)}</li>`)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return [
|
|
168
|
+
"<!doctype html>",
|
|
169
|
+
"<html><head><meta charset=\"utf-8\"><title>gitxplain</title></head><body>",
|
|
170
|
+
"<h1>gitxplain</h1>",
|
|
171
|
+
`<ul>${metaItems.join("")}</ul>`,
|
|
172
|
+
`<pre>${escapeHtml(explanation)}</pre>`,
|
|
173
|
+
"</body></html>"
|
|
174
|
+
].join("");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function formatJsonOutput({ mode, commitData, explanation, responseMeta, promptMeta }) {
|
|
178
|
+
return JSON.stringify(
|
|
179
|
+
{
|
|
180
|
+
mode,
|
|
181
|
+
commit: {
|
|
182
|
+
id: commitData.commitId,
|
|
183
|
+
ref: commitData.displayRef,
|
|
184
|
+
type: commitData.analysisType,
|
|
185
|
+
count: commitData.commitCount,
|
|
186
|
+
message: commitData.commitMessage,
|
|
187
|
+
filesChanged: commitData.filesChanged,
|
|
188
|
+
stats: commitData.stats
|
|
189
|
+
},
|
|
190
|
+
prompt: promptMeta,
|
|
191
|
+
response: responseMeta,
|
|
192
|
+
explanation
|
|
193
|
+
},
|
|
194
|
+
null,
|
|
195
|
+
2
|
|
196
|
+
);
|
|
197
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const PROMPT_DIR = path.resolve(__dirname, "../../prompts");
|
|
8
|
+
|
|
9
|
+
const PROMPT_FILES = {
|
|
10
|
+
full: "master.txt",
|
|
11
|
+
summary: "summary.txt",
|
|
12
|
+
issues: "issue.txt",
|
|
13
|
+
fix: "junior.txt",
|
|
14
|
+
impact: "impact.txt",
|
|
15
|
+
lines: "lines.txt",
|
|
16
|
+
review: "review.txt",
|
|
17
|
+
security: "security.txt"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function fillTemplate(template, values) {
|
|
21
|
+
return Object.entries(values).reduce((result, [key, value]) => {
|
|
22
|
+
return result.replaceAll(`{{${key}}}`, value);
|
|
23
|
+
}, template);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function truncateDiff(diff, maxDiffLines) {
|
|
27
|
+
const diffLines = diff.split("\n");
|
|
28
|
+
|
|
29
|
+
if (diffLines.length <= maxDiffLines) {
|
|
30
|
+
return {
|
|
31
|
+
diff,
|
|
32
|
+
truncated: false,
|
|
33
|
+
diffLineCount: diffLines.length,
|
|
34
|
+
keptDiffLines: diffLines.length,
|
|
35
|
+
warning: null
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const keptLines = diffLines.slice(0, maxDiffLines);
|
|
40
|
+
return {
|
|
41
|
+
diff: `${keptLines.join("\n")}\n\n[Diff truncated: kept ${maxDiffLines} of ${diffLines.length} lines.]`,
|
|
42
|
+
truncated: true,
|
|
43
|
+
diffLineCount: diffLines.length,
|
|
44
|
+
keptDiffLines: maxDiffLines,
|
|
45
|
+
warning: `Diff truncated to ${maxDiffLines} of ${diffLines.length} lines before sending to the model.`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildRangePrelude(commitData) {
|
|
50
|
+
if (commitData.analysisType !== "range") {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
"This analysis covers a range of commits rather than a single commit.",
|
|
56
|
+
"Treat the output like a changelog or release summary when appropriate.",
|
|
57
|
+
`Commit Count: ${commitData.commitCount}`,
|
|
58
|
+
`Commit List:\n${commitData.commits.map((commit) => `- ${commit.hash.slice(0, 7)} ${commit.subject}`).join("\n")}`,
|
|
59
|
+
""
|
|
60
|
+
].join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildPrompt(mode, commitData, options = {}) {
|
|
64
|
+
const filename = PROMPT_FILES[mode] ?? PROMPT_FILES.full;
|
|
65
|
+
const template = readFileSync(path.join(PROMPT_DIR, filename), "utf8");
|
|
66
|
+
const truncation = truncateDiff(commitData.diff, options.maxDiffLines ?? 800);
|
|
67
|
+
const prompt = fillTemplate(`${buildRangePrelude(commitData)}${template}`, {
|
|
68
|
+
commit_message: commitData.commitMessage,
|
|
69
|
+
files_changed: commitData.filesChanged.join("\n"),
|
|
70
|
+
stats: commitData.stats,
|
|
71
|
+
diff: truncation.diff
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
prompt,
|
|
76
|
+
promptMeta: {
|
|
77
|
+
truncated: truncation.truncated,
|
|
78
|
+
diffLineCount: truncation.diffLineCount,
|
|
79
|
+
keptDiffLines: truncation.keptDiffLines,
|
|
80
|
+
warnings: truncation.warning ? [truncation.warning] : []
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gitxplain",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI-powered Git commit explainer CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gitxplain": "./cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node ./cli/index.js",
|
|
11
|
+
"lint": "node --check ./cli/index.js && node --check ./cli/services/aiService.js && node --check ./cli/services/cacheService.js && node --check ./cli/services/clipboardService.js && node --check ./cli/services/configService.js && node --check ./cli/services/gitService.js && node --check ./cli/services/hookService.js && node --check ./cli/services/promptService.js && node --check ./cli/services/outputFormatter.js",
|
|
12
|
+
"test": "node --test"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"git",
|
|
16
|
+
"cli",
|
|
17
|
+
"ai",
|
|
18
|
+
"developer-tools"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT"
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Determine if this commit fixes a bug.
|
|
2
|
+
|
|
3
|
+
If yes:
|
|
4
|
+
- Describe the issue
|
|
5
|
+
- Explain how it was fixed
|
|
6
|
+
- Mention affected components
|
|
7
|
+
|
|
8
|
+
If no:
|
|
9
|
+
- Explain the main purpose of the commit instead
|
|
10
|
+
|
|
11
|
+
Commit Message:
|
|
12
|
+
{{commit_message}}
|
|
13
|
+
|
|
14
|
+
Files Changed:
|
|
15
|
+
{{files_changed}}
|
|
16
|
+
|
|
17
|
+
Diff:
|
|
18
|
+
{{diff}}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Explain every changed line in this commit in a developer-friendly way.
|
|
2
|
+
|
|
3
|
+
Commit Message:
|
|
4
|
+
{{commit_message}}
|
|
5
|
+
|
|
6
|
+
Files Changed:
|
|
7
|
+
{{files_changed}}
|
|
8
|
+
|
|
9
|
+
Stats:
|
|
10
|
+
{{stats}}
|
|
11
|
+
|
|
12
|
+
Diff:
|
|
13
|
+
{{diff}}
|
|
14
|
+
|
|
15
|
+
Instructions:
|
|
16
|
+
- Go file by file
|
|
17
|
+
- For each diff hunk, explain what the removed lines did before
|
|
18
|
+
- Explain what each added line does now
|
|
19
|
+
- Call out important renamed variables, condition changes, function call changes, and control-flow changes
|
|
20
|
+
- Keep the order aligned with the diff so a developer can read the explanation side by side with the patch
|
|
21
|
+
- If a line is context-only and unchanged, mention it only when it helps explain surrounding modifications
|
|
22
|
+
- End each file section with a short summary of the behavior change in that file
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
You are a senior software engineer and code reviewer.
|
|
2
|
+
|
|
3
|
+
Analyze the following Git commit and generate a structured explanation.
|
|
4
|
+
|
|
5
|
+
Commit Message:
|
|
6
|
+
{{commit_message}}
|
|
7
|
+
|
|
8
|
+
Files Changed:
|
|
9
|
+
{{files_changed}}
|
|
10
|
+
|
|
11
|
+
Stats:
|
|
12
|
+
{{stats}}
|
|
13
|
+
|
|
14
|
+
Code Diff:
|
|
15
|
+
{{diff}}
|
|
16
|
+
|
|
17
|
+
Return the output in the following structured format:
|
|
18
|
+
|
|
19
|
+
1. Summary:
|
|
20
|
+
- One clear sentence explaining the purpose of the commit
|
|
21
|
+
|
|
22
|
+
2. Issue:
|
|
23
|
+
- What problem or bug was being addressed?
|
|
24
|
+
|
|
25
|
+
3. Root Cause:
|
|
26
|
+
- Why did the issue occur?
|
|
27
|
+
|
|
28
|
+
4. Fix:
|
|
29
|
+
- How was the issue resolved?
|
|
30
|
+
|
|
31
|
+
5. Impact:
|
|
32
|
+
- What effect does this change have on the system?
|
|
33
|
+
|
|
34
|
+
6. Risk Level:
|
|
35
|
+
- Low / Medium / High with reasoning
|
|
36
|
+
|
|
37
|
+
7. Technical Breakdown:
|
|
38
|
+
- Explain code changes in a clear, developer-friendly way
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Review this commit like a senior engineer performing code review.
|
|
2
|
+
|
|
3
|
+
Commit Message:
|
|
4
|
+
{{commit_message}}
|
|
5
|
+
|
|
6
|
+
Files Changed:
|
|
7
|
+
{{files_changed}}
|
|
8
|
+
|
|
9
|
+
Stats:
|
|
10
|
+
{{stats}}
|
|
11
|
+
|
|
12
|
+
Diff:
|
|
13
|
+
{{diff}}
|
|
14
|
+
|
|
15
|
+
Return:
|
|
16
|
+
|
|
17
|
+
1. Review Findings:
|
|
18
|
+
- List actionable findings ordered by severity
|
|
19
|
+
- Include likely bugs, regressions, correctness risks, performance concerns, and maintainability issues
|
|
20
|
+
- If there are no significant findings, say so explicitly
|
|
21
|
+
|
|
22
|
+
2. Suggestions:
|
|
23
|
+
- Give concrete follow-up improvements or tests to add
|
|
24
|
+
|
|
25
|
+
3. Risk Level:
|
|
26
|
+
- Low / Medium / High with reasoning
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Analyze this commit from a security perspective.
|
|
2
|
+
|
|
3
|
+
Commit Message:
|
|
4
|
+
{{commit_message}}
|
|
5
|
+
|
|
6
|
+
Files Changed:
|
|
7
|
+
{{files_changed}}
|
|
8
|
+
|
|
9
|
+
Stats:
|
|
10
|
+
{{stats}}
|
|
11
|
+
|
|
12
|
+
Diff:
|
|
13
|
+
{{diff}}
|
|
14
|
+
|
|
15
|
+
Focus on:
|
|
16
|
+
- Input validation issues
|
|
17
|
+
- Injection risks
|
|
18
|
+
- Authentication or authorization regressions
|
|
19
|
+
- Sensitive data exposure
|
|
20
|
+
- Insecure defaults
|
|
21
|
+
- Dependency or configuration risks
|
|
22
|
+
|
|
23
|
+
Return:
|
|
24
|
+
|
|
25
|
+
1. Security Findings:
|
|
26
|
+
- List any vulnerabilities or suspicious changes
|
|
27
|
+
- If none are apparent, say so clearly
|
|
28
|
+
|
|
29
|
+
2. Severity:
|
|
30
|
+
- Low / Medium / High with reasoning
|
|
31
|
+
|
|
32
|
+
3. Recommended Mitigations:
|
|
33
|
+
- Suggest concrete hardening steps or tests
|