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