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.
@@ -1,11 +1,11 @@
1
- name: Publish to npm
1
+ name: release to npm
2
2
 
3
3
  on:
4
4
  push:
5
5
  branches: [main]
6
6
 
7
7
  jobs:
8
- build-and-publish:
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
  [![npm version](https://img.shields.io/npm/v/gims.svg)](https://npmjs.org/package/gims)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![Node.js Version](https://img.shields.io/node/v/gims.svg)](https://nodejs.org/)
8
- [![AI Powered](https://img.shields.io/badge/AI-Powered-blueviolet.svg)](https://github.com/s41r4j/gims)
8
+ [![AI Powered](https://img.shields.io/badge/AI-Powered-blueviolet.svg)](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
- - **OpenAI GPT-4o-mini** integration for intelligent commit message generation
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 token management
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
- - **One command commits**: `g o` - analyze, commit, and push in seconds
44
- - **Smart suggestions**: `g s` - get AI-generated messages copied to clipboard
45
- - **Local commits**: `g l` - commit locally with AI messages
46
- - **Instant setup**: `g i` - initialize repos in a flash
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
- - Handles everything from bug fixes to feature additions
52
- - Graceful fallbacks for extremely large changesets
51
+ - Graceful fallbacks for extremely large changesets and offline use
53
52
 
54
53
  ### πŸ› οΈ **Developer-Friendly**
55
- - **Numbered commit history**: Easy navigation with `g ls`
56
- - **Smart branching**: `g b 5` creates branch from commit #5
57
- - **Safe operations**: Built-in error handling and validation
58
- - **Clean interface**: Intuitive commands that just make sense
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 (Recommended)**
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 (Faster)**
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
- ### 🎯 **Perfect Workflow**
167
+ ### 🎯 Perfect Workflow
141
168
  ```bash
142
- # Daily development cycle
143
- g p # Pull latest changes
169
+ g p # Pull latest changes
144
170
  # ... code your features ...
145
- g s # Preview AI suggestion
146
- g l # Commit locally first
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 # Push with AI commit message
175
+ g o --set-upstream # Push with automatic upstream setup on first push
149
176
  ```
150
177
 
151
- ### 🧠 **Smart Branching**
178
+ ### 🧠 Smart Branching
152
179
  ```bash
153
- g ls # See numbered history
154
- g b 5 hotfix # Branch from commit #5
155
- g l # Make changes and commit
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
- ### πŸ›‘οΈ **Safe Experimentation**
186
+ ### πŸ›‘οΈ Safe Experimentation
160
187
  ```bash
161
- g l # Commit your experiment
188
+ g l # Commit your experiment
162
189
  # ... code breaks something ...
163
- g r 1 --soft # Soft reset to previous commit
164
- # ... fix and try again ...
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 | Required |
172
- |----------|---------|----------|
173
- | `OPENAI_API_KEY` | OpenAI API access | One of these |
174
- | `GEMINI_API_KEY` | Google Gemini API access | One of these |
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
- - **πŸ”„ Large diffs**: Automatically switches to file summary mode
181
- - **πŸ“Š Massive changes**: Falls back to status-based analysis
182
- - **πŸ›œ No API key**: Uses sensible default messages
183
- - **⚠️ API failures**: Graceful degradation with helpful errors
184
- - **πŸͺ™ Token limits**: Intelligent content chunking with 100K token limit
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/s41r4j/gims/issues)
200
- 2. Create a [new issue](https://github.com/s41r4j/gims/issues/new) with:
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
- - ⚑ **10x faster** commits than traditional Git workflow
231
- - 🎯 **95%+ accuracy** in commit message relevance
232
- - πŸ“š **Zero learning curve** - if you know Git, you know GIMS
233
- - 🌍 **Works everywhere** - Mac, Windows, Linux, WSL
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 Β© [GIMS](https://github.com/s41r4j/gims)
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/s41r4j/gims/issues) β€’ [Request Feature](https://github.com/s41r4j/gims/issues) β€’ [Documentation](https://github.com/s41r4j/gims#readme)
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
- // Take only the first line if multiple lines exist
39
- const firstLine = cleaned.split('\n')[0].trim();
40
-
41
- // Ensure it's not too long
42
- return firstLine.length > 72 ? firstLine.substring(0, 69) + '...' : firstLine;
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 prompt = `${prompts[strategy]}\n${content}`;
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 (process.env.GEMINI_API_KEY) {
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 (process.env.OPENAI_API_KEY) {
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.75,
139
- max_tokens: 100 // Limit response length
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('AI generation failed:', error.message);
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 >= all.length) throw new Error('Index out of range');
159
- return all[idx].hash;
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.name('gims').alias('g').version('0.4.3');
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 () => { await git.init(); console.log('Initialized repo.'); });
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) { console.error('Clone error:', e.message); }
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
- if (!(await hasChanges())) {
186
- return console.log('No changes to suggest.');
187
- }
372
+ await ensureRepo();
373
+ const opts = getOpts();
188
374
 
189
- const { all } = await safeLog();
190
- const isFirst = all.length === 0;
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
- program.command('commit').alias('cm')
216
- .description('Interactive commit message generation')
217
- .action(async () => {
218
- if (!(await hasChanges())) {
219
- return console.log('No changes to commit.');
220
- }
221
-
222
- // Get diff for message generation
223
- let rawDiff = await git.diff();
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
- if (!process.env.GEMINI_API_KEY && !process.env.OPENAI_API_KEY) {
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
- const readline = require('readline');
240
- const rl = readline.createInterface({
241
- input: process.stdin,
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
- // Generate new message
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
- if (!(await hasChanges())) {
285
- return console.log('No changes to commit.');
286
- }
413
+ await ensureRepo();
414
+ const opts = getOpts();
287
415
 
288
- const { all } = await safeLog();
289
- const isFirst = all.length === 0;
290
-
291
- // Always add changes first
292
- await git.add('.');
293
-
294
- // Get the appropriate diff
295
- const rawDiff = await git.diff(['--cached']);
296
-
297
- if (!rawDiff.trim()) {
298
- return console.log('No changes to commit.');
299
- }
300
-
301
- const msg = await generateCommitMessage(rawDiff);
302
-
303
- if (msg === null) {
304
- return console.log('Please set GEMINI_API_KEY or OPENAI_API_KEY environment variable');
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
- if (!(await hasChanges())) {
315
- return console.log('No changes to commit.');
316
- }
449
+ await ensureRepo();
450
+ const opts = getOpts();
317
451
 
318
- const { all } = await safeLog();
319
- const isFirst = all.length === 0;
320
-
321
- // Always add changes first
322
- await git.add('.');
323
-
324
- // Get the appropriate diff
325
- const rawDiff = await git.diff(['--cached']);
326
-
327
- if (!rawDiff.trim()) {
328
- return console.log('No changes to commit.');
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
- const msg = await generateCommitMessage(rawDiff);
332
-
333
- if (msg === null) {
334
- return console.log('Please set GEMINI_API_KEY or OPENAI_API_KEY environment variable');
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) { console.error('Pull error:', e.message); }
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
- const { all } = await safeLog();
353
- all.reverse().forEach((c, i) => console.log(`${i+1}. ${c.hash.slice(0,7)} ${c.message}`));
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
- const { all } = await safeLog();
360
- all.reverse().forEach((c, i) => {
361
- const date = new Date(c.date).toLocaleString();
362
- console.log(`${i+1}. ${c.hash.slice(0,7)} | ${date} | ${c.author_name} β†’ ${c.message}`);
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) { console.error('Branch error:', e.message); }
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, opts) => {
377
- try { const sha = await resolveCommit(c); const mode = opts.hard? '--hard':'--soft'; await git.raw(['reset', mode, sha]); console.log(`Reset (${mode}) to ${sha}`); }
378
- catch (e) { console.error('Reset error:', e.message); }
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
- try { const sha = await resolveCommit(c); await git.revert(sha); console.log(`Reverted ${sha}`); }
385
- catch (e) { console.error('Revert error:', e.message); }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gims",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Git Made Simple – AI‑powered git helper using Gemini / OpenAI",
5
5
  "author": "S41R4J",
6
6
  "license": "MIT",
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