gims 0.4.3 β 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 +25 -0
- package/LICENSE +21 -0
- package/README.md +236 -72
- package/bin/gims.js +433 -109
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: release to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout repository
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Set up Node.js
|
|
16
|
+
uses: actions/setup-node@v4
|
|
17
|
+
with:
|
|
18
|
+
node-version: 20
|
|
19
|
+
cache: 'npm'
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: npm ci
|
|
23
|
+
|
|
24
|
+
- name: Run tests
|
|
25
|
+
run: npm test
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Sairaj Jawalikar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,140 +1,304 @@
|
|
|
1
|
+
# π GIMS - Git Made Simple
|
|
1
2
|
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://npmjs.org/package/gims)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://nodejs.org/)
|
|
8
|
+
[](https://github.com/yourusername/gims)
|
|
2
9
|
|
|
3
|
-
|
|
10
|
+
**The AI-powered Git CLI that writes your commit messages for you**
|
|
11
|
+
|
|
12
|
+
*Because life's too short for "fix stuff" commits* π―
|
|
4
13
|
|
|
5
|
-
|
|
6
|
-
npm install -g gims
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
This installs the `gims` command and a shortcut alias:
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
g # shortcut for gims
|
|
13
|
-
```
|
|
14
|
+
</div>
|
|
14
15
|
|
|
15
16
|
---
|
|
16
17
|
|
|
17
|
-
##
|
|
18
|
+
## β¨ What is GIMS?
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
GIMS is a revolutionary Git CLI tool that uses AI to automatically generate meaningful commit messages from your code changes. Say goodbye to generic "update code" commits and hello to descriptive, professional commit messages that actually tell a story.
|
|
20
21
|
|
|
21
|
-
###
|
|
22
|
+
### π¬ See It In Action
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
|
-
|
|
25
|
+
# Traditional Git workflow π΄
|
|
26
|
+
git add .
|
|
27
|
+
git commit -m "update stuff" # π€¦ββοΈ
|
|
28
|
+
git push
|
|
29
|
+
|
|
30
|
+
# GIMS workflow β‘
|
|
31
|
+
g o # AI analyzes changes, commits with perfect message, and pushes!
|
|
25
32
|
```
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
## π Features
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
### π€ **AI-Powered Commit Messages**
|
|
37
|
+
- OpenAI, Google Gemini, and Groq support with automatic provider selection
|
|
38
|
+
- Smart diff analysis that understands your code changes
|
|
39
|
+
- Handles large codebases with intelligent summarization and safe truncation
|
|
40
|
+
- Optional Conventional Commits formatting and optional commit body generation (`--conventional`, `--body`)
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
### β‘ **Lightning Fast Workflow**
|
|
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)
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
### π§ **Intelligent Code Analysis**
|
|
49
|
+
- Analyzes actual code changes, not just file names
|
|
50
|
+
- Understands context from function changes, imports, and logic
|
|
51
|
+
- Graceful fallbacks for extremely large changesets and offline use
|
|
52
|
+
|
|
53
|
+
### π οΈ **Developer-Friendly**
|
|
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"`
|
|
36
60
|
|
|
37
|
-
##
|
|
61
|
+
## π Quick Start
|
|
38
62
|
|
|
39
|
-
###
|
|
63
|
+
### Installation
|
|
40
64
|
|
|
41
65
|
```bash
|
|
42
|
-
|
|
66
|
+
npm install -g gims
|
|
43
67
|
```
|
|
44
68
|
|
|
45
|
-
|
|
69
|
+
### Setup AI (Choose One)
|
|
46
70
|
|
|
47
|
-
|
|
71
|
+
**Option 1: OpenAI**
|
|
72
|
+
```bash
|
|
73
|
+
export OPENAI_API_KEY="your-api-key-here"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Option 2: Google Gemini**
|
|
77
|
+
```bash
|
|
78
|
+
export GEMINI_API_KEY="your-api-key-here"
|
|
79
|
+
```
|
|
48
80
|
|
|
81
|
+
**Option 3: Groq**
|
|
49
82
|
```bash
|
|
50
|
-
|
|
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"
|
|
51
86
|
```
|
|
52
87
|
|
|
53
|
-
|
|
88
|
+
GIMS auto-detects configured providers. If none are configured, it uses a local heuristic to generate sensible messages.
|
|
54
89
|
|
|
55
|
-
|
|
90
|
+
### Your First AI Commit
|
|
56
91
|
|
|
57
|
-
|
|
92
|
+
```bash
|
|
93
|
+
# Make some changes to your code
|
|
94
|
+
echo "console.log('Hello GIMS!');" > hello.js
|
|
58
95
|
|
|
59
|
-
|
|
96
|
+
# Let AI commit it for you
|
|
97
|
+
g o
|
|
98
|
+
# Output: Committed & pushed: "Add hello world console log"
|
|
99
|
+
```
|
|
60
100
|
|
|
101
|
+
## π Commands Reference
|
|
102
|
+
|
|
103
|
+
| Command | Alias | Description | Example |
|
|
104
|
+
|---------|-------|-------------|---------|
|
|
105
|
+
| `gims init` | `g i` | Initialize new Git repo | `g i` |
|
|
106
|
+
| `gims clone <repo>` | `g c` | Clone repository | `g c https://github.com/user/repo` |
|
|
107
|
+
| `gims suggest` | `g s` | Generate & copy commit message from staged changes (use `--all` to stage) | `g s --all` |
|
|
108
|
+
| `gims local` | `g l` | AI commit locally | `g l` |
|
|
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"` |
|
|
111
|
+
| `gims pull` | `g p` | Pull latest changes | `g p` |
|
|
112
|
+
| `gims list` | `g ls` | Show numbered commit history | `g ls` |
|
|
113
|
+
| `gims largelist` | `g ll` | Detailed commit history | `g ll` |
|
|
114
|
+
| `gims branch <n>` | `g b` | Branch from commit #n | `g b 3 feature-x` |
|
|
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
|
|
134
|
+
|
|
135
|
+
## π‘ Real-World Examples
|
|
136
|
+
|
|
137
|
+
### π§ Bug Fix
|
|
61
138
|
```bash
|
|
62
|
-
|
|
139
|
+
# You fix a null pointer exception
|
|
140
|
+
g o
|
|
141
|
+
# AI generates: "Fix null pointer exception in user authentication"
|
|
63
142
|
```
|
|
64
143
|
|
|
65
|
-
|
|
144
|
+
### β¨ New Feature
|
|
145
|
+
```bash
|
|
146
|
+
# You add a search function
|
|
147
|
+
g o
|
|
148
|
+
# AI generates: "Add search functionality with pagination support"
|
|
149
|
+
```
|
|
66
150
|
|
|
151
|
+
### π Documentation
|
|
152
|
+
```bash
|
|
153
|
+
# You update README and add comments
|
|
154
|
+
g o
|
|
155
|
+
# AI generates: "Update documentation and add inline code comments"
|
|
67
156
|
```
|
|
68
|
-
|
|
69
|
-
|
|
157
|
+
|
|
158
|
+
### π¨ Refactoring
|
|
159
|
+
```bash
|
|
160
|
+
# You clean up code structure
|
|
161
|
+
g o
|
|
162
|
+
# AI generates: "Refactor authentication module for better maintainability"
|
|
70
163
|
```
|
|
71
164
|
|
|
72
|
-
|
|
165
|
+
## π₯ Pro Tips
|
|
73
166
|
|
|
167
|
+
### π― Perfect Workflow
|
|
74
168
|
```bash
|
|
75
|
-
g
|
|
169
|
+
g p # Pull latest changes
|
|
170
|
+
# ... code your features ...
|
|
171
|
+
g s # Preview AI suggestion from staged changes
|
|
172
|
+
g s --all # Or stage everything and suggest
|
|
173
|
+
g l # Commit locally first
|
|
174
|
+
# ... test your changes ...
|
|
175
|
+
g o --set-upstream # Push with automatic upstream setup on first push
|
|
76
176
|
```
|
|
77
177
|
|
|
78
|
-
|
|
178
|
+
### π§ Smart Branching
|
|
179
|
+
```bash
|
|
180
|
+
g ls # See numbered history
|
|
181
|
+
g b 5 hotfix # Branch from commit #5
|
|
182
|
+
g l # Make changes and commit
|
|
183
|
+
g checkout main && g pull # Back to main
|
|
184
|
+
```
|
|
79
185
|
|
|
80
|
-
|
|
186
|
+
### π‘οΈ Safe Experimentation
|
|
187
|
+
```bash
|
|
188
|
+
g l # Commit your experiment
|
|
189
|
+
# ... code breaks something ...
|
|
190
|
+
g r 1 --soft --yes # Soft reset to previous commit (confirmed)
|
|
191
|
+
# ... or ...
|
|
192
|
+
g u --yes # Undo last commit (soft)
|
|
193
|
+
```
|
|
81
194
|
|
|
82
|
-
##
|
|
195
|
+
## βοΈ Configuration
|
|
83
196
|
|
|
84
|
-
###
|
|
197
|
+
### Environment Variables
|
|
85
198
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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 |
|
|
89
209
|
|
|
90
|
-
|
|
210
|
+
### .gimsrc (optional)
|
|
91
211
|
|
|
92
|
-
|
|
212
|
+
Place a `.gimsrc` JSON file in your repo root or home directory to set defaults:
|
|
93
213
|
|
|
94
|
-
```
|
|
95
|
-
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"provider": "auto",
|
|
217
|
+
"model": "gpt-4o-mini",
|
|
218
|
+
"conventional": true,
|
|
219
|
+
"copy": true
|
|
220
|
+
}
|
|
96
221
|
```
|
|
97
222
|
|
|
98
|
-
|
|
223
|
+
### Smart Fallbacks
|
|
99
224
|
|
|
100
|
-
|
|
225
|
+
GIMS handles edge cases gracefully:
|
|
101
226
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
105
232
|
|
|
106
|
-
|
|
233
|
+
## π€ Contributing
|
|
107
234
|
|
|
108
|
-
|
|
235
|
+
We love contributions! Here's how to get involved:
|
|
109
236
|
|
|
110
|
-
|
|
237
|
+
1. **π΄ Fork** the repository
|
|
238
|
+
2. **πΏ Create** your feature branch: `git checkout -b amazing-feature`
|
|
239
|
+
3. **π» Code** your improvements
|
|
240
|
+
4. **π§ͺ Test** thoroughly
|
|
241
|
+
5. **π Commit** with GIMS: `g l` (dogfooding!)
|
|
242
|
+
6. **π Push** and create a Pull Request
|
|
111
243
|
|
|
112
|
-
|
|
113
|
-
g i # or gims init
|
|
114
|
-
```
|
|
244
|
+
### π Found a Bug?
|
|
115
245
|
|
|
116
|
-
|
|
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:
|
|
248
|
+
- Clear description
|
|
249
|
+
- Steps to reproduce
|
|
250
|
+
- Expected vs actual behavior
|
|
251
|
+
- Your environment details
|
|
117
252
|
|
|
118
|
-
|
|
253
|
+
## π Why GIMS?
|
|
119
254
|
|
|
120
|
-
|
|
255
|
+
### Before GIMS π«
|
|
256
|
+
```bash
|
|
257
|
+
git log --oneline
|
|
258
|
+
abc1234 fix
|
|
259
|
+
def5678 update
|
|
260
|
+
ghi9012 changes
|
|
261
|
+
jkl3456 stuff
|
|
262
|
+
mno7890 final fix
|
|
263
|
+
```
|
|
121
264
|
|
|
265
|
+
### After GIMS β¨
|
|
122
266
|
```bash
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
267
|
+
git log --oneline
|
|
268
|
+
abc1234 Fix authentication timeout in user login service
|
|
269
|
+
def5678 Add responsive design for mobile navigation menu
|
|
270
|
+
ghi9012 Refactor database connection pool for better performance
|
|
271
|
+
jkl3456 Update API documentation with new endpoint examples
|
|
272
|
+
mno7890 Fix memory leak in image processing pipeline
|
|
129
273
|
```
|
|
130
274
|
|
|
275
|
+
## π Stats
|
|
276
|
+
|
|
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
|
|
281
|
+
|
|
282
|
+
## πΊοΈ Roadmap
|
|
283
|
+
|
|
284
|
+
- [ ] π Plugin system for custom AI providers
|
|
285
|
+
- [ ] π Commit message templates and customization
|
|
286
|
+
- [ ] π Multi-language commit message support
|
|
287
|
+
- [ ] π Integration with popular Git GUIs
|
|
288
|
+
- [ ] π± Mobile companion app
|
|
289
|
+
|
|
290
|
+
## π License
|
|
291
|
+
|
|
292
|
+
MIT Β© [Your Name](https://github.com/yourusername)
|
|
293
|
+
|
|
131
294
|
---
|
|
132
295
|
|
|
133
|
-
|
|
296
|
+
<div align="center">
|
|
297
|
+
|
|
298
|
+
**β Star this repo if GIMS makes your Git workflow awesome!**
|
|
134
299
|
|
|
135
|
-
|
|
136
|
-
* [`openai`](https://www.npmjs.com/package/openai) β OpenAI SDK
|
|
137
|
-
* [`@google-ai/gemini`](https://www.npmjs.com/package/@google-ai/gemini) β Gemini SDK
|
|
138
|
-
* [`commander`](https://www.npmjs.com/package/commander) β CLI argument parser
|
|
300
|
+
[Report Bug](https://github.com/yourusername/gims/issues) β’ [Request Feature](https://github.com/yourusername/gims/issues) β’ [Documentation](https://github.com/yourusername/gims/wiki)
|
|
139
301
|
|
|
302
|
+
*Made with β€οΈ by developers who hate writing commit messages*
|
|
140
303
|
|
|
304
|
+
</div>
|
package/bin/gims.js
CHANGED
|
@@ -9,10 +9,82 @@ const clipboard = require('clipboardy');
|
|
|
9
9
|
const process = require('process');
|
|
10
10
|
const { OpenAI } = require('openai');
|
|
11
11
|
const { GoogleGenAI } = require('@google/genai');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
12
14
|
|
|
13
15
|
const program = new Command();
|
|
14
16
|
const git = simpleGit();
|
|
15
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
|
+
|
|
16
88
|
// Safe log: returns { all: [] } on empty repo
|
|
17
89
|
async function safeLog() {
|
|
18
90
|
try {
|
|
@@ -24,7 +96,8 @@ async function safeLog() {
|
|
|
24
96
|
}
|
|
25
97
|
|
|
26
98
|
// Clean up AI-generated commit message
|
|
27
|
-
function cleanCommitMessage(message) {
|
|
99
|
+
function cleanCommitMessage(message, { body = false } = {}) {
|
|
100
|
+
if (!message) return 'Update project code';
|
|
28
101
|
// Remove markdown code blocks and formatting
|
|
29
102
|
let cleaned = message
|
|
30
103
|
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
@@ -34,28 +107,98 @@ function cleanCommitMessage(message) {
|
|
|
34
107
|
.replace(/^\s*#+\s*/gm, '') // Remove headers
|
|
35
108
|
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold formatting
|
|
36
109
|
.replace(/\*(.*?)\*/g, '$1') // Remove italic formatting
|
|
110
|
+
.replace(/[\u{1F300}-\u{1FAFF}]/gu, '') // strip most emojis
|
|
111
|
+
.replace(/[\t\r]+/g, ' ')
|
|
37
112
|
.trim();
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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;
|
|
44
126
|
}
|
|
45
127
|
|
|
46
128
|
// Estimate tokens (rough approximation: 1 token β 4 characters)
|
|
47
129
|
function estimateTokens(text) {
|
|
48
|
-
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;
|
|
49
189
|
}
|
|
50
190
|
|
|
51
191
|
// Generate commit message with multiple fallback strategies
|
|
52
|
-
async function generateCommitMessage(rawDiff) {
|
|
192
|
+
async function generateCommitMessage(rawDiff, options = {}) {
|
|
193
|
+
const { conventional = false, body = false, provider: prefProvider = 'auto', model = '', verbose = false } = options;
|
|
53
194
|
const MAX_TOKENS = 100000; // Conservative limit (well below 128k)
|
|
54
195
|
const MAX_CHARS = MAX_TOKENS * 4;
|
|
55
|
-
|
|
196
|
+
|
|
56
197
|
let content = rawDiff;
|
|
57
198
|
let strategy = 'full';
|
|
58
199
|
|
|
200
|
+
const logv = (m) => { if (verbose) console.log(color.cyan(`[gims] ${m}`)); };
|
|
201
|
+
|
|
59
202
|
// Strategy 1: Check if full diff is too large
|
|
60
203
|
if (estimateTokens(rawDiff) > MAX_TOKENS) {
|
|
61
204
|
strategy = 'summary';
|
|
@@ -78,13 +221,15 @@ async function generateCommitMessage(rawDiff) {
|
|
|
78
221
|
const modified = status.modified.slice(0, 10);
|
|
79
222
|
const created = status.created.slice(0, 10);
|
|
80
223
|
const deleted = status.deleted.slice(0, 10);
|
|
81
|
-
|
|
224
|
+
const renamed = status.renamed.map(r => `${r.from}β${r.to}`).slice(0, 10);
|
|
225
|
+
|
|
82
226
|
content = [
|
|
83
227
|
modified.length > 0 ? `Modified: ${modified.join(', ')}` : '',
|
|
84
228
|
created.length > 0 ? `Added: ${created.join(', ')}` : '',
|
|
85
|
-
deleted.length > 0 ? `Deleted: ${deleted.join(', ')}` : ''
|
|
229
|
+
deleted.length > 0 ? `Deleted: ${deleted.join(', ')}` : '',
|
|
230
|
+
renamed.length > 0 ? `Renamed: ${renamed.join(', ')}` : '',
|
|
86
231
|
].filter(Boolean).join('\n');
|
|
87
|
-
|
|
232
|
+
|
|
88
233
|
if (status.files.length > 30) {
|
|
89
234
|
content += `\n... and ${status.files.length - 30} more files`;
|
|
90
235
|
}
|
|
@@ -105,54 +250,81 @@ async function generateCommitMessage(rawDiff) {
|
|
|
105
250
|
summary: 'Changes are large; using summary. Write a concise git commit message for these changes:',
|
|
106
251
|
status: 'Many files changed. Write a concise git commit message based on these file changes:',
|
|
107
252
|
truncated: 'Large diff truncated. Write a concise git commit message for these changes:',
|
|
108
|
-
fallback: 'Write a concise git commit message for:'
|
|
253
|
+
fallback: 'Write a concise git commit message for:',
|
|
109
254
|
};
|
|
110
255
|
|
|
111
|
-
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}`;
|
|
112
259
|
|
|
113
260
|
// Final safety check
|
|
114
261
|
if (estimateTokens(prompt) > MAX_TOKENS) {
|
|
115
|
-
console.warn('Changes too large for AI analysis, using default message');
|
|
116
|
-
return 'Update multiple files';
|
|
262
|
+
console.warn(color.yellow('Changes too large for AI analysis, using default message'));
|
|
263
|
+
return cleanCommitMessage('Update multiple files', { body });
|
|
117
264
|
}
|
|
118
265
|
|
|
119
266
|
let message = 'Update project code'; // Default fallback
|
|
267
|
+
const provider = resolveProvider(prefProvider);
|
|
268
|
+
logv(`strategy=${strategy}, provider=${provider}${model ? `, model=${model}` : ''}`);
|
|
120
269
|
|
|
121
270
|
try {
|
|
122
|
-
if (
|
|
271
|
+
if (provider === 'gemini') {
|
|
123
272
|
const genai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
|
124
|
-
const res = await genai.models.generateContent({
|
|
125
|
-
model: 'gemini-2.0-flash',
|
|
126
|
-
contents: prompt
|
|
273
|
+
const res = await genai.models.generateContent({
|
|
274
|
+
model: model || 'gemini-2.0-flash',
|
|
275
|
+
contents: prompt,
|
|
127
276
|
});
|
|
128
277
|
message = (await res.response.text()).trim();
|
|
129
|
-
} else if (
|
|
278
|
+
} else if (provider === 'openai') {
|
|
130
279
|
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
131
280
|
const res = await openai.chat.completions.create({
|
|
132
|
-
model: 'gpt-4o-mini',
|
|
281
|
+
model: model || 'gpt-4o-mini',
|
|
133
282
|
messages: [{ role: 'user', content: prompt }],
|
|
134
|
-
temperature: 0.
|
|
135
|
-
max_tokens:
|
|
283
|
+
temperature: 0.3,
|
|
284
|
+
max_tokens: body ? 200 : 80,
|
|
136
285
|
});
|
|
137
|
-
message = res.choices[0].message.content.trim();
|
|
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',
|
|
292
|
+
messages: [{ role: 'user', content: prompt }],
|
|
293
|
+
temperature: 0.3,
|
|
294
|
+
max_tokens: body ? 200 : 80,
|
|
295
|
+
});
|
|
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}`;
|
|
138
303
|
}
|
|
139
304
|
} catch (error) {
|
|
140
|
-
if (error.code === 'context_length_exceeded') {
|
|
141
|
-
console.warn('Content still too large for AI, using default message');
|
|
142
|
-
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 });
|
|
143
308
|
}
|
|
144
|
-
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}`;
|
|
145
315
|
}
|
|
146
316
|
|
|
147
|
-
return cleanCommitMessage(message);
|
|
317
|
+
return cleanCommitMessage(message, { body });
|
|
148
318
|
}
|
|
149
319
|
|
|
150
320
|
async function resolveCommit(input) {
|
|
151
321
|
if (/^\d+$/.test(input)) {
|
|
152
322
|
const { all } = await safeLog();
|
|
323
|
+
// Align with list/largelist which show oldest -> newest
|
|
324
|
+
const ordered = [...all].reverse();
|
|
153
325
|
const idx = Number(input) - 1;
|
|
154
|
-
if (idx < 0 || idx >=
|
|
155
|
-
return
|
|
326
|
+
if (idx < 0 || idx >= ordered.length) throw new Error('Index out of range');
|
|
327
|
+
return ordered[idx].hash;
|
|
156
328
|
}
|
|
157
329
|
return input;
|
|
158
330
|
}
|
|
@@ -162,144 +334,296 @@ async function hasChanges() {
|
|
|
162
334
|
return status.files.length > 0;
|
|
163
335
|
}
|
|
164
336
|
|
|
165
|
-
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');
|
|
166
354
|
|
|
167
355
|
program.command('init').alias('i')
|
|
168
356
|
.description('Initialize a new Git repository')
|
|
169
|
-
.action(async () => {
|
|
357
|
+
.action(async () => {
|
|
358
|
+
try { await git.init(); console.log('Initialized repo.'); }
|
|
359
|
+
catch (e) { handleError('Init error', e); }
|
|
360
|
+
});
|
|
170
361
|
|
|
171
362
|
program.command('clone <repo>').alias('c')
|
|
172
363
|
.description('Clone a Git repository')
|
|
173
364
|
.action(async (repo) => {
|
|
174
365
|
try { await git.clone(repo); console.log(`Cloned ${repo}`); }
|
|
175
|
-
catch (e) {
|
|
366
|
+
catch (e) { handleError('Clone error', e); }
|
|
176
367
|
});
|
|
177
368
|
|
|
178
369
|
program.command('suggest').alias('s')
|
|
179
370
|
.description('Suggest commit message and copy to clipboard')
|
|
180
371
|
.action(async () => {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
372
|
+
await ensureRepo();
|
|
373
|
+
const opts = getOpts();
|
|
184
374
|
|
|
185
|
-
const { all } = await safeLog();
|
|
186
|
-
const isFirst = all.length === 0;
|
|
187
|
-
|
|
188
|
-
// Always add changes first
|
|
189
|
-
await git.add('.');
|
|
190
|
-
|
|
191
|
-
// Get the appropriate diff
|
|
192
|
-
const rawDiff = await git.diff(['--cached']);
|
|
193
|
-
|
|
194
|
-
if (!rawDiff.trim()) {
|
|
195
|
-
return console.log('No changes to suggest.');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const msg = await generateCommitMessage(rawDiff);
|
|
199
|
-
|
|
200
375
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
376
|
+
if (opts.all) {
|
|
377
|
+
await git.add('.');
|
|
378
|
+
}
|
|
379
|
+
|
|
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;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const msg = await generateCommitMessage(rawDiff, opts);
|
|
392
|
+
|
|
393
|
+
if (opts.json) {
|
|
394
|
+
const out = { message: msg };
|
|
395
|
+
console.log(JSON.stringify(out));
|
|
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)')}`); }
|
|
402
|
+
} else {
|
|
403
|
+
console.log(`Suggested: "${msg}"`);
|
|
404
|
+
}
|
|
405
|
+
} catch (e) {
|
|
406
|
+
handleError('Suggest error', e);
|
|
205
407
|
}
|
|
206
408
|
});
|
|
207
409
|
|
|
208
410
|
program.command('local').alias('l')
|
|
209
411
|
.description('AI-powered local commit')
|
|
210
412
|
.action(async () => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
413
|
+
await ensureRepo();
|
|
414
|
+
const opts = getOpts();
|
|
214
415
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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);
|
|
226
443
|
}
|
|
227
|
-
|
|
228
|
-
const msg = await generateCommitMessage(rawDiff);
|
|
229
|
-
await git.commit(msg);
|
|
230
|
-
console.log(`Committed locally: "${msg}"`);
|
|
231
444
|
});
|
|
232
445
|
|
|
233
446
|
program.command('online').alias('o')
|
|
234
447
|
.description('AI commit + push')
|
|
235
448
|
.action(async () => {
|
|
236
|
-
|
|
237
|
-
|
|
449
|
+
await ensureRepo();
|
|
450
|
+
const opts = getOpts();
|
|
451
|
+
|
|
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);
|
|
238
497
|
}
|
|
498
|
+
});
|
|
239
499
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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);
|
|
251
534
|
}
|
|
252
|
-
|
|
253
|
-
const msg = await generateCommitMessage(rawDiff);
|
|
254
|
-
await git.commit(msg);
|
|
255
|
-
await git.push();
|
|
256
|
-
console.log(`Committed & pushed: "${msg}"`);
|
|
257
535
|
});
|
|
258
536
|
|
|
259
537
|
program.command('pull').alias('p')
|
|
260
538
|
.description('Pull latest changes')
|
|
261
539
|
.action(async () => {
|
|
540
|
+
await ensureRepo();
|
|
262
541
|
try { await git.pull(); console.log('Pulled latest.'); }
|
|
263
|
-
catch (e) {
|
|
542
|
+
catch (e) { handleError('Pull error', e); }
|
|
264
543
|
});
|
|
265
544
|
|
|
266
545
|
program.command('list').alias('ls')
|
|
267
546
|
.description('Short numbered git log (oldest β newest)')
|
|
268
547
|
.action(async () => {
|
|
269
|
-
|
|
270
|
-
|
|
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); }
|
|
271
553
|
});
|
|
272
554
|
|
|
273
555
|
program.command('largelist').alias('ll')
|
|
274
556
|
.description('Full numbered git log (oldest β newest)')
|
|
275
557
|
.action(async () => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
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); }
|
|
281
566
|
});
|
|
282
567
|
|
|
283
568
|
program.command('branch <c> [name]').alias('b')
|
|
284
569
|
.description('Branch from commit/index')
|
|
285
570
|
.action(async (c, name) => {
|
|
571
|
+
await ensureRepo();
|
|
286
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}`); }
|
|
287
|
-
catch (e) {
|
|
573
|
+
catch (e) { handleError('Branch error', e); }
|
|
288
574
|
});
|
|
289
575
|
|
|
290
576
|
program.command('reset <c>').alias('r')
|
|
291
577
|
.description('Reset branch to commit/index')
|
|
292
578
|
.option('--hard','hard reset')
|
|
293
|
-
.action(async (c,
|
|
294
|
-
|
|
295
|
-
|
|
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); }
|
|
296
593
|
});
|
|
297
594
|
|
|
298
595
|
program.command('revert <c>').alias('rv')
|
|
299
596
|
.description('Revert commit/index safely')
|
|
300
597
|
.action(async (c) => {
|
|
301
|
-
|
|
302
|
-
|
|
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); }
|
|
303
627
|
});
|
|
304
628
|
|
|
305
|
-
program.parse(process.argv);
|
|
629
|
+
program.parse(process.argv);
|