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.
- package/CHANGELOG.md +80 -0
- package/README.md +33 -13
- package/bin/gims.js +834 -103
- 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
package/bin/lib/ai/providers.js
CHANGED
|
@@ -18,7 +18,7 @@ class AIProviderManager {
|
|
|
18
18
|
if (preference === 'openai') return process.env.OPENAI_API_KEY ? 'openai' : 'none';
|
|
19
19
|
if (preference === 'gemini') return process.env.GEMINI_API_KEY ? 'gemini' : 'none';
|
|
20
20
|
if (preference === 'groq') return process.env.GROQ_API_KEY ? 'groq' : 'none';
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
// Auto-detection with preference order (Gemini first - fastest and cheapest)
|
|
23
23
|
if (process.env.GEMINI_API_KEY) return 'gemini';
|
|
24
24
|
if (process.env.OPENAI_API_KEY) return 'openai';
|
|
@@ -48,12 +48,12 @@ class AIProviderManager {
|
|
|
48
48
|
|
|
49
49
|
setCache(cacheKey, result, usedLocal = false) {
|
|
50
50
|
if (!this.config.cacheEnabled) return;
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
if (this.cache.size >= this.maxCacheSize) {
|
|
53
53
|
const firstKey = this.cache.keys().next().value;
|
|
54
54
|
this.cache.delete(firstKey);
|
|
55
55
|
}
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
this.cache.set(cacheKey, {
|
|
58
58
|
result,
|
|
59
59
|
usedLocal,
|
|
@@ -103,9 +103,9 @@ class AIProviderManager {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
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'
|
|
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
109
|
});
|
|
110
110
|
const actualModel = model || this.getDefaultModel('groq');
|
|
111
111
|
const response = await groq.chat.completions.create({
|
|
@@ -118,11 +118,11 @@ class AIProviderManager {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
async generateCommitMessage(diff, options = {}) {
|
|
121
|
-
const {
|
|
122
|
-
provider: preferredProvider = 'auto',
|
|
123
|
-
conventional = false,
|
|
121
|
+
const {
|
|
122
|
+
provider: preferredProvider = 'auto',
|
|
123
|
+
conventional = false,
|
|
124
124
|
body = false,
|
|
125
|
-
verbose = false
|
|
125
|
+
verbose = false
|
|
126
126
|
} = options;
|
|
127
127
|
|
|
128
128
|
// Check cache first
|
|
@@ -134,11 +134,11 @@ class AIProviderManager {
|
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
const providerChain = this.buildProviderChain(preferredProvider);
|
|
137
|
-
|
|
137
|
+
|
|
138
138
|
for (const provider of providerChain) {
|
|
139
139
|
try {
|
|
140
140
|
if (verbose) Progress.info(`Trying provider: ${provider}`);
|
|
141
|
-
|
|
141
|
+
|
|
142
142
|
if (provider === 'local') {
|
|
143
143
|
const result = await this.generateLocalHeuristic(diff, options);
|
|
144
144
|
this.setCache(cacheKey, result, true);
|
|
@@ -148,10 +148,10 @@ class AIProviderManager {
|
|
|
148
148
|
const prompt = this.buildPrompt(diff, { conventional, body });
|
|
149
149
|
const result = await this.generateWithProvider(provider, prompt, options);
|
|
150
150
|
const cleaned = this.cleanCommitMessage(result, { body });
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
this.setCache(cacheKey, cleaned, false);
|
|
153
153
|
return { message: cleaned, usedLocal: false };
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
} catch (error) {
|
|
156
156
|
if (verbose) Progress.warning(`${provider} failed: ${error.message}`);
|
|
157
157
|
continue;
|
|
@@ -166,7 +166,7 @@ class AIProviderManager {
|
|
|
166
166
|
|
|
167
167
|
buildProviderChain(preferred) {
|
|
168
168
|
const available = [];
|
|
169
|
-
|
|
169
|
+
|
|
170
170
|
if (preferred !== 'auto' && preferred !== 'none') {
|
|
171
171
|
const resolved = this.resolveProvider(preferred);
|
|
172
172
|
if (resolved !== 'none') available.push(resolved);
|
|
@@ -175,28 +175,28 @@ class AIProviderManager {
|
|
|
175
175
|
if (process.env.OPENAI_API_KEY) available.push('openai');
|
|
176
176
|
if (process.env.GROQ_API_KEY) available.push('groq');
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
available.push('local');
|
|
180
180
|
return [...new Set(available)];
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
buildPrompt(diff, options) {
|
|
184
184
|
const { conventional, body } = options;
|
|
185
|
-
|
|
186
|
-
const style = conventional
|
|
185
|
+
|
|
186
|
+
const style = conventional
|
|
187
187
|
? 'Use Conventional Commits format (e.g., feat:, fix:, chore:) for the subject.'
|
|
188
188
|
: 'Subject must be a single short line.';
|
|
189
|
-
|
|
190
|
-
const bodyInstr = body
|
|
189
|
+
|
|
190
|
+
const bodyInstr = body
|
|
191
191
|
? 'Provide a short subject line followed by an optional body separated by a blank line.'
|
|
192
192
|
: 'Return only a short subject line without extra quotes.';
|
|
193
|
-
|
|
193
|
+
|
|
194
194
|
return `Write a concise git commit message for these changes:\n${diff}\n\n${style} ${bodyInstr}`;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
cleanCommitMessage(message, options = {}) {
|
|
198
198
|
if (!message) return 'Update project code';
|
|
199
|
-
|
|
199
|
+
|
|
200
200
|
// Remove markdown formatting
|
|
201
201
|
let cleaned = message
|
|
202
202
|
.replace(/```[\s\S]*?```/g, '')
|
|
@@ -212,7 +212,7 @@ class AIProviderManager {
|
|
|
212
212
|
|
|
213
213
|
const lines = cleaned.split('\n').map(l => l.trim()).filter(Boolean);
|
|
214
214
|
let subject = (lines[0] || '').replace(/\s{2,}/g, ' ').replace(/[\s:,.!;]+$/g, '').trim();
|
|
215
|
-
|
|
215
|
+
|
|
216
216
|
if (subject.length === 0) subject = 'Update project code';
|
|
217
217
|
// No length restriction - allow AI to generate full commit messages
|
|
218
218
|
|
|
@@ -226,16 +226,16 @@ class AIProviderManager {
|
|
|
226
226
|
async generateLocalHeuristic(diff, options) {
|
|
227
227
|
// This would need access to git status - simplified version
|
|
228
228
|
const { conventional = false } = options;
|
|
229
|
-
|
|
229
|
+
|
|
230
230
|
// Analyze diff for patterns
|
|
231
231
|
const lines = diff.split('\n');
|
|
232
232
|
const additions = lines.filter(l => l.startsWith('+')).length;
|
|
233
233
|
const deletions = lines.filter(l => l.startsWith('-')).length;
|
|
234
234
|
const files = (diff.match(/diff --git/g) || []).length;
|
|
235
|
-
|
|
235
|
+
|
|
236
236
|
let type = 'chore';
|
|
237
237
|
let subject = 'update files';
|
|
238
|
-
|
|
238
|
+
|
|
239
239
|
if (additions > deletions * 2) {
|
|
240
240
|
type = 'feat';
|
|
241
241
|
subject = files === 1 ? 'add new functionality' : `add features to ${files} files`;
|
|
@@ -249,37 +249,39 @@ class AIProviderManager {
|
|
|
249
249
|
type = 'docs';
|
|
250
250
|
subject = 'update documentation';
|
|
251
251
|
}
|
|
252
|
-
|
|
252
|
+
|
|
253
253
|
return conventional ? `${type}: ${subject}` : subject.charAt(0).toUpperCase() + subject.slice(1);
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
async generateMultipleSuggestions(diff, options = {}, count = 3) {
|
|
257
257
|
const suggestions = [];
|
|
258
258
|
const baseOptions = { ...options };
|
|
259
|
-
|
|
259
|
+
|
|
260
260
|
// Generate different styles
|
|
261
261
|
const variants = [
|
|
262
262
|
{ ...baseOptions, conventional: false },
|
|
263
263
|
{ ...baseOptions, conventional: true },
|
|
264
264
|
{ ...baseOptions, conventional: true, body: true }
|
|
265
265
|
];
|
|
266
|
-
|
|
266
|
+
|
|
267
267
|
for (let i = 0; i < Math.min(count, variants.length); i++) {
|
|
268
268
|
try {
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
269
|
+
const result = await this.generateCommitMessage(diff, variants[i]);
|
|
270
|
+
// Extract message string from result object
|
|
271
|
+
const message = result.message || result;
|
|
272
|
+
if (message && !suggestions.includes(message)) {
|
|
273
|
+
suggestions.push(message);
|
|
272
274
|
}
|
|
273
275
|
} catch (error) {
|
|
274
276
|
// Skip failed generations
|
|
275
277
|
}
|
|
276
278
|
}
|
|
277
|
-
|
|
279
|
+
|
|
278
280
|
// Ensure we have at least one suggestion
|
|
279
281
|
if (suggestions.length === 0) {
|
|
280
282
|
suggestions.push('Update project files');
|
|
281
283
|
}
|
|
282
|
-
|
|
284
|
+
|
|
283
285
|
return suggestions;
|
|
284
286
|
}
|
|
285
287
|
}
|
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
|
|