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.
Files changed (3) hide show
  1. package/README.md +0 -10
  2. package/bin/gims.js +228 -90
  3. 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
- // Setup AI clients
41
- const hasGemini = !!process.env.GEMINI_API_KEY;
42
- const hasOpenAI = !hasGemini && !!process.env.OPENAI_API_KEY;
43
- let genai, openai;
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
- if (hasGemini) {
46
- genai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
47
- } else if (hasOpenAI) {
48
- openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
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
- async function generateCommitMessage(diff) {
52
- if (hasGemini) {
53
- const res = await genai.models.generateContent({
54
- model: 'gemini-2.0-flash',
55
- contents: `Write a concise git commit message for these changes:\n${diff}`,
56
- });
57
- return res.text.trim();
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
- if (hasOpenAI) {
61
- const res = await openai.chat.completions.create({
62
- model: 'gpt-4o-mini',
63
- messages: [{ role: 'user', content: `Write a concise git commit message for these changes:\n${diff}` }],
64
- temperature: 0.5,
65
- });
66
- return res.choices[0].message.content.trim();
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
- // fallback generic
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 git.log();
76
- const idx = parseInt(input, 10) - 1;
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
- program.name('gims').alias('g').version('0.4.2');
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 git.init();
89
- console.log('Initialized repo.');
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
- // stage all changes first
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
- // get diff of staged changes
99
- const diff = await git.diff(['--cached']);
100
- if (!diff) return console.log('No changes to commit.');
101
- console.log('Generating commit message...');
102
- const msg = await generateCommitMessage(diff);
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
- const diff = await git.diff(['--cached']);
113
- if (!diff) return console.log('No changes to commit.');
114
- console.log('Generating commit message...');
115
- const msg = await generateCommitMessage(diff);
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 git.log();
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 without pager')
274
+ .description('Full numbered git log (oldest → newest)')
130
275
  .action(async () => {
131
- console.log(await git.raw(['--no-pager', 'log']));
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
- const sha = await resolveCommit(c);
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', 'hard reset')
292
+ .option('--hard','hard reset')
148
293
  .action(async (c, opts) => {
149
- try {
150
- const sha = await resolveCommit(c);
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
- const sha = await resolveCommit(c);
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.2",
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"