gims 0.6.6 β 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.
- package/CHANGELOG.md +89 -0
- package/README.md +33 -13
- package/bin/gims.js +848 -105
- package/bin/lib/ai/providers.js +36 -34
- package/bin/lib/git/analyzer.js +118 -47
- package/bin/lib/utils/colors.js +15 -0
- package/bin/lib/utils/intelligence.js +421 -0
- package/bin/lib/utils/progress.js +70 -1
- package/package.json +3 -3
|
@@ -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
|
-
|
|
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.
|
|
4
|
-
"description": "Git Made Simple β AIβpowered git helper
|
|
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
|
+
}
|