gims 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -10
- package/bin/gims.js +228 -90
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
Here’s a **complete usage guide** for `gims` (Git Made Simple) CLI tool:
|
|
2
1
|
|
|
3
|
-
---
|
|
4
2
|
|
|
5
3
|
## 🚀 Installation
|
|
6
4
|
|
|
@@ -139,12 +137,4 @@ g b 1 try-feature # Branch out from earlier version
|
|
|
139
137
|
* [`@google-ai/gemini`](https://www.npmjs.com/package/@google-ai/gemini) – Gemini SDK
|
|
140
138
|
* [`commander`](https://www.npmjs.com/package/commander) – CLI argument parser
|
|
141
139
|
|
|
142
|
-
---
|
|
143
|
-
|
|
144
|
-
Let me know if you’d like to:
|
|
145
|
-
|
|
146
|
-
* Add auto-push branch creation
|
|
147
|
-
* Show full diff preview
|
|
148
|
-
* Add emojis, scopes, or conventional commits
|
|
149
|
-
* Bundle this as a GUI as well!
|
|
150
140
|
|
package/bin/gims.js
CHANGED
|
@@ -2,34 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
4
|
gims (Git Made Simple) CLI
|
|
5
|
-
Features:
|
|
6
|
-
- Initialize repository (init alias: i)
|
|
7
|
-
- Auto commit with AI-generated messages (Gemini or OpenAI)
|
|
8
|
-
Modes:
|
|
9
|
-
* local (alias: l)
|
|
10
|
-
* online (alias: o)
|
|
11
|
-
- Navigation:
|
|
12
|
-
* list (alias: ls): numbered git log --oneline
|
|
13
|
-
* largelist (alias: ll): full git log --no-pager
|
|
14
|
-
* branch <commit|#> [name] (alias: b)
|
|
15
|
-
* reset <commit|#> [--hard] (alias: r)
|
|
16
|
-
* revert <commit|#> (alias: rv)
|
|
17
|
-
|
|
18
|
-
Env vars:
|
|
19
|
-
- GEMINI_API_KEY: use Google Gemini API
|
|
20
|
-
- OPENAI_API_KEY: fallback to OpenAI if Gemini not set
|
|
21
|
-
- none: fallback to generic commit messages
|
|
22
|
-
|
|
23
|
-
Usage:
|
|
24
|
-
npm install -g gims
|
|
25
|
-
export GEMINI_API_KEY=...
|
|
26
|
-
export OPENAI_API_KEY=...
|
|
27
|
-
gims init # or g i
|
|
28
|
-
gims local # or g l
|
|
29
5
|
*/
|
|
30
|
-
|
|
31
6
|
const { Command } = require('commander');
|
|
32
7
|
const simpleGit = require('simple-git');
|
|
8
|
+
const clipboard = require('clipboardy');
|
|
33
9
|
const process = require('process');
|
|
34
10
|
const { OpenAI } = require('openai');
|
|
35
11
|
const { GoogleGenAI } = require('@google/genai');
|
|
@@ -37,131 +13,293 @@ const { GoogleGenAI } = require('@google/genai');
|
|
|
37
13
|
const program = new Command();
|
|
38
14
|
const git = simpleGit();
|
|
39
15
|
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
16
|
+
// Safe log: returns { all: [] } on empty repo
|
|
17
|
+
async function safeLog() {
|
|
18
|
+
try {
|
|
19
|
+
return await git.log();
|
|
20
|
+
} catch (e) {
|
|
21
|
+
if (/does not have any commits/.test(e.message)) return { all: [] };
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
44
25
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
26
|
+
// Clean up AI-generated commit message
|
|
27
|
+
function cleanCommitMessage(message) {
|
|
28
|
+
// Remove markdown code blocks and formatting
|
|
29
|
+
let cleaned = message
|
|
30
|
+
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
|
31
|
+
.replace(/`([^`]+)`/g, '$1') // Remove inline code formatting
|
|
32
|
+
.replace(/^\s*[-*+]\s*/gm, '') // Remove bullet points
|
|
33
|
+
.replace(/^\s*\d+\.\s*/gm, '') // Remove numbered lists
|
|
34
|
+
.replace(/^\s*#+\s*/gm, '') // Remove headers
|
|
35
|
+
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold formatting
|
|
36
|
+
.replace(/\*(.*?)\*/g, '$1') // Remove italic formatting
|
|
37
|
+
.trim();
|
|
38
|
+
|
|
39
|
+
// Take only the first line if multiple lines exist
|
|
40
|
+
const firstLine = cleaned.split('\n')[0].trim();
|
|
41
|
+
|
|
42
|
+
// Ensure it's not too long
|
|
43
|
+
return firstLine.length > 72 ? firstLine.substring(0, 69) + '...' : firstLine;
|
|
49
44
|
}
|
|
50
45
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
// Estimate tokens (rough approximation: 1 token ≈ 4 characters)
|
|
47
|
+
function estimateTokens(text) {
|
|
48
|
+
return Math.ceil(text.length / 4);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Generate commit message with multiple fallback strategies
|
|
52
|
+
async function generateCommitMessage(rawDiff) {
|
|
53
|
+
const MAX_TOKENS = 100000; // Conservative limit (well below 128k)
|
|
54
|
+
const MAX_CHARS = MAX_TOKENS * 4;
|
|
55
|
+
|
|
56
|
+
let content = rawDiff;
|
|
57
|
+
let strategy = 'full';
|
|
58
|
+
|
|
59
|
+
// Strategy 1: Check if full diff is too large
|
|
60
|
+
if (estimateTokens(rawDiff) > MAX_TOKENS) {
|
|
61
|
+
strategy = 'summary';
|
|
62
|
+
try {
|
|
63
|
+
const summary = await git.diffSummary();
|
|
64
|
+
content = summary.files
|
|
65
|
+
.map(f => `${f.file}: +${f.insertions} -${f.deletions}`)
|
|
66
|
+
.join('\n');
|
|
67
|
+
} catch (e) {
|
|
68
|
+
strategy = 'fallback';
|
|
69
|
+
content = 'Large changes across multiple files';
|
|
70
|
+
}
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
// Strategy 2: If summary is still too large, use status
|
|
74
|
+
if (strategy === 'summary' && estimateTokens(content) > MAX_TOKENS) {
|
|
75
|
+
strategy = 'status';
|
|
76
|
+
try {
|
|
77
|
+
const status = await git.status();
|
|
78
|
+
const modified = status.modified.slice(0, 10);
|
|
79
|
+
const created = status.created.slice(0, 10);
|
|
80
|
+
const deleted = status.deleted.slice(0, 10);
|
|
81
|
+
|
|
82
|
+
content = [
|
|
83
|
+
modified.length > 0 ? `Modified: ${modified.join(', ')}` : '',
|
|
84
|
+
created.length > 0 ? `Added: ${created.join(', ')}` : '',
|
|
85
|
+
deleted.length > 0 ? `Deleted: ${deleted.join(', ')}` : ''
|
|
86
|
+
].filter(Boolean).join('\n');
|
|
87
|
+
|
|
88
|
+
if (status.files.length > 30) {
|
|
89
|
+
content += `\n... and ${status.files.length - 30} more files`;
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
strategy = 'fallback';
|
|
93
|
+
content = 'Large changes across multiple files';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Strategy 3: If still too large, truncate
|
|
98
|
+
if (estimateTokens(content) > MAX_TOKENS) {
|
|
99
|
+
strategy = 'truncated';
|
|
100
|
+
content = content.substring(0, MAX_CHARS - 1000) + '\n... (truncated)';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const prompts = {
|
|
104
|
+
full: 'Write a concise git commit message for these changes:',
|
|
105
|
+
summary: 'Changes are large; using summary. Write a concise git commit message for these changes:',
|
|
106
|
+
status: 'Many files changed. Write a concise git commit message based on these file changes:',
|
|
107
|
+
truncated: 'Large diff truncated. Write a concise git commit message for these changes:',
|
|
108
|
+
fallback: 'Write a concise git commit message for:'
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const prompt = `${prompts[strategy]}\n${content}`;
|
|
112
|
+
|
|
113
|
+
// Final safety check
|
|
114
|
+
if (estimateTokens(prompt) > MAX_TOKENS) {
|
|
115
|
+
console.warn('Changes too large for AI analysis, using default message');
|
|
116
|
+
return 'Update multiple files';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let message = 'Update project code'; // Default fallback
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (process.env.GEMINI_API_KEY) {
|
|
123
|
+
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
|
|
127
|
+
});
|
|
128
|
+
message = (await res.response.text()).trim();
|
|
129
|
+
} else if (process.env.OPENAI_API_KEY) {
|
|
130
|
+
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
131
|
+
const res = await openai.chat.completions.create({
|
|
132
|
+
model: 'gpt-4o-mini',
|
|
133
|
+
messages: [{ role: 'user', content: prompt }],
|
|
134
|
+
temperature: 0.5,
|
|
135
|
+
max_tokens: 100 // Limit response length
|
|
136
|
+
});
|
|
137
|
+
message = res.choices[0].message.content.trim();
|
|
138
|
+
}
|
|
139
|
+
} 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';
|
|
143
|
+
}
|
|
144
|
+
console.warn('AI generation failed:', error.message);
|
|
67
145
|
}
|
|
68
146
|
|
|
69
|
-
|
|
70
|
-
return 'Update project code';
|
|
147
|
+
return cleanCommitMessage(message);
|
|
71
148
|
}
|
|
72
149
|
|
|
73
150
|
async function resolveCommit(input) {
|
|
74
151
|
if (/^\d+$/.test(input)) {
|
|
75
|
-
const { all } = await
|
|
76
|
-
const idx =
|
|
152
|
+
const { all } = await safeLog();
|
|
153
|
+
const idx = Number(input) - 1;
|
|
77
154
|
if (idx < 0 || idx >= all.length) throw new Error('Index out of range');
|
|
78
155
|
return all[idx].hash;
|
|
79
156
|
}
|
|
80
157
|
return input;
|
|
81
158
|
}
|
|
82
159
|
|
|
83
|
-
|
|
160
|
+
async function hasChanges() {
|
|
161
|
+
const status = await git.status();
|
|
162
|
+
return status.files.length > 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
program.name('gims').alias('g').version('0.4.3');
|
|
84
166
|
|
|
85
167
|
program.command('init').alias('i')
|
|
86
168
|
.description('Initialize a new Git repository')
|
|
169
|
+
.action(async () => { await git.init(); console.log('Initialized repo.'); });
|
|
170
|
+
|
|
171
|
+
program.command('clone <repo>').alias('c')
|
|
172
|
+
.description('Clone a Git repository')
|
|
173
|
+
.action(async (repo) => {
|
|
174
|
+
try { await git.clone(repo); console.log(`Cloned ${repo}`); }
|
|
175
|
+
catch (e) { console.error('Clone error:', e.message); }
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
program.command('suggest').alias('s')
|
|
179
|
+
.description('Suggest commit message and copy to clipboard')
|
|
87
180
|
.action(async () => {
|
|
88
|
-
await
|
|
89
|
-
|
|
181
|
+
if (!(await hasChanges())) {
|
|
182
|
+
return console.log('No changes to suggest.');
|
|
183
|
+
}
|
|
184
|
+
|
|
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
|
+
try {
|
|
201
|
+
clipboard.writeSync(msg);
|
|
202
|
+
console.log(`Suggested: "${msg}" (copied to clipboard)`);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.log(`Suggested: "${msg}" (clipboard copy failed)`);
|
|
205
|
+
}
|
|
90
206
|
});
|
|
91
207
|
|
|
92
|
-
// Local commit: stage all, get staged diff, AI message, commit
|
|
93
208
|
program.command('local').alias('l')
|
|
94
209
|
.description('AI-powered local commit')
|
|
95
210
|
.action(async () => {
|
|
96
|
-
|
|
211
|
+
if (!(await hasChanges())) {
|
|
212
|
+
return console.log('No changes to commit.');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { all } = await safeLog();
|
|
216
|
+
const isFirst = all.length === 0;
|
|
217
|
+
|
|
218
|
+
// Always add changes first
|
|
97
219
|
await git.add('.');
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
220
|
+
|
|
221
|
+
// Get the appropriate diff
|
|
222
|
+
const rawDiff = await git.diff(['--cached']);
|
|
223
|
+
|
|
224
|
+
if (!rawDiff.trim()) {
|
|
225
|
+
return console.log('No changes to commit.');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const msg = await generateCommitMessage(rawDiff);
|
|
103
229
|
await git.commit(msg);
|
|
104
230
|
console.log(`Committed locally: "${msg}"`);
|
|
105
231
|
});
|
|
106
232
|
|
|
107
|
-
// Online commit: stage, diff, message, commit, push
|
|
108
233
|
program.command('online').alias('o')
|
|
109
234
|
.description('AI commit + push')
|
|
110
235
|
.action(async () => {
|
|
236
|
+
if (!(await hasChanges())) {
|
|
237
|
+
return console.log('No changes to commit.');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const { all } = await safeLog();
|
|
241
|
+
const isFirst = all.length === 0;
|
|
242
|
+
|
|
243
|
+
// Always add changes first
|
|
111
244
|
await git.add('.');
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
245
|
+
|
|
246
|
+
// Get the appropriate diff
|
|
247
|
+
const rawDiff = await git.diff(['--cached']);
|
|
248
|
+
|
|
249
|
+
if (!rawDiff.trim()) {
|
|
250
|
+
return console.log('No changes to commit.');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const msg = await generateCommitMessage(rawDiff);
|
|
116
254
|
await git.commit(msg);
|
|
117
255
|
await git.push();
|
|
118
256
|
console.log(`Committed & pushed: "${msg}"`);
|
|
119
257
|
});
|
|
120
258
|
|
|
259
|
+
program.command('pull').alias('p')
|
|
260
|
+
.description('Pull latest changes')
|
|
261
|
+
.action(async () => {
|
|
262
|
+
try { await git.pull(); console.log('Pulled latest.'); }
|
|
263
|
+
catch (e) { console.error('Pull error:', e.message); }
|
|
264
|
+
});
|
|
265
|
+
|
|
121
266
|
program.command('list').alias('ls')
|
|
122
|
-
.description('Short numbered git log')
|
|
267
|
+
.description('Short numbered git log (oldest → newest)')
|
|
123
268
|
.action(async () => {
|
|
124
|
-
const { all } = await
|
|
125
|
-
all.forEach((c, i) => console.log(`${i+1}. ${c.hash.slice(0,7)} ${c.message}`));
|
|
269
|
+
const { all } = await safeLog();
|
|
270
|
+
all.reverse().forEach((c, i) => console.log(`${i+1}. ${c.hash.slice(0,7)} ${c.message}`));
|
|
126
271
|
});
|
|
127
272
|
|
|
128
273
|
program.command('largelist').alias('ll')
|
|
129
|
-
.description('Full git log
|
|
274
|
+
.description('Full numbered git log (oldest → newest)')
|
|
130
275
|
.action(async () => {
|
|
131
|
-
|
|
276
|
+
const { all } = await safeLog();
|
|
277
|
+
all.reverse().forEach((c, i) => {
|
|
278
|
+
const date = new Date(c.date).toLocaleString();
|
|
279
|
+
console.log(`${i+1}. ${c.hash.slice(0,7)} | ${date} | ${c.author_name} → ${c.message}`);
|
|
280
|
+
});
|
|
132
281
|
});
|
|
133
282
|
|
|
134
283
|
program.command('branch <c> [name]').alias('b')
|
|
135
284
|
.description('Branch from commit/index')
|
|
136
285
|
.action(async (c, name) => {
|
|
137
|
-
try {
|
|
138
|
-
|
|
139
|
-
const br = name || `branch-${sha.slice(0,7)}`;
|
|
140
|
-
await git.checkout(['-b', br, sha]);
|
|
141
|
-
console.log(`Switched to branch ${br} at ${sha}`);
|
|
142
|
-
} catch (e) { console.error(e.message); }
|
|
286
|
+
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) { console.error('Branch error:', e.message); }
|
|
143
288
|
});
|
|
144
289
|
|
|
145
290
|
program.command('reset <c>').alias('r')
|
|
146
291
|
.description('Reset branch to commit/index')
|
|
147
|
-
.option('--hard',
|
|
292
|
+
.option('--hard','hard reset')
|
|
148
293
|
.action(async (c, opts) => {
|
|
149
|
-
try {
|
|
150
|
-
|
|
151
|
-
const mode = opts.hard ? '--hard' : '--soft';
|
|
152
|
-
await git.raw(['reset', mode, sha]);
|
|
153
|
-
console.log(`Reset (${mode}) to ${sha}`);
|
|
154
|
-
} catch (e) { console.error(e.message); }
|
|
294
|
+
try { const sha = await resolveCommit(c); const mode = opts.hard? '--hard':'--soft'; await git.raw(['reset', mode, sha]); console.log(`Reset (${mode}) to ${sha}`); }
|
|
295
|
+
catch (e) { console.error('Reset error:', e.message); }
|
|
155
296
|
});
|
|
156
297
|
|
|
157
298
|
program.command('revert <c>').alias('rv')
|
|
158
299
|
.description('Revert commit/index safely')
|
|
159
300
|
.action(async (c) => {
|
|
160
|
-
try {
|
|
161
|
-
|
|
162
|
-
await git.revert(sha);
|
|
163
|
-
console.log(`Reverted ${sha}`);
|
|
164
|
-
} catch (e) { console.error(e.message); }
|
|
301
|
+
try { const sha = await resolveCommit(c); await git.revert(sha); console.log(`Reverted ${sha}`); }
|
|
302
|
+
catch (e) { console.error('Revert error:', e.message); }
|
|
165
303
|
});
|
|
166
304
|
|
|
167
|
-
program.parse(process.argv);
|
|
305
|
+
program.parse(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gims",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Git Made Simple – AI‑powered git helper using Gemini / OpenAI",
|
|
5
5
|
"author": "S41R4J",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@google/genai": "^1.5.1",
|
|
36
|
+
"clipboardy": "^3.0.0",
|
|
36
37
|
"commander": "^11.1.0",
|
|
37
38
|
"openai": "^4.0.0",
|
|
38
39
|
"simple-git": "^3.19.1"
|