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.
@@ -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("<", "&lt;")
140
+ .replaceAll(">", "&gt;");
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,15 @@
1
+ Compare the code before and after this commit.
2
+
3
+ Explain:
4
+ - Key differences
5
+ - Improvements made
6
+ - Behavioral changes
7
+
8
+ Commit Message:
9
+ {{commit_message}}
10
+
11
+ Files Changed:
12
+ {{files_changed}}
13
+
14
+ Diff:
15
+ {{diff}}
@@ -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,15 @@
1
+ Explain this commit in simple terms for a junior developer.
2
+
3
+ Focus on:
4
+ - What changed
5
+ - Why it changed
6
+ - Simple explanation of logic
7
+
8
+ Commit Message:
9
+ {{commit_message}}
10
+
11
+ Files Changed:
12
+ {{files_changed}}
13
+
14
+ Diff:
15
+ {{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
@@ -0,0 +1,10 @@
1
+ Summarize this commit in one sentence.
2
+
3
+ Commit Message:
4
+ {{commit_message}}
5
+
6
+ Files Changed:
7
+ {{files_changed}}
8
+
9
+ Diff:
10
+ {{diff}}