@entro314labs/ai-changelog-generator 3.0.5
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 +801 -0
- package/LICENSE +21 -0
- package/README.md +393 -0
- package/ai-changelog-mcp.sh +93 -0
- package/ai-changelog.sh +103 -0
- package/bin/ai-changelog-dxt.js +35 -0
- package/bin/ai-changelog-mcp.js +34 -0
- package/bin/ai-changelog.js +18 -0
- package/package.json +135 -0
- package/src/ai-changelog-generator.js +258 -0
- package/src/application/orchestrators/changelog.orchestrator.js +730 -0
- package/src/application/services/application.service.js +301 -0
- package/src/cli.js +157 -0
- package/src/domains/ai/ai-analysis.service.js +486 -0
- package/src/domains/analysis/analysis.engine.js +445 -0
- package/src/domains/changelog/changelog.service.js +1761 -0
- package/src/domains/changelog/workspace-changelog.service.js +505 -0
- package/src/domains/git/git-repository.analyzer.js +588 -0
- package/src/domains/git/git.service.js +302 -0
- package/src/infrastructure/cli/cli.controller.js +517 -0
- package/src/infrastructure/config/configuration.manager.js +538 -0
- package/src/infrastructure/interactive/interactive-workflow.service.js +444 -0
- package/src/infrastructure/mcp/mcp-server.service.js +540 -0
- package/src/infrastructure/metrics/metrics.collector.js +362 -0
- package/src/infrastructure/providers/core/base-provider.js +184 -0
- package/src/infrastructure/providers/implementations/anthropic.js +329 -0
- package/src/infrastructure/providers/implementations/azure.js +296 -0
- package/src/infrastructure/providers/implementations/bedrock.js +393 -0
- package/src/infrastructure/providers/implementations/dummy.js +112 -0
- package/src/infrastructure/providers/implementations/google.js +320 -0
- package/src/infrastructure/providers/implementations/huggingface.js +301 -0
- package/src/infrastructure/providers/implementations/lmstudio.js +189 -0
- package/src/infrastructure/providers/implementations/mock.js +275 -0
- package/src/infrastructure/providers/implementations/ollama.js +151 -0
- package/src/infrastructure/providers/implementations/openai.js +273 -0
- package/src/infrastructure/providers/implementations/vertex.js +438 -0
- package/src/infrastructure/providers/provider-management.service.js +415 -0
- package/src/infrastructure/providers/provider-manager.service.js +363 -0
- package/src/infrastructure/providers/utils/base-provider-helpers.js +660 -0
- package/src/infrastructure/providers/utils/model-config.js +610 -0
- package/src/infrastructure/providers/utils/provider-utils.js +286 -0
- package/src/shared/constants/colors.js +370 -0
- package/src/shared/utils/cli-entry-utils.js +525 -0
- package/src/shared/utils/error-classes.js +423 -0
- package/src/shared/utils/json-utils.js +318 -0
- package/src/shared/utils/utils.js +1997 -0
- package/types/index.d.ts +464 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { runInteractiveMode, analyzeChangesForCommitMessage, selectSpecificCommits } from '../../shared/utils/utils.js';
|
|
2
|
+
import colors from '../../shared/constants/colors.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Interactive Workflow Service
|
|
6
|
+
*
|
|
7
|
+
* Interactive workflow management and commit message utilities
|
|
8
|
+
* Provides user interaction features including:
|
|
9
|
+
* - Interactive mode for guided changelog generation
|
|
10
|
+
* - Commit message validation and suggestions
|
|
11
|
+
* - Commit selection interfaces
|
|
12
|
+
* - Change analysis for commit message generation
|
|
13
|
+
*/
|
|
14
|
+
export class InteractiveWorkflowService {
|
|
15
|
+
constructor(gitService, aiAnalysisService, changelogService) {
|
|
16
|
+
this.gitService = gitService;
|
|
17
|
+
this.aiAnalysisService = aiAnalysisService;
|
|
18
|
+
this.changelogService = changelogService;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async runInteractiveMode() {
|
|
22
|
+
return await runInteractiveMode();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async validateCommitMessage(message) {
|
|
26
|
+
if (!message || message.trim().length === 0) {
|
|
27
|
+
return {
|
|
28
|
+
valid: false,
|
|
29
|
+
issues: ['Commit message is empty'],
|
|
30
|
+
suggestions: ['Provide a descriptive commit message']
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const issues = [];
|
|
35
|
+
const suggestions = [];
|
|
36
|
+
|
|
37
|
+
// Length validation
|
|
38
|
+
if (message.length < 10) {
|
|
39
|
+
issues.push('Commit message is too short (minimum 10 characters)');
|
|
40
|
+
suggestions.push('Add more detail about what was changed');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (message.length > 72) {
|
|
44
|
+
issues.push('Commit message first line is too long (maximum 72 characters)');
|
|
45
|
+
suggestions.push('Keep the first line concise, add details in the body');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Format validation
|
|
49
|
+
const lines = message.split('\n');
|
|
50
|
+
const firstLine = lines[0];
|
|
51
|
+
|
|
52
|
+
// Check for conventional commit format
|
|
53
|
+
const conventionalPattern = /^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .+/;
|
|
54
|
+
if (!conventionalPattern.test(firstLine)) {
|
|
55
|
+
suggestions.push('Consider using conventional commit format: type(scope): description');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check for imperative mood
|
|
59
|
+
const imperativeWords = ['add', 'fix', 'update', 'remove', 'create', 'implement', 'refactor'];
|
|
60
|
+
const firstWord = firstLine.split(' ')[0].toLowerCase().replace(/[^a-z]/g, '');
|
|
61
|
+
|
|
62
|
+
if (!imperativeWords.some(word => firstWord.startsWith(word))) {
|
|
63
|
+
suggestions.push('Use imperative mood (e.g., "Add feature" not "Added feature")');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for body separation
|
|
67
|
+
if (lines.length > 1 && lines[1].trim() !== '') {
|
|
68
|
+
issues.push('Missing blank line between subject and body');
|
|
69
|
+
suggestions.push('Add a blank line after the first line if including a body');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
valid: issues.length === 0,
|
|
74
|
+
issues,
|
|
75
|
+
suggestions,
|
|
76
|
+
score: Math.max(0, 100 - (issues.length * 20) - (suggestions.length * 10))
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async generateCommitSuggestion(message = null) {
|
|
81
|
+
try {
|
|
82
|
+
// If no message provided, analyze current changes
|
|
83
|
+
let analysisContext = '';
|
|
84
|
+
|
|
85
|
+
if (!message) {
|
|
86
|
+
// Use the shared utility function for getting working directory changes
|
|
87
|
+
const { getWorkingDirectoryChanges } = await import('../../shared/utils/utils.js');
|
|
88
|
+
const changes = getWorkingDirectoryChanges();
|
|
89
|
+
if (changes && changes.length > 0) {
|
|
90
|
+
analysisContext = this.analyzeChangesForCommitMessage(changes, true);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Use AI to suggest better commit message
|
|
95
|
+
if (this.aiAnalysisService.hasAI) {
|
|
96
|
+
const prompt = message
|
|
97
|
+
? `Improve this commit message following conventional commit format and best practices:
|
|
98
|
+
|
|
99
|
+
Original: "${message}"
|
|
100
|
+
|
|
101
|
+
Requirements:
|
|
102
|
+
- Use conventional commit format (type(scope): description)
|
|
103
|
+
- Keep first line under 72 characters
|
|
104
|
+
- Use imperative mood
|
|
105
|
+
- Be specific and descriptive
|
|
106
|
+
|
|
107
|
+
Provide 3 alternative suggestions.`
|
|
108
|
+
: `Suggest a commit message for these changes:
|
|
109
|
+
|
|
110
|
+
${analysisContext}
|
|
111
|
+
|
|
112
|
+
Requirements:
|
|
113
|
+
- Use conventional commit format (type(scope): description)
|
|
114
|
+
- Keep first line under 72 characters
|
|
115
|
+
- Use imperative mood
|
|
116
|
+
- Be specific and descriptive
|
|
117
|
+
|
|
118
|
+
Provide 3 suggestions.`;
|
|
119
|
+
|
|
120
|
+
const response = await this.aiAnalysisService.aiProvider.generateCompletion([{
|
|
121
|
+
role: 'user',
|
|
122
|
+
content: prompt
|
|
123
|
+
}], { max_tokens: 200 });
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
success: true,
|
|
127
|
+
original: message,
|
|
128
|
+
suggestions: this.parseCommitSuggestions(response.content),
|
|
129
|
+
context: analysisContext
|
|
130
|
+
};
|
|
131
|
+
} else {
|
|
132
|
+
// Rule-based suggestions
|
|
133
|
+
return this.generateRuleBasedCommitSuggestion(message, analysisContext);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error(colors.errorMessage(`Error generating commit suggestion: ${error.message}`));
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
error: error.message,
|
|
141
|
+
fallback: this.generateRuleBasedCommitSuggestion(message)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
analyzeChangesForCommitMessage(changes, includeScope = false) {
|
|
147
|
+
if (!changes || changes.length === 0) {
|
|
148
|
+
return 'No changes detected';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const categories = this.categorizeChanges(changes);
|
|
152
|
+
const primaryCategory = Object.keys(categories)[0];
|
|
153
|
+
const fileCount = changes.length;
|
|
154
|
+
|
|
155
|
+
let summary = `${fileCount} file${fileCount === 1 ? '' : 's'} changed`;
|
|
156
|
+
|
|
157
|
+
if (primaryCategory) {
|
|
158
|
+
summary += ` in ${primaryCategory}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (includeScope) {
|
|
162
|
+
const scopes = this.extractScopes(changes);
|
|
163
|
+
if (scopes.length > 0) {
|
|
164
|
+
summary += ` (scope: ${scopes.join(', ')})`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Add change details
|
|
169
|
+
const additions = changes.filter(c => c.status === 'A').length;
|
|
170
|
+
const modifications = changes.filter(c => c.status === 'M').length;
|
|
171
|
+
const deletions = changes.filter(c => c.status === 'D').length;
|
|
172
|
+
|
|
173
|
+
const details = [];
|
|
174
|
+
if (additions > 0) details.push(`${additions} added`);
|
|
175
|
+
if (modifications > 0) details.push(`${modifications} modified`);
|
|
176
|
+
if (deletions > 0) details.push(`${deletions} deleted`);
|
|
177
|
+
|
|
178
|
+
if (details.length > 0) {
|
|
179
|
+
summary += ` (${details.join(', ')})`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return summary;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async selectSpecificCommits() {
|
|
186
|
+
return await selectSpecificCommits();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async generateChangelogForRecentCommits(count = 10) {
|
|
190
|
+
try {
|
|
191
|
+
console.log(colors.processingMessage(`š Generating changelog for recent ${count} commits...`));
|
|
192
|
+
|
|
193
|
+
const commits = await this.gitService.getCommitsSince(null);
|
|
194
|
+
const recentCommits = commits.slice(0, count);
|
|
195
|
+
|
|
196
|
+
if (recentCommits.length === 0) {
|
|
197
|
+
console.log(colors.infoMessage('No recent commits found.'));
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const result = await this.changelogService.generateChangelogBatch(
|
|
202
|
+
recentCommits.map(c => c.hash)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
console.log(colors.successMessage(`ā
Generated changelog for ${recentCommits.length} commits`));
|
|
206
|
+
return result;
|
|
207
|
+
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error(colors.errorMessage(`Error generating changelog for recent commits: ${error.message}`));
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async generateChangelogForCommits(commitHashes) {
|
|
215
|
+
if (!commitHashes || commitHashes.length === 0) {
|
|
216
|
+
console.log(colors.warningMessage('No commit hashes provided'));
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
console.log(colors.processingMessage(`š Generating changelog for ${commitHashes.length} specific commits...`));
|
|
222
|
+
|
|
223
|
+
// Validate commit hashes
|
|
224
|
+
const validCommits = [];
|
|
225
|
+
for (const hash of commitHashes) {
|
|
226
|
+
const analysis = await this.gitService.getCommitAnalysis(hash);
|
|
227
|
+
if (analysis) {
|
|
228
|
+
validCommits.push(analysis);
|
|
229
|
+
} else {
|
|
230
|
+
console.warn(colors.warningMessage(`Invalid or inaccessible commit: ${hash}`));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (validCommits.length === 0) {
|
|
235
|
+
console.log(colors.errorMessage('No valid commits found'));
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Generate AI summaries for each commit
|
|
240
|
+
const analyzedCommits = [];
|
|
241
|
+
for (let i = 0; i < validCommits.length; i++) {
|
|
242
|
+
const commit = validCommits[i];
|
|
243
|
+
console.log(colors.processingMessage(`Processing commit ${i + 1}/${validCommits.length}: ${colors.hash(commit.hash)}`));
|
|
244
|
+
|
|
245
|
+
const selectedModel = await this.aiAnalysisService.selectOptimalModel(commit);
|
|
246
|
+
const aiSummary = await this.aiAnalysisService.generateAISummary(commit, selectedModel);
|
|
247
|
+
|
|
248
|
+
if (aiSummary) {
|
|
249
|
+
analyzedCommits.push({
|
|
250
|
+
...commit,
|
|
251
|
+
aiSummary,
|
|
252
|
+
type: aiSummary.category || 'other',
|
|
253
|
+
breaking: aiSummary.breaking || false
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (analyzedCommits.length === 0) {
|
|
259
|
+
console.log(colors.errorMessage('No commits could be analyzed'));
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Generate release insights and build changelog
|
|
264
|
+
const insights = await this.changelogService.generateReleaseInsights(analyzedCommits, 'Selected Commits');
|
|
265
|
+
const changelog = await this.changelogService.buildChangelog(analyzedCommits, insights, 'Selected Commits');
|
|
266
|
+
|
|
267
|
+
console.log(colors.successMessage(`ā
Generated changelog for ${analyzedCommits.length} commits`));
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
changelog,
|
|
271
|
+
insights,
|
|
272
|
+
analyzedCommits,
|
|
273
|
+
requestedCommits: commitHashes.length,
|
|
274
|
+
processedCommits: analyzedCommits.length
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error(colors.errorMessage(`Error generating changelog for commits: ${error.message}`));
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Helper methods
|
|
284
|
+
categorizeChanges(changes) {
|
|
285
|
+
const categories = {};
|
|
286
|
+
changes.forEach(change => {
|
|
287
|
+
const category = this.getFileCategory(change.path);
|
|
288
|
+
if (!categories[category]) categories[category] = [];
|
|
289
|
+
categories[category].push(change);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Sort by count
|
|
293
|
+
return Object.fromEntries(
|
|
294
|
+
Object.entries(categories).sort(([,a], [,b]) => b.length - a.length)
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
getFileCategory(filePath) {
|
|
299
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
300
|
+
return 'other';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const path = filePath.toLowerCase();
|
|
304
|
+
|
|
305
|
+
if (path.includes('/test/') || path.includes('.test.') || path.includes('.spec.')) {
|
|
306
|
+
return 'tests';
|
|
307
|
+
}
|
|
308
|
+
if (path.includes('/doc/') || path.endsWith('.md') || path.endsWith('.txt')) {
|
|
309
|
+
return 'documentation';
|
|
310
|
+
}
|
|
311
|
+
if (path.includes('/config/') || path.endsWith('.json') || path.endsWith('.yaml')) {
|
|
312
|
+
return 'configuration';
|
|
313
|
+
}
|
|
314
|
+
if (path.includes('/src/') || path.includes('/lib/')) {
|
|
315
|
+
return 'source';
|
|
316
|
+
}
|
|
317
|
+
if (path.includes('/style/') || path.endsWith('.css') || path.endsWith('.scss')) {
|
|
318
|
+
return 'styles';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return 'other';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
extractScopes(changes) {
|
|
325
|
+
const scopes = new Set();
|
|
326
|
+
|
|
327
|
+
changes.forEach(change => {
|
|
328
|
+
const parts = change.path.split('/');
|
|
329
|
+
if (parts.length > 1) {
|
|
330
|
+
// Extract directory name as scope
|
|
331
|
+
const scope = parts[parts.length - 2];
|
|
332
|
+
if (scope && scope !== '.' && scope !== '..') {
|
|
333
|
+
scopes.add(scope);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return Array.from(scopes).slice(0, 3); // Limit to 3 scopes
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
parseCommitSuggestions(content) {
|
|
342
|
+
// Parse AI response to extract suggestions
|
|
343
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
344
|
+
const suggestions = [];
|
|
345
|
+
|
|
346
|
+
lines.forEach(line => {
|
|
347
|
+
// Remove numbering and clean up
|
|
348
|
+
const cleaned = line.replace(/^\d+\.\s*/, '').replace(/^-\s*/, '').trim();
|
|
349
|
+
if (cleaned && cleaned.length > 10 && cleaned.includes(':')) {
|
|
350
|
+
suggestions.push(cleaned);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return suggestions.length > 0 ? suggestions : [content.trim()];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
generateRuleBasedCommitSuggestion(message, context) {
|
|
358
|
+
const suggestions = [];
|
|
359
|
+
|
|
360
|
+
if (message) {
|
|
361
|
+
// Improve existing message
|
|
362
|
+
const improved = this.improveCommitMessage(message);
|
|
363
|
+
suggestions.push(improved);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (context) {
|
|
367
|
+
// Generate from context
|
|
368
|
+
const fromContext = this.generateFromContext(context);
|
|
369
|
+
suggestions.push(fromContext);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Add generic suggestions
|
|
373
|
+
suggestions.push(
|
|
374
|
+
'feat: add new functionality',
|
|
375
|
+
'fix: resolve issue with component',
|
|
376
|
+
'docs: update documentation'
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
success: true,
|
|
381
|
+
suggestions: suggestions.slice(0, 3),
|
|
382
|
+
source: 'rule-based'
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
improveCommitMessage(message) {
|
|
387
|
+
// Basic improvements
|
|
388
|
+
let improved = message.trim();
|
|
389
|
+
|
|
390
|
+
// Add conventional commit prefix if missing
|
|
391
|
+
if (!/^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)/.test(improved)) {
|
|
392
|
+
improved = `feat: ${improved}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Ensure imperative mood
|
|
396
|
+
improved = improved.replace(/^(\w+)ed\s/, '$1 ');
|
|
397
|
+
improved = improved.replace(/^(\w+)s\s/, '$1 ');
|
|
398
|
+
|
|
399
|
+
return improved;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
generateFromContext(context) {
|
|
403
|
+
if (!context || typeof context !== 'string') {
|
|
404
|
+
return 'feat: implement changes';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (context.includes('test')) {
|
|
408
|
+
return 'test: add test coverage';
|
|
409
|
+
}
|
|
410
|
+
if (context.includes('doc')) {
|
|
411
|
+
return 'docs: update documentation';
|
|
412
|
+
}
|
|
413
|
+
if (context.includes('config')) {
|
|
414
|
+
return 'chore: update configuration';
|
|
415
|
+
}
|
|
416
|
+
if (context.includes('fix') || context.includes('bug')) {
|
|
417
|
+
return 'fix: resolve issue';
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return 'feat: implement changes';
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Utility method for displaying interactive results
|
|
424
|
+
displayInteractiveResults(results) {
|
|
425
|
+
if (!results) return;
|
|
426
|
+
|
|
427
|
+
console.log(colors.header('\nš Interactive Session Results:'));
|
|
428
|
+
|
|
429
|
+
if (results.changelog) {
|
|
430
|
+
console.log(colors.subheader('Generated Changelog:'));
|
|
431
|
+
console.log(results.changelog);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (results.insights) {
|
|
435
|
+
console.log(colors.subheader('\nš Insights:'));
|
|
436
|
+
console.log(`Total commits: ${colors.number(results.insights.totalCommits)}`);
|
|
437
|
+
console.log(`Risk level: ${colors.highlight(results.insights.riskLevel)}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (results.analyzedCommits) {
|
|
441
|
+
console.log(colors.subheader(`\nš Processed ${colors.number(results.analyzedCommits.length)} commits`));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|