@vavasilva/git-commit-ai 0.2.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/README.md +137 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +821 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# git-commit-ai
|
|
2
|
+
|
|
3
|
+
Generate commit messages using local LLMs (Ollama).
|
|
4
|
+
|
|
5
|
+
A CLI tool that analyzes your staged changes and generates [Karma-style](https://karma-runner.github.io/6.4/dev/git-commit-msg.html) commit messages using a local LLM.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Local LLM** - Uses Ollama (llama3.1:8b by default), no API keys needed
|
|
10
|
+
- **Karma Convention** - Generates `type(scope): subject` format commits
|
|
11
|
+
- **Interactive Flow** - Confirm, Edit, Regenerate, or Abort before committing
|
|
12
|
+
- **Individual Commits** - Option to commit each file separately
|
|
13
|
+
- **Git Hook** - Auto-generate messages on `git commit`
|
|
14
|
+
- **Summarize** - Preview changes in plain English before committing
|
|
15
|
+
- **Debug Mode** - Troubleshoot LLM responses
|
|
16
|
+
- **Configurable** - Customize model, temperature, and more via config file
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Requires Node.js 20+
|
|
22
|
+
npm install -g git-commit-ai
|
|
23
|
+
|
|
24
|
+
# Make sure Ollama is running
|
|
25
|
+
brew install ollama
|
|
26
|
+
brew services start ollama
|
|
27
|
+
ollama pull llama3.1:8b
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Make changes to your code
|
|
34
|
+
echo "console.log('hello')" > hello.js
|
|
35
|
+
|
|
36
|
+
# 2. Stage your changes
|
|
37
|
+
git add hello.js
|
|
38
|
+
|
|
39
|
+
# 3. Generate commit message and commit
|
|
40
|
+
git-commit-ai
|
|
41
|
+
|
|
42
|
+
# Output:
|
|
43
|
+
# 📝 Generated commit message
|
|
44
|
+
# feat: add hello.js script
|
|
45
|
+
# [C]onfirm [E]dit [R]egenerate [A]bort? c
|
|
46
|
+
# ✓ Committed: feat: add hello.js script
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Basic: stage + generate + confirm + commit
|
|
53
|
+
git add .
|
|
54
|
+
git-commit-ai
|
|
55
|
+
|
|
56
|
+
# Auto-commit without confirmation
|
|
57
|
+
git add .
|
|
58
|
+
git-commit-ai -y
|
|
59
|
+
|
|
60
|
+
# Commit and push in one command
|
|
61
|
+
git add .
|
|
62
|
+
git-commit-ai --push
|
|
63
|
+
|
|
64
|
+
# Commit each modified file separately
|
|
65
|
+
git-commit-ai --individual
|
|
66
|
+
|
|
67
|
+
# Preview changes before committing
|
|
68
|
+
git add .
|
|
69
|
+
git-commit-ai summarize
|
|
70
|
+
|
|
71
|
+
# Enable debug output for troubleshooting
|
|
72
|
+
git-commit-ai --debug
|
|
73
|
+
|
|
74
|
+
# Show current config
|
|
75
|
+
git-commit-ai config
|
|
76
|
+
|
|
77
|
+
# Create/edit config file
|
|
78
|
+
git-commit-ai config --edit
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Git Hook (Auto-generate on commit)
|
|
82
|
+
|
|
83
|
+
Install a git hook to automatically generate commit messages:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Install the hook
|
|
87
|
+
git-commit-ai hook --install
|
|
88
|
+
|
|
89
|
+
# Now just use git commit normally!
|
|
90
|
+
git add .
|
|
91
|
+
git commit
|
|
92
|
+
# Message is auto-generated and opens in your editor
|
|
93
|
+
|
|
94
|
+
# Check hook status
|
|
95
|
+
git-commit-ai hook --status
|
|
96
|
+
|
|
97
|
+
# Remove the hook
|
|
98
|
+
git-commit-ai hook --remove
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Interactive Flow
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
📝 Generated commit message
|
|
105
|
+
|
|
106
|
+
feat(auth): add login validation
|
|
107
|
+
|
|
108
|
+
[C]onfirm [E]dit [R]egenerate [A]bort? _
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Configuration
|
|
112
|
+
|
|
113
|
+
Config file location: `~/.config/git-commit-ai/config.toml`
|
|
114
|
+
|
|
115
|
+
```toml
|
|
116
|
+
model = "llama3.1:8b"
|
|
117
|
+
ollama_url = "http://localhost:11434"
|
|
118
|
+
temperature = 0.7
|
|
119
|
+
retry_temperatures = [0.5, 0.3, 0.2]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Commit Types (Karma Convention)
|
|
123
|
+
|
|
124
|
+
| Type | Description |
|
|
125
|
+
|------|-------------|
|
|
126
|
+
| `feat` | New feature |
|
|
127
|
+
| `fix` | Bug fix |
|
|
128
|
+
| `docs` | Documentation |
|
|
129
|
+
| `style` | Formatting (no code change) |
|
|
130
|
+
| `refactor` | Code restructuring |
|
|
131
|
+
| `test` | Adding tests |
|
|
132
|
+
| `build` | Build system or dependencies |
|
|
133
|
+
| `chore` | Maintenance tasks |
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk2 from "chalk";
|
|
6
|
+
import ora from "ora";
|
|
7
|
+
import { createInterface } from "readline";
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join, dirname } from "path";
|
|
13
|
+
import { parse as parseToml } from "smol-toml";
|
|
14
|
+
var DEFAULT_CONFIG = {
|
|
15
|
+
model: "llama3.1:8b",
|
|
16
|
+
ollama_url: "http://localhost:11434",
|
|
17
|
+
temperature: 0.7,
|
|
18
|
+
retry_temperatures: [0.5, 0.3, 0.2]
|
|
19
|
+
};
|
|
20
|
+
function getConfigPath() {
|
|
21
|
+
return join(homedir(), ".config", "git-commit-ai", "config.toml");
|
|
22
|
+
}
|
|
23
|
+
function loadConfig() {
|
|
24
|
+
const configPath = getConfigPath();
|
|
25
|
+
if (!existsSync(configPath)) {
|
|
26
|
+
return { ...DEFAULT_CONFIG };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const content = readFileSync(configPath, "utf-8");
|
|
30
|
+
const data = parseToml(content);
|
|
31
|
+
return {
|
|
32
|
+
model: data.model ?? DEFAULT_CONFIG.model,
|
|
33
|
+
ollama_url: data.ollama_url ?? DEFAULT_CONFIG.ollama_url,
|
|
34
|
+
temperature: data.temperature ?? DEFAULT_CONFIG.temperature,
|
|
35
|
+
retry_temperatures: data.retry_temperatures ?? DEFAULT_CONFIG.retry_temperatures
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
return { ...DEFAULT_CONFIG };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function saveConfig(config) {
|
|
42
|
+
const configPath = getConfigPath();
|
|
43
|
+
const dir = dirname(configPath);
|
|
44
|
+
if (!existsSync(dir)) {
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
const content = `# git-commit-ai configuration
|
|
48
|
+
model = "${config.model}"
|
|
49
|
+
ollama_url = "${config.ollama_url}"
|
|
50
|
+
temperature = ${config.temperature}
|
|
51
|
+
retry_temperatures = [${config.retry_temperatures.join(", ")}]
|
|
52
|
+
`;
|
|
53
|
+
writeFileSync(configPath, content, "utf-8");
|
|
54
|
+
}
|
|
55
|
+
function showConfig(config) {
|
|
56
|
+
return `Configuration:
|
|
57
|
+
Model: ${config.model}
|
|
58
|
+
Ollama URL: ${config.ollama_url}
|
|
59
|
+
Temperature: ${config.temperature}
|
|
60
|
+
Retry temperatures: [${config.retry_temperatures.join(", ")}]
|
|
61
|
+
Config file: ${getConfigPath()}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/backends/ollama.ts
|
|
65
|
+
var OllamaBackend = class {
|
|
66
|
+
model;
|
|
67
|
+
baseUrl;
|
|
68
|
+
constructor(model = "llama3.1:8b", baseUrl = "http://localhost:11434") {
|
|
69
|
+
this.model = model;
|
|
70
|
+
this.baseUrl = baseUrl;
|
|
71
|
+
}
|
|
72
|
+
async generate(prompt, temperature = 0.7) {
|
|
73
|
+
const response = await fetch(`${this.baseUrl}/api/generate`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "Content-Type": "application/json" },
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
model: this.model,
|
|
78
|
+
prompt,
|
|
79
|
+
temperature,
|
|
80
|
+
stream: false
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw new Error(`Ollama API error: ${response.status}`);
|
|
85
|
+
}
|
|
86
|
+
const data = await response.json();
|
|
87
|
+
return data.response ?? "";
|
|
88
|
+
}
|
|
89
|
+
async isAvailable() {
|
|
90
|
+
try {
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
93
|
+
const response = await fetch(`${this.baseUrl}/api/tags`, {
|
|
94
|
+
signal: controller.signal
|
|
95
|
+
});
|
|
96
|
+
clearTimeout(timeoutId);
|
|
97
|
+
return response.status === 200;
|
|
98
|
+
} catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async hasModel(model) {
|
|
103
|
+
const checkModel = model ?? this.model;
|
|
104
|
+
try {
|
|
105
|
+
const controller = new AbortController();
|
|
106
|
+
const timeoutId = setTimeout(() => controller.abort(), 5e3);
|
|
107
|
+
const response = await fetch(`${this.baseUrl}/api/tags`, {
|
|
108
|
+
signal: controller.signal
|
|
109
|
+
});
|
|
110
|
+
clearTimeout(timeoutId);
|
|
111
|
+
if (response.status !== 200) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const data = await response.json();
|
|
115
|
+
const models = data.models?.map((m) => m.name ?? "") ?? [];
|
|
116
|
+
return models.some((m) => m.includes(checkModel));
|
|
117
|
+
} catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// src/git.ts
|
|
124
|
+
import { execSync } from "child_process";
|
|
125
|
+
var GitError = class extends Error {
|
|
126
|
+
constructor(message) {
|
|
127
|
+
super(message);
|
|
128
|
+
this.name = "GitError";
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
function runGit(...args) {
|
|
132
|
+
try {
|
|
133
|
+
const result = execSync(["git", ...args].join(" "), {
|
|
134
|
+
encoding: "utf-8",
|
|
135
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
136
|
+
});
|
|
137
|
+
return result.trim();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const err = error;
|
|
140
|
+
const message = err.stderr?.trim() || err.message;
|
|
141
|
+
throw new GitError(`Git command failed: ${message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function runGitSafe(...args) {
|
|
145
|
+
try {
|
|
146
|
+
return runGit(...args);
|
|
147
|
+
} catch {
|
|
148
|
+
return "";
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function getStagedDiff() {
|
|
152
|
+
const diff = runGitSafe("diff", "--cached");
|
|
153
|
+
const stats = runGitSafe("diff", "--cached", "--stat");
|
|
154
|
+
const filesOutput = runGitSafe("diff", "--cached", "--name-only");
|
|
155
|
+
const files = filesOutput.split("\n").filter((f) => f);
|
|
156
|
+
return {
|
|
157
|
+
diff,
|
|
158
|
+
stats,
|
|
159
|
+
files,
|
|
160
|
+
isEmpty: !diff.trim()
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function getFileDiff(filePath) {
|
|
164
|
+
const diff = runGitSafe("diff", "--cached", "--", filePath);
|
|
165
|
+
const stats = runGitSafe("diff", "--cached", "--stat", "--", filePath);
|
|
166
|
+
const files = diff ? [filePath] : [];
|
|
167
|
+
return {
|
|
168
|
+
diff,
|
|
169
|
+
stats,
|
|
170
|
+
files,
|
|
171
|
+
isEmpty: !diff.trim()
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function addFiles(...paths) {
|
|
175
|
+
if (paths.length === 0) {
|
|
176
|
+
paths = ["."];
|
|
177
|
+
}
|
|
178
|
+
runGit("add", ...paths);
|
|
179
|
+
}
|
|
180
|
+
function commit(message) {
|
|
181
|
+
runGit("commit", "-m", `"${message.replace(/"/g, '\\"')}"`);
|
|
182
|
+
return runGit("rev-parse", "HEAD");
|
|
183
|
+
}
|
|
184
|
+
function push() {
|
|
185
|
+
const branch = getCurrentBranch();
|
|
186
|
+
runGit("push", "origin", branch);
|
|
187
|
+
}
|
|
188
|
+
function getCurrentBranch() {
|
|
189
|
+
return runGit("rev-parse", "--abbrev-ref", "HEAD");
|
|
190
|
+
}
|
|
191
|
+
function getModifiedFiles() {
|
|
192
|
+
const files = /* @__PURE__ */ new Set();
|
|
193
|
+
const staged = runGitSafe("diff", "--cached", "--name-only");
|
|
194
|
+
if (staged) {
|
|
195
|
+
staged.split("\n").forEach((f) => files.add(f));
|
|
196
|
+
}
|
|
197
|
+
const modified = runGitSafe("diff", "--name-only");
|
|
198
|
+
if (modified) {
|
|
199
|
+
modified.split("\n").forEach((f) => files.add(f));
|
|
200
|
+
}
|
|
201
|
+
const untracked = runGitSafe("ls-files", "--others", "--exclude-standard");
|
|
202
|
+
if (untracked) {
|
|
203
|
+
untracked.split("\n").forEach((f) => files.add(f));
|
|
204
|
+
}
|
|
205
|
+
return Array.from(files).filter((f) => f);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/prompts.ts
|
|
209
|
+
var SUMMARIZE_PROMPT = `Summarize the following code changes in plain English.
|
|
210
|
+
|
|
211
|
+
Provide a brief, clear summary that explains:
|
|
212
|
+
1. What files were changed
|
|
213
|
+
2. What was added, removed, or modified
|
|
214
|
+
3. The likely purpose of these changes
|
|
215
|
+
|
|
216
|
+
Keep it concise (3-5 bullet points). Focus on the "what" and "why".
|
|
217
|
+
|
|
218
|
+
{context}
|
|
219
|
+
|
|
220
|
+
DIFF:
|
|
221
|
+
\`\`\`
|
|
222
|
+
{diff}
|
|
223
|
+
\`\`\`
|
|
224
|
+
|
|
225
|
+
Provide only the summary, no additional commentary.`;
|
|
226
|
+
var KARMA_PROMPT = `Analyze the git diff below and create a commit message following Karma convention.
|
|
227
|
+
|
|
228
|
+
FORMAT: <type>(<scope>): <subject>
|
|
229
|
+
|
|
230
|
+
TYPES (use the most appropriate):
|
|
231
|
+
- feat: new feature or capability
|
|
232
|
+
- fix: bug fix
|
|
233
|
+
- docs: documentation changes (README, comments, docstrings)
|
|
234
|
+
- style: formatting only (whitespace, semicolons)
|
|
235
|
+
- refactor: code change that neither fixes bug nor adds feature
|
|
236
|
+
- test: adding or modifying tests
|
|
237
|
+
- build: build system, dependencies, package config
|
|
238
|
+
- chore: maintenance tasks
|
|
239
|
+
|
|
240
|
+
RULES:
|
|
241
|
+
- Scope is optional - use the main file/module name if relevant
|
|
242
|
+
- Subject must describe WHAT changed in the diff, not a generic message
|
|
243
|
+
- Use imperative mood: "add" not "added", "fix" not "fixed"
|
|
244
|
+
- Lowercase, no period at end, max 72 chars
|
|
245
|
+
|
|
246
|
+
EXAMPLES based on diff content:
|
|
247
|
+
- Adding README.md \u2192 docs: add README with usage instructions
|
|
248
|
+
- Fixing null check in auth.py \u2192 fix(auth): handle null user in login
|
|
249
|
+
- New API endpoint \u2192 feat(api): add user profile endpoint
|
|
250
|
+
- Updating dependencies \u2192 build: update httpx to 0.25.0
|
|
251
|
+
|
|
252
|
+
IMPORTANT: Base your message ONLY on the actual changes shown in the diff below.
|
|
253
|
+
Do NOT use the examples above if they don't match the diff content.
|
|
254
|
+
|
|
255
|
+
{context}
|
|
256
|
+
|
|
257
|
+
DIFF TO ANALYZE:
|
|
258
|
+
\`\`\`
|
|
259
|
+
{diff}
|
|
260
|
+
\`\`\`
|
|
261
|
+
|
|
262
|
+
Reply with ONLY the commit message, nothing else. No quotes, no explanation.`;
|
|
263
|
+
var KARMA_PATTERN = /^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\([^)]+\))?:\s*.+/;
|
|
264
|
+
var ACTION_TO_TYPE = {
|
|
265
|
+
add: "feat",
|
|
266
|
+
added: "feat",
|
|
267
|
+
adding: "feat",
|
|
268
|
+
create: "feat",
|
|
269
|
+
implement: "feat",
|
|
270
|
+
fix: "fix",
|
|
271
|
+
fixed: "fix",
|
|
272
|
+
fixing: "fix",
|
|
273
|
+
repair: "fix",
|
|
274
|
+
update: "refactor",
|
|
275
|
+
updated: "refactor",
|
|
276
|
+
updating: "refactor",
|
|
277
|
+
improve: "refactor",
|
|
278
|
+
remove: "refactor",
|
|
279
|
+
removed: "refactor",
|
|
280
|
+
removing: "refactor",
|
|
281
|
+
delete: "refactor",
|
|
282
|
+
document: "docs",
|
|
283
|
+
documented: "docs",
|
|
284
|
+
test: "test",
|
|
285
|
+
tested: "test",
|
|
286
|
+
testing: "test"
|
|
287
|
+
};
|
|
288
|
+
var MAX_DIFF_CHARS = 8e3;
|
|
289
|
+
function truncateDiff(diff, maxChars = MAX_DIFF_CHARS) {
|
|
290
|
+
if (diff.length <= maxChars) {
|
|
291
|
+
return diff;
|
|
292
|
+
}
|
|
293
|
+
let truncated = diff.slice(0, maxChars);
|
|
294
|
+
const lastNewline = truncated.lastIndexOf("\n");
|
|
295
|
+
if (lastNewline > maxChars * 0.8) {
|
|
296
|
+
truncated = truncated.slice(0, lastNewline);
|
|
297
|
+
}
|
|
298
|
+
return truncated + "\n\n[... diff truncated for brevity ...]";
|
|
299
|
+
}
|
|
300
|
+
function buildPrompt(diff, context) {
|
|
301
|
+
const truncatedDiff = truncateDiff(diff);
|
|
302
|
+
return KARMA_PROMPT.replace("{diff}", truncatedDiff).replace("{context}", context);
|
|
303
|
+
}
|
|
304
|
+
function buildSummarizePrompt(diff, context) {
|
|
305
|
+
const truncatedDiff = truncateDiff(diff);
|
|
306
|
+
return SUMMARIZE_PROMPT.replace("{diff}", truncatedDiff).replace("{context}", context);
|
|
307
|
+
}
|
|
308
|
+
function validateMessage(message) {
|
|
309
|
+
return KARMA_PATTERN.test(message.trim());
|
|
310
|
+
}
|
|
311
|
+
function cleanMessage(message) {
|
|
312
|
+
let cleaned = message.trim().split("\n")[0];
|
|
313
|
+
const prefixes = ["Here is ", "I've ", "The commit message is:", "Commit message:", "Here's "];
|
|
314
|
+
for (const prefix of prefixes) {
|
|
315
|
+
if (cleaned.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
316
|
+
cleaned = cleaned.slice(prefix.length);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
cleaned = cleaned.trim().replace(/\.$/, "");
|
|
320
|
+
return cleaned;
|
|
321
|
+
}
|
|
322
|
+
function fixMessage(message) {
|
|
323
|
+
const cleaned = cleanMessage(message);
|
|
324
|
+
if (validateMessage(cleaned)) {
|
|
325
|
+
return cleaned;
|
|
326
|
+
}
|
|
327
|
+
const words = cleaned.split(/\s+/);
|
|
328
|
+
if (words.length > 0) {
|
|
329
|
+
const firstWord = words[0].toLowerCase().replace(/:$/, "");
|
|
330
|
+
const commitType = ACTION_TO_TYPE[firstWord] ?? "chore";
|
|
331
|
+
const subject = words.join(" ").toLowerCase();
|
|
332
|
+
if (!subject.endsWith(":")) {
|
|
333
|
+
return `${commitType}: ${subject}`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return `chore: ${cleaned.toLowerCase()}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/hook.ts
|
|
340
|
+
import { execSync as execSync2 } from "child_process";
|
|
341
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, existsSync as existsSync2, chmodSync, mkdirSync as mkdirSync2 } from "fs";
|
|
342
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
343
|
+
var HOOK_SCRIPT = `#!/bin/sh
|
|
344
|
+
# git-commit-ai prepare-commit-msg hook
|
|
345
|
+
# This hook generates commit messages using AI
|
|
346
|
+
|
|
347
|
+
COMMIT_MSG_FILE="$1"
|
|
348
|
+
COMMIT_SOURCE="$2"
|
|
349
|
+
|
|
350
|
+
# Only run for regular commits (not merge, squash, etc.)
|
|
351
|
+
if [ -n "$COMMIT_SOURCE" ]; then
|
|
352
|
+
exit 0
|
|
353
|
+
fi
|
|
354
|
+
|
|
355
|
+
# Check if there's already a message (e.g., from -m flag)
|
|
356
|
+
if [ -s "$COMMIT_MSG_FILE" ]; then
|
|
357
|
+
# File is not empty, check if it's just the default template
|
|
358
|
+
FIRST_LINE=$(head -n 1 "$COMMIT_MSG_FILE")
|
|
359
|
+
if [ -n "$FIRST_LINE" ] && ! echo "$FIRST_LINE" | grep -q "^#"; then
|
|
360
|
+
# There's actual content, don't override
|
|
361
|
+
exit 0
|
|
362
|
+
fi
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
# Generate commit message using git-commit-ai
|
|
366
|
+
MESSAGE=$(git-commit-ai --hook-mode 2>/dev/null)
|
|
367
|
+
|
|
368
|
+
if [ $? -eq 0 ] && [ -n "$MESSAGE" ]; then
|
|
369
|
+
# Write the generated message, preserving any existing comments
|
|
370
|
+
COMMENTS=$(grep "^#" "$COMMIT_MSG_FILE" 2>/dev/null || true)
|
|
371
|
+
echo "$MESSAGE" > "$COMMIT_MSG_FILE"
|
|
372
|
+
if [ -n "$COMMENTS" ]; then
|
|
373
|
+
echo "" >> "$COMMIT_MSG_FILE"
|
|
374
|
+
echo "$COMMENTS" >> "$COMMIT_MSG_FILE"
|
|
375
|
+
fi
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
exit 0
|
|
379
|
+
`;
|
|
380
|
+
var HOOK_NAME = "prepare-commit-msg";
|
|
381
|
+
function getGitDir() {
|
|
382
|
+
try {
|
|
383
|
+
const result = execSync2("git rev-parse --git-dir", {
|
|
384
|
+
encoding: "utf-8",
|
|
385
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
386
|
+
});
|
|
387
|
+
return result.trim();
|
|
388
|
+
} catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function getHookPath() {
|
|
393
|
+
const gitDir = getGitDir();
|
|
394
|
+
if (!gitDir) return null;
|
|
395
|
+
return join2(gitDir, "hooks", HOOK_NAME);
|
|
396
|
+
}
|
|
397
|
+
function isHookInstalled() {
|
|
398
|
+
const hookPath = getHookPath();
|
|
399
|
+
if (!hookPath || !existsSync2(hookPath)) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
const content = readFileSync2(hookPath, "utf-8");
|
|
403
|
+
return content.includes("git-commit-ai");
|
|
404
|
+
}
|
|
405
|
+
function installHook() {
|
|
406
|
+
const hookPath = getHookPath();
|
|
407
|
+
if (!hookPath) {
|
|
408
|
+
return { success: false, message: "Not in a git repository" };
|
|
409
|
+
}
|
|
410
|
+
let returnMsg = "Hook installed successfully";
|
|
411
|
+
if (existsSync2(hookPath)) {
|
|
412
|
+
const content = readFileSync2(hookPath, "utf-8");
|
|
413
|
+
if (content.includes("git-commit-ai")) {
|
|
414
|
+
return { success: true, message: "Hook already installed" };
|
|
415
|
+
}
|
|
416
|
+
const backupPath = hookPath + ".backup";
|
|
417
|
+
writeFileSync2(backupPath, content, "utf-8");
|
|
418
|
+
returnMsg = `Existing hook backed up to ${HOOK_NAME}.backup`;
|
|
419
|
+
}
|
|
420
|
+
const hooksDir = dirname2(hookPath);
|
|
421
|
+
if (!existsSync2(hooksDir)) {
|
|
422
|
+
mkdirSync2(hooksDir, { recursive: true });
|
|
423
|
+
}
|
|
424
|
+
writeFileSync2(hookPath, HOOK_SCRIPT, "utf-8");
|
|
425
|
+
chmodSync(hookPath, 493);
|
|
426
|
+
return { success: true, message: returnMsg };
|
|
427
|
+
}
|
|
428
|
+
function removeHook() {
|
|
429
|
+
const hookPath = getHookPath();
|
|
430
|
+
if (!hookPath) {
|
|
431
|
+
return { success: false, message: "Not in a git repository" };
|
|
432
|
+
}
|
|
433
|
+
if (!existsSync2(hookPath)) {
|
|
434
|
+
return { success: true, message: "No hook installed" };
|
|
435
|
+
}
|
|
436
|
+
const content = readFileSync2(hookPath, "utf-8");
|
|
437
|
+
if (!content.includes("git-commit-ai")) {
|
|
438
|
+
return { success: false, message: "Hook exists but was not installed by git-commit-ai" };
|
|
439
|
+
}
|
|
440
|
+
unlinkSync(hookPath);
|
|
441
|
+
const backupPath = hookPath + ".backup";
|
|
442
|
+
if (existsSync2(backupPath)) {
|
|
443
|
+
const backupContent = readFileSync2(backupPath, "utf-8");
|
|
444
|
+
writeFileSync2(hookPath, backupContent, "utf-8");
|
|
445
|
+
unlinkSync(backupPath);
|
|
446
|
+
return { success: true, message: "Hook removed, previous hook restored" };
|
|
447
|
+
}
|
|
448
|
+
return { success: true, message: "Hook removed successfully" };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/debug.ts
|
|
452
|
+
import chalk from "chalk";
|
|
453
|
+
var debugEnabled = false;
|
|
454
|
+
function enableDebug() {
|
|
455
|
+
debugEnabled = true;
|
|
456
|
+
}
|
|
457
|
+
function getTimestamp() {
|
|
458
|
+
const now = /* @__PURE__ */ new Date();
|
|
459
|
+
return now.toTimeString().slice(0, 8);
|
|
460
|
+
}
|
|
461
|
+
function debug(message, data) {
|
|
462
|
+
if (!debugEnabled) return;
|
|
463
|
+
console.error(chalk.dim(`[${getTimestamp()}]`), chalk.cyan("[DEBUG]"), message);
|
|
464
|
+
if (data) {
|
|
465
|
+
const truncated = data.length > 500 ? data.slice(0, 500) + `
|
|
466
|
+
... (${data.length} chars total, truncated)` : data;
|
|
467
|
+
console.error(chalk.dim(truncated));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function debugConfig(cfg) {
|
|
471
|
+
debug(
|
|
472
|
+
"Config loaded:",
|
|
473
|
+
`
|
|
474
|
+
Model: ${cfg.model}
|
|
475
|
+
Ollama URL: ${cfg.ollama_url}
|
|
476
|
+
Temperature: ${cfg.temperature}
|
|
477
|
+
Retry temps: [${cfg.retry_temperatures.join(", ")}]`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
function debugDiff(diff, files) {
|
|
481
|
+
const filesList = files.slice(0, 5).join(", ") + (files.length > 5 ? " ..." : "");
|
|
482
|
+
debug(`Diff size: ${diff.length} chars, Files: ${files.length}`, `Files: ${filesList}`);
|
|
483
|
+
}
|
|
484
|
+
function debugPrompt(prompt) {
|
|
485
|
+
const truncated = prompt.length > 500 ? prompt.slice(0, 500) + "..." : prompt;
|
|
486
|
+
debug("Prompt to LLM:", truncated);
|
|
487
|
+
}
|
|
488
|
+
function debugResponse(response) {
|
|
489
|
+
debug("Raw LLM response:", response);
|
|
490
|
+
}
|
|
491
|
+
function debugValidation(message, isValid, fixed) {
|
|
492
|
+
const status = isValid ? chalk.green("valid") : chalk.red("invalid");
|
|
493
|
+
debug(`Message validation: ${status}`, message);
|
|
494
|
+
if (fixed && fixed !== message) {
|
|
495
|
+
debug("Fixed message:", fixed);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/cli.ts
|
|
500
|
+
async function promptUser(question, choices) {
|
|
501
|
+
const rl = createInterface({
|
|
502
|
+
input: process.stdin,
|
|
503
|
+
output: process.stdout
|
|
504
|
+
});
|
|
505
|
+
return new Promise((resolve) => {
|
|
506
|
+
rl.question(question, (answer) => {
|
|
507
|
+
rl.close();
|
|
508
|
+
const normalized = answer.trim().toLowerCase();
|
|
509
|
+
if (choices.includes(normalized)) {
|
|
510
|
+
resolve(normalized);
|
|
511
|
+
} else {
|
|
512
|
+
resolve(choices[0]);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
async function promptEdit(currentMessage) {
|
|
518
|
+
const rl = createInterface({
|
|
519
|
+
input: process.stdin,
|
|
520
|
+
output: process.stdout
|
|
521
|
+
});
|
|
522
|
+
return new Promise((resolve) => {
|
|
523
|
+
console.log(chalk2.dim("\nEnter new commit message (or press Enter to keep current):"));
|
|
524
|
+
rl.question(`Message [${currentMessage}]: `, (answer) => {
|
|
525
|
+
rl.close();
|
|
526
|
+
resolve(answer.trim() || currentMessage);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
async function generateMessage(backend, diffContent, context, temperatures) {
|
|
531
|
+
const prompt = buildPrompt(diffContent, context);
|
|
532
|
+
debugPrompt(prompt);
|
|
533
|
+
for (const temp of temperatures) {
|
|
534
|
+
debug(`Trying temperature: ${temp}`);
|
|
535
|
+
try {
|
|
536
|
+
const rawMessage = await backend.generate(prompt, temp);
|
|
537
|
+
debugResponse(rawMessage);
|
|
538
|
+
const message = cleanMessage(rawMessage);
|
|
539
|
+
const isValid = validateMessage(message);
|
|
540
|
+
debugValidation(message, isValid);
|
|
541
|
+
if (isValid) {
|
|
542
|
+
return message;
|
|
543
|
+
}
|
|
544
|
+
const fixed = fixMessage(message);
|
|
545
|
+
if (validateMessage(fixed)) {
|
|
546
|
+
debugValidation(fixed, true, fixed);
|
|
547
|
+
return fixed;
|
|
548
|
+
}
|
|
549
|
+
} catch (e) {
|
|
550
|
+
const error = e;
|
|
551
|
+
debug(`Generation error: ${error.message}`);
|
|
552
|
+
console.log(chalk2.yellow(`Warning: Generation failed at temp ${temp}: ${error.message}`));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
function showMessage(message) {
|
|
558
|
+
console.log();
|
|
559
|
+
console.log(chalk2.green("\u250C\u2500") + chalk2.green("\u2500".repeat(68)) + chalk2.green("\u2500\u2510"));
|
|
560
|
+
console.log(chalk2.green("\u2502") + chalk2.bold(" \u{1F4DD} Generated commit message") + " ".repeat(40) + chalk2.green("\u2502"));
|
|
561
|
+
console.log(chalk2.green("\u251C\u2500") + chalk2.green("\u2500".repeat(68)) + chalk2.green("\u2500\u2524"));
|
|
562
|
+
console.log(chalk2.green("\u2502") + " " + message.padEnd(67) + chalk2.green("\u2502"));
|
|
563
|
+
console.log(chalk2.green("\u2514\u2500") + chalk2.green("\u2500".repeat(68)) + chalk2.green("\u2500\u2518"));
|
|
564
|
+
console.log();
|
|
565
|
+
}
|
|
566
|
+
async function promptAction(message) {
|
|
567
|
+
showMessage(message);
|
|
568
|
+
return promptUser(
|
|
569
|
+
"[C]onfirm [E]dit [R]egenerate [A]bort? ",
|
|
570
|
+
["c", "e", "r", "a"]
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
async function runCommitFlow(backend, cfg, diffContent, context, skipConfirm) {
|
|
574
|
+
const temperatures = [cfg.temperature, ...cfg.retry_temperatures];
|
|
575
|
+
const spinner = ora("Generating commit message...").start();
|
|
576
|
+
while (true) {
|
|
577
|
+
let message;
|
|
578
|
+
try {
|
|
579
|
+
message = await generateMessage(backend, diffContent, context, temperatures);
|
|
580
|
+
} finally {
|
|
581
|
+
spinner.stop();
|
|
582
|
+
}
|
|
583
|
+
if (message === null) {
|
|
584
|
+
console.log(chalk2.red("Error: Failed to generate a valid commit message."));
|
|
585
|
+
message = "chore: update files";
|
|
586
|
+
console.log(chalk2.yellow(`Using fallback: ${message}`));
|
|
587
|
+
}
|
|
588
|
+
if (skipConfirm) {
|
|
589
|
+
return message;
|
|
590
|
+
}
|
|
591
|
+
const action = await promptAction(message);
|
|
592
|
+
if (action === "c") {
|
|
593
|
+
return message;
|
|
594
|
+
} else if (action === "e") {
|
|
595
|
+
return promptEdit(message);
|
|
596
|
+
} else if (action === "r") {
|
|
597
|
+
console.log(chalk2.dim("Regenerating..."));
|
|
598
|
+
spinner.start("Generating commit message...");
|
|
599
|
+
continue;
|
|
600
|
+
} else if (action === "a") {
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function handleSingleCommit(backend, cfg, skipConfirm) {
|
|
606
|
+
const diffResult = getStagedDiff();
|
|
607
|
+
if (diffResult.isEmpty) {
|
|
608
|
+
console.log(chalk2.yellow("No changes to commit."));
|
|
609
|
+
process.exit(0);
|
|
610
|
+
}
|
|
611
|
+
debugDiff(diffResult.diff, diffResult.files);
|
|
612
|
+
const context = `Files changed:
|
|
613
|
+
${diffResult.files.slice(0, 5).join("\n")}
|
|
614
|
+
Stats: ${diffResult.stats}`;
|
|
615
|
+
const message = await runCommitFlow(backend, cfg, diffResult.diff, context, skipConfirm);
|
|
616
|
+
if (message === null) {
|
|
617
|
+
console.log(chalk2.yellow("Aborted."));
|
|
618
|
+
process.exit(0);
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
commit(message);
|
|
622
|
+
debug(`Commit successful: ${message}`);
|
|
623
|
+
console.log(chalk2.green("\u2713 Committed:"), message);
|
|
624
|
+
} catch (e) {
|
|
625
|
+
const error = e;
|
|
626
|
+
debug(`Commit failed: ${error.message}`);
|
|
627
|
+
console.log(chalk2.red(`Error: ${error.message}`));
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async function handleIndividualCommits(backend, cfg, skipConfirm) {
|
|
632
|
+
const filesToCommit = getModifiedFiles();
|
|
633
|
+
if (filesToCommit.length === 0) {
|
|
634
|
+
console.log(chalk2.yellow("No files to commit."));
|
|
635
|
+
process.exit(0);
|
|
636
|
+
}
|
|
637
|
+
console.log(chalk2.dim(`Found ${filesToCommit.length} files to commit individually.`));
|
|
638
|
+
for (const filePath of filesToCommit) {
|
|
639
|
+
addFiles(filePath);
|
|
640
|
+
const diffResult = getFileDiff(filePath);
|
|
641
|
+
if (diffResult.isEmpty) {
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
console.log(chalk2.bold(`
|
|
645
|
+
Processing: ${filePath}`));
|
|
646
|
+
const context = `File: ${filePath}
|
|
647
|
+
Stats: ${diffResult.stats}`;
|
|
648
|
+
const message = await runCommitFlow(backend, cfg, diffResult.diff, context, skipConfirm);
|
|
649
|
+
if (message === null) {
|
|
650
|
+
console.log(chalk2.yellow(`Skipped: ${filePath}`));
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
commit(message);
|
|
655
|
+
console.log(chalk2.green("\u2713 Committed:"), message);
|
|
656
|
+
} catch (e) {
|
|
657
|
+
const error = e;
|
|
658
|
+
console.log(chalk2.red(`Error committing ${filePath}: ${error.message}`));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function createProgram() {
|
|
663
|
+
const program2 = new Command();
|
|
664
|
+
program2.name("git-commit-ai").description("Generate commit messages using local LLMs").version("0.2.0").option("-p, --push", "Push after commit").option("-y, --yes", "Skip confirmation").option("-i, --individual", "Commit files individually").option("-d, --debug", "Enable debug output").option("--hook-mode", "Called by git hook (outputs message only)").action(async (options) => {
|
|
665
|
+
if (options.debug) {
|
|
666
|
+
enableDebug();
|
|
667
|
+
debug("Debug mode enabled");
|
|
668
|
+
}
|
|
669
|
+
const cfg = loadConfig();
|
|
670
|
+
debugConfig(cfg);
|
|
671
|
+
const backend = new OllamaBackend(cfg.model, cfg.ollama_url);
|
|
672
|
+
const available = await backend.isAvailable();
|
|
673
|
+
if (!available) {
|
|
674
|
+
if (options.hookMode) {
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
console.log(chalk2.red("Error: Ollama is not running."));
|
|
678
|
+
console.log(chalk2.dim("Start it with: brew services start ollama"));
|
|
679
|
+
process.exit(1);
|
|
680
|
+
}
|
|
681
|
+
if (options.hookMode) {
|
|
682
|
+
const diffResult = getStagedDiff();
|
|
683
|
+
if (diffResult.isEmpty) {
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
const context = `Files changed:
|
|
687
|
+
${diffResult.files.slice(0, 5).join("\n")}
|
|
688
|
+
Stats: ${diffResult.stats}`;
|
|
689
|
+
const temperatures = [cfg.temperature, ...cfg.retry_temperatures];
|
|
690
|
+
const message = await generateMessage(backend, diffResult.diff, context, temperatures);
|
|
691
|
+
if (message) {
|
|
692
|
+
console.log(message);
|
|
693
|
+
process.exit(0);
|
|
694
|
+
}
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
697
|
+
addFiles(".");
|
|
698
|
+
if (options.individual) {
|
|
699
|
+
await handleIndividualCommits(backend, cfg, options.yes);
|
|
700
|
+
} else {
|
|
701
|
+
await handleSingleCommit(backend, cfg, options.yes);
|
|
702
|
+
}
|
|
703
|
+
if (options.push) {
|
|
704
|
+
try {
|
|
705
|
+
push();
|
|
706
|
+
console.log(chalk2.green("\u2713 Changes pushed to remote."));
|
|
707
|
+
} catch (e) {
|
|
708
|
+
const error = e;
|
|
709
|
+
console.log(chalk2.red(`Error pushing: ${error.message}`));
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
program2.command("config").description("Show or edit configuration").option("-e, --edit", "Create/edit configuration file").action((options) => {
|
|
715
|
+
const cfg = loadConfig();
|
|
716
|
+
if (options.edit) {
|
|
717
|
+
console.log(chalk2.dim("Creating default config file..."));
|
|
718
|
+
saveConfig(cfg);
|
|
719
|
+
console.log(chalk2.green(`Config saved to: ${getConfigPath()}`));
|
|
720
|
+
console.log(chalk2.dim("Edit this file to customize settings."));
|
|
721
|
+
} else {
|
|
722
|
+
console.log(showConfig(cfg));
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
program2.command("summarize").description("Summarize staged changes in plain English").option("--diff", "Also show the raw diff").option("-d, --debug", "Enable debug output").action(async (options) => {
|
|
726
|
+
if (options.debug) {
|
|
727
|
+
enableDebug();
|
|
728
|
+
}
|
|
729
|
+
const cfg = loadConfig();
|
|
730
|
+
const backend = new OllamaBackend(cfg.model, cfg.ollama_url);
|
|
731
|
+
const available = await backend.isAvailable();
|
|
732
|
+
if (!available) {
|
|
733
|
+
console.log(chalk2.red("Error: Ollama is not running."));
|
|
734
|
+
console.log(chalk2.dim("Start it with: brew services start ollama"));
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
const diffResult = getStagedDiff();
|
|
738
|
+
if (diffResult.isEmpty) {
|
|
739
|
+
console.log(chalk2.yellow("No staged changes to summarize."));
|
|
740
|
+
console.log(chalk2.dim("Stage changes with: git add <files>"));
|
|
741
|
+
process.exit(0);
|
|
742
|
+
}
|
|
743
|
+
debugDiff(diffResult.diff, diffResult.files);
|
|
744
|
+
console.log(chalk2.bold(`
|
|
745
|
+
Files to summarize: ${diffResult.files.length}`));
|
|
746
|
+
for (const f of diffResult.files.slice(0, 10)) {
|
|
747
|
+
console.log(` \u2022 ${f}`);
|
|
748
|
+
}
|
|
749
|
+
if (diffResult.files.length > 10) {
|
|
750
|
+
console.log(` ... and ${diffResult.files.length - 10} more`);
|
|
751
|
+
}
|
|
752
|
+
const context = `Files changed: ${diffResult.files.slice(0, 5).join(", ")}
|
|
753
|
+
Stats: ${diffResult.stats}`;
|
|
754
|
+
const prompt = buildSummarizePrompt(diffResult.diff, context);
|
|
755
|
+
debugPrompt(prompt);
|
|
756
|
+
const spinner = ora("Generating summary...").start();
|
|
757
|
+
try {
|
|
758
|
+
const summary = await backend.generate(prompt, cfg.temperature);
|
|
759
|
+
spinner.stop();
|
|
760
|
+
debugResponse(summary);
|
|
761
|
+
console.log();
|
|
762
|
+
console.log(chalk2.blue("\u250C\u2500") + chalk2.blue("\u2500".repeat(68)) + chalk2.blue("\u2500\u2510"));
|
|
763
|
+
console.log(chalk2.blue("\u2502") + chalk2.bold(" \u{1F4CB} Summary") + " ".repeat(58) + chalk2.blue("\u2502"));
|
|
764
|
+
console.log(chalk2.blue("\u251C\u2500") + chalk2.blue("\u2500".repeat(68)) + chalk2.blue("\u2500\u2524"));
|
|
765
|
+
for (const line of summary.trim().split("\n")) {
|
|
766
|
+
console.log(chalk2.blue("\u2502") + " " + line.padEnd(67) + chalk2.blue("\u2502"));
|
|
767
|
+
}
|
|
768
|
+
console.log(chalk2.blue("\u2514\u2500") + chalk2.blue("\u2500".repeat(68)) + chalk2.blue("\u2500\u2518"));
|
|
769
|
+
if (options.diff) {
|
|
770
|
+
console.log();
|
|
771
|
+
console.log(chalk2.dim("\u250C\u2500 \u{1F4C4} Diff \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510"));
|
|
772
|
+
console.log(chalk2.dim(diffResult.diff));
|
|
773
|
+
console.log(chalk2.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"));
|
|
774
|
+
}
|
|
775
|
+
} catch (e) {
|
|
776
|
+
spinner.stop();
|
|
777
|
+
const error = e;
|
|
778
|
+
debug(`Summary generation error: ${error.message}`);
|
|
779
|
+
console.log(chalk2.red(`Error generating summary: ${error.message}`));
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
program2.command("hook").description("Manage git hook for automatic commit message generation").option("--install", "Install git hook").option("--remove", "Remove git hook").option("--status", "Check hook status").action((options) => {
|
|
784
|
+
const showStatus = !options.install && !options.remove;
|
|
785
|
+
if (showStatus || options.status) {
|
|
786
|
+
if (isHookInstalled()) {
|
|
787
|
+
console.log(chalk2.green("\u2713 git-commit-ai hook is installed"));
|
|
788
|
+
} else {
|
|
789
|
+
console.log(chalk2.yellow("\u2717 git-commit-ai hook is not installed"));
|
|
790
|
+
console.log(chalk2.dim("Install with: git-commit-ai hook --install"));
|
|
791
|
+
}
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (options.install) {
|
|
795
|
+
const result = installHook();
|
|
796
|
+
if (result.success) {
|
|
797
|
+
console.log(chalk2.green(`\u2713 ${result.message}`));
|
|
798
|
+
console.log(chalk2.dim("Now 'git commit' will auto-generate messages!"));
|
|
799
|
+
} else {
|
|
800
|
+
console.log(chalk2.red(`\u2717 ${result.message}`));
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (options.remove) {
|
|
806
|
+
const result = removeHook();
|
|
807
|
+
if (result.success) {
|
|
808
|
+
console.log(chalk2.green(`\u2713 ${result.message}`));
|
|
809
|
+
} else {
|
|
810
|
+
console.log(chalk2.red(`\u2717 ${result.message}`));
|
|
811
|
+
process.exit(1);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
return program2;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/index.ts
|
|
819
|
+
var program = createProgram();
|
|
820
|
+
program.parse();
|
|
821
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/config.ts","../src/backends/ollama.ts","../src/git.ts","../src/prompts.ts","../src/hook.ts","../src/debug.ts","../src/index.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport ora from \"ora\";\nimport { createInterface } from \"node:readline\";\n\nimport { loadConfig, saveConfig, showConfig, getConfigPath } from \"./config.js\";\nimport { OllamaBackend } from \"./backends/ollama.js\";\nimport {\n getStagedDiff,\n getFileDiff,\n addFiles,\n commit,\n push,\n getModifiedFiles,\n GitError,\n} from \"./git.js\";\nimport {\n buildPrompt,\n buildSummarizePrompt,\n cleanMessage,\n validateMessage,\n fixMessage,\n} from \"./prompts.js\";\nimport { installHook, removeHook, isHookInstalled } from \"./hook.js\";\nimport {\n enableDebug,\n debug,\n debugConfig,\n debugDiff,\n debugPrompt,\n debugResponse,\n debugValidation,\n} from \"./debug.js\";\nimport type { Config, DiffResult } from \"./types.js\";\n\nasync function promptUser(question: string, choices: string[]): Promise<string> {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n return new Promise((resolve) => {\n rl.question(question, (answer) => {\n rl.close();\n const normalized = answer.trim().toLowerCase();\n if (choices.includes(normalized)) {\n resolve(normalized);\n } else {\n resolve(choices[0]); // default to first choice\n }\n });\n });\n}\n\nasync function promptEdit(currentMessage: string): Promise<string> {\n const rl = createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n\n return new Promise((resolve) => {\n console.log(chalk.dim(\"\\nEnter new commit message (or press Enter to keep current):\"));\n rl.question(`Message [${currentMessage}]: `, (answer) => {\n rl.close();\n resolve(answer.trim() || currentMessage);\n });\n });\n}\n\nasync function generateMessage(\n backend: OllamaBackend,\n diffContent: string,\n context: string,\n temperatures: number[]\n): Promise<string | null> {\n const prompt = buildPrompt(diffContent, context);\n debugPrompt(prompt);\n\n for (const temp of temperatures) {\n debug(`Trying temperature: ${temp}`);\n try {\n const rawMessage = await backend.generate(prompt, temp);\n debugResponse(rawMessage);\n\n const message = cleanMessage(rawMessage);\n const isValid = validateMessage(message);\n debugValidation(message, isValid);\n\n if (isValid) {\n return message;\n }\n\n // Try to fix the message\n const fixed = fixMessage(message);\n if (validateMessage(fixed)) {\n debugValidation(fixed, true, fixed);\n return fixed;\n }\n } catch (e) {\n const error = e as Error;\n debug(`Generation error: ${error.message}`);\n console.log(chalk.yellow(`Warning: Generation failed at temp ${temp}: ${error.message}`));\n }\n }\n\n return null;\n}\n\nfunction showMessage(message: string): void {\n console.log();\n console.log(chalk.green(\"┌─\") + chalk.green(\"─\".repeat(68)) + chalk.green(\"─┐\"));\n console.log(chalk.green(\"│\") + chalk.bold(\" 📝 Generated commit message\") + \" \".repeat(40) + chalk.green(\"│\"));\n console.log(chalk.green(\"├─\") + chalk.green(\"─\".repeat(68)) + chalk.green(\"─┤\"));\n console.log(chalk.green(\"│\") + \" \" + message.padEnd(67) + chalk.green(\"│\"));\n console.log(chalk.green(\"└─\") + chalk.green(\"─\".repeat(68)) + chalk.green(\"─┘\"));\n console.log();\n}\n\nasync function promptAction(message: string): Promise<string> {\n showMessage(message);\n return promptUser(\n \"[C]onfirm [E]dit [R]egenerate [A]bort? \",\n [\"c\", \"e\", \"r\", \"a\"]\n );\n}\n\nasync function runCommitFlow(\n backend: OllamaBackend,\n cfg: Config,\n diffContent: string,\n context: string,\n skipConfirm: boolean\n): Promise<string | null> {\n const temperatures = [cfg.temperature, ...cfg.retry_temperatures];\n const spinner = ora(\"Generating commit message...\").start();\n\n while (true) {\n let message: string | null;\n try {\n message = await generateMessage(backend, diffContent, context, temperatures);\n } finally {\n spinner.stop();\n }\n\n if (message === null) {\n console.log(chalk.red(\"Error: Failed to generate a valid commit message.\"));\n message = \"chore: update files\";\n console.log(chalk.yellow(`Using fallback: ${message}`));\n }\n\n if (skipConfirm) {\n return message;\n }\n\n const action = await promptAction(message);\n\n if (action === \"c\") {\n return message;\n } else if (action === \"e\") {\n return promptEdit(message);\n } else if (action === \"r\") {\n console.log(chalk.dim(\"Regenerating...\"));\n spinner.start(\"Generating commit message...\");\n continue;\n } else if (action === \"a\") {\n return null;\n }\n }\n}\n\nasync function handleSingleCommit(\n backend: OllamaBackend,\n cfg: Config,\n skipConfirm: boolean\n): Promise<void> {\n const diffResult = getStagedDiff();\n\n if (diffResult.isEmpty) {\n console.log(chalk.yellow(\"No changes to commit.\"));\n process.exit(0);\n }\n\n debugDiff(diffResult.diff, diffResult.files);\n const context = `Files changed:\\n${diffResult.files.slice(0, 5).join(\"\\n\")}\\nStats: ${diffResult.stats}`;\n\n const message = await runCommitFlow(backend, cfg, diffResult.diff, context, skipConfirm);\n\n if (message === null) {\n console.log(chalk.yellow(\"Aborted.\"));\n process.exit(0);\n }\n\n try {\n commit(message);\n debug(`Commit successful: ${message}`);\n console.log(chalk.green(\"✓ Committed:\"), message);\n } catch (e) {\n const error = e as GitError;\n debug(`Commit failed: ${error.message}`);\n console.log(chalk.red(`Error: ${error.message}`));\n process.exit(1);\n }\n}\n\nasync function handleIndividualCommits(\n backend: OllamaBackend,\n cfg: Config,\n skipConfirm: boolean\n): Promise<void> {\n const filesToCommit = getModifiedFiles();\n\n if (filesToCommit.length === 0) {\n console.log(chalk.yellow(\"No files to commit.\"));\n process.exit(0);\n }\n\n console.log(chalk.dim(`Found ${filesToCommit.length} files to commit individually.`));\n\n for (const filePath of filesToCommit) {\n addFiles(filePath);\n const diffResult = getFileDiff(filePath);\n\n if (diffResult.isEmpty) {\n continue;\n }\n\n console.log(chalk.bold(`\\nProcessing: ${filePath}`));\n\n const context = `File: ${filePath}\\nStats: ${diffResult.stats}`;\n const message = await runCommitFlow(backend, cfg, diffResult.diff, context, skipConfirm);\n\n if (message === null) {\n console.log(chalk.yellow(`Skipped: ${filePath}`));\n continue;\n }\n\n try {\n commit(message);\n console.log(chalk.green(\"✓ Committed:\"), message);\n } catch (e) {\n const error = e as GitError;\n console.log(chalk.red(`Error committing ${filePath}: ${error.message}`));\n }\n }\n}\n\nexport function createProgram(): Command {\n const program = new Command();\n\n program\n .name(\"git-commit-ai\")\n .description(\"Generate commit messages using local LLMs\")\n .version(\"0.2.0\")\n .option(\"-p, --push\", \"Push after commit\")\n .option(\"-y, --yes\", \"Skip confirmation\")\n .option(\"-i, --individual\", \"Commit files individually\")\n .option(\"-d, --debug\", \"Enable debug output\")\n .option(\"--hook-mode\", \"Called by git hook (outputs message only)\")\n .action(async (options) => {\n if (options.debug) {\n enableDebug();\n debug(\"Debug mode enabled\");\n }\n\n const cfg = loadConfig();\n debugConfig(cfg);\n\n const backend = new OllamaBackend(cfg.model, cfg.ollama_url);\n\n // Check if Ollama is available\n const available = await backend.isAvailable();\n if (!available) {\n if (options.hookMode) {\n process.exit(1);\n }\n console.log(chalk.red(\"Error: Ollama is not running.\"));\n console.log(chalk.dim(\"Start it with: brew services start ollama\"));\n process.exit(1);\n }\n\n // Hook mode: just output the message\n if (options.hookMode) {\n const diffResult = getStagedDiff();\n if (diffResult.isEmpty) {\n process.exit(1);\n }\n\n const context = `Files changed:\\n${diffResult.files.slice(0, 5).join(\"\\n\")}\\nStats: ${diffResult.stats}`;\n const temperatures = [cfg.temperature, ...cfg.retry_temperatures];\n const message = await generateMessage(backend, diffResult.diff, context, temperatures);\n\n if (message) {\n console.log(message);\n process.exit(0);\n }\n process.exit(1);\n }\n\n // Stage all files\n addFiles(\".\");\n\n if (options.individual) {\n await handleIndividualCommits(backend, cfg, options.yes);\n } else {\n await handleSingleCommit(backend, cfg, options.yes);\n }\n\n if (options.push) {\n try {\n push();\n console.log(chalk.green(\"✓ Changes pushed to remote.\"));\n } catch (e) {\n const error = e as GitError;\n console.log(chalk.red(`Error pushing: ${error.message}`));\n process.exit(1);\n }\n }\n });\n\n program\n .command(\"config\")\n .description(\"Show or edit configuration\")\n .option(\"-e, --edit\", \"Create/edit configuration file\")\n .action((options) => {\n const cfg = loadConfig();\n\n if (options.edit) {\n console.log(chalk.dim(\"Creating default config file...\"));\n saveConfig(cfg);\n console.log(chalk.green(`Config saved to: ${getConfigPath()}`));\n console.log(chalk.dim(\"Edit this file to customize settings.\"));\n } else {\n console.log(showConfig(cfg));\n }\n });\n\n program\n .command(\"summarize\")\n .description(\"Summarize staged changes in plain English\")\n .option(\"--diff\", \"Also show the raw diff\")\n .option(\"-d, --debug\", \"Enable debug output\")\n .action(async (options) => {\n if (options.debug) {\n enableDebug();\n }\n\n const cfg = loadConfig();\n const backend = new OllamaBackend(cfg.model, cfg.ollama_url);\n\n const available = await backend.isAvailable();\n if (!available) {\n console.log(chalk.red(\"Error: Ollama is not running.\"));\n console.log(chalk.dim(\"Start it with: brew services start ollama\"));\n process.exit(1);\n }\n\n const diffResult = getStagedDiff();\n\n if (diffResult.isEmpty) {\n console.log(chalk.yellow(\"No staged changes to summarize.\"));\n console.log(chalk.dim(\"Stage changes with: git add <files>\"));\n process.exit(0);\n }\n\n debugDiff(diffResult.diff, diffResult.files);\n\n console.log(chalk.bold(`\\nFiles to summarize: ${diffResult.files.length}`));\n for (const f of diffResult.files.slice(0, 10)) {\n console.log(` • ${f}`);\n }\n if (diffResult.files.length > 10) {\n console.log(` ... and ${diffResult.files.length - 10} more`);\n }\n\n const context = `Files changed: ${diffResult.files.slice(0, 5).join(\", \")}\\nStats: ${diffResult.stats}`;\n const prompt = buildSummarizePrompt(diffResult.diff, context);\n debugPrompt(prompt);\n\n const spinner = ora(\"Generating summary...\").start();\n\n try {\n const summary = await backend.generate(prompt, cfg.temperature);\n spinner.stop();\n debugResponse(summary);\n\n console.log();\n console.log(chalk.blue(\"┌─\") + chalk.blue(\"─\".repeat(68)) + chalk.blue(\"─┐\"));\n console.log(chalk.blue(\"│\") + chalk.bold(\" 📋 Summary\") + \" \".repeat(58) + chalk.blue(\"│\"));\n console.log(chalk.blue(\"├─\") + chalk.blue(\"─\".repeat(68)) + chalk.blue(\"─┤\"));\n for (const line of summary.trim().split(\"\\n\")) {\n console.log(chalk.blue(\"│\") + \" \" + line.padEnd(67) + chalk.blue(\"│\"));\n }\n console.log(chalk.blue(\"└─\") + chalk.blue(\"─\".repeat(68)) + chalk.blue(\"─┘\"));\n\n if (options.diff) {\n console.log();\n console.log(chalk.dim(\"┌─ 📄 Diff ─────────────────────────────────────────────────────────┐\"));\n console.log(chalk.dim(diffResult.diff));\n console.log(chalk.dim(\"└───────────────────────────────────────────────────────────────────┘\"));\n }\n } catch (e) {\n spinner.stop();\n const error = e as Error;\n debug(`Summary generation error: ${error.message}`);\n console.log(chalk.red(`Error generating summary: ${error.message}`));\n process.exit(1);\n }\n });\n\n program\n .command(\"hook\")\n .description(\"Manage git hook for automatic commit message generation\")\n .option(\"--install\", \"Install git hook\")\n .option(\"--remove\", \"Remove git hook\")\n .option(\"--status\", \"Check hook status\")\n .action((options) => {\n const showStatus = !options.install && !options.remove;\n\n if (showStatus || options.status) {\n if (isHookInstalled()) {\n console.log(chalk.green(\"✓ git-commit-ai hook is installed\"));\n } else {\n console.log(chalk.yellow(\"✗ git-commit-ai hook is not installed\"));\n console.log(chalk.dim(\"Install with: git-commit-ai hook --install\"));\n }\n return;\n }\n\n if (options.install) {\n const result = installHook();\n if (result.success) {\n console.log(chalk.green(`✓ ${result.message}`));\n console.log(chalk.dim(\"Now 'git commit' will auto-generate messages!\"));\n } else {\n console.log(chalk.red(`✗ ${result.message}`));\n process.exit(1);\n }\n return;\n }\n\n if (options.remove) {\n const result = removeHook();\n if (result.success) {\n console.log(chalk.green(`✓ ${result.message}`));\n } else {\n console.log(chalk.red(`✗ ${result.message}`));\n process.exit(1);\n }\n }\n });\n\n return program;\n}\n","import { readFileSync, writeFileSync, mkdirSync, existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, dirname } from \"node:path\";\nimport { parse as parseToml } from \"smol-toml\";\nimport type { Config } from \"./types.js\";\n\nconst DEFAULT_CONFIG: Config = {\n model: \"llama3.1:8b\",\n ollama_url: \"http://localhost:11434\",\n temperature: 0.7,\n retry_temperatures: [0.5, 0.3, 0.2],\n};\n\nexport function getConfigPath(): string {\n return join(homedir(), \".config\", \"git-commit-ai\", \"config.toml\");\n}\n\nexport function loadConfig(): Config {\n const configPath = getConfigPath();\n\n if (!existsSync(configPath)) {\n return { ...DEFAULT_CONFIG };\n }\n\n try {\n const content = readFileSync(configPath, \"utf-8\");\n const data = parseToml(content) as Partial<Config>;\n\n return {\n model: data.model ?? DEFAULT_CONFIG.model,\n ollama_url: data.ollama_url ?? DEFAULT_CONFIG.ollama_url,\n temperature: data.temperature ?? DEFAULT_CONFIG.temperature,\n retry_temperatures:\n data.retry_temperatures ?? DEFAULT_CONFIG.retry_temperatures,\n };\n } catch {\n return { ...DEFAULT_CONFIG };\n }\n}\n\nexport function saveConfig(config: Config): void {\n const configPath = getConfigPath();\n const dir = dirname(configPath);\n\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const content = `# git-commit-ai configuration\nmodel = \"${config.model}\"\nollama_url = \"${config.ollama_url}\"\ntemperature = ${config.temperature}\nretry_temperatures = [${config.retry_temperatures.join(\", \")}]\n`;\n\n writeFileSync(configPath, content, \"utf-8\");\n}\n\nexport function showConfig(config: Config): string {\n return `Configuration:\n Model: ${config.model}\n Ollama URL: ${config.ollama_url}\n Temperature: ${config.temperature}\n Retry temperatures: [${config.retry_temperatures.join(\", \")}]\n Config file: ${getConfigPath()}`;\n}\n","import type { Backend } from \"../types.js\";\n\nexport class OllamaBackend implements Backend {\n private model: string;\n private baseUrl: string;\n\n constructor(model = \"llama3.1:8b\", baseUrl = \"http://localhost:11434\") {\n this.model = model;\n this.baseUrl = baseUrl;\n }\n\n async generate(prompt: string, temperature = 0.7): Promise<string> {\n const response = await fetch(`${this.baseUrl}/api/generate`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n model: this.model,\n prompt,\n temperature,\n stream: false,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`Ollama API error: ${response.status}`);\n }\n\n const data = (await response.json()) as { response?: string };\n return data.response ?? \"\";\n }\n\n async isAvailable(): Promise<boolean> {\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 5000);\n\n const response = await fetch(`${this.baseUrl}/api/tags`, {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n return response.status === 200;\n } catch {\n return false;\n }\n }\n\n async hasModel(model?: string): Promise<boolean> {\n const checkModel = model ?? this.model;\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 5000);\n\n const response = await fetch(`${this.baseUrl}/api/tags`, {\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n if (response.status !== 200) {\n return false;\n }\n\n const data = (await response.json()) as {\n models?: Array<{ name?: string }>;\n };\n const models = data.models?.map((m) => m.name ?? \"\") ?? [];\n return models.some((m) => m.includes(checkModel));\n } catch {\n return false;\n }\n }\n}\n","import { execSync } from \"node:child_process\";\nimport type { DiffResult } from \"./types.js\";\n\nexport class GitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"GitError\";\n }\n}\n\nfunction runGit(...args: string[]): string {\n try {\n const result = execSync([\"git\", ...args].join(\" \"), {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n return result.trim();\n } catch (error) {\n const err = error as { stderr?: string; message: string };\n const message = err.stderr?.trim() || err.message;\n throw new GitError(`Git command failed: ${message}`);\n }\n}\n\nfunction runGitSafe(...args: string[]): string {\n try {\n return runGit(...args);\n } catch {\n return \"\";\n }\n}\n\nexport function getStagedDiff(): DiffResult {\n const diff = runGitSafe(\"diff\", \"--cached\");\n const stats = runGitSafe(\"diff\", \"--cached\", \"--stat\");\n const filesOutput = runGitSafe(\"diff\", \"--cached\", \"--name-only\");\n const files = filesOutput.split(\"\\n\").filter((f) => f);\n\n return {\n diff,\n stats,\n files,\n isEmpty: !diff.trim(),\n };\n}\n\nexport function getFileDiff(filePath: string): DiffResult {\n const diff = runGitSafe(\"diff\", \"--cached\", \"--\", filePath);\n const stats = runGitSafe(\"diff\", \"--cached\", \"--stat\", \"--\", filePath);\n const files = diff ? [filePath] : [];\n\n return {\n diff,\n stats,\n files,\n isEmpty: !diff.trim(),\n };\n}\n\nexport function addFiles(...paths: string[]): void {\n if (paths.length === 0) {\n paths = [\".\"];\n }\n runGit(\"add\", ...paths);\n}\n\nexport function commit(message: string): string {\n runGit(\"commit\", \"-m\", `\"${message.replace(/\"/g, '\\\\\"')}\"`);\n return runGit(\"rev-parse\", \"HEAD\");\n}\n\nexport function push(): void {\n const branch = getCurrentBranch();\n runGit(\"push\", \"origin\", branch);\n}\n\nexport function getCurrentBranch(): string {\n return runGit(\"rev-parse\", \"--abbrev-ref\", \"HEAD\");\n}\n\nexport function getModifiedFiles(): string[] {\n const files = new Set<string>();\n\n // Modified and staged files\n const staged = runGitSafe(\"diff\", \"--cached\", \"--name-only\");\n if (staged) {\n staged.split(\"\\n\").forEach((f) => files.add(f));\n }\n\n // Modified but not staged\n const modified = runGitSafe(\"diff\", \"--name-only\");\n if (modified) {\n modified.split(\"\\n\").forEach((f) => files.add(f));\n }\n\n // Untracked files\n const untracked = runGitSafe(\"ls-files\", \"--others\", \"--exclude-standard\");\n if (untracked) {\n untracked.split(\"\\n\").forEach((f) => files.add(f));\n }\n\n return Array.from(files).filter((f) => f);\n}\n\nexport function hasStagedChanges(): boolean {\n const diff = runGitSafe(\"diff\", \"--cached\", \"--name-only\");\n return Boolean(diff.trim());\n}\n","const SUMMARIZE_PROMPT = `Summarize the following code changes in plain English.\n\nProvide a brief, clear summary that explains:\n1. What files were changed\n2. What was added, removed, or modified\n3. The likely purpose of these changes\n\nKeep it concise (3-5 bullet points). Focus on the \"what\" and \"why\".\n\n{context}\n\nDIFF:\n\\`\\`\\`\n{diff}\n\\`\\`\\`\n\nProvide only the summary, no additional commentary.`;\n\nconst KARMA_PROMPT = `Analyze the git diff below and create a commit message following Karma convention.\n\nFORMAT: <type>(<scope>): <subject>\n\nTYPES (use the most appropriate):\n- feat: new feature or capability\n- fix: bug fix\n- docs: documentation changes (README, comments, docstrings)\n- style: formatting only (whitespace, semicolons)\n- refactor: code change that neither fixes bug nor adds feature\n- test: adding or modifying tests\n- build: build system, dependencies, package config\n- chore: maintenance tasks\n\nRULES:\n- Scope is optional - use the main file/module name if relevant\n- Subject must describe WHAT changed in the diff, not a generic message\n- Use imperative mood: \"add\" not \"added\", \"fix\" not \"fixed\"\n- Lowercase, no period at end, max 72 chars\n\nEXAMPLES based on diff content:\n- Adding README.md → docs: add README with usage instructions\n- Fixing null check in auth.py → fix(auth): handle null user in login\n- New API endpoint → feat(api): add user profile endpoint\n- Updating dependencies → build: update httpx to 0.25.0\n\nIMPORTANT: Base your message ONLY on the actual changes shown in the diff below.\nDo NOT use the examples above if they don't match the diff content.\n\n{context}\n\nDIFF TO ANALYZE:\n\\`\\`\\`\n{diff}\n\\`\\`\\`\n\nReply with ONLY the commit message, nothing else. No quotes, no explanation.`;\n\nconst KARMA_PATTERN =\n /^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\\([^)]+\\))?:\\s*.+/;\n\nconst ACTION_TO_TYPE: Record<string, string> = {\n add: \"feat\",\n added: \"feat\",\n adding: \"feat\",\n create: \"feat\",\n implement: \"feat\",\n fix: \"fix\",\n fixed: \"fix\",\n fixing: \"fix\",\n repair: \"fix\",\n update: \"refactor\",\n updated: \"refactor\",\n updating: \"refactor\",\n improve: \"refactor\",\n remove: \"refactor\",\n removed: \"refactor\",\n removing: \"refactor\",\n delete: \"refactor\",\n document: \"docs\",\n documented: \"docs\",\n test: \"test\",\n tested: \"test\",\n testing: \"test\",\n};\n\nconst MAX_DIFF_CHARS = 8000;\n\nexport function truncateDiff(diff: string, maxChars = MAX_DIFF_CHARS): string {\n if (diff.length <= maxChars) {\n return diff;\n }\n\n let truncated = diff.slice(0, maxChars);\n const lastNewline = truncated.lastIndexOf(\"\\n\");\n if (lastNewline > maxChars * 0.8) {\n truncated = truncated.slice(0, lastNewline);\n }\n\n return truncated + \"\\n\\n[... diff truncated for brevity ...]\";\n}\n\nexport function buildPrompt(diff: string, context: string): string {\n const truncatedDiff = truncateDiff(diff);\n return KARMA_PROMPT.replace(\"{diff}\", truncatedDiff).replace(\"{context}\", context);\n}\n\nexport function buildSummarizePrompt(diff: string, context: string): string {\n const truncatedDiff = truncateDiff(diff);\n return SUMMARIZE_PROMPT.replace(\"{diff}\", truncatedDiff).replace(\"{context}\", context);\n}\n\nexport function validateMessage(message: string): boolean {\n return KARMA_PATTERN.test(message.trim());\n}\n\nexport function cleanMessage(message: string): string {\n // Take only the first line\n let cleaned = message.trim().split(\"\\n\")[0];\n\n // Remove common prefixes that models sometimes add\n const prefixes = [\"Here is \", \"I've \", \"The commit message is:\", \"Commit message:\", \"Here's \"];\n\n for (const prefix of prefixes) {\n if (cleaned.toLowerCase().startsWith(prefix.toLowerCase())) {\n cleaned = cleaned.slice(prefix.length);\n }\n }\n\n // Strip whitespace and trailing period\n cleaned = cleaned.trim().replace(/\\.$/, \"\");\n\n return cleaned;\n}\n\nexport function fixMessage(message: string): string {\n const cleaned = cleanMessage(message);\n\n // If already valid, return as-is\n if (validateMessage(cleaned)) {\n return cleaned;\n }\n\n // Try to infer type from first word\n const words = cleaned.split(/\\s+/);\n if (words.length > 0) {\n const firstWord = words[0].toLowerCase().replace(/:$/, \"\");\n const commitType = ACTION_TO_TYPE[firstWord] ?? \"chore\";\n\n // Build the message\n const subject = words.join(\" \").toLowerCase();\n if (!subject.endsWith(\":\")) {\n return `${commitType}: ${subject}`;\n }\n }\n\n return `chore: ${cleaned.toLowerCase()}`;\n}\n","import { execSync } from \"node:child_process\";\nimport { readFileSync, writeFileSync, unlinkSync, existsSync, chmodSync, mkdirSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport type { HookResult } from \"./types.js\";\n\nconst HOOK_SCRIPT = `#!/bin/sh\n# git-commit-ai prepare-commit-msg hook\n# This hook generates commit messages using AI\n\nCOMMIT_MSG_FILE=\"$1\"\nCOMMIT_SOURCE=\"$2\"\n\n# Only run for regular commits (not merge, squash, etc.)\nif [ -n \"$COMMIT_SOURCE\" ]; then\n exit 0\nfi\n\n# Check if there's already a message (e.g., from -m flag)\nif [ -s \"$COMMIT_MSG_FILE\" ]; then\n # File is not empty, check if it's just the default template\n FIRST_LINE=$(head -n 1 \"$COMMIT_MSG_FILE\")\n if [ -n \"$FIRST_LINE\" ] && ! echo \"$FIRST_LINE\" | grep -q \"^#\"; then\n # There's actual content, don't override\n exit 0\n fi\nfi\n\n# Generate commit message using git-commit-ai\nMESSAGE=$(git-commit-ai --hook-mode 2>/dev/null)\n\nif [ $? -eq 0 ] && [ -n \"$MESSAGE\" ]; then\n # Write the generated message, preserving any existing comments\n COMMENTS=$(grep \"^#\" \"$COMMIT_MSG_FILE\" 2>/dev/null || true)\n echo \"$MESSAGE\" > \"$COMMIT_MSG_FILE\"\n if [ -n \"$COMMENTS\" ]; then\n echo \"\" >> \"$COMMIT_MSG_FILE\"\n echo \"$COMMENTS\" >> \"$COMMIT_MSG_FILE\"\n fi\nfi\n\nexit 0\n`;\n\nconst HOOK_NAME = \"prepare-commit-msg\";\n\nfunction getGitDir(): string | null {\n try {\n const result = execSync(\"git rev-parse --git-dir\", {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n return result.trim();\n } catch {\n return null;\n }\n}\n\nfunction getHookPath(): string | null {\n const gitDir = getGitDir();\n if (!gitDir) return null;\n return join(gitDir, \"hooks\", HOOK_NAME);\n}\n\nexport function isHookInstalled(): boolean {\n const hookPath = getHookPath();\n if (!hookPath || !existsSync(hookPath)) {\n return false;\n }\n\n const content = readFileSync(hookPath, \"utf-8\");\n return content.includes(\"git-commit-ai\");\n}\n\nexport function installHook(): HookResult {\n const hookPath = getHookPath();\n if (!hookPath) {\n return { success: false, message: \"Not in a git repository\" };\n }\n\n let returnMsg = \"Hook installed successfully\";\n\n // Check if hook already exists\n if (existsSync(hookPath)) {\n const content = readFileSync(hookPath, \"utf-8\");\n if (content.includes(\"git-commit-ai\")) {\n return { success: true, message: \"Hook already installed\" };\n }\n // Backup existing hook\n const backupPath = hookPath + \".backup\";\n writeFileSync(backupPath, content, \"utf-8\");\n returnMsg = `Existing hook backed up to ${HOOK_NAME}.backup`;\n }\n\n // Create hooks directory if needed\n const hooksDir = dirname(hookPath);\n if (!existsSync(hooksDir)) {\n mkdirSync(hooksDir, { recursive: true });\n }\n\n // Write the hook script\n writeFileSync(hookPath, HOOK_SCRIPT, \"utf-8\");\n\n // Make it executable\n chmodSync(hookPath, 0o755);\n\n return { success: true, message: returnMsg };\n}\n\nexport function removeHook(): HookResult {\n const hookPath = getHookPath();\n if (!hookPath) {\n return { success: false, message: \"Not in a git repository\" };\n }\n\n if (!existsSync(hookPath)) {\n return { success: true, message: \"No hook installed\" };\n }\n\n // Check if it's our hook\n const content = readFileSync(hookPath, \"utf-8\");\n if (!content.includes(\"git-commit-ai\")) {\n return { success: false, message: \"Hook exists but was not installed by git-commit-ai\" };\n }\n\n // Remove the hook\n unlinkSync(hookPath);\n\n // Restore backup if exists\n const backupPath = hookPath + \".backup\";\n if (existsSync(backupPath)) {\n const backupContent = readFileSync(backupPath, \"utf-8\");\n writeFileSync(hookPath, backupContent, \"utf-8\");\n unlinkSync(backupPath);\n return { success: true, message: \"Hook removed, previous hook restored\" };\n }\n\n return { success: true, message: \"Hook removed successfully\" };\n}\n","import chalk from \"chalk\";\nimport type { Config } from \"./types.js\";\n\nlet debugEnabled = false;\n\nexport function enableDebug(): void {\n debugEnabled = true;\n}\n\nexport function isDebugEnabled(): boolean {\n return debugEnabled;\n}\n\nfunction getTimestamp(): string {\n const now = new Date();\n return now.toTimeString().slice(0, 8);\n}\n\nexport function debug(message: string, data?: string): void {\n if (!debugEnabled) return;\n\n console.error(chalk.dim(`[${getTimestamp()}]`), chalk.cyan(\"[DEBUG]\"), message);\n\n if (data) {\n const truncated =\n data.length > 500 ? data.slice(0, 500) + `\\n... (${data.length} chars total, truncated)` : data;\n console.error(chalk.dim(truncated));\n }\n}\n\nexport function debugConfig(cfg: Config): void {\n debug(\n \"Config loaded:\",\n `\n Model: ${cfg.model}\n Ollama URL: ${cfg.ollama_url}\n Temperature: ${cfg.temperature}\n Retry temps: [${cfg.retry_temperatures.join(\", \")}]`\n );\n}\n\nexport function debugDiff(diff: string, files: string[]): void {\n const filesList = files.slice(0, 5).join(\", \") + (files.length > 5 ? \" ...\" : \"\");\n debug(`Diff size: ${diff.length} chars, Files: ${files.length}`, `Files: ${filesList}`);\n}\n\nexport function debugPrompt(prompt: string): void {\n const truncated = prompt.length > 500 ? prompt.slice(0, 500) + \"...\" : prompt;\n debug(\"Prompt to LLM:\", truncated);\n}\n\nexport function debugResponse(response: string): void {\n debug(\"Raw LLM response:\", response);\n}\n\nexport function debugValidation(message: string, isValid: boolean, fixed?: string): void {\n const status = isValid ? chalk.green(\"valid\") : chalk.red(\"invalid\");\n debug(`Message validation: ${status}`, message);\n if (fixed && fixed !== message) {\n debug(\"Fixed message:\", fixed);\n }\n}\n","import { createProgram } from \"./cli.js\";\n\nconst program = createProgram();\nprogram.parse();\n"],"mappings":"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","names":["chalk","execSync","readFileSync","writeFileSync","existsSync","mkdirSync","join","dirname","chalk","program"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vavasilva/git-commit-ai",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Generate commit messages using local LLMs (Ollama)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"git-commit-ai": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"dev": "tsup --watch",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/vavasilva/git-commit-ai.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/vavasilva/git-commit-ai/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/vavasilva/git-commit-ai#readme",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"git",
|
|
33
|
+
"commit",
|
|
34
|
+
"ai",
|
|
35
|
+
"ollama",
|
|
36
|
+
"llm",
|
|
37
|
+
"cli",
|
|
38
|
+
"karma",
|
|
39
|
+
"conventional-commits"
|
|
40
|
+
],
|
|
41
|
+
"author": "Wagner Silva",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^20.10.0",
|
|
45
|
+
"tsup": "^8.0.1",
|
|
46
|
+
"typescript": "^5.3.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"chalk": "^5.3.0",
|
|
50
|
+
"commander": "^12.0.0",
|
|
51
|
+
"ora": "^8.0.1",
|
|
52
|
+
"smol-toml": "^1.1.4"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=20.0.0"
|
|
56
|
+
}
|
|
57
|
+
}
|