@vavasilva/git-commit-ai 0.2.3 → 0.3.1

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 CHANGED
@@ -1,15 +1,19 @@
1
1
  # git-commit-ai
2
2
 
3
- Generate commit messages using local LLMs (Ollama).
3
+ > AI-powered commit message generator for Git. Automate your git workflow with intelligent, conventional commit messages.
4
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.
5
+ A command-line tool that uses Large Language Models (LLMs) to analyze your staged changes and automatically generate [Karma-style](https://karma-runner.github.io/6.4/dev/git-commit-msg.html) / [Conventional Commits](https://www.conventionalcommits.org/) messages.
6
+
7
+ **Supported AI backends:** Ollama, llama.cpp, OpenAI (GPT-4), Anthropic (Claude), Groq (LLaMA)
6
8
 
7
9
  ## Features
8
10
 
9
- - **Local LLM** - Uses Ollama (llama3.1:8b by default), no API keys needed
11
+ - **Multiple Backends** - Ollama (local), llama.cpp (local), OpenAI, Anthropic Claude, Groq
12
+ - **Auto-Detection** - Automatically selects available backend
10
13
  - **Karma Convention** - Generates `type(scope): subject` format commits
11
14
  - **Interactive Flow** - Confirm, Edit, Regenerate, or Abort before committing
12
15
  - **Individual Commits** - Option to commit each file separately
16
+ - **Dry Run** - Preview messages without committing
13
17
  - **Git Hook** - Auto-generate messages on `git commit`
14
18
  - **Summarize** - Preview changes in plain English before committing
15
19
  - **Debug Mode** - Troubleshoot LLM responses
@@ -19,14 +23,233 @@ A CLI tool that analyzes your staged changes and generates [Karma-style](https:/
19
23
 
20
24
  ```bash
21
25
  # Requires Node.js 20+
22
- npm install -g git-commit-ai
26
+ npm install -g @vavasilva/git-commit-ai
27
+ ```
28
+
29
+ ### Backend Setup
23
30
 
24
- # Make sure Ollama is running
31
+ Choose at least one backend:
32
+
33
+ **Ollama (Local, Free)**
34
+ ```bash
35
+ # macOS
25
36
  brew install ollama
26
37
  brew services start ollama
38
+
39
+ # Linux
40
+ curl -fsSL https://ollama.com/install.sh | sh
41
+ sudo systemctl start ollama
42
+
43
+ # Windows - download installer from:
44
+ # https://ollama.com/download/windows
45
+
46
+ # Pull a model (all platforms)
27
47
  ollama pull llama3.1:8b
28
48
  ```
29
49
 
50
+ **llama.cpp (Local, Free, Low Memory)**
51
+
52
+ Run local GGUF models with `llama-server` (auto-detected on port 8080):
53
+
54
+ ```bash
55
+ # Install llama.cpp
56
+ # macOS
57
+ brew install llama.cpp
58
+
59
+ # Linux (Ubuntu/Debian) - build from source
60
+ sudo apt install build-essential cmake
61
+ git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
62
+ cmake -B build && cmake --build build --config Release
63
+ sudo cp build/bin/llama-server /usr/local/bin/
64
+
65
+ # Windows - download pre-built binaries from:
66
+ # https://github.com/ggml-org/llama.cpp/releases
67
+
68
+ # Start the server (downloads model automatically from Hugging Face)
69
+ llama-server -hf Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF -ngl 99 --port 8080
70
+
71
+ # Use with git-commit-ai (auto-detected if running on port 8080)
72
+ git-commit-ai
73
+
74
+ # Or explicitly use llamacpp backend
75
+ git-commit-ai --backend llamacpp
76
+
77
+ # Configure as default backend
78
+ git-commit-ai config --set backend=llamacpp
79
+ ```
80
+
81
+ **Run llama-server as a service**
82
+
83
+ <details>
84
+ <summary><strong>macOS (launchd)</strong></summary>
85
+
86
+ ```bash
87
+ # Create launchd service
88
+ cat > ~/Library/LaunchAgents/com.llamacpp.server.plist << 'EOF'
89
+ <?xml version="1.0" encoding="UTF-8"?>
90
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
91
+ <plist version="1.0">
92
+ <dict>
93
+ <key>Label</key>
94
+ <string>com.llamacpp.server</string>
95
+ <key>ProgramArguments</key>
96
+ <array>
97
+ <string>/opt/homebrew/bin/llama-server</string>
98
+ <string>-hf</string>
99
+ <string>Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF</string>
100
+ <string>-ngl</string>
101
+ <string>99</string>
102
+ <string>--port</string>
103
+ <string>8080</string>
104
+ </array>
105
+ <key>RunAtLoad</key>
106
+ <true/>
107
+ <key>KeepAlive</key>
108
+ <true/>
109
+ <key>StandardOutPath</key>
110
+ <string>/tmp/llama-server.log</string>
111
+ <key>StandardErrorPath</key>
112
+ <string>/tmp/llama-server.err</string>
113
+ </dict>
114
+ </plist>
115
+ EOF
116
+
117
+ # Start the service
118
+ launchctl load ~/Library/LaunchAgents/com.llamacpp.server.plist
119
+
120
+ # Stop the service
121
+ launchctl unload ~/Library/LaunchAgents/com.llamacpp.server.plist
122
+
123
+ # Check logs
124
+ tail -f /tmp/llama-server.log
125
+ ```
126
+
127
+ </details>
128
+
129
+ <details>
130
+ <summary><strong>Linux (systemd)</strong></summary>
131
+
132
+ ```bash
133
+ # Create systemd service
134
+ sudo cat > /etc/systemd/system/llama-server.service << 'EOF'
135
+ [Unit]
136
+ Description=llama.cpp Server
137
+ After=network.target
138
+
139
+ [Service]
140
+ Type=simple
141
+ User=$USER
142
+ ExecStart=/usr/local/bin/llama-server -hf Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF -ngl 99 --port 8080
143
+ Restart=on-failure
144
+ RestartSec=10
145
+ StandardOutput=append:/var/log/llama-server.log
146
+ StandardError=append:/var/log/llama-server.err
147
+
148
+ [Install]
149
+ WantedBy=multi-user.target
150
+ EOF
151
+
152
+ # Replace $USER with your username
153
+ sudo sed -i "s/\$USER/$USER/" /etc/systemd/system/llama-server.service
154
+
155
+ # Enable and start the service
156
+ sudo systemctl daemon-reload
157
+ sudo systemctl enable llama-server
158
+ sudo systemctl start llama-server
159
+
160
+ # Check status
161
+ sudo systemctl status llama-server
162
+
163
+ # View logs
164
+ journalctl -u llama-server -f
165
+ ```
166
+
167
+ </details>
168
+
169
+ <details>
170
+ <summary><strong>Windows (Task Scheduler)</strong></summary>
171
+
172
+ **Option 1: PowerShell script with Task Scheduler**
173
+
174
+ 1. Create a startup script `C:\llama-server\start-llama.ps1`:
175
+ ```powershell
176
+ # start-llama.ps1
177
+ Start-Process -FilePath "C:\llama-server\llama-server.exe" `
178
+ -ArgumentList "-hf", "Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF", "-ngl", "99", "--port", "8080" `
179
+ -WindowStyle Hidden `
180
+ -RedirectStandardOutput "C:\llama-server\llama-server.log" `
181
+ -RedirectStandardError "C:\llama-server\llama-server.err"
182
+ ```
183
+
184
+ 2. Create a scheduled task (run in PowerShell as Administrator):
185
+ ```powershell
186
+ $action = New-ScheduledTaskAction -Execute "powershell.exe" `
187
+ -Argument "-ExecutionPolicy Bypass -File C:\llama-server\start-llama.ps1"
188
+ $trigger = New-ScheduledTaskTrigger -AtStartup
189
+ $principal = New-ScheduledTaskPrincipal -UserId "$env:USERNAME" -LogonType S4U
190
+ Register-ScheduledTask -TaskName "LlamaServer" -Action $action -Trigger $trigger -Principal $principal
191
+
192
+ # Start immediately
193
+ Start-ScheduledTask -TaskName "LlamaServer"
194
+
195
+ # Stop the service
196
+ Stop-ScheduledTask -TaskName "LlamaServer"
197
+
198
+ # Remove the service
199
+ Unregister-ScheduledTask -TaskName "LlamaServer" -Confirm:$false
200
+ ```
201
+
202
+ **Option 2: Using NSSM (Non-Sucking Service Manager)**
203
+
204
+ ```powershell
205
+ # Install NSSM (using chocolatey)
206
+ choco install nssm
207
+
208
+ # Install llama-server as a Windows service
209
+ nssm install LlamaServer "C:\llama-server\llama-server.exe" "-hf Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF -ngl 99 --port 8080"
210
+ nssm set LlamaServer AppDirectory "C:\llama-server"
211
+ nssm set LlamaServer AppStdout "C:\llama-server\llama-server.log"
212
+ nssm set LlamaServer AppStderr "C:\llama-server\llama-server.err"
213
+
214
+ # Start the service
215
+ nssm start LlamaServer
216
+
217
+ # Stop the service
218
+ nssm stop LlamaServer
219
+
220
+ # Remove the service
221
+ nssm remove LlamaServer confirm
222
+ ```
223
+
224
+ </details>
225
+
226
+ **OpenAI**
227
+ ```bash
228
+ export OPENAI_API_KEY="your-api-key"
229
+ ```
230
+
231
+ **OpenAI-Compatible APIs**
232
+
233
+ Any OpenAI-compatible API can be used by setting `OPENAI_BASE_URL`:
234
+ ```bash
235
+ # Local server (llama.cpp, vLLM, etc.)
236
+ export OPENAI_BASE_URL="http://localhost:8080/v1"
237
+
238
+ # Or other providers (Together AI, Anyscale, etc.)
239
+ export OPENAI_BASE_URL="https://api.together.xyz/v1"
240
+ export OPENAI_API_KEY="your-api-key"
241
+ ```
242
+
243
+ **Anthropic (Claude)**
244
+ ```bash
245
+ export ANTHROPIC_API_KEY="your-api-key"
246
+ ```
247
+
248
+ **Groq (Fast & Free tier)**
249
+ ```bash
250
+ export GROQ_API_KEY="your-api-key"
251
+ ```
252
+
30
253
  ## Quick Start
31
254
 
32
255
  ```bash
@@ -64,6 +287,44 @@ git-commit-ai --push
64
287
  # Commit each modified file separately
65
288
  git-commit-ai --individual
66
289
 
290
+ # Preview message without committing (dry run)
291
+ git add .
292
+ git-commit-ai --dry-run
293
+
294
+ # Amend the last commit with a new message
295
+ git-commit-ai --amend
296
+
297
+ # Force a specific scope and type
298
+ git-commit-ai --scope auth --type fix
299
+
300
+ # Generate message in a specific language
301
+ git-commit-ai --lang pt
302
+
303
+ # Reference an issue
304
+ git-commit-ai --issue 123
305
+
306
+ # Mark as breaking change
307
+ git-commit-ai --breaking
308
+
309
+ # Add co-authors
310
+ git-commit-ai --co-author "Jane Doe <jane@example.com>"
311
+
312
+ # Provide additional context
313
+ git-commit-ai --context "This fixes the login bug reported by QA"
314
+
315
+ # Use a specific backend
316
+ git-commit-ai --backend llamacpp
317
+ git-commit-ai --backend openai
318
+ git-commit-ai --backend anthropic
319
+ git-commit-ai --backend groq
320
+
321
+ # Override model
322
+ git-commit-ai --model gpt-4o
323
+ git-commit-ai --model claude-3-sonnet-20240229
324
+
325
+ # Adjust creativity (temperature)
326
+ git-commit-ai --temperature 0.3
327
+
67
328
  # Preview changes before committing
68
329
  git add .
69
330
  git-commit-ai summarize
@@ -74,7 +335,21 @@ git-commit-ai --debug
74
335
  # Show current config
75
336
  git-commit-ai config
76
337
 
77
- # Create/edit config file
338
+ # Set a config value
339
+ git-commit-ai config --set backend=llamacpp
340
+ git-commit-ai config --set model=gpt-4o
341
+ git-commit-ai config --set temperature=0.5
342
+
343
+ # Use short aliases
344
+ git-commit-ai config --set lang=pt # → default_language
345
+ git-commit-ai config --set scope=api # → default_scope
346
+ git-commit-ai config --set type=feat # → default_type
347
+ git-commit-ai config --set temp=0.5 # → temperature
348
+
349
+ # List valid config keys and aliases
350
+ git-commit-ai config --list-keys
351
+
352
+ # Create/edit config file manually
78
353
  git-commit-ai config --edit
79
354
  ```
80
355
 
@@ -110,15 +385,85 @@ git-commit-ai hook --remove
110
385
 
111
386
  ## Configuration
112
387
 
113
- Config file location: `~/.config/git-commit-ai/config.toml`
388
+ ### Global Config
389
+
390
+ Location: `~/.config/git-commit-ai/config.toml`
114
391
 
115
392
  ```toml
393
+ # Backend: ollama, llamacpp, openai, anthropic, groq
394
+ backend = "ollama"
116
395
  model = "llama3.1:8b"
117
396
  ollama_url = "http://localhost:11434"
118
397
  temperature = 0.7
119
398
  retry_temperatures = [0.5, 0.3, 0.2]
399
+
400
+ # OpenAI Base URL - change this to use OpenAI-compatible APIs
401
+ # Examples:
402
+ # - Default OpenAI: https://api.openai.com/v1
403
+ # - llama.cpp: http://localhost:8080/v1
404
+ # - Together AI: https://api.together.xyz/v1
405
+ openai_base_url = "https://api.openai.com/v1"
406
+
407
+ # Optional: Ignore files from diff analysis
408
+ ignore_patterns = ["*.lock", "package-lock.json", "*.min.js"]
409
+
410
+ # Optional: Set defaults for commit messages
411
+ default_scope = "api" # Default scope if not specified
412
+ default_type = "feat" # Default commit type
413
+ default_language = "en" # Default language (en, pt, es, fr, de)
120
414
  ```
121
415
 
416
+ ### Local Config (per-project)
417
+
418
+ Create `.gitcommitai` or `.gitcommitai.toml` in your project root to override global settings:
419
+
420
+ ```toml
421
+ # .gitcommitai
422
+ default_scope = "frontend"
423
+ default_language = "pt"
424
+ ignore_patterns = ["dist/*", "*.generated.ts"]
425
+ ```
426
+
427
+ ### Default Models by Backend
428
+
429
+ | Backend | Default Model |
430
+ |---------|---------------|
431
+ | ollama | llama3.1:8b |
432
+ | llamacpp | gpt-4o-mini (alias) |
433
+ | openai | gpt-4o-mini |
434
+ | anthropic | claude-3-haiku-20240307 |
435
+ | groq | llama-3.1-8b-instant |
436
+
437
+ ## CLI Options
438
+
439
+ | Option | Description |
440
+ |--------|-------------|
441
+ | `-p, --push` | Push after commit |
442
+ | `-y, --yes` | Skip confirmation |
443
+ | `-i, --individual` | Commit files individually |
444
+ | `-d, --debug` | Enable debug output |
445
+ | `--dry-run` | Show message without committing |
446
+ | `--amend` | Regenerate and amend the last commit |
447
+ | `-b, --backend <name>` | Backend to use |
448
+ | `-m, --model <name>` | Override model |
449
+ | `-t, --temperature <n>` | Override temperature (0.0-1.0) |
450
+ | `-s, --scope <scope>` | Force a specific scope (e.g., auth, api) |
451
+ | `--type <type>` | Force commit type (feat, fix, docs, etc.) |
452
+ | `-c, --context <text>` | Provide additional context for generation |
453
+ | `-l, --lang <code>` | Language for message (en, pt, es, fr, de) |
454
+ | `--issue <ref>` | Reference an issue (e.g., 123 or #123) |
455
+ | `--breaking` | Mark as breaking change (adds ! to type) |
456
+ | `--co-author <author>` | Add co-author (can be repeated) |
457
+
458
+ ## Config Commands
459
+
460
+ | Command | Description |
461
+ |---------|-------------|
462
+ | `config` | Show current configuration |
463
+ | `config --edit` | Create/edit config file manually |
464
+ | `config --set <key=value>` | Set a config value |
465
+ | `config --list-keys` | List all valid config keys |
466
+
122
467
  ## Commit Types (Karma Convention)
123
468
 
124
469
  | Type | Description |
@@ -132,6 +477,15 @@ retry_temperatures = [0.5, 0.3, 0.2]
132
477
  | `build` | Build system or dependencies |
133
478
  | `chore` | Maintenance tasks |
134
479
 
480
+ ## Environment Variables
481
+
482
+ | Variable | Description |
483
+ |----------|-------------|
484
+ | `OPENAI_API_KEY` | OpenAI API key |
485
+ | `OPENAI_BASE_URL` | OpenAI-compatible API base URL (default: `https://api.openai.com/v1`) |
486
+ | `ANTHROPIC_API_KEY` | Anthropic API key |
487
+ | `GROQ_API_KEY` | Groq API key |
488
+
135
489
  ## License
136
490
 
137
491
  MIT
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/git.ts
4
+ import { execSync } from "child_process";
5
+ function matchesPattern(filePath, pattern) {
6
+ let regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "<<GLOBSTAR>>").replace(/\*/g, "[^/]*").replace(/<<GLOBSTAR>>/g, ".*").replace(/\?/g, ".");
7
+ if (!pattern.startsWith("/") && !pattern.startsWith("*")) {
8
+ regexStr = `(^|/)${regexStr}`;
9
+ }
10
+ regexStr = `${regexStr}($|/)`;
11
+ try {
12
+ const regex = new RegExp(regexStr);
13
+ return regex.test(filePath);
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
18
+ function shouldIgnoreFile(filePath, patterns) {
19
+ return patterns.some((pattern) => matchesPattern(filePath, pattern));
20
+ }
21
+ function filterDiffByPatterns(diff, patterns) {
22
+ if (!patterns || patterns.length === 0) {
23
+ return diff;
24
+ }
25
+ const sections = diff.split(/(?=^diff --git)/m);
26
+ const filtered = sections.filter((section) => {
27
+ const match = section.match(/^diff --git a\/(.+?) b\//m);
28
+ if (!match) {
29
+ return true;
30
+ }
31
+ const filePath = match[1];
32
+ return !shouldIgnoreFile(filePath, patterns);
33
+ });
34
+ return filtered.join("");
35
+ }
36
+ var GitError = class extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "GitError";
40
+ }
41
+ };
42
+ function runGit(...args) {
43
+ try {
44
+ const result = execSync(["git", ...args].join(" "), {
45
+ encoding: "utf-8",
46
+ stdio: ["pipe", "pipe", "pipe"]
47
+ });
48
+ return result.trim();
49
+ } catch (error) {
50
+ const err = error;
51
+ const message = err.stderr?.trim() || err.message;
52
+ throw new GitError(`Git command failed: ${message}`);
53
+ }
54
+ }
55
+ function runGitSafe(...args) {
56
+ try {
57
+ return runGit(...args);
58
+ } catch {
59
+ return "";
60
+ }
61
+ }
62
+ function getStagedDiff() {
63
+ const diff = runGitSafe("diff", "--cached");
64
+ const stats = runGitSafe("diff", "--cached", "--stat");
65
+ const filesOutput = runGitSafe("diff", "--cached", "--name-only");
66
+ const files = filesOutput.split("\n").filter((f) => f);
67
+ const statusOutput = runGitSafe("diff", "--cached", "--name-status");
68
+ const filesAdded = [];
69
+ const filesDeleted = [];
70
+ const filesModified = [];
71
+ statusOutput.split("\n").filter((f) => f).forEach((line) => {
72
+ const [status, ...pathParts] = line.split(" ");
73
+ const filePath = pathParts.join(" ");
74
+ if (status.startsWith("A")) {
75
+ filesAdded.push(filePath);
76
+ } else if (status.startsWith("D")) {
77
+ filesDeleted.push(filePath);
78
+ } else if (status.startsWith("M") || status.startsWith("R")) {
79
+ filesModified.push(filePath);
80
+ }
81
+ });
82
+ return {
83
+ diff,
84
+ stats,
85
+ files,
86
+ filesAdded,
87
+ filesDeleted,
88
+ filesModified,
89
+ isEmpty: !diff.trim()
90
+ };
91
+ }
92
+ function getFileDiff(filePath) {
93
+ const diff = runGitSafe("diff", "--cached", "--", filePath);
94
+ const stats = runGitSafe("diff", "--cached", "--stat", "--", filePath);
95
+ const files = diff ? [filePath] : [];
96
+ const statusOutput = runGitSafe("diff", "--cached", "--name-status", "--", filePath);
97
+ const filesAdded = [];
98
+ const filesDeleted = [];
99
+ const filesModified = [];
100
+ if (statusOutput) {
101
+ const [status] = statusOutput.split(" ");
102
+ if (status.startsWith("A")) {
103
+ filesAdded.push(filePath);
104
+ } else if (status.startsWith("D")) {
105
+ filesDeleted.push(filePath);
106
+ } else if (status.startsWith("M") || status.startsWith("R")) {
107
+ filesModified.push(filePath);
108
+ }
109
+ }
110
+ return {
111
+ diff,
112
+ stats,
113
+ files,
114
+ filesAdded,
115
+ filesDeleted,
116
+ filesModified,
117
+ isEmpty: !diff.trim()
118
+ };
119
+ }
120
+ function addFiles(...paths) {
121
+ if (paths.length === 0) {
122
+ paths = ["."];
123
+ }
124
+ try {
125
+ runGit("add", ...paths);
126
+ return true;
127
+ } catch (error) {
128
+ const err = error;
129
+ if (err.message.includes("ignored by one of your .gitignore") || err.message.includes("pathspec") && err.message.includes("did not match")) {
130
+ return false;
131
+ }
132
+ throw error;
133
+ }
134
+ }
135
+ function commit(message) {
136
+ runGit("commit", "-m", `"${message.replace(/"/g, '\\"')}"`);
137
+ return runGit("rev-parse", "HEAD");
138
+ }
139
+ function push() {
140
+ const branch = getCurrentBranch();
141
+ runGit("push", "origin", branch);
142
+ }
143
+ function getCurrentBranch() {
144
+ return runGit("rev-parse", "--abbrev-ref", "HEAD");
145
+ }
146
+ function getModifiedFiles() {
147
+ const files = /* @__PURE__ */ new Set();
148
+ const staged = runGitSafe("diff", "--cached", "--name-only");
149
+ if (staged) {
150
+ staged.split("\n").forEach((f) => files.add(f));
151
+ }
152
+ const modified = runGitSafe("diff", "--name-only");
153
+ if (modified) {
154
+ modified.split("\n").forEach((f) => files.add(f));
155
+ }
156
+ const untracked = runGitSafe("ls-files", "--others", "--exclude-standard");
157
+ if (untracked) {
158
+ untracked.split("\n").forEach((f) => files.add(f));
159
+ }
160
+ return Array.from(files).filter((f) => f);
161
+ }
162
+ function hasStagedChanges() {
163
+ const diff = runGitSafe("diff", "--cached", "--name-only");
164
+ return Boolean(diff.trim());
165
+ }
166
+ function getStagedFiles() {
167
+ const output = runGitSafe("diff", "--cached", "--name-only");
168
+ return output.split("\n").filter((f) => f);
169
+ }
170
+ function resetStaged() {
171
+ runGitSafe("reset", "HEAD");
172
+ }
173
+ function getLastCommitDiff() {
174
+ const diff = runGitSafe("diff", "HEAD~1", "HEAD");
175
+ const stats = runGitSafe("diff", "HEAD~1", "HEAD", "--stat");
176
+ const filesOutput = runGitSafe("diff", "HEAD~1", "HEAD", "--name-only");
177
+ const files = filesOutput.split("\n").filter((f) => f);
178
+ const statusOutput = runGitSafe("diff", "HEAD~1", "HEAD", "--name-status");
179
+ const filesAdded = [];
180
+ const filesDeleted = [];
181
+ const filesModified = [];
182
+ statusOutput.split("\n").filter((f) => f).forEach((line) => {
183
+ const [status, ...pathParts] = line.split(" ");
184
+ const filePath = pathParts.join(" ");
185
+ if (status.startsWith("A")) {
186
+ filesAdded.push(filePath);
187
+ } else if (status.startsWith("D")) {
188
+ filesDeleted.push(filePath);
189
+ } else if (status.startsWith("M") || status.startsWith("R")) {
190
+ filesModified.push(filePath);
191
+ }
192
+ });
193
+ return {
194
+ diff,
195
+ stats,
196
+ files,
197
+ filesAdded,
198
+ filesDeleted,
199
+ filesModified,
200
+ isEmpty: !diff.trim()
201
+ };
202
+ }
203
+ function commitAmend(message) {
204
+ runGit("commit", "--amend", "-m", `"${message.replace(/"/g, '\\"')}"`);
205
+ return runGit("rev-parse", "HEAD");
206
+ }
207
+
208
+ export {
209
+ shouldIgnoreFile,
210
+ filterDiffByPatterns,
211
+ GitError,
212
+ getStagedDiff,
213
+ getFileDiff,
214
+ addFiles,
215
+ commit,
216
+ push,
217
+ getCurrentBranch,
218
+ getModifiedFiles,
219
+ hasStagedChanges,
220
+ getStagedFiles,
221
+ resetStaged,
222
+ getLastCommitDiff,
223
+ commitAmend
224
+ };
225
+ //# sourceMappingURL=chunk-5MPJCPJ4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/git.ts"],"sourcesContent":["import { execSync } from \"node:child_process\";\nimport type { DiffResult } from \"./types.js\";\n\nfunction matchesPattern(filePath: string, pattern: string): boolean {\n // Convert glob pattern to regex\n // Support: *, **, ?\n let regexStr = pattern\n .replace(/\\./g, \"\\\\.\")\n .replace(/\\*\\*/g, \"<<GLOBSTAR>>\")\n .replace(/\\*/g, \"[^/]*\")\n .replace(/<<GLOBSTAR>>/g, \".*\")\n .replace(/\\?/g, \".\");\n\n // If pattern doesn't start with / or *, it can match anywhere in path\n if (!pattern.startsWith(\"/\") && !pattern.startsWith(\"*\")) {\n regexStr = `(^|/)${regexStr}`;\n }\n\n // Match end of string or after /\n regexStr = `${regexStr}($|/)`;\n\n try {\n const regex = new RegExp(regexStr);\n return regex.test(filePath);\n } catch {\n return false;\n }\n}\n\nexport function shouldIgnoreFile(filePath: string, patterns: string[]): boolean {\n return patterns.some((pattern) => matchesPattern(filePath, pattern));\n}\n\nexport function filterDiffByPatterns(diff: string, patterns: string[]): string {\n if (!patterns || patterns.length === 0) {\n return diff;\n }\n\n // Split diff into file sections\n const sections = diff.split(/(?=^diff --git)/m);\n const filtered = sections.filter((section) => {\n // Extract file path from diff header\n const match = section.match(/^diff --git a\\/(.+?) b\\//m);\n if (!match) {\n return true; // Keep sections without proper header\n }\n const filePath = match[1];\n return !shouldIgnoreFile(filePath, patterns);\n });\n\n return filtered.join(\"\");\n}\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 // Get file status (A=added, D=deleted, M=modified, R=renamed)\n const statusOutput = runGitSafe(\"diff\", \"--cached\", \"--name-status\");\n const filesAdded: string[] = [];\n const filesDeleted: string[] = [];\n const filesModified: string[] = [];\n\n statusOutput.split(\"\\n\").filter((f) => f).forEach((line) => {\n const [status, ...pathParts] = line.split(\"\\t\");\n const filePath = pathParts.join(\"\\t\"); // Handle filenames with tabs\n if (status.startsWith(\"A\")) {\n filesAdded.push(filePath);\n } else if (status.startsWith(\"D\")) {\n filesDeleted.push(filePath);\n } else if (status.startsWith(\"M\") || status.startsWith(\"R\")) {\n filesModified.push(filePath);\n }\n });\n\n return {\n diff,\n stats,\n files,\n filesAdded,\n filesDeleted,\n filesModified,\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 // Get file status for this specific file\n const statusOutput = runGitSafe(\"diff\", \"--cached\", \"--name-status\", \"--\", filePath);\n const filesAdded: string[] = [];\n const filesDeleted: string[] = [];\n const filesModified: string[] = [];\n\n if (statusOutput) {\n const [status] = statusOutput.split(\"\\t\");\n if (status.startsWith(\"A\")) {\n filesAdded.push(filePath);\n } else if (status.startsWith(\"D\")) {\n filesDeleted.push(filePath);\n } else if (status.startsWith(\"M\") || status.startsWith(\"R\")) {\n filesModified.push(filePath);\n }\n }\n\n return {\n diff,\n stats,\n files,\n filesAdded,\n filesDeleted,\n filesModified,\n isEmpty: !diff.trim(),\n };\n}\n\nexport function addFiles(...paths: string[]): boolean {\n if (paths.length === 0) {\n paths = [\".\"];\n }\n try {\n runGit(\"add\", ...paths);\n return true;\n } catch (error) {\n const err = error as GitError;\n // Ignore \"ignored file\" errors and \"pathspec did not match\" (deleted files)\n if (\n err.message.includes(\"ignored by one of your .gitignore\") ||\n err.message.includes(\"pathspec\") && err.message.includes(\"did not match\")\n ) {\n return false;\n }\n throw error;\n }\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\nexport function getStagedFiles(): string[] {\n const output = runGitSafe(\"diff\", \"--cached\", \"--name-only\");\n return output.split(\"\\n\").filter((f) => f);\n}\n\nexport function resetStaged(): void {\n runGitSafe(\"reset\", \"HEAD\");\n}\n\nexport function getLastCommitDiff(): DiffResult {\n const diff = runGitSafe(\"diff\", \"HEAD~1\", \"HEAD\");\n const stats = runGitSafe(\"diff\", \"HEAD~1\", \"HEAD\", \"--stat\");\n const filesOutput = runGitSafe(\"diff\", \"HEAD~1\", \"HEAD\", \"--name-only\");\n const files = filesOutput.split(\"\\n\").filter((f) => f);\n\n // Get file status for the last commit\n const statusOutput = runGitSafe(\"diff\", \"HEAD~1\", \"HEAD\", \"--name-status\");\n const filesAdded: string[] = [];\n const filesDeleted: string[] = [];\n const filesModified: string[] = [];\n\n statusOutput.split(\"\\n\").filter((f) => f).forEach((line) => {\n const [status, ...pathParts] = line.split(\"\\t\");\n const filePath = pathParts.join(\"\\t\");\n if (status.startsWith(\"A\")) {\n filesAdded.push(filePath);\n } else if (status.startsWith(\"D\")) {\n filesDeleted.push(filePath);\n } else if (status.startsWith(\"M\") || status.startsWith(\"R\")) {\n filesModified.push(filePath);\n }\n });\n\n return {\n diff,\n stats,\n files,\n filesAdded,\n filesDeleted,\n filesModified,\n isEmpty: !diff.trim(),\n };\n}\n\nexport function commitAmend(message: string): string {\n runGit(\"commit\", \"--amend\", \"-m\", `\"${message.replace(/\"/g, '\\\\\"')}\"`);\n return runGit(\"rev-parse\", \"HEAD\");\n}\n"],"mappings":"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","names":[]}