gims 0.4.4 β 0.5.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/.github/workflows/release.yml +2 -16
- package/README.md +100 -53
- package/bin/gims.js +428 -187
- package/package.json +1 -1
- package/CLAUDE.md +0 -43
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: release to npm
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
branches: [main]
|
|
6
6
|
|
|
7
7
|
jobs:
|
|
8
|
-
build
|
|
8
|
+
build:
|
|
9
9
|
runs-on: ubuntu-latest
|
|
10
10
|
|
|
11
11
|
steps:
|
|
@@ -17,23 +17,9 @@ jobs:
|
|
|
17
17
|
with:
|
|
18
18
|
node-version: 20
|
|
19
19
|
cache: 'npm'
|
|
20
|
-
registry-url: 'https://registry.npmjs.org/'
|
|
21
20
|
|
|
22
21
|
- name: Install dependencies
|
|
23
22
|
run: npm ci
|
|
24
23
|
|
|
25
24
|
- name: Run tests
|
|
26
25
|
run: npm test
|
|
27
|
-
|
|
28
|
-
- name: Set Git identity
|
|
29
|
-
run: |
|
|
30
|
-
git config user.name "github-actions[bot]"
|
|
31
|
-
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
32
|
-
|
|
33
|
-
- name: Bump version
|
|
34
|
-
run: npm version patch --no-git-tag-version
|
|
35
|
-
|
|
36
|
-
- name: Publish to npm
|
|
37
|
-
run: npm publish
|
|
38
|
-
env:
|
|
39
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://npmjs.org/package/gims)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://nodejs.org/)
|
|
8
|
-
[](https://github.com/
|
|
8
|
+
[](https://github.com/yourusername/gims)
|
|
9
9
|
|
|
10
10
|
**The AI-powered Git CLI that writes your commit messages for you**
|
|
11
11
|
|
|
@@ -34,28 +34,29 @@ g o # AI analyzes changes, commits with perfect message, and pushes!
|
|
|
34
34
|
## π Features
|
|
35
35
|
|
|
36
36
|
### π€ **AI-Powered Commit Messages**
|
|
37
|
-
-
|
|
38
|
-
- **Google Gemini 2.0 Flash** support for lightning-fast analysis
|
|
37
|
+
- OpenAI, Google Gemini, and Groq support with automatic provider selection
|
|
39
38
|
- Smart diff analysis that understands your code changes
|
|
40
|
-
- Handles large codebases with intelligent summarization and
|
|
39
|
+
- Handles large codebases with intelligent summarization and safe truncation
|
|
40
|
+
- Optional Conventional Commits formatting and optional commit body generation (`--conventional`, `--body`)
|
|
41
41
|
|
|
42
42
|
### β‘ **Lightning Fast Workflow**
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
43
|
+
- One command commits: `g o` - analyze, commit, and push
|
|
44
|
+
- Smart suggestions: `g s` - get AI-generated messages copied to clipboard
|
|
45
|
+
- Local commits: `g l` - commit locally with AI messages
|
|
46
|
+
- Staged-only by default for suggestions for precise control (use `--all` to stage everything)
|
|
47
47
|
|
|
48
48
|
### π§ **Intelligent Code Analysis**
|
|
49
49
|
- Analyzes actual code changes, not just file names
|
|
50
50
|
- Understands context from function changes, imports, and logic
|
|
51
|
-
-
|
|
52
|
-
- Graceful fallbacks for extremely large changesets
|
|
51
|
+
- Graceful fallbacks for extremely large changesets and offline use
|
|
53
52
|
|
|
54
53
|
### π οΈ **Developer-Friendly**
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
54
|
+
- Numbered commit history with `g ls` / `g ll` and index-aware commands
|
|
55
|
+
- Smart branching: `g b 5` creates branch from commit #5
|
|
56
|
+
- Safe operations with confirmations and dry-run support
|
|
57
|
+
- JSON output for editor integrations
|
|
58
|
+
- Quality-of-life: `--amend`, `undo` command, and automatic upstream setup on push
|
|
59
|
+
- Manual commit command for custom messages: `g m "your message"`
|
|
59
60
|
|
|
60
61
|
## π Quick Start
|
|
61
62
|
|
|
@@ -67,16 +68,25 @@ npm install -g gims
|
|
|
67
68
|
|
|
68
69
|
### Setup AI (Choose One)
|
|
69
70
|
|
|
70
|
-
**Option 1: OpenAI
|
|
71
|
+
**Option 1: OpenAI**
|
|
71
72
|
```bash
|
|
72
73
|
export OPENAI_API_KEY="your-api-key-here"
|
|
73
74
|
```
|
|
74
75
|
|
|
75
|
-
**Option 2: Google Gemini
|
|
76
|
+
**Option 2: Google Gemini**
|
|
76
77
|
```bash
|
|
77
78
|
export GEMINI_API_KEY="your-api-key-here"
|
|
78
79
|
```
|
|
79
80
|
|
|
81
|
+
**Option 3: Groq**
|
|
82
|
+
```bash
|
|
83
|
+
export GROQ_API_KEY="your-api-key-here"
|
|
84
|
+
# Optional, if self-hosting/proxying
|
|
85
|
+
export GROQ_BASE_URL="https://api.groq.com/openai/v1"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
GIMS auto-detects configured providers. If none are configured, it uses a local heuristic to generate sensible messages.
|
|
89
|
+
|
|
80
90
|
### Your First AI Commit
|
|
81
91
|
|
|
82
92
|
```bash
|
|
@@ -94,16 +104,33 @@ g o
|
|
|
94
104
|
|---------|-------|-------------|---------|
|
|
95
105
|
| `gims init` | `g i` | Initialize new Git repo | `g i` |
|
|
96
106
|
| `gims clone <repo>` | `g c` | Clone repository | `g c https://github.com/user/repo` |
|
|
97
|
-
| `gims suggest` | `g s` | Generate & copy commit message | `g s` |
|
|
98
|
-
| `gims commit` | `g cm` | Interactive commit message generation | `g cm` |
|
|
107
|
+
| `gims suggest` | `g s` | Generate & copy commit message from staged changes (use `--all` to stage) | `g s --all` |
|
|
99
108
|
| `gims local` | `g l` | AI commit locally | `g l` |
|
|
100
|
-
| `gims online` | `g o` | AI commit + push | `g o` |
|
|
109
|
+
| `gims online` | `g o` | AI commit + push (use `--set-upstream` on first push) | `g o --set-upstream` |
|
|
110
|
+
| `gims commit <message...>` | `g m` | Commit with a custom message (no AI) | `g m "fix: handle empty input"` |
|
|
101
111
|
| `gims pull` | `g p` | Pull latest changes | `g p` |
|
|
102
112
|
| `gims list` | `g ls` | Show numbered commit history | `g ls` |
|
|
103
113
|
| `gims largelist` | `g ll` | Detailed commit history | `g ll` |
|
|
104
114
|
| `gims branch <n>` | `g b` | Branch from commit #n | `g b 3 feature-x` |
|
|
105
|
-
| `gims reset <n>` | `g r` | Reset to commit #n | `g r 5 --hard` |
|
|
106
|
-
| `gims revert <n>` | `g rv` | Safely revert commit #n | `g rv 2` |
|
|
115
|
+
| `gims reset <n>` | `g r` | Reset to commit #n (`--hard` needs `--yes`) | `g r 5 --hard --yes` |
|
|
116
|
+
| `gims revert <n>` | `g rv` | Safely revert commit #n (requires `--yes`) | `g rv 2 --yes` |
|
|
117
|
+
| `gims undo` | `g u` | Undo last commit (soft reset by default) | `g u` or `g u --hard --yes` |
|
|
118
|
+
|
|
119
|
+
### Global Options
|
|
120
|
+
|
|
121
|
+
- `--provider <name>`: AI provider: `auto` | `openai` | `gemini` | `groq` | `none`
|
|
122
|
+
- `--model <name>`: Override model identifier for the chosen provider
|
|
123
|
+
- `--staged-only`: Use only staged changes (default behavior for `g s`)
|
|
124
|
+
- `--all`: Stage all changes before running
|
|
125
|
+
- `--no-clipboard`: Do not copy suggestion to clipboard (for `g s`)
|
|
126
|
+
- `--body`: Generate a commit body in addition to subject
|
|
127
|
+
- `--conventional`: Format subject using Conventional Commits
|
|
128
|
+
- `--dry-run`: Print what would happen without committing/pushing
|
|
129
|
+
- `--verbose`: Verbose logging
|
|
130
|
+
- `--json`: Machine-readable output for `g s`
|
|
131
|
+
- `--yes`: Confirm destructive actions without prompting (e.g., reset/revert/undo)
|
|
132
|
+
- `--amend`: Amend the last commit instead of creating a new one
|
|
133
|
+
- `--set-upstream`: On push, set upstream if the current branch has none
|
|
107
134
|
|
|
108
135
|
## π‘ Real-World Examples
|
|
109
136
|
|
|
@@ -137,51 +164,71 @@ g o
|
|
|
137
164
|
|
|
138
165
|
## π₯ Pro Tips
|
|
139
166
|
|
|
140
|
-
### π―
|
|
167
|
+
### π― Perfect Workflow
|
|
141
168
|
```bash
|
|
142
|
-
#
|
|
143
|
-
g p # Pull latest changes
|
|
169
|
+
g p # Pull latest changes
|
|
144
170
|
# ... code your features ...
|
|
145
|
-
g s
|
|
146
|
-
g
|
|
171
|
+
g s # Preview AI suggestion from staged changes
|
|
172
|
+
g s --all # Or stage everything and suggest
|
|
173
|
+
g l # Commit locally first
|
|
147
174
|
# ... test your changes ...
|
|
148
|
-
g o
|
|
175
|
+
g o --set-upstream # Push with automatic upstream setup on first push
|
|
149
176
|
```
|
|
150
177
|
|
|
151
|
-
### π§
|
|
178
|
+
### π§ Smart Branching
|
|
152
179
|
```bash
|
|
153
|
-
g ls
|
|
154
|
-
g b 5 hotfix
|
|
155
|
-
g l
|
|
180
|
+
g ls # See numbered history
|
|
181
|
+
g b 5 hotfix # Branch from commit #5
|
|
182
|
+
g l # Make changes and commit
|
|
156
183
|
g checkout main && g pull # Back to main
|
|
157
184
|
```
|
|
158
185
|
|
|
159
|
-
### π‘οΈ
|
|
186
|
+
### π‘οΈ Safe Experimentation
|
|
160
187
|
```bash
|
|
161
|
-
g l
|
|
188
|
+
g l # Commit your experiment
|
|
162
189
|
# ... code breaks something ...
|
|
163
|
-
g r 1 --soft # Soft reset to previous commit
|
|
164
|
-
# ...
|
|
190
|
+
g r 1 --soft --yes # Soft reset to previous commit (confirmed)
|
|
191
|
+
# ... or ...
|
|
192
|
+
g u --yes # Undo last commit (soft)
|
|
165
193
|
```
|
|
166
194
|
|
|
167
195
|
## βοΈ Configuration
|
|
168
196
|
|
|
169
197
|
### Environment Variables
|
|
170
198
|
|
|
171
|
-
| Variable | Purpose |
|
|
172
|
-
|
|
173
|
-
| `OPENAI_API_KEY` | OpenAI API access |
|
|
174
|
-
| `GEMINI_API_KEY` | Google Gemini API access |
|
|
199
|
+
| Variable | Purpose |
|
|
200
|
+
|----------|---------|
|
|
201
|
+
| `OPENAI_API_KEY` | OpenAI API access |
|
|
202
|
+
| `GEMINI_API_KEY` | Google Gemini API access |
|
|
203
|
+
| `GROQ_API_KEY` | Groq API access (OpenAI-compatible) |
|
|
204
|
+
| `GROQ_BASE_URL` | Groq API base URL (optional) |
|
|
205
|
+
| `GIMS_PROVIDER` | Default provider: `auto` | `openai` | `gemini` | `groq` | `none` |
|
|
206
|
+
| `GIMS_MODEL` | Default model identifier for provider |
|
|
207
|
+
| `GIMS_CONVENTIONAL` | `1` to enable Conventional Commits by default |
|
|
208
|
+
| `GIMS_COPY` | `0` to disable clipboard copying in `g s` by default |
|
|
209
|
+
|
|
210
|
+
### .gimsrc (optional)
|
|
211
|
+
|
|
212
|
+
Place a `.gimsrc` JSON file in your repo root or home directory to set defaults:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"provider": "auto",
|
|
217
|
+
"model": "gpt-4o-mini",
|
|
218
|
+
"conventional": true,
|
|
219
|
+
"copy": true
|
|
220
|
+
}
|
|
221
|
+
```
|
|
175
222
|
|
|
176
223
|
### Smart Fallbacks
|
|
177
224
|
|
|
178
225
|
GIMS handles edge cases gracefully:
|
|
179
226
|
|
|
180
|
-
-
|
|
181
|
-
-
|
|
182
|
-
-
|
|
183
|
-
-
|
|
184
|
-
-
|
|
227
|
+
- π Large diffs: Automatically switches to summary or status view
|
|
228
|
+
- βοΈ Massive text: Truncates safely with informative context
|
|
229
|
+
- π No API key: Uses a local heuristic that summarizes your changes
|
|
230
|
+
- β οΈ API failures: Clear errors and local fallback so you keep moving
|
|
231
|
+
- π Privacy-first: Only sends diffs when you explicitly run AI features
|
|
185
232
|
|
|
186
233
|
## π€ Contributing
|
|
187
234
|
|
|
@@ -196,8 +243,8 @@ We love contributions! Here's how to get involved:
|
|
|
196
243
|
|
|
197
244
|
### π Found a Bug?
|
|
198
245
|
|
|
199
|
-
1. Check [existing issues](https://github.com/
|
|
200
|
-
2. Create a [new issue](https://github.com/
|
|
246
|
+
1. Check [existing issues](https://github.com/yourusername/gims/issues)
|
|
247
|
+
2. Create a [new issue](https://github.com/yourusername/gims/issues/new) with:
|
|
201
248
|
- Clear description
|
|
202
249
|
- Steps to reproduce
|
|
203
250
|
- Expected vs actual behavior
|
|
@@ -227,10 +274,10 @@ mno7890 Fix memory leak in image processing pipeline
|
|
|
227
274
|
|
|
228
275
|
## π Stats
|
|
229
276
|
|
|
230
|
-
- β‘
|
|
231
|
-
- π―
|
|
232
|
-
- π
|
|
233
|
-
- π
|
|
277
|
+
- β‘ Faster commits than traditional Git workflow
|
|
278
|
+
- π― High accuracy in commit message relevance
|
|
279
|
+
- π Zero learning curve - if you know Git, you know GIMS
|
|
280
|
+
- π Works everywhere - Mac, Windows, Linux, WSL
|
|
234
281
|
|
|
235
282
|
## πΊοΈ Roadmap
|
|
236
283
|
|
|
@@ -242,7 +289,7 @@ mno7890 Fix memory leak in image processing pipeline
|
|
|
242
289
|
|
|
243
290
|
## π License
|
|
244
291
|
|
|
245
|
-
MIT Β© [
|
|
292
|
+
MIT Β© [Your Name](https://github.com/yourusername)
|
|
246
293
|
|
|
247
294
|
---
|
|
248
295
|
|
|
@@ -250,8 +297,8 @@ MIT Β© [GIMS](https://github.com/s41r4j/gims)
|
|
|
250
297
|
|
|
251
298
|
**β Star this repo if GIMS makes your Git workflow awesome!**
|
|
252
299
|
|
|
253
|
-
[Report Bug](https://github.com/
|
|
300
|
+
[Report Bug](https://github.com/yourusername/gims/issues) β’ [Request Feature](https://github.com/yourusername/gims/issues) β’ [Documentation](https://github.com/yourusername/gims/wiki)
|
|
254
301
|
|
|
255
302
|
*Made with β€οΈ by developers who hate writing commit messages*
|
|
256
303
|
|
|
257
|
-
</div>
|
|
304
|
+
</div>
|
package/bin/gims.js
CHANGED
|
@@ -5,13 +5,86 @@
|
|
|
5
5
|
*/
|
|
6
6
|
const { Command } = require('commander');
|
|
7
7
|
const simpleGit = require('simple-git');
|
|
8
|
+
const clipboard = require('clipboardy');
|
|
8
9
|
const process = require('process');
|
|
9
10
|
const { OpenAI } = require('openai');
|
|
10
11
|
const { GoogleGenAI } = require('@google/genai');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
11
14
|
|
|
12
15
|
const program = new Command();
|
|
13
16
|
const git = simpleGit();
|
|
14
17
|
|
|
18
|
+
// Utility: ANSI colors without extra deps
|
|
19
|
+
const color = {
|
|
20
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
21
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
22
|
+
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
23
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
24
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Load simple config from .gimsrc (JSON) in cwd or home and env vars
|
|
28
|
+
function loadConfig() {
|
|
29
|
+
const defaults = {
|
|
30
|
+
provider: process.env.GIMS_PROVIDER || 'auto', // auto | openai | gemini | groq | none
|
|
31
|
+
model: process.env.GIMS_MODEL || '',
|
|
32
|
+
conventional: !!(process.env.GIMS_CONVENTIONAL === '1'),
|
|
33
|
+
copy: process.env.GIMS_COPY !== '0',
|
|
34
|
+
};
|
|
35
|
+
const tryFiles = [
|
|
36
|
+
path.join(process.cwd(), '.gimsrc'),
|
|
37
|
+
path.join(process.env.HOME || process.cwd(), '.gimsrc'),
|
|
38
|
+
];
|
|
39
|
+
for (const fp of tryFiles) {
|
|
40
|
+
try {
|
|
41
|
+
if (fs.existsSync(fp)) {
|
|
42
|
+
const txt = fs.readFileSync(fp, 'utf8');
|
|
43
|
+
const json = JSON.parse(txt);
|
|
44
|
+
return { ...defaults, ...json };
|
|
45
|
+
}
|
|
46
|
+
} catch (_) {
|
|
47
|
+
// ignore malformed config
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return defaults;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getOpts() {
|
|
54
|
+
// Merge precedence: CLI > config > env handled in loadConfig
|
|
55
|
+
const cfg = loadConfig();
|
|
56
|
+
const cli = program.opts();
|
|
57
|
+
return {
|
|
58
|
+
provider: cli.provider || cfg.provider,
|
|
59
|
+
model: cli.model || cfg.model,
|
|
60
|
+
stagedOnly: !!cli.stagedOnly,
|
|
61
|
+
all: !!cli.all,
|
|
62
|
+
noClipboard: !!cli.noClipboard || cfg.copy === false,
|
|
63
|
+
body: !!cli.body,
|
|
64
|
+
conventional: !!cli.conventional || cfg.conventional,
|
|
65
|
+
dryRun: !!cli.dryRun,
|
|
66
|
+
verbose: !!cli.verbose,
|
|
67
|
+
json: !!cli.json,
|
|
68
|
+
yes: !!cli.yes,
|
|
69
|
+
amend: !!cli.amend,
|
|
70
|
+
setUpstream: !!cli.setUpstream,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function ensureRepo() {
|
|
75
|
+
const isRepo = await git.checkIsRepo();
|
|
76
|
+
if (!isRepo) {
|
|
77
|
+
console.error(color.red('Not a git repository (or any of the parent directories).'));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function handleError(prefix, err) {
|
|
83
|
+
const msg = err && err.message ? err.message : String(err);
|
|
84
|
+
console.error(color.red(`${prefix}: ${msg}`));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
15
88
|
// Safe log: returns { all: [] } on empty repo
|
|
16
89
|
async function safeLog() {
|
|
17
90
|
try {
|
|
@@ -23,7 +96,8 @@ async function safeLog() {
|
|
|
23
96
|
}
|
|
24
97
|
|
|
25
98
|
// Clean up AI-generated commit message
|
|
26
|
-
function cleanCommitMessage(message) {
|
|
99
|
+
function cleanCommitMessage(message, { body = false } = {}) {
|
|
100
|
+
if (!message) return 'Update project code';
|
|
27
101
|
// Remove markdown code blocks and formatting
|
|
28
102
|
let cleaned = message
|
|
29
103
|
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
@@ -33,28 +107,98 @@ function cleanCommitMessage(message) {
|
|
|
33
107
|
.replace(/^\s*#+\s*/gm, '') // Remove headers
|
|
34
108
|
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold formatting
|
|
35
109
|
.replace(/\*(.*?)\*/g, '$1') // Remove italic formatting
|
|
110
|
+
.replace(/[\u{1F300}-\u{1FAFF}]/gu, '') // strip most emojis
|
|
111
|
+
.replace(/[\t\r]+/g, ' ')
|
|
36
112
|
.trim();
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
113
|
+
|
|
114
|
+
// If a body is allowed, split subject/body, otherwise keep first line only
|
|
115
|
+
const lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
|
|
116
|
+
let subject = (lines[0] || '').replace(/\s{2,}/g, ' ').replace(/[\s:,.!;]+$/g, '').trim();
|
|
117
|
+
if (subject.length === 0) subject = 'Update project code';
|
|
118
|
+
// Enforce concise subject
|
|
119
|
+
if (subject.length > 72) subject = subject.substring(0, 69) + '...';
|
|
120
|
+
|
|
121
|
+
if (!body) return subject;
|
|
122
|
+
|
|
123
|
+
const bodyLines = lines.slice(1).filter(l => l.length > 0);
|
|
124
|
+
const bodyText = bodyLines.join('\n').trim();
|
|
125
|
+
return bodyText ? `${subject}\n\n${bodyText}` : subject;
|
|
43
126
|
}
|
|
44
127
|
|
|
45
128
|
// Estimate tokens (rough approximation: 1 token β 4 characters)
|
|
46
129
|
function estimateTokens(text) {
|
|
47
|
-
return Math.ceil(text.length / 4);
|
|
130
|
+
return Math.ceil((text || '').length / 4);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveProvider(pref) {
|
|
134
|
+
// pref: auto|openai|gemini|groq|none
|
|
135
|
+
if (pref === 'none') return 'none';
|
|
136
|
+
if (pref === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : 'none';
|
|
137
|
+
if (pref === 'gemini') return process.env.GEMINI_API_KEY ? 'gemini' : 'none';
|
|
138
|
+
if (pref === 'groq') return process.env.GROQ_API_KEY ? 'groq' : 'none';
|
|
139
|
+
// auto
|
|
140
|
+
if (process.env.GEMINI_API_KEY) return 'gemini';
|
|
141
|
+
if (process.env.OPENAI_API_KEY) return 'openai';
|
|
142
|
+
if (process.env.GROQ_API_KEY) return 'groq';
|
|
143
|
+
return 'none';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function getHumanReadableChanges(limitPerList = 10) {
|
|
147
|
+
try {
|
|
148
|
+
const status = await git.status();
|
|
149
|
+
const modified = status.modified.slice(0, limitPerList);
|
|
150
|
+
const created = status.created.slice(0, limitPerList);
|
|
151
|
+
const deleted = status.deleted.slice(0, limitPerList);
|
|
152
|
+
const renamed = status.renamed.map(r => `${r.from}β${r.to}`).slice(0, limitPerList);
|
|
153
|
+
const parts = [];
|
|
154
|
+
if (created.length) parts.push(`Added: ${created.join(', ')}`);
|
|
155
|
+
if (modified.length) parts.push(`Modified: ${modified.join(', ')}`);
|
|
156
|
+
if (deleted.length) parts.push(`Deleted: ${deleted.join(', ')}`);
|
|
157
|
+
if (renamed.length) parts.push(`Renamed: ${renamed.join(', ')}`);
|
|
158
|
+
return parts.join('\n');
|
|
159
|
+
} catch (_) {
|
|
160
|
+
return 'Multiple file changes.';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function localHeuristicMessage(status, { conventional = false } = {}) {
|
|
165
|
+
const created = status.created.length;
|
|
166
|
+
const modified = status.modified.length;
|
|
167
|
+
const deleted = status.deleted.length;
|
|
168
|
+
const total = created + modified + deleted + status.renamed.length;
|
|
169
|
+
|
|
170
|
+
const listFew = (arr) => arr.slice(0, 3).join(', ') + (arr.length > 3 ? ` and ${arr.length - 3} more` : '');
|
|
171
|
+
|
|
172
|
+
let type = 'chore';
|
|
173
|
+
let subject = 'update files';
|
|
174
|
+
if (created > 0 && modified === 0 && deleted === 0) {
|
|
175
|
+
type = 'feat';
|
|
176
|
+
subject = created <= 3 ? `add ${listFew(status.created)}` : `add ${created} files`;
|
|
177
|
+
} else if (deleted > 0 && created === 0 && modified === 0) {
|
|
178
|
+
type = 'chore';
|
|
179
|
+
subject = deleted <= 3 ? `remove ${listFew(status.deleted)}` : `remove ${deleted} files`;
|
|
180
|
+
} else if (modified > 0 && created === 0 && deleted === 0) {
|
|
181
|
+
type = 'chore';
|
|
182
|
+
subject = modified <= 3 ? `update ${listFew(status.modified)}` : `update ${modified} files`;
|
|
183
|
+
} else if (created > 0 || deleted > 0 || modified > 0) {
|
|
184
|
+
type = 'chore';
|
|
185
|
+
subject = `update ${total} files`;
|
|
186
|
+
}
|
|
187
|
+
const msg = conventional ? `${type}: ${subject}` : subject.charAt(0).toUpperCase() + subject.slice(1);
|
|
188
|
+
return msg;
|
|
48
189
|
}
|
|
49
190
|
|
|
50
191
|
// Generate commit message with multiple fallback strategies
|
|
51
|
-
async function generateCommitMessage(rawDiff) {
|
|
192
|
+
async function generateCommitMessage(rawDiff, options = {}) {
|
|
193
|
+
const { conventional = false, body = false, provider: prefProvider = 'auto', model = '', verbose = false } = options;
|
|
52
194
|
const MAX_TOKENS = 100000; // Conservative limit (well below 128k)
|
|
53
195
|
const MAX_CHARS = MAX_TOKENS * 4;
|
|
54
|
-
|
|
196
|
+
|
|
55
197
|
let content = rawDiff;
|
|
56
198
|
let strategy = 'full';
|
|
57
199
|
|
|
200
|
+
const logv = (m) => { if (verbose) console.log(color.cyan(`[gims] ${m}`)); };
|
|
201
|
+
|
|
58
202
|
// Strategy 1: Check if full diff is too large
|
|
59
203
|
if (estimateTokens(rawDiff) > MAX_TOKENS) {
|
|
60
204
|
strategy = 'summary';
|
|
@@ -77,13 +221,15 @@ async function generateCommitMessage(rawDiff) {
|
|
|
77
221
|
const modified = status.modified.slice(0, 10);
|
|
78
222
|
const created = status.created.slice(0, 10);
|
|
79
223
|
const deleted = status.deleted.slice(0, 10);
|
|
80
|
-
|
|
224
|
+
const renamed = status.renamed.map(r => `${r.from}β${r.to}`).slice(0, 10);
|
|
225
|
+
|
|
81
226
|
content = [
|
|
82
227
|
modified.length > 0 ? `Modified: ${modified.join(', ')}` : '',
|
|
83
228
|
created.length > 0 ? `Added: ${created.join(', ')}` : '',
|
|
84
|
-
deleted.length > 0 ? `Deleted: ${deleted.join(', ')}` : ''
|
|
229
|
+
deleted.length > 0 ? `Deleted: ${deleted.join(', ')}` : '',
|
|
230
|
+
renamed.length > 0 ? `Renamed: ${renamed.join(', ')}` : '',
|
|
85
231
|
].filter(Boolean).join('\n');
|
|
86
|
-
|
|
232
|
+
|
|
87
233
|
if (status.files.length > 30) {
|
|
88
234
|
content += `\n... and ${status.files.length - 30} more files`;
|
|
89
235
|
}
|
|
@@ -104,59 +250,81 @@ async function generateCommitMessage(rawDiff) {
|
|
|
104
250
|
summary: 'Changes are large; using summary. Write a concise git commit message for these changes:',
|
|
105
251
|
status: 'Many files changed. Write a concise git commit message based on these file changes:',
|
|
106
252
|
truncated: 'Large diff truncated. Write a concise git commit message for these changes:',
|
|
107
|
-
fallback: 'Write a concise git commit message for:'
|
|
253
|
+
fallback: 'Write a concise git commit message for:',
|
|
108
254
|
};
|
|
109
255
|
|
|
110
|
-
const
|
|
256
|
+
const style = conventional ? 'Use Conventional Commits (e.g., feat:, fix:, chore:) for the subject.' : 'Subject must be a single short line.';
|
|
257
|
+
const bodyInstr = body ? 'Provide a short subject line followed by an optional body separated by a blank line.' : 'Return only a short subject line without extra quotes.';
|
|
258
|
+
const prompt = `${prompts[strategy]}\n${content}\n\n${style} ${bodyInstr}`;
|
|
111
259
|
|
|
112
260
|
// Final safety check
|
|
113
261
|
if (estimateTokens(prompt) > MAX_TOKENS) {
|
|
114
|
-
console.warn('Changes too large for AI analysis, using default message');
|
|
115
|
-
return 'Update multiple files';
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Check if API key is available
|
|
119
|
-
if (!process.env.GEMINI_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
120
|
-
return null; // Signal that no API key is available
|
|
262
|
+
console.warn(color.yellow('Changes too large for AI analysis, using default message'));
|
|
263
|
+
return cleanCommitMessage('Update multiple files', { body });
|
|
121
264
|
}
|
|
122
265
|
|
|
123
266
|
let message = 'Update project code'; // Default fallback
|
|
267
|
+
const provider = resolveProvider(prefProvider);
|
|
268
|
+
logv(`strategy=${strategy}, provider=${provider}${model ? `, model=${model}` : ''}`);
|
|
124
269
|
|
|
125
270
|
try {
|
|
126
|
-
if (
|
|
271
|
+
if (provider === 'gemini') {
|
|
127
272
|
const genai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
128
|
-
const res = await genai.models.generateContent({
|
|
129
|
-
model: 'gemini-2.0-flash',
|
|
130
|
-
contents: prompt
|
|
273
|
+
const res = await genai.models.generateContent({
|
|
274
|
+
model: model || 'gemini-2.0-flash',
|
|
275
|
+
contents: prompt,
|
|
131
276
|
});
|
|
132
|
-
message = res.text.trim();
|
|
133
|
-
} else if (
|
|
277
|
+
message = (await res.response.text()).trim();
|
|
278
|
+
} else if (provider === 'openai') {
|
|
134
279
|
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
135
280
|
const res = await openai.chat.completions.create({
|
|
136
|
-
model: 'gpt-4o-mini',
|
|
281
|
+
model: model || 'gpt-4o-mini',
|
|
282
|
+
messages: [{ role: 'user', content: prompt }],
|
|
283
|
+
temperature: 0.3,
|
|
284
|
+
max_tokens: body ? 200 : 80,
|
|
285
|
+
});
|
|
286
|
+
message = (res.choices[0] && res.choices[0].message && res.choices[0].message.content || '').trim();
|
|
287
|
+
} else if (provider === 'groq') {
|
|
288
|
+
// Use OpenAI-compatible API via baseURL
|
|
289
|
+
const groq = new OpenAI({ apiKey: process.env.GROQ_API_KEY, baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1' });
|
|
290
|
+
const res = await groq.chat.completions.create({
|
|
291
|
+
model: model || 'llama-3.1-8b-instant',
|
|
137
292
|
messages: [{ role: 'user', content: prompt }],
|
|
138
|
-
temperature: 0.
|
|
139
|
-
max_tokens:
|
|
293
|
+
temperature: 0.3,
|
|
294
|
+
max_tokens: body ? 200 : 80,
|
|
140
295
|
});
|
|
141
|
-
message = res.choices[0].message.content.trim();
|
|
296
|
+
message = (res.choices[0] && res.choices[0].message && res.choices[0].message.content || '').trim();
|
|
297
|
+
} else {
|
|
298
|
+
// Local heuristic fallback
|
|
299
|
+
const status = await git.status();
|
|
300
|
+
message = localHeuristicMessage(status, { conventional });
|
|
301
|
+
const human = await getHumanReadableChanges();
|
|
302
|
+
if (body) message = `${message}\n\n${human}`;
|
|
142
303
|
}
|
|
143
304
|
} catch (error) {
|
|
144
|
-
if (error.code === 'context_length_exceeded') {
|
|
145
|
-
console.warn('Content still too large for AI, using default message');
|
|
146
|
-
return 'Update multiple files';
|
|
305
|
+
if (error && error.code === 'context_length_exceeded') {
|
|
306
|
+
console.warn(color.yellow('Content still too large for AI, using default message'));
|
|
307
|
+
return cleanCommitMessage('Update multiple files', { body });
|
|
147
308
|
}
|
|
148
|
-
console.warn(
|
|
309
|
+
console.warn(color.yellow(`AI generation failed: ${error && error.message ? error.message : error}`));
|
|
310
|
+
// fallback to local heuristic
|
|
311
|
+
const status = await git.status();
|
|
312
|
+
message = localHeuristicMessage(status, { conventional });
|
|
313
|
+
const human = await getHumanReadableChanges();
|
|
314
|
+
if (body) message = `${message}\n\n${human}`;
|
|
149
315
|
}
|
|
150
316
|
|
|
151
|
-
return cleanCommitMessage(message);
|
|
317
|
+
return cleanCommitMessage(message, { body });
|
|
152
318
|
}
|
|
153
319
|
|
|
154
320
|
async function resolveCommit(input) {
|
|
155
321
|
if (/^\d+$/.test(input)) {
|
|
156
322
|
const { all } = await safeLog();
|
|
323
|
+
// Align with list/largelist which show oldest -> newest
|
|
324
|
+
const ordered = [...all].reverse();
|
|
157
325
|
const idx = Number(input) - 1;
|
|
158
|
-
if (idx < 0 || idx >=
|
|
159
|
-
return
|
|
326
|
+
if (idx < 0 || idx >= ordered.length) throw new Error('Index out of range');
|
|
327
|
+
return ordered[idx].hash;
|
|
160
328
|
}
|
|
161
329
|
return input;
|
|
162
330
|
}
|
|
@@ -166,223 +334,296 @@ async function hasChanges() {
|
|
|
166
334
|
return status.files.length > 0;
|
|
167
335
|
}
|
|
168
336
|
|
|
169
|
-
program
|
|
337
|
+
program
|
|
338
|
+
.name('gims')
|
|
339
|
+
.alias('g')
|
|
340
|
+
.version(require('../package.json').version)
|
|
341
|
+
.option('--provider <name>', 'AI provider: auto|openai|gemini|groq|none')
|
|
342
|
+
.option('--model <name>', 'Model identifier for provider')
|
|
343
|
+
.option('--staged-only', 'Use only staged changes (default for suggest)')
|
|
344
|
+
.option('--all', 'Stage all changes before running')
|
|
345
|
+
.option('--no-clipboard', 'Do not copy suggestions to clipboard')
|
|
346
|
+
.option('--body', 'Generate a commit body in addition to subject')
|
|
347
|
+
.option('--conventional', 'Format messages using Conventional Commits')
|
|
348
|
+
.option('--dry-run', 'Do not perform writes (no commit or push)')
|
|
349
|
+
.option('--verbose', 'Verbose logging')
|
|
350
|
+
.option('--json', 'JSON output for suggest')
|
|
351
|
+
.option('--yes', 'Assume yes for confirmations')
|
|
352
|
+
.option('--amend', 'Amend the last commit instead of creating a new one')
|
|
353
|
+
.option('--set-upstream', 'Set upstream on push if missing');
|
|
170
354
|
|
|
171
355
|
program.command('init').alias('i')
|
|
172
356
|
.description('Initialize a new Git repository')
|
|
173
|
-
.action(async () => {
|
|
357
|
+
.action(async () => {
|
|
358
|
+
try { await git.init(); console.log('Initialized repo.'); }
|
|
359
|
+
catch (e) { handleError('Init error', e); }
|
|
360
|
+
});
|
|
174
361
|
|
|
175
362
|
program.command('clone <repo>').alias('c')
|
|
176
363
|
.description('Clone a Git repository')
|
|
177
364
|
.action(async (repo) => {
|
|
178
365
|
try { await git.clone(repo); console.log(`Cloned ${repo}`); }
|
|
179
|
-
catch (e) {
|
|
366
|
+
catch (e) { handleError('Clone error', e); }
|
|
180
367
|
});
|
|
181
368
|
|
|
182
369
|
program.command('suggest').alias('s')
|
|
183
|
-
.description('Suggest commit message')
|
|
370
|
+
.description('Suggest commit message and copy to clipboard')
|
|
184
371
|
.action(async () => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
372
|
+
await ensureRepo();
|
|
373
|
+
const opts = getOpts();
|
|
188
374
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
// Get diff of unstaged changes
|
|
193
|
-
let rawDiff = await git.diff();
|
|
194
|
-
|
|
195
|
-
// If no diff from tracked files, check for untracked files
|
|
196
|
-
if (!rawDiff.trim()) {
|
|
197
|
-
const status = await git.status();
|
|
198
|
-
if (status.not_added.length > 0) {
|
|
199
|
-
// For untracked files, show file list since we can't diff them
|
|
200
|
-
rawDiff = `New files:\n${status.not_added.join('\n')}`;
|
|
201
|
-
} else {
|
|
202
|
-
return console.log('No changes to suggest.');
|
|
375
|
+
try {
|
|
376
|
+
if (opts.all) {
|
|
377
|
+
await git.add('.');
|
|
203
378
|
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const msg = await generateCommitMessage(rawDiff);
|
|
207
|
-
|
|
208
|
-
if (msg === null) {
|
|
209
|
-
return console.log('Please set GEMINI_API_KEY or OPENAI_API_KEY environment variable');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
console.log(`git add . && git commit -m "${msg}"`);
|
|
213
|
-
});
|
|
214
379
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// If no diff from tracked files, check for untracked files
|
|
226
|
-
if (!rawDiff.trim()) {
|
|
227
|
-
const status = await git.status();
|
|
228
|
-
if (status.not_added.length > 0) {
|
|
229
|
-
rawDiff = `New files:\n${status.not_added.join('\n')}`;
|
|
230
|
-
} else {
|
|
231
|
-
return console.log('No changes to commit.');
|
|
380
|
+
// Use staged changes only; do not auto-stage unless --all
|
|
381
|
+
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
382
|
+
if (!rawDiff.trim()) {
|
|
383
|
+
if (opts.all) {
|
|
384
|
+
console.log('No changes to suggest.');
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
console.log('No staged changes. Use --all to stage everything or stage files manually.');
|
|
388
|
+
return;
|
|
232
389
|
}
|
|
233
|
-
}
|
|
234
390
|
|
|
235
|
-
|
|
236
|
-
return console.log('Please set GEMINI_API_KEY or OPENAI_API_KEY environment variable');
|
|
237
|
-
}
|
|
391
|
+
const msg = await generateCommitMessage(rawDiff, opts);
|
|
238
392
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
output: process.stdout
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
const askForInput = () => {
|
|
246
|
-
return new Promise((resolve) => {
|
|
247
|
-
rl.question('> ', (answer) => {
|
|
248
|
-
resolve(answer.toLowerCase().trim());
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
let currentMessage = await generateCommitMessage(rawDiff);
|
|
254
|
-
console.log(`\n=== Interactive Commit ===`);
|
|
255
|
-
console.log(`ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ`);
|
|
256
|
-
console.log(`β Press "Enter" to generate new message, "c" to commit, or "q" to quit β`);
|
|
257
|
-
console.log(`ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n`);
|
|
258
|
-
console.log(`Suggested: "${currentMessage}"`);
|
|
259
|
-
|
|
260
|
-
while (true) {
|
|
261
|
-
const input = await askForInput();
|
|
262
|
-
|
|
263
|
-
if (input === 'q' || input === 'quit') {
|
|
264
|
-
console.log('Cancelled.');
|
|
265
|
-
rl.close();
|
|
266
|
-
return;
|
|
267
|
-
} else if (input === 'c' || input === 'commit') {
|
|
268
|
-
await git.add('.');
|
|
269
|
-
await git.commit(currentMessage);
|
|
270
|
-
console.log(`Committed: "${currentMessage}"`);
|
|
271
|
-
rl.close();
|
|
393
|
+
if (opts.json) {
|
|
394
|
+
const out = { message: msg };
|
|
395
|
+
console.log(JSON.stringify(out));
|
|
272
396
|
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (!opts.noClipboard) {
|
|
400
|
+
try { clipboard.writeSync(msg); console.log(`Suggested: "${msg}" ${color.green('(copied to clipboard)')}`); }
|
|
401
|
+
catch (_) { console.log(`Suggested: "${msg}" ${color.yellow('(clipboard copy failed)')}`); }
|
|
273
402
|
} else {
|
|
274
|
-
|
|
275
|
-
currentMessage = await generateCommitMessage(rawDiff);
|
|
276
|
-
console.log(`Suggested: "${currentMessage}"`);
|
|
403
|
+
console.log(`Suggested: "${msg}"`);
|
|
277
404
|
}
|
|
405
|
+
} catch (e) {
|
|
406
|
+
handleError('Suggest error', e);
|
|
278
407
|
}
|
|
279
408
|
});
|
|
280
409
|
|
|
281
410
|
program.command('local').alias('l')
|
|
282
411
|
.description('AI-powered local commit')
|
|
283
412
|
.action(async () => {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
413
|
+
await ensureRepo();
|
|
414
|
+
const opts = getOpts();
|
|
287
415
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
416
|
+
try {
|
|
417
|
+
if (!(await hasChanges()) && !opts.all) {
|
|
418
|
+
console.log('No changes to commit.');
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (opts.all) await git.add('.');
|
|
423
|
+
|
|
424
|
+
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
425
|
+
if (!rawDiff.trim()) { console.log('No staged changes to commit.'); return; }
|
|
426
|
+
|
|
427
|
+
const msg = await generateCommitMessage(rawDiff, opts);
|
|
428
|
+
|
|
429
|
+
if (opts.dryRun) {
|
|
430
|
+
console.log(color.yellow('[dry-run] Would commit with message:'));
|
|
431
|
+
console.log(msg);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (opts.amend) {
|
|
436
|
+
await git.raw(['commit', '--amend', '-m', msg]);
|
|
437
|
+
} else {
|
|
438
|
+
await git.commit(msg);
|
|
439
|
+
}
|
|
440
|
+
console.log(`Committed locally: "${msg}"`);
|
|
441
|
+
} catch (e) {
|
|
442
|
+
handleError('Local commit error', e);
|
|
305
443
|
}
|
|
306
|
-
|
|
307
|
-
await git.commit(msg);
|
|
308
|
-
console.log(`Committed locally: "${msg}"`);
|
|
309
444
|
});
|
|
310
445
|
|
|
311
446
|
program.command('online').alias('o')
|
|
312
447
|
.description('AI commit + push')
|
|
313
448
|
.action(async () => {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
449
|
+
await ensureRepo();
|
|
450
|
+
const opts = getOpts();
|
|
317
451
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
452
|
+
try {
|
|
453
|
+
if (!(await hasChanges()) && !opts.all) {
|
|
454
|
+
console.log('No changes to commit.');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (opts.all) await git.add('.');
|
|
459
|
+
|
|
460
|
+
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
461
|
+
if (!rawDiff.trim()) { console.log('No staged changes to commit.'); return; }
|
|
462
|
+
|
|
463
|
+
const msg = await generateCommitMessage(rawDiff, opts);
|
|
464
|
+
|
|
465
|
+
if (opts.dryRun) {
|
|
466
|
+
console.log(color.yellow('[dry-run] Would commit & push with message:'));
|
|
467
|
+
console.log(msg);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (opts.amend) {
|
|
472
|
+
await git.raw(['commit', '--amend', '-m', msg]);
|
|
473
|
+
} else {
|
|
474
|
+
await git.commit(msg);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
await git.push();
|
|
479
|
+
console.log(`Committed & pushed: "${msg}"`);
|
|
480
|
+
} catch (pushErr) {
|
|
481
|
+
const msgErr = pushErr && pushErr.message ? pushErr.message : String(pushErr);
|
|
482
|
+
if (/no upstream|set the remote as upstream|have no upstream/.test(msgErr)) {
|
|
483
|
+
// Try to set upstream if requested
|
|
484
|
+
if (opts.setUpstream) {
|
|
485
|
+
const branch = (await git.raw(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
486
|
+
await git.push(['--set-upstream', 'origin', branch]);
|
|
487
|
+
console.log(`Committed & pushed (upstream set to origin/${branch}): "${msg}"`);
|
|
488
|
+
} else {
|
|
489
|
+
console.log(color.yellow('Current branch has no upstream. Use --set-upstream to set origin/<branch> automatically.'));
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
throw pushErr;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
} catch (e) {
|
|
496
|
+
handleError('Online commit error', e);
|
|
329
497
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
program.command('commit <message...>').alias('m')
|
|
501
|
+
.description('Commit with a custom message (no AI)')
|
|
502
|
+
.action(async (messageParts) => {
|
|
503
|
+
await ensureRepo();
|
|
504
|
+
const opts = getOpts();
|
|
505
|
+
|
|
506
|
+
try {
|
|
507
|
+
const msg = (messageParts || []).join(' ').trim();
|
|
508
|
+
if (!msg) { console.log('Provide a commit message.'); return; }
|
|
509
|
+
|
|
510
|
+
if (!(await hasChanges()) && !opts.all) {
|
|
511
|
+
console.log('No changes to commit.');
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (opts.all) await git.add('.');
|
|
516
|
+
|
|
517
|
+
const rawDiff = await git.diff(['--cached', '--no-ext-diff']);
|
|
518
|
+
if (!rawDiff.trim()) { console.log('No staged changes to commit.'); return; }
|
|
519
|
+
|
|
520
|
+
if (opts.dryRun) {
|
|
521
|
+
console.log(color.yellow('[dry-run] Would commit with custom message:'));
|
|
522
|
+
console.log(msg);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (opts.amend) {
|
|
527
|
+
await git.raw(['commit', '--amend', '-m', msg]);
|
|
528
|
+
} else {
|
|
529
|
+
await git.commit(msg);
|
|
530
|
+
}
|
|
531
|
+
console.log(`Committed locally: "${msg}"`);
|
|
532
|
+
} catch (e) {
|
|
533
|
+
handleError('Commit error', e);
|
|
335
534
|
}
|
|
336
|
-
|
|
337
|
-
await git.commit(msg);
|
|
338
|
-
await git.push();
|
|
339
|
-
console.log(`Committed & pushed: "${msg}"`);
|
|
340
535
|
});
|
|
341
536
|
|
|
342
537
|
program.command('pull').alias('p')
|
|
343
538
|
.description('Pull latest changes')
|
|
344
539
|
.action(async () => {
|
|
540
|
+
await ensureRepo();
|
|
345
541
|
try { await git.pull(); console.log('Pulled latest.'); }
|
|
346
|
-
catch (e) {
|
|
542
|
+
catch (e) { handleError('Pull error', e); }
|
|
347
543
|
});
|
|
348
544
|
|
|
349
545
|
program.command('list').alias('ls')
|
|
350
546
|
.description('Short numbered git log (oldest β newest)')
|
|
351
547
|
.action(async () => {
|
|
352
|
-
|
|
353
|
-
|
|
548
|
+
await ensureRepo();
|
|
549
|
+
try {
|
|
550
|
+
const { all } = await safeLog();
|
|
551
|
+
[...all].reverse().forEach((c, i) => console.log(`${i+1}. ${c.hash.slice(0,7)} ${c.message}`));
|
|
552
|
+
} catch (e) { handleError('List error', e); }
|
|
354
553
|
});
|
|
355
554
|
|
|
356
555
|
program.command('largelist').alias('ll')
|
|
357
556
|
.description('Full numbered git log (oldest β newest)')
|
|
358
557
|
.action(async () => {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
558
|
+
await ensureRepo();
|
|
559
|
+
try {
|
|
560
|
+
const { all } = await safeLog();
|
|
561
|
+
[...all].reverse().forEach((c, i) => {
|
|
562
|
+
const date = new Date(c.date).toLocaleString();
|
|
563
|
+
console.log(`${i+1}. ${c.hash.slice(0,7)} | ${date} | ${c.author_name} β ${c.message}`);
|
|
564
|
+
});
|
|
565
|
+
} catch (e) { handleError('Largelist error', e); }
|
|
364
566
|
});
|
|
365
567
|
|
|
366
568
|
program.command('branch <c> [name]').alias('b')
|
|
367
569
|
.description('Branch from commit/index')
|
|
368
570
|
.action(async (c, name) => {
|
|
571
|
+
await ensureRepo();
|
|
369
572
|
try { const sha = await resolveCommit(c); const br = name || `branch-${sha.slice(0,7)}`; await git.checkout(['-b', br, sha]); console.log(`Switched to branch ${br} at ${sha}`); }
|
|
370
|
-
catch (e) {
|
|
573
|
+
catch (e) { handleError('Branch error', e); }
|
|
371
574
|
});
|
|
372
575
|
|
|
373
576
|
program.command('reset <c>').alias('r')
|
|
374
577
|
.description('Reset branch to commit/index')
|
|
375
578
|
.option('--hard','hard reset')
|
|
376
|
-
.action(async (c,
|
|
377
|
-
|
|
378
|
-
|
|
579
|
+
.action(async (c, optsCmd) => {
|
|
580
|
+
await ensureRepo();
|
|
581
|
+
try {
|
|
582
|
+
const sha = await resolveCommit(c);
|
|
583
|
+
const mode = optsCmd.hard? '--hard':'--soft';
|
|
584
|
+
const opts = getOpts();
|
|
585
|
+
if (!opts.yes) {
|
|
586
|
+
console.log(color.yellow(`About to run: git reset ${mode} ${sha}. Use --yes to confirm.`));
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
await git.raw(['reset', mode, sha]);
|
|
590
|
+
console.log(`Reset (${mode}) to ${sha}`);
|
|
591
|
+
}
|
|
592
|
+
catch (e) { handleError('Reset error', e); }
|
|
379
593
|
});
|
|
380
594
|
|
|
381
595
|
program.command('revert <c>').alias('rv')
|
|
382
596
|
.description('Revert commit/index safely')
|
|
383
597
|
.action(async (c) => {
|
|
384
|
-
|
|
385
|
-
|
|
598
|
+
await ensureRepo();
|
|
599
|
+
try {
|
|
600
|
+
const sha = await resolveCommit(c);
|
|
601
|
+
const opts = getOpts();
|
|
602
|
+
if (!opts.yes) {
|
|
603
|
+
console.log(color.yellow(`About to run: git revert ${sha}. Use --yes to confirm.`));
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
await git.revert(sha);
|
|
607
|
+
console.log(`Reverted ${sha}`);
|
|
608
|
+
}
|
|
609
|
+
catch (e) { handleError('Revert error', e); }
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
program.command('undo').alias('u')
|
|
613
|
+
.description('Undo last commit (soft reset to HEAD~1)')
|
|
614
|
+
.option('--hard', 'Hard reset instead (destructive)')
|
|
615
|
+
.action(async (cmd) => {
|
|
616
|
+
await ensureRepo();
|
|
617
|
+
try {
|
|
618
|
+
const mode = cmd.hard ? '--hard' : '--soft';
|
|
619
|
+
const opts = getOpts();
|
|
620
|
+
if (!opts.yes) {
|
|
621
|
+
console.log(color.yellow(`About to run: git reset ${mode} HEAD~1. Use --yes to confirm.`));
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
await git.raw(['reset', mode, 'HEAD~1']);
|
|
625
|
+
console.log(`Reset (${mode}) to HEAD~1`);
|
|
626
|
+
} catch (e) { handleError('Undo error', e); }
|
|
386
627
|
});
|
|
387
628
|
|
|
388
629
|
program.parse(process.argv);
|
package/package.json
CHANGED
package/CLAUDE.md
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Project Overview
|
|
6
|
-
|
|
7
|
-
GIMS (Git Made Simple) is an AI-powered Git CLI tool that automatically generates meaningful commit messages from code changes. It's a Node.js package published to npm that integrates with OpenAI GPT-4 and Google Gemini APIs.
|
|
8
|
-
|
|
9
|
-
## Architecture
|
|
10
|
-
|
|
11
|
-
- **Single-file CLI**: All functionality is contained in `bin/gims.js`
|
|
12
|
-
- **AI Integration**: Supports both OpenAI and Google Gemini APIs with intelligent fallback strategies
|
|
13
|
-
- **Git Wrapper**: Built on top of `simple-git` library for Git operations
|
|
14
|
-
- **Token Management**: Implements sophisticated content chunking to handle large diffs within AI token limits
|
|
15
|
-
|
|
16
|
-
## Key Components
|
|
17
|
-
|
|
18
|
-
- **Command System**: Uses `commander.js` for CLI argument parsing with aliases (e.g., `g o` for `gims online`)
|
|
19
|
-
- **AI Message Generation**: Multi-strategy approach that falls back from full diff β summary β status β truncated content
|
|
20
|
-
- **Commit Resolution**: Supports both commit hashes and numbered indices for referencing commits
|
|
21
|
-
- **Safe Operations**: Includes error handling for empty repositories and edge cases
|
|
22
|
-
|
|
23
|
-
## Environment Setup
|
|
24
|
-
|
|
25
|
-
Required environment variables (at least one):
|
|
26
|
-
- `OPENAI_API_KEY` - For OpenAI GPT-4o-mini integration
|
|
27
|
-
- `GEMINI_API_KEY` - For Google Gemini 2.0 Flash integration
|
|
28
|
-
|
|
29
|
-
## Common Commands
|
|
30
|
-
|
|
31
|
-
- **Install globally**: `npm install -g .`
|
|
32
|
-
- **Test locally**: `node bin/gims.js --help`
|
|
33
|
-
- **Test specific command**: `node bin/gims.js suggest`
|
|
34
|
-
- **Run with alias**: `g o` (after global install)
|
|
35
|
-
|
|
36
|
-
## Development Notes
|
|
37
|
-
|
|
38
|
-
- No test framework is currently configured (package.json shows placeholder test script)
|
|
39
|
-
- Node.js version requirement: >=20.0.0
|
|
40
|
-
- Uses CommonJS modules (`require`/`module.exports`)
|
|
41
|
-
- Dependencies are minimal and focused on core functionality
|
|
42
|
-
- Token estimation uses 4 characters per token approximation
|
|
43
|
-
- Maximum context limit set conservatively at 100,000 tokens
|