gims 0.6.7 β†’ 0.8.1

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.
@@ -0,0 +1,421 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Intelligence utilities for smart git operations
6
+ */
7
+ class Intelligence {
8
+ constructor(git) {
9
+ this.git = git;
10
+ }
11
+
12
+ /**
13
+ * Get file type emoji based on file extension
14
+ */
15
+ static getFileEmoji(filename) {
16
+ const ext = path.extname(filename).toLowerCase();
17
+ const name = path.basename(filename).toLowerCase();
18
+
19
+ // Special files
20
+ if (name === 'package.json' || name === 'package-lock.json') return 'πŸ“¦';
21
+ if (name === 'readme.md' || name === 'readme') return 'πŸ“š';
22
+ if (name === '.gitignore') return 'πŸ™ˆ';
23
+ if (name === '.env' || name.startsWith('.env.')) return 'πŸ”';
24
+ if (name === 'dockerfile' || name === 'docker-compose.yml') return '🐳';
25
+ if (name.includes('config') || name.includes('rc')) return 'βš™οΈ';
26
+ if (name === 'license' || name === 'license.md') return 'πŸ“œ';
27
+
28
+ // Test files
29
+ if (filename.includes('.test.') || filename.includes('.spec.') || filename.includes('__tests__')) return 'πŸ§ͺ';
30
+
31
+ // By extension
32
+ const emojiMap = {
33
+ '.js': 'πŸ“„', '.jsx': 'βš›οΈ', '.ts': 'πŸ“˜', '.tsx': 'βš›οΈ',
34
+ '.css': '🎨', '.scss': '🎨', '.sass': '🎨', '.less': '🎨',
35
+ '.html': '🌐', '.htm': '🌐',
36
+ '.json': 'πŸ“‹', '.yaml': 'πŸ“‹', '.yml': 'πŸ“‹', '.toml': 'πŸ“‹',
37
+ '.md': 'πŸ“', '.mdx': 'πŸ“', '.txt': 'πŸ“„',
38
+ '.py': '🐍', '.rb': 'πŸ’Ž', '.go': 'πŸ”·', '.rs': 'πŸ¦€', '.java': 'β˜•',
39
+ '.sh': '🐚', '.bash': '🐚', '.zsh': '🐚',
40
+ '.sql': 'πŸ—ƒοΈ', '.graphql': 'πŸ”—', '.gql': 'πŸ”—',
41
+ '.png': 'πŸ–ΌοΈ', '.jpg': 'πŸ–ΌοΈ', '.jpeg': 'πŸ–ΌοΈ', '.gif': 'πŸ–ΌοΈ', '.svg': '🎯',
42
+ '.mp3': '🎡', '.wav': '🎡', '.mp4': '🎬', '.mov': '🎬',
43
+ '.zip': 'πŸ“¦', '.tar': 'πŸ“¦', '.gz': 'πŸ“¦',
44
+ '.lock': 'πŸ”’',
45
+ };
46
+
47
+ return emojiMap[ext] || 'πŸ“„';
48
+ }
49
+
50
+ /**
51
+ * Analyze user's commit patterns from history
52
+ */
53
+ async analyzeCommitPatterns(limit = 50) {
54
+ try {
55
+ const log = await this.git.log({ maxCount: limit });
56
+ const commits = log.all;
57
+
58
+ if (commits.length === 0) {
59
+ return { hasHistory: false };
60
+ }
61
+
62
+ // Analyze patterns
63
+ const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)(\(.+\))?:/i;
64
+ const conventionalCommits = commits.filter(c => conventionalPattern.test(c.message));
65
+
66
+ // Calculate average message length
67
+ const avgLength = Math.round(commits.reduce((sum, c) => sum + c.message.split('\n')[0].length, 0) / commits.length);
68
+
69
+ // Detect common scopes
70
+ const scopes = {};
71
+ commits.forEach(c => {
72
+ const match = c.message.match(/^\w+\(([^)]+)\)/);
73
+ if (match) {
74
+ scopes[match[1]] = (scopes[match[1]] || 0) + 1;
75
+ }
76
+ });
77
+ const topScopes = Object.entries(scopes)
78
+ .sort((a, b) => b[1] - a[1])
79
+ .slice(0, 5)
80
+ .map(([scope]) => scope);
81
+
82
+ // Detect if user uses emojis
83
+ const emojiPattern = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]/u;
84
+ const usesEmojis = commits.some(c => emojiPattern.test(c.message));
85
+
86
+ // Detect commit message style (imperative vs past tense)
87
+ const imperativeWords = ['add', 'fix', 'update', 'remove', 'change', 'implement', 'refactor'];
88
+ const pastTenseWords = ['added', 'fixed', 'updated', 'removed', 'changed', 'implemented', 'refactored'];
89
+
90
+ let imperativeCount = 0;
91
+ let pastTenseCount = 0;
92
+ commits.forEach(c => {
93
+ const firstWord = c.message.split(/[\s(:]/)[0].toLowerCase();
94
+ if (imperativeWords.includes(firstWord)) imperativeCount++;
95
+ if (pastTenseWords.includes(firstWord)) pastTenseCount++;
96
+ });
97
+
98
+ return {
99
+ hasHistory: true,
100
+ totalAnalyzed: commits.length,
101
+ conventionalRatio: (conventionalCommits.length / commits.length * 100).toFixed(0),
102
+ usesConventional: conventionalCommits.length > commits.length * 0.5,
103
+ avgMessageLength: avgLength,
104
+ topScopes,
105
+ usesEmojis,
106
+ style: imperativeCount >= pastTenseCount ? 'imperative' : 'past-tense',
107
+ authors: [...new Set(commits.map(c => c.author_name))],
108
+ };
109
+ } catch (error) {
110
+ return { hasHistory: false, error: error.message };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Detect semantic meaning of changes
116
+ */
117
+ async detectSemanticChanges(diff) {
118
+ const changes = {
119
+ type: 'misc',
120
+ labels: [],
121
+ suggestions: [],
122
+ breakingChange: false,
123
+ };
124
+
125
+ if (!diff || !diff.trim()) return changes;
126
+
127
+ const lines = diff.split('\n');
128
+ const addedLines = lines.filter(l => l.startsWith('+') && !l.startsWith('+++'));
129
+ const removedLines = lines.filter(l => l.startsWith('-') && !l.startsWith('---'));
130
+
131
+ // Detect breaking changes
132
+ if (removedLines.some(l => l.includes('export ') && l.includes('function ')) ||
133
+ removedLines.some(l => l.includes('module.exports'))) {
134
+ changes.labels.push('⚠️ Potential breaking change');
135
+ changes.breakingChange = true;
136
+ }
137
+
138
+ // Detect new features
139
+ if (addedLines.some(l => l.includes('export ') && l.includes('function ')) ||
140
+ addedLines.some(l => l.includes('class ')) ||
141
+ addedLines.some(l => l.includes('async function '))) {
142
+ changes.labels.push('✨ New functionality');
143
+ changes.type = 'feat';
144
+ }
145
+
146
+ // Detect bug fixes
147
+ if (addedLines.some(l => l.includes('catch') || l.includes('try {')) ||
148
+ addedLines.some(l => l.includes('|| null') || l.includes('?? ') || l.includes('?.'))) {
149
+ changes.labels.push('πŸ› Error handling');
150
+ if (changes.type === 'misc') changes.type = 'fix';
151
+ }
152
+
153
+ // Detect refactoring
154
+ if (addedLines.length > 20 && removedLines.length > 20 &&
155
+ Math.abs(addedLines.length - removedLines.length) < 10) {
156
+ changes.labels.push('♻️ Refactoring');
157
+ if (changes.type === 'misc') changes.type = 'refactor';
158
+ }
159
+
160
+ // Detect documentation
161
+ if (addedLines.some(l => l.includes('/**') || l.includes('* @') || l.includes('// '))) {
162
+ changes.labels.push('πŸ“ Documentation');
163
+ if (changes.type === 'misc') changes.type = 'docs';
164
+ }
165
+
166
+ // Detect test changes
167
+ if (diff.includes('.test.') || diff.includes('.spec.') ||
168
+ addedLines.some(l => l.includes('describe(') || l.includes('it(') || l.includes('test('))) {
169
+ changes.labels.push('πŸ§ͺ Tests');
170
+ if (changes.type === 'misc') changes.type = 'test';
171
+ }
172
+
173
+ // Detect dependency changes
174
+ if (diff.includes('package.json') && (diff.includes('"dependencies"') || diff.includes('"devDependencies"'))) {
175
+ changes.labels.push('πŸ“¦ Dependencies');
176
+ }
177
+
178
+ // Detect style/formatting
179
+ if (addedLines.every(l => l.trim().length < 3 || l.includes(' ') || l === '+')) {
180
+ changes.labels.push('🎨 Formatting');
181
+ if (changes.type === 'misc') changes.type = 'style';
182
+ }
183
+
184
+ return changes;
185
+ }
186
+
187
+ /**
188
+ * Suggest how to split a large changeset
189
+ */
190
+ async suggestCommitSplit(status) {
191
+ const files = status.files || [];
192
+ if (files.length < 5) return null;
193
+
194
+ const groups = {
195
+ tests: [],
196
+ config: [],
197
+ docs: [],
198
+ styles: [],
199
+ core: [],
200
+ };
201
+
202
+ files.forEach(file => {
203
+ const f = typeof file === 'string' ? file : file.path;
204
+ if (!f) return;
205
+
206
+ if (f.includes('.test.') || f.includes('.spec.') || f.includes('__tests__')) {
207
+ groups.tests.push(f);
208
+ } else if (f.includes('config') || f.endsWith('.json') || f.endsWith('.yml') || f.endsWith('.yaml') || f.includes('rc')) {
209
+ groups.config.push(f);
210
+ } else if (f.endsWith('.md') || f.includes('docs/') || f.includes('README')) {
211
+ groups.docs.push(f);
212
+ } else if (f.endsWith('.css') || f.endsWith('.scss') || f.endsWith('.sass')) {
213
+ groups.styles.push(f);
214
+ } else {
215
+ groups.core.push(f);
216
+ }
217
+ });
218
+
219
+ const suggestions = [];
220
+ if (groups.tests.length > 0) suggestions.push({ type: 'test', files: groups.tests, message: 'test: add/update tests' });
221
+ if (groups.config.length > 0) suggestions.push({ type: 'chore', files: groups.config, message: 'chore: update configuration' });
222
+ if (groups.docs.length > 0) suggestions.push({ type: 'docs', files: groups.docs, message: 'docs: update documentation' });
223
+ if (groups.styles.length > 0) suggestions.push({ type: 'style', files: groups.styles, message: 'style: update styles' });
224
+ if (groups.core.length > 0) suggestions.push({ type: 'feat', files: groups.core, message: 'feat: update core functionality' });
225
+
226
+ return suggestions.length > 1 ? suggestions : null;
227
+ }
228
+
229
+ /**
230
+ * Get session statistics
231
+ */
232
+ async getSessionStats() {
233
+ try {
234
+ const log = await this.git.log({ maxCount: 1 });
235
+ const lastCommit = log.all[0];
236
+
237
+ let timeSinceLastCommit = 'No commits yet';
238
+ let lastCommitMessage = null;
239
+
240
+ if (lastCommit) {
241
+ const lastCommitDate = new Date(lastCommit.date);
242
+ const now = new Date();
243
+ const diffMs = now - lastCommitDate;
244
+ const diffMins = Math.floor(diffMs / 60000);
245
+ const diffHours = Math.floor(diffMins / 60);
246
+ const diffDays = Math.floor(diffHours / 24);
247
+
248
+ if (diffDays > 0) {
249
+ timeSinceLastCommit = `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
250
+ } else if (diffHours > 0) {
251
+ timeSinceLastCommit = `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
252
+ } else if (diffMins > 0) {
253
+ timeSinceLastCommit = `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
254
+ } else {
255
+ timeSinceLastCommit = 'Just now';
256
+ }
257
+
258
+ lastCommitMessage = lastCommit.message.split('\n')[0];
259
+ }
260
+
261
+ // Get today's commits
262
+ const today = new Date();
263
+ today.setHours(0, 0, 0, 0);
264
+ const todayLog = await this.git.log({ '--since': today.toISOString() });
265
+ const todayCommits = todayLog.all.length;
266
+
267
+ return {
268
+ timeSinceLastCommit,
269
+ lastCommitMessage,
270
+ commitsToday: todayCommits,
271
+ };
272
+ } catch (error) {
273
+ return {
274
+ timeSinceLastCommit: 'Unknown',
275
+ lastCommitMessage: null,
276
+ commitsToday: 0,
277
+ };
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Detect branch context for smarter commit messages
283
+ */
284
+ async detectBranchContext() {
285
+ try {
286
+ const branch = await this.git.branch();
287
+ const currentBranch = branch.current;
288
+
289
+ // Common patterns: feat/xyz, fix/xyz, feature/xyz, bugfix/xyz, hotfix/xyz
290
+ const patterns = [
291
+ { regex: /^feat(?:ure)?\/(.+)$/i, type: 'feat', scope: null },
292
+ { regex: /^fix\/(.+)$/i, type: 'fix', scope: null },
293
+ { regex: /^bugfix\/(.+)$/i, type: 'fix', scope: null },
294
+ { regex: /^hotfix\/(.+)$/i, type: 'fix', scope: null },
295
+ { regex: /^docs?\/(.+)$/i, type: 'docs', scope: null },
296
+ { regex: /^refactor\/(.+)$/i, type: 'refactor', scope: null },
297
+ { regex: /^test\/(.+)$/i, type: 'test', scope: null },
298
+ { regex: /^chore\/(.+)$/i, type: 'chore', scope: null },
299
+ { regex: /^style\/(.+)$/i, type: 'style', scope: null },
300
+ ];
301
+
302
+ for (const pattern of patterns) {
303
+ const match = currentBranch.match(pattern.regex);
304
+ if (match) {
305
+ // Extract potential scope and description from branch name
306
+ const branchPart = match[1].replace(/-/g, ' ').replace(/_/g, ' ');
307
+ return {
308
+ branch: currentBranch,
309
+ type: pattern.type,
310
+ description: branchPart,
311
+ detected: true,
312
+ };
313
+ }
314
+ }
315
+
316
+ // Check for issue references like PROJ-123 or #123
317
+ const issueMatch = currentBranch.match(/([A-Z]+-\d+|#\d+)/i);
318
+ if (issueMatch) {
319
+ return {
320
+ branch: currentBranch,
321
+ issueRef: issueMatch[1],
322
+ detected: true,
323
+ };
324
+ }
325
+
326
+ return {
327
+ branch: currentBranch,
328
+ detected: false,
329
+ };
330
+ } catch (error) {
331
+ return { branch: 'unknown', detected: false };
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Get commit statistics for the user
337
+ */
338
+ async getCommitStats(days = 30) {
339
+ try {
340
+ const since = new Date();
341
+ since.setDate(since.getDate() - days);
342
+
343
+ const log = await this.git.log({ '--since': since.toISOString() });
344
+ const commits = log.all;
345
+
346
+ if (commits.length === 0) {
347
+ return { totalCommits: 0, hasData: false };
348
+ }
349
+
350
+ // Group by day
351
+ const byDay = {};
352
+ commits.forEach(c => {
353
+ const day = new Date(c.date).toLocaleDateString();
354
+ byDay[day] = (byDay[day] || 0) + 1;
355
+ });
356
+
357
+ // Calculate streaks
358
+ const sortedDays = Object.keys(byDay).sort((a, b) => new Date(b) - new Date(a));
359
+ let currentStreak = 0;
360
+ let longestStreak = 0;
361
+ let tempStreak = 0;
362
+
363
+ const today = new Date().toLocaleDateString();
364
+ const yesterday = new Date(Date.now() - 86400000).toLocaleDateString();
365
+
366
+ // Check if today or yesterday has commits
367
+ if (byDay[today] || byDay[yesterday]) {
368
+ sortedDays.forEach((day, i) => {
369
+ if (i === 0) {
370
+ tempStreak = 1;
371
+ } else {
372
+ const prevDate = new Date(sortedDays[i - 1]);
373
+ const currDate = new Date(day);
374
+ const diffDays = Math.round((prevDate - currDate) / 86400000);
375
+ if (diffDays === 1) {
376
+ tempStreak++;
377
+ } else {
378
+ if (tempStreak > longestStreak) longestStreak = tempStreak;
379
+ tempStreak = 1;
380
+ }
381
+ }
382
+ });
383
+ if (tempStreak > longestStreak) longestStreak = tempStreak;
384
+ currentStreak = byDay[today] ? tempStreak : (byDay[yesterday] ? tempStreak : 0);
385
+ }
386
+
387
+ // Commit types breakdown
388
+ const typeBreakdown = { feat: 0, fix: 0, docs: 0, style: 0, refactor: 0, test: 0, chore: 0, other: 0 };
389
+ const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)/i;
390
+
391
+ commits.forEach(c => {
392
+ const match = c.message.match(conventionalPattern);
393
+ if (match) {
394
+ const type = match[1].toLowerCase();
395
+ if (typeBreakdown.hasOwnProperty(type)) {
396
+ typeBreakdown[type]++;
397
+ } else {
398
+ typeBreakdown.other++;
399
+ }
400
+ } else {
401
+ typeBreakdown.other++;
402
+ }
403
+ });
404
+
405
+ return {
406
+ hasData: true,
407
+ totalCommits: commits.length,
408
+ daysActive: Object.keys(byDay).length,
409
+ avgPerDay: (commits.length / days).toFixed(1),
410
+ currentStreak,
411
+ longestStreak,
412
+ typeBreakdown,
413
+ topDay: Object.entries(byDay).sort((a, b) => b[1] - a[1])[0],
414
+ };
415
+ } catch (error) {
416
+ return { hasData: false, error: error.message };
417
+ }
418
+ }
419
+ }
420
+
421
+ module.exports = { Intelligence };
@@ -7,8 +7,11 @@ class Progress {
7
7
  static spinner = ['β ‹', 'β ™', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ‡', '⠏'];
8
8
  static current = 0;
9
9
  static interval = null;
10
+ static startTime = null;
10
11
 
11
12
  static start(message) {
13
+ this.startTime = Date.now();
14
+ this.lastMessage = message;
12
15
  process.stdout.write(`${message} ${this.spinner[0]}`);
13
16
  this.interval = setInterval(() => {
14
17
  this.current = (this.current + 1) % this.spinner.length;
@@ -21,7 +24,25 @@ class Progress {
21
24
  clearInterval(this.interval);
22
25
  this.interval = null;
23
26
  }
24
- process.stdout.write(`\r${finalMessage}\n`);
27
+ const elapsed = this.getElapsed();
28
+ const elapsedStr = elapsed ? ` ${color.dim(`(${elapsed})`)}` : '';
29
+ // Clear the entire line before writing final message
30
+ const clearLine = '\r\x1b[K'; // Carriage return + clear to end of line
31
+ if (finalMessage) {
32
+ process.stdout.write(`${clearLine}${finalMessage}${elapsedStr}\n`);
33
+ } else {
34
+ // Just clear the spinner line completely
35
+ process.stdout.write(`${clearLine}`);
36
+ }
37
+ this.startTime = null;
38
+ this.lastMessage = null;
39
+ }
40
+
41
+ static getElapsed() {
42
+ if (!this.startTime) return null;
43
+ const ms = Date.now() - this.startTime;
44
+ if (ms < 1000) return `${ms}ms`;
45
+ return `${(ms / 1000).toFixed(1)}s`;
25
46
  }
26
47
 
27
48
  static success(message) {
@@ -44,6 +65,54 @@ class Progress {
44
65
  const progress = `[${step}/${total}]`;
45
66
  console.log(`${color.dim(progress)} ${message}`);
46
67
  }
68
+
69
+ static cached(message) {
70
+ console.log(`${color.magenta('⚑')} ${message} ${color.dim('(cached)')}`);
71
+ }
72
+
73
+ static tip(message) {
74
+ console.log(`${color.blue('πŸ’‘')} ${color.dim('Tip:')} ${message}`);
75
+ }
76
+
77
+ static box(title, lines) {
78
+ const maxLen = Math.max(title.length, ...lines.map(l => l.replace(/\x1b\[[0-9;]*m/g, '').length));
79
+ const top = `β”Œ${'─'.repeat(maxLen + 2)}┐`;
80
+ const bottom = `β””${'─'.repeat(maxLen + 2)}β”˜`;
81
+ const titleLine = `β”‚ ${color.bold(title.padEnd(maxLen))} β”‚`;
82
+
83
+ console.log(top);
84
+ console.log(titleLine);
85
+ console.log(`β”œ${'─'.repeat(maxLen + 2)}─`);
86
+ lines.forEach(line => {
87
+ const plainLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
88
+ const padding = ' '.repeat(maxLen - plainLen);
89
+ console.log(`β”‚ ${line}${padding} β”‚`);
90
+ });
91
+ console.log(bottom);
92
+ }
93
+
94
+ // Random tips for contextual help
95
+ static tips = [
96
+ 'Use `g int` for interactive commit wizard with multiple AI suggestions',
97
+ 'Use `g o` to commit and push in one command',
98
+ 'Use `g sg --multiple` to get 3 different commit message suggestions',
99
+ 'Configure your preferences with `g config --set key=value`',
100
+ 'Use `--all` flag to auto-stage all changes',
101
+ 'Use `g a` to amend your last commit',
102
+ 'Use `g sync --rebase` for cleaner history',
103
+ 'Set GEMINI_API_KEY for free AI-powered commits',
104
+ 'Use `g wip` for quick work-in-progress commits',
105
+ 'Use `g stats` to see your commit statistics',
106
+ ];
107
+
108
+ static showRandomTip() {
109
+ const shouldShow = Math.random() < 0.15; // 15% chance
110
+ if (shouldShow) {
111
+ const tip = this.tips[Math.floor(Math.random() * this.tips.length)];
112
+ console.log();
113
+ this.tip(tip);
114
+ }
115
+ }
47
116
  }
48
117
 
49
118
  module.exports = { Progress };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gims",
3
- "version": "0.6.7",
4
- "description": "Git Made Simple – AI‑powered git helper using Gemini / OpenAI",
3
+ "version": "0.8.1",
4
+ "description": "Git Made Simple – AI‑powered git helper with smart insights, stats & code review",
5
5
  "author": "S41R4J",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -43,4 +43,4 @@
43
43
  "test": "echo \"Enhanced GIMS v$(node -p \"require('./package.json').version\") - All systems operational!\"",
44
44
  "postinstall": "echo \"πŸš€ GIMS installed! Quick start: 'g setup --api-key gemini' then 'g s' to see status. Full help: 'g --help'\""
45
45
  }
46
- }
46
+ }