gims 0.6.7 → 0.8.2
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 +80 -0
- package/README.md +280 -40
- package/bin/gims.js +834 -103
- package/bin/lib/ai/providers.js +58 -51
- 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
package/bin/lib/ai/providers.js
CHANGED
|
@@ -15,22 +15,25 @@ class AIProviderManager {
|
|
|
15
15
|
|
|
16
16
|
resolveProvider(preference = 'auto') {
|
|
17
17
|
if (preference === 'none') return 'none';
|
|
18
|
-
if (preference === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : 'none';
|
|
19
|
-
if (preference === 'gemini') return process.env.GEMINI_API_KEY ? 'gemini' : 'none';
|
|
20
|
-
if (preference === 'groq') return process.env.GROQ_API_KEY ? 'groq' : 'none';
|
|
21
18
|
|
|
22
|
-
//
|
|
19
|
+
// Check if preferred provider's key is available
|
|
20
|
+
if (preference === 'openai' && process.env.OPENAI_API_KEY) return 'openai';
|
|
21
|
+
if (preference === 'gemini' && process.env.GEMINI_API_KEY) return 'gemini';
|
|
22
|
+
if (preference === 'groq' && process.env.GROQ_API_KEY) return 'groq';
|
|
23
|
+
|
|
24
|
+
// Fallback: try any available provider (priority: Gemini → OpenAI → Groq)
|
|
23
25
|
if (process.env.GEMINI_API_KEY) return 'gemini';
|
|
24
26
|
if (process.env.OPENAI_API_KEY) return 'openai';
|
|
25
27
|
if (process.env.GROQ_API_KEY) return 'groq';
|
|
28
|
+
|
|
26
29
|
return 'none';
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
getDefaultModel(provider) {
|
|
30
33
|
const defaults = {
|
|
31
|
-
'gemini': 'gemini-
|
|
32
|
-
'openai': 'gpt-5',
|
|
33
|
-
'groq': 'groq/compound'
|
|
34
|
+
'gemini': 'gemini-3-flash-preview', // Latest Gemini model
|
|
35
|
+
'openai': 'gpt-5.2-2025-12-11', // Latest GPT model
|
|
36
|
+
'groq': 'groq/compound' // Latest Groq model
|
|
34
37
|
};
|
|
35
38
|
return defaults[provider] || '';
|
|
36
39
|
}
|
|
@@ -48,12 +51,12 @@ class AIProviderManager {
|
|
|
48
51
|
|
|
49
52
|
setCache(cacheKey, result, usedLocal = false) {
|
|
50
53
|
if (!this.config.cacheEnabled) return;
|
|
51
|
-
|
|
54
|
+
|
|
52
55
|
if (this.cache.size >= this.maxCacheSize) {
|
|
53
56
|
const firstKey = this.cache.keys().next().value;
|
|
54
57
|
this.cache.delete(firstKey);
|
|
55
58
|
}
|
|
56
|
-
|
|
59
|
+
|
|
57
60
|
this.cache.set(cacheKey, {
|
|
58
61
|
result,
|
|
59
62
|
usedLocal,
|
|
@@ -67,11 +70,11 @@ class AIProviderManager {
|
|
|
67
70
|
try {
|
|
68
71
|
switch (provider) {
|
|
69
72
|
case 'gemini':
|
|
70
|
-
return await this.generateWithGemini(prompt, model || 'gemini
|
|
73
|
+
return await this.generateWithGemini(prompt, model || this.getDefaultModel('gemini'), options);
|
|
71
74
|
case 'openai':
|
|
72
|
-
return await this.generateWithOpenAI(prompt, model || '
|
|
75
|
+
return await this.generateWithOpenAI(prompt, model || this.getDefaultModel('openai'), options);
|
|
73
76
|
case 'groq':
|
|
74
|
-
return await this.generateWithGroq(prompt, model ||
|
|
77
|
+
return await this.generateWithGroq(prompt, model || this.getDefaultModel('groq'), options);
|
|
75
78
|
default:
|
|
76
79
|
throw new Error(`Unknown provider: ${provider}`);
|
|
77
80
|
}
|
|
@@ -103,9 +106,9 @@ class AIProviderManager {
|
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
async generateWithGroq(prompt, model, options) {
|
|
106
|
-
const groq = new OpenAI({
|
|
107
|
-
apiKey: process.env.GROQ_API_KEY,
|
|
108
|
-
baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1'
|
|
109
|
+
const groq = new OpenAI({
|
|
110
|
+
apiKey: process.env.GROQ_API_KEY,
|
|
111
|
+
baseURL: process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1'
|
|
109
112
|
});
|
|
110
113
|
const actualModel = model || this.getDefaultModel('groq');
|
|
111
114
|
const response = await groq.chat.completions.create({
|
|
@@ -118,11 +121,11 @@ class AIProviderManager {
|
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
async generateCommitMessage(diff, options = {}) {
|
|
121
|
-
const {
|
|
122
|
-
provider: preferredProvider = 'auto',
|
|
123
|
-
conventional = false,
|
|
124
|
+
const {
|
|
125
|
+
provider: preferredProvider = this.config.provider || 'auto',
|
|
126
|
+
conventional = this.config.conventional || false,
|
|
124
127
|
body = false,
|
|
125
|
-
verbose = false
|
|
128
|
+
verbose = false
|
|
126
129
|
} = options;
|
|
127
130
|
|
|
128
131
|
// Check cache first
|
|
@@ -134,11 +137,9 @@ class AIProviderManager {
|
|
|
134
137
|
}
|
|
135
138
|
|
|
136
139
|
const providerChain = this.buildProviderChain(preferredProvider);
|
|
137
|
-
|
|
140
|
+
|
|
138
141
|
for (const provider of providerChain) {
|
|
139
142
|
try {
|
|
140
|
-
if (verbose) Progress.info(`Trying provider: ${provider}`);
|
|
141
|
-
|
|
142
143
|
if (provider === 'local') {
|
|
143
144
|
const result = await this.generateLocalHeuristic(diff, options);
|
|
144
145
|
this.setCache(cacheKey, result, true);
|
|
@@ -148,10 +149,10 @@ class AIProviderManager {
|
|
|
148
149
|
const prompt = this.buildPrompt(diff, { conventional, body });
|
|
149
150
|
const result = await this.generateWithProvider(provider, prompt, options);
|
|
150
151
|
const cleaned = this.cleanCommitMessage(result, { body });
|
|
151
|
-
|
|
152
|
+
|
|
152
153
|
this.setCache(cacheKey, cleaned, false);
|
|
153
154
|
return { message: cleaned, usedLocal: false };
|
|
154
|
-
|
|
155
|
+
|
|
155
156
|
} catch (error) {
|
|
156
157
|
if (verbose) Progress.warning(`${provider} failed: ${error.message}`);
|
|
157
158
|
continue;
|
|
@@ -166,37 +167,41 @@ class AIProviderManager {
|
|
|
166
167
|
|
|
167
168
|
buildProviderChain(preferred) {
|
|
168
169
|
const available = [];
|
|
169
|
-
|
|
170
|
+
|
|
171
|
+
// First, try the preferred provider if its key is available
|
|
170
172
|
if (preferred !== 'auto' && preferred !== 'none') {
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
if (process.env.GEMINI_API_KEY) available.push('gemini');
|
|
175
|
-
if (process.env.OPENAI_API_KEY) available.push('openai');
|
|
176
|
-
if (process.env.GROQ_API_KEY) available.push('groq');
|
|
173
|
+
if (preferred === 'gemini' && process.env.GEMINI_API_KEY) available.push('gemini');
|
|
174
|
+
else if (preferred === 'openai' && process.env.OPENAI_API_KEY) available.push('openai');
|
|
175
|
+
else if (preferred === 'groq' && process.env.GROQ_API_KEY) available.push('groq');
|
|
177
176
|
}
|
|
178
|
-
|
|
177
|
+
|
|
178
|
+
// Then add all other available providers as fallbacks
|
|
179
|
+
if (process.env.GEMINI_API_KEY && !available.includes('gemini')) available.push('gemini');
|
|
180
|
+
if (process.env.OPENAI_API_KEY && !available.includes('openai')) available.push('openai');
|
|
181
|
+
if (process.env.GROQ_API_KEY && !available.includes('groq')) available.push('groq');
|
|
182
|
+
|
|
183
|
+
// Local heuristics as final fallback
|
|
179
184
|
available.push('local');
|
|
180
|
-
return
|
|
185
|
+
return available;
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
buildPrompt(diff, options) {
|
|
184
189
|
const { conventional, body } = options;
|
|
185
|
-
|
|
186
|
-
const style = conventional
|
|
190
|
+
|
|
191
|
+
const style = conventional
|
|
187
192
|
? 'Use Conventional Commits format (e.g., feat:, fix:, chore:) for the subject.'
|
|
188
193
|
: 'Subject must be a single short line.';
|
|
189
|
-
|
|
190
|
-
const bodyInstr = body
|
|
194
|
+
|
|
195
|
+
const bodyInstr = body
|
|
191
196
|
? 'Provide a short subject line followed by an optional body separated by a blank line.'
|
|
192
197
|
: 'Return only a short subject line without extra quotes.';
|
|
193
|
-
|
|
198
|
+
|
|
194
199
|
return `Write a concise git commit message for these changes:\n${diff}\n\n${style} ${bodyInstr}`;
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
cleanCommitMessage(message, options = {}) {
|
|
198
203
|
if (!message) return 'Update project code';
|
|
199
|
-
|
|
204
|
+
|
|
200
205
|
// Remove markdown formatting
|
|
201
206
|
let cleaned = message
|
|
202
207
|
.replace(/```[\s\S]*?```/g, '')
|
|
@@ -212,7 +217,7 @@ class AIProviderManager {
|
|
|
212
217
|
|
|
213
218
|
const lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
|
|
214
219
|
let subject = (lines[0] || '').replace(/\s{2,}/g, ' ').replace(/[\s:,.!;]+$/g, '').trim();
|
|
215
|
-
|
|
220
|
+
|
|
216
221
|
if (subject.length === 0) subject = 'Update project code';
|
|
217
222
|
// No length restriction - allow AI to generate full commit messages
|
|
218
223
|
|
|
@@ -226,16 +231,16 @@ class AIProviderManager {
|
|
|
226
231
|
async generateLocalHeuristic(diff, options) {
|
|
227
232
|
// This would need access to git status - simplified version
|
|
228
233
|
const { conventional = false } = options;
|
|
229
|
-
|
|
234
|
+
|
|
230
235
|
// Analyze diff for patterns
|
|
231
236
|
const lines = diff.split('\n');
|
|
232
237
|
const additions = lines.filter(l => l.startsWith('+')).length;
|
|
233
238
|
const deletions = lines.filter(l => l.startsWith('-')).length;
|
|
234
239
|
const files = (diff.match(/diff --git/g) || []).length;
|
|
235
|
-
|
|
240
|
+
|
|
236
241
|
let type = 'chore';
|
|
237
242
|
let subject = 'update files';
|
|
238
|
-
|
|
243
|
+
|
|
239
244
|
if (additions > deletions * 2) {
|
|
240
245
|
type = 'feat';
|
|
241
246
|
subject = files === 1 ? 'add new functionality' : `add features to ${files} files`;
|
|
@@ -249,37 +254,39 @@ class AIProviderManager {
|
|
|
249
254
|
type = 'docs';
|
|
250
255
|
subject = 'update documentation';
|
|
251
256
|
}
|
|
252
|
-
|
|
257
|
+
|
|
253
258
|
return conventional ? `${type}: ${subject}` : subject.charAt(0).toUpperCase() + subject.slice(1);
|
|
254
259
|
}
|
|
255
260
|
|
|
256
261
|
async generateMultipleSuggestions(diff, options = {}, count = 3) {
|
|
257
262
|
const suggestions = [];
|
|
258
263
|
const baseOptions = { ...options };
|
|
259
|
-
|
|
264
|
+
|
|
260
265
|
// Generate different styles
|
|
261
266
|
const variants = [
|
|
262
267
|
{ ...baseOptions, conventional: false },
|
|
263
268
|
{ ...baseOptions, conventional: true },
|
|
264
269
|
{ ...baseOptions, conventional: true, body: true }
|
|
265
270
|
];
|
|
266
|
-
|
|
271
|
+
|
|
267
272
|
for (let i = 0; i < Math.min(count, variants.length); i++) {
|
|
268
273
|
try {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
const result = await this.generateCommitMessage(diff, variants[i]);
|
|
275
|
+
// Extract message string from result object
|
|
276
|
+
const message = result.message || result;
|
|
277
|
+
if (message && !suggestions.includes(message)) {
|
|
278
|
+
suggestions.push(message);
|
|
272
279
|
}
|
|
273
280
|
} catch (error) {
|
|
274
281
|
// Skip failed generations
|
|
275
282
|
}
|
|
276
283
|
}
|
|
277
|
-
|
|
284
|
+
|
|
278
285
|
// Ensure we have at least one suggestion
|
|
279
286
|
if (suggestions.length === 0) {
|
|
280
287
|
suggestions.push('Update project files');
|
|
281
288
|
}
|
|
282
|
-
|
|
289
|
+
|
|
283
290
|
return suggestions;
|
|
284
291
|
}
|
|
285
292
|
}
|
package/bin/lib/git/analyzer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { color } = require('../utils/colors');
|
|
2
|
+
const { Intelligence } = require('../utils/intelligence');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Enhanced git analysis and insights
|
|
@@ -6,17 +7,22 @@ const { color } = require('../utils/colors');
|
|
|
6
7
|
class GitAnalyzer {
|
|
7
8
|
constructor(git) {
|
|
8
9
|
this.git = git;
|
|
10
|
+
this.intelligence = new Intelligence(git);
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
async getEnhancedStatus() {
|
|
12
14
|
try {
|
|
13
15
|
const status = await this.git.status();
|
|
14
16
|
const insights = await this.generateStatusInsights(status);
|
|
15
|
-
|
|
17
|
+
const sessionStats = await this.intelligence.getSessionStats();
|
|
18
|
+
const branchContext = await this.intelligence.detectBranchContext();
|
|
19
|
+
|
|
16
20
|
return {
|
|
17
21
|
...status,
|
|
18
22
|
insights,
|
|
19
|
-
summary: this.generateStatusSummary(status)
|
|
23
|
+
summary: this.generateStatusSummary(status),
|
|
24
|
+
sessionStats,
|
|
25
|
+
branchContext,
|
|
20
26
|
};
|
|
21
27
|
} catch (error) {
|
|
22
28
|
throw new Error(`Failed to get git status: ${error.message}`);
|
|
@@ -32,47 +38,48 @@ class GitAnalyzer {
|
|
|
32
38
|
const untracked = Array.isArray(status.not_added) ? status.not_added : [];
|
|
33
39
|
|
|
34
40
|
if (files.length === 0) return 'Working tree clean';
|
|
35
|
-
|
|
41
|
+
|
|
36
42
|
const parts = [];
|
|
37
43
|
if (staged.length > 0) parts.push(`${staged.length} staged`);
|
|
38
44
|
if (modified.length > 0) parts.push(`${modified.length} modified`);
|
|
39
45
|
if (created.length > 0) parts.push(`${created.length} new`);
|
|
40
46
|
if (deleted.length > 0) parts.push(`${deleted.length} deleted`);
|
|
41
47
|
if (untracked.length > 0) parts.push(`${untracked.length} untracked`);
|
|
42
|
-
|
|
48
|
+
|
|
43
49
|
return parts.join(', ');
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
async generateStatusInsights(status) {
|
|
47
53
|
const insights = [];
|
|
48
|
-
|
|
54
|
+
|
|
49
55
|
// Ensure arrays exist and have proper methods
|
|
50
56
|
const modified = Array.isArray(status.modified) ? status.modified : [];
|
|
51
57
|
const created = Array.isArray(status.created) ? status.created : [];
|
|
52
58
|
const deleted = Array.isArray(status.deleted) ? status.deleted : [];
|
|
53
59
|
const files = Array.isArray(status.files) ? status.files : [];
|
|
54
|
-
|
|
60
|
+
const staged = Array.isArray(status.staged) ? status.staged : [];
|
|
61
|
+
|
|
55
62
|
// Check for common patterns
|
|
56
63
|
if (modified.some(f => String(f).includes('package.json'))) {
|
|
57
64
|
insights.push('📦 Dependencies may have changed - consider updating package-lock.json');
|
|
58
65
|
}
|
|
59
|
-
|
|
66
|
+
|
|
60
67
|
if (created.some(f => String(f).includes('.env'))) {
|
|
61
68
|
insights.push('🔐 New environment file detected - ensure it\'s in .gitignore');
|
|
62
69
|
}
|
|
63
|
-
|
|
70
|
+
|
|
64
71
|
if (modified.some(f => String(f).includes('README'))) {
|
|
65
72
|
insights.push('📚 Documentation updated - good practice!');
|
|
66
73
|
}
|
|
67
|
-
|
|
74
|
+
|
|
68
75
|
if (deleted.length > created.length + modified.length) {
|
|
69
76
|
insights.push('🧹 Cleanup operation detected - removing more than adding');
|
|
70
77
|
}
|
|
71
|
-
|
|
78
|
+
|
|
72
79
|
if (files.length > 20) {
|
|
73
|
-
insights.push('📊 Large changeset - consider
|
|
80
|
+
insights.push('📊 Large changeset - consider using `g split` to break into smaller commits');
|
|
74
81
|
}
|
|
75
|
-
|
|
82
|
+
|
|
76
83
|
// Check for test files
|
|
77
84
|
const testFiles = files.filter(f => {
|
|
78
85
|
const fileName = String(f);
|
|
@@ -81,7 +88,7 @@ class GitAnalyzer {
|
|
|
81
88
|
if (testFiles.length > 0) {
|
|
82
89
|
insights.push('🧪 Test files modified - great for code quality!');
|
|
83
90
|
}
|
|
84
|
-
|
|
91
|
+
|
|
85
92
|
// Check for config files
|
|
86
93
|
const configFiles = files.filter(f => {
|
|
87
94
|
const fileName = String(f);
|
|
@@ -90,7 +97,12 @@ class GitAnalyzer {
|
|
|
90
97
|
if (configFiles.length > 0) {
|
|
91
98
|
insights.push('⚙️ Configuration changes detected');
|
|
92
99
|
}
|
|
93
|
-
|
|
100
|
+
|
|
101
|
+
// Suggest staging if nothing staged
|
|
102
|
+
if (staged.length === 0 && files.length > 0) {
|
|
103
|
+
insights.push('💡 Nothing staged yet - use `g o --all` to stage and commit everything');
|
|
104
|
+
}
|
|
105
|
+
|
|
94
106
|
return insights;
|
|
95
107
|
}
|
|
96
108
|
|
|
@@ -98,7 +110,11 @@ class GitAnalyzer {
|
|
|
98
110
|
try {
|
|
99
111
|
const log = await this.git.log({ maxCount: limit });
|
|
100
112
|
const commits = log.all;
|
|
101
|
-
|
|
113
|
+
|
|
114
|
+
if (commits.length === 0) {
|
|
115
|
+
return { totalCommits: 0 };
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
const analysis = {
|
|
103
119
|
totalCommits: commits.length,
|
|
104
120
|
authors: [...new Set(commits.map(c => c.author_name))],
|
|
@@ -106,7 +122,7 @@ class GitAnalyzer {
|
|
|
106
122
|
conventionalCommits: commits.filter(c => /^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?:/.test(c.message)).length,
|
|
107
123
|
recentActivity: this.analyzeRecentActivity(commits)
|
|
108
124
|
};
|
|
109
|
-
|
|
125
|
+
|
|
110
126
|
return analysis;
|
|
111
127
|
} catch (error) {
|
|
112
128
|
return { error: error.message };
|
|
@@ -117,10 +133,10 @@ class GitAnalyzer {
|
|
|
117
133
|
const now = new Date();
|
|
118
134
|
const oneDayAgo = new Date(now - 24 * 60 * 60 * 1000);
|
|
119
135
|
const oneWeekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000);
|
|
120
|
-
|
|
136
|
+
|
|
121
137
|
const recentCommits = commits.filter(c => new Date(c.date) > oneDayAgo);
|
|
122
138
|
const weeklyCommits = commits.filter(c => new Date(c.date) > oneWeekAgo);
|
|
123
|
-
|
|
139
|
+
|
|
124
140
|
return {
|
|
125
141
|
last24h: recentCommits.length,
|
|
126
142
|
lastWeek: weeklyCommits.length,
|
|
@@ -133,16 +149,20 @@ class GitAnalyzer {
|
|
|
133
149
|
const additions = lines.filter(l => l.startsWith('+')).length;
|
|
134
150
|
const deletions = lines.filter(l => l.startsWith('-')).length;
|
|
135
151
|
const files = (diff.match(/diff --git/g) || []).length;
|
|
136
|
-
|
|
152
|
+
|
|
137
153
|
let complexity = 'simple';
|
|
154
|
+
let emoji = '🟢';
|
|
138
155
|
if (files > 10 || additions + deletions > 500) {
|
|
139
156
|
complexity = 'complex';
|
|
157
|
+
emoji = '🔴';
|
|
140
158
|
} else if (files > 5 || additions + deletions > 100) {
|
|
141
159
|
complexity = 'moderate';
|
|
160
|
+
emoji = '🟡';
|
|
142
161
|
}
|
|
143
|
-
|
|
162
|
+
|
|
144
163
|
return {
|
|
145
164
|
complexity,
|
|
165
|
+
emoji,
|
|
146
166
|
files,
|
|
147
167
|
additions,
|
|
148
168
|
deletions,
|
|
@@ -151,81 +171,132 @@ class GitAnalyzer {
|
|
|
151
171
|
}
|
|
152
172
|
|
|
153
173
|
formatStatusOutput(enhancedStatus) {
|
|
154
|
-
const {
|
|
155
|
-
files = [],
|
|
156
|
-
staged = [],
|
|
157
|
-
modified = [],
|
|
158
|
-
created = [],
|
|
159
|
-
deleted = [],
|
|
160
|
-
not_added = [],
|
|
161
|
-
insights = [],
|
|
162
|
-
summary = 'Unknown status'
|
|
174
|
+
const {
|
|
175
|
+
files = [],
|
|
176
|
+
staged = [],
|
|
177
|
+
modified = [],
|
|
178
|
+
created = [],
|
|
179
|
+
deleted = [],
|
|
180
|
+
not_added = [],
|
|
181
|
+
insights = [],
|
|
182
|
+
summary = 'Unknown status',
|
|
183
|
+
sessionStats = {},
|
|
184
|
+
branchContext = {},
|
|
163
185
|
} = enhancedStatus;
|
|
164
|
-
|
|
186
|
+
|
|
165
187
|
let output = '';
|
|
166
|
-
|
|
167
|
-
// Header
|
|
168
|
-
output += `${color.bold('Git Status')}
|
|
169
|
-
|
|
170
|
-
|
|
188
|
+
|
|
189
|
+
// Header with branch info
|
|
190
|
+
output += `${color.bold('Git Status')}`;
|
|
191
|
+
if (branchContext.branch) {
|
|
192
|
+
output += ` ${color.dim('on')} ${color.cyan(branchContext.branch)}`;
|
|
193
|
+
if (branchContext.type) {
|
|
194
|
+
output += ` ${color.dim(`(${branchContext.type})`)}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
output += '\n';
|
|
198
|
+
output += `${color.dim(summary)}\n`;
|
|
199
|
+
|
|
200
|
+
// Session stats
|
|
201
|
+
if (sessionStats.timeSinceLastCommit) {
|
|
202
|
+
output += `${color.dim(`Last commit: ${sessionStats.timeSinceLastCommit}`)}`;
|
|
203
|
+
if (sessionStats.commitsToday > 0) {
|
|
204
|
+
output += `${color.dim(` • ${sessionStats.commitsToday} commit${sessionStats.commitsToday > 1 ? 's' : ''} today`)}`;
|
|
205
|
+
}
|
|
206
|
+
output += '\n';
|
|
207
|
+
}
|
|
208
|
+
output += '\n';
|
|
209
|
+
|
|
171
210
|
// Staged changes
|
|
172
211
|
if (staged.length > 0) {
|
|
173
212
|
output += `${color.green('Staged for commit:')}\n`;
|
|
174
213
|
staged.forEach(file => {
|
|
175
|
-
|
|
214
|
+
const emoji = Intelligence.getFileEmoji(file);
|
|
215
|
+
output += ` ${color.green('+')} ${emoji} ${file}\n`;
|
|
176
216
|
});
|
|
177
217
|
output += '\n';
|
|
178
218
|
}
|
|
179
|
-
|
|
219
|
+
|
|
180
220
|
// Modified files
|
|
181
221
|
if (modified.length > 0) {
|
|
182
222
|
output += `${color.yellow('Modified (not staged):')}\n`;
|
|
183
223
|
modified.forEach(file => {
|
|
184
|
-
|
|
224
|
+
const emoji = Intelligence.getFileEmoji(file);
|
|
225
|
+
output += ` ${color.yellow('M')} ${emoji} ${file}\n`;
|
|
185
226
|
});
|
|
186
227
|
output += '\n';
|
|
187
228
|
}
|
|
188
|
-
|
|
229
|
+
|
|
189
230
|
// New files
|
|
190
231
|
if (created.length > 0) {
|
|
191
232
|
output += `${color.cyan('New files:')}\n`;
|
|
192
233
|
created.forEach(file => {
|
|
193
|
-
|
|
234
|
+
const emoji = Intelligence.getFileEmoji(file);
|
|
235
|
+
output += ` ${color.cyan('N')} ${emoji} ${file}\n`;
|
|
194
236
|
});
|
|
195
237
|
output += '\n';
|
|
196
238
|
}
|
|
197
|
-
|
|
239
|
+
|
|
198
240
|
// Deleted files
|
|
199
241
|
if (deleted.length > 0) {
|
|
200
242
|
output += `${color.red('Deleted:')}\n`;
|
|
201
243
|
deleted.forEach(file => {
|
|
202
|
-
|
|
244
|
+
const emoji = Intelligence.getFileEmoji(file);
|
|
245
|
+
output += ` ${color.red('D')} ${emoji} ${file}\n`;
|
|
203
246
|
});
|
|
204
247
|
output += '\n';
|
|
205
248
|
}
|
|
206
|
-
|
|
249
|
+
|
|
207
250
|
// Untracked files
|
|
208
251
|
if (not_added.length > 0) {
|
|
209
252
|
output += `${color.dim('Untracked files:')}\n`;
|
|
210
253
|
not_added.slice(0, 10).forEach(file => {
|
|
211
|
-
|
|
254
|
+
const emoji = Intelligence.getFileEmoji(file);
|
|
255
|
+
output += ` ${color.dim('?')} ${emoji} ${file}\n`;
|
|
212
256
|
});
|
|
213
257
|
if (not_added.length > 10) {
|
|
214
258
|
output += ` ${color.dim(`... and ${not_added.length - 10} more`)}\n`;
|
|
215
259
|
}
|
|
216
260
|
output += '\n';
|
|
217
261
|
}
|
|
218
|
-
|
|
262
|
+
|
|
219
263
|
// AI Insights
|
|
220
264
|
if (insights.length > 0) {
|
|
221
|
-
output += `${color.cyan('💡
|
|
265
|
+
output += `${color.cyan('💡 Insights:')}\n`;
|
|
222
266
|
insights.forEach(insight => {
|
|
223
267
|
output += ` ${insight}\n`;
|
|
224
268
|
});
|
|
225
269
|
}
|
|
226
|
-
|
|
270
|
+
|
|
227
271
|
return output;
|
|
228
272
|
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get today's commits formatted nicely
|
|
276
|
+
*/
|
|
277
|
+
async getTodayCommits() {
|
|
278
|
+
try {
|
|
279
|
+
const today = new Date();
|
|
280
|
+
today.setHours(0, 0, 0, 0);
|
|
281
|
+
const log = await this.git.log({ '--since': today.toISOString() });
|
|
282
|
+
return log.all;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Format commit for display
|
|
290
|
+
*/
|
|
291
|
+
formatCommit(commit, index) {
|
|
292
|
+
const hash = color.yellow(commit.hash.substring(0, 7));
|
|
293
|
+
const message = commit.message.split('\n')[0];
|
|
294
|
+
const time = new Date(commit.date).toLocaleTimeString('en-US', {
|
|
295
|
+
hour: '2-digit',
|
|
296
|
+
minute: '2-digit'
|
|
297
|
+
});
|
|
298
|
+
return ` ${color.dim(`${index + 1}.`)} ${hash} ${message} ${color.dim(`(${time})`)}`;
|
|
299
|
+
}
|
|
229
300
|
}
|
|
230
301
|
|
|
231
302
|
module.exports = { GitAnalyzer };
|
package/bin/lib/utils/colors.js
CHANGED
|
@@ -8,8 +8,23 @@ const color = {
|
|
|
8
8
|
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
9
9
|
blue: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
10
10
|
magenta: (s) => `\x1b[35m${s}\x1b[0m`,
|
|
11
|
+
white: (s) => `\x1b[37m${s}\x1b[0m`,
|
|
12
|
+
gray: (s) => `\x1b[90m${s}\x1b[0m`,
|
|
11
13
|
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
12
14
|
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
15
|
+
italic: (s) => `\x1b[3m${s}\x1b[0m`,
|
|
16
|
+
underline: (s) => `\x1b[4m${s}\x1b[0m`,
|
|
17
|
+
// Background colors
|
|
18
|
+
bgGreen: (s) => `\x1b[42m\x1b[30m${s}\x1b[0m`,
|
|
19
|
+
bgYellow: (s) => `\x1b[43m\x1b[30m${s}\x1b[0m`,
|
|
20
|
+
bgRed: (s) => `\x1b[41m\x1b[37m${s}\x1b[0m`,
|
|
21
|
+
bgCyan: (s) => `\x1b[46m\x1b[30m${s}\x1b[0m`,
|
|
22
|
+
bgBlue: (s) => `\x1b[44m\x1b[37m${s}\x1b[0m`,
|
|
23
|
+
// Compound styles
|
|
24
|
+
success: (s) => `\x1b[32m✓\x1b[0m ${s}`,
|
|
25
|
+
warning: (s) => `\x1b[33m⚠\x1b[0m ${s}`,
|
|
26
|
+
error: (s) => `\x1b[31m✗\x1b[0m ${s}`,
|
|
27
|
+
info: (s) => `\x1b[36mℹ\x1b[0m ${s}`,
|
|
13
28
|
reset: '\x1b[0m'
|
|
14
29
|
};
|
|
15
30
|
|