@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +801 -0
  2. package/LICENSE +21 -0
  3. package/README.md +393 -0
  4. package/ai-changelog-mcp.sh +93 -0
  5. package/ai-changelog.sh +103 -0
  6. package/bin/ai-changelog-dxt.js +35 -0
  7. package/bin/ai-changelog-mcp.js +34 -0
  8. package/bin/ai-changelog.js +18 -0
  9. package/package.json +135 -0
  10. package/src/ai-changelog-generator.js +258 -0
  11. package/src/application/orchestrators/changelog.orchestrator.js +730 -0
  12. package/src/application/services/application.service.js +301 -0
  13. package/src/cli.js +157 -0
  14. package/src/domains/ai/ai-analysis.service.js +486 -0
  15. package/src/domains/analysis/analysis.engine.js +445 -0
  16. package/src/domains/changelog/changelog.service.js +1761 -0
  17. package/src/domains/changelog/workspace-changelog.service.js +505 -0
  18. package/src/domains/git/git-repository.analyzer.js +588 -0
  19. package/src/domains/git/git.service.js +302 -0
  20. package/src/infrastructure/cli/cli.controller.js +517 -0
  21. package/src/infrastructure/config/configuration.manager.js +538 -0
  22. package/src/infrastructure/interactive/interactive-workflow.service.js +444 -0
  23. package/src/infrastructure/mcp/mcp-server.service.js +540 -0
  24. package/src/infrastructure/metrics/metrics.collector.js +362 -0
  25. package/src/infrastructure/providers/core/base-provider.js +184 -0
  26. package/src/infrastructure/providers/implementations/anthropic.js +329 -0
  27. package/src/infrastructure/providers/implementations/azure.js +296 -0
  28. package/src/infrastructure/providers/implementations/bedrock.js +393 -0
  29. package/src/infrastructure/providers/implementations/dummy.js +112 -0
  30. package/src/infrastructure/providers/implementations/google.js +320 -0
  31. package/src/infrastructure/providers/implementations/huggingface.js +301 -0
  32. package/src/infrastructure/providers/implementations/lmstudio.js +189 -0
  33. package/src/infrastructure/providers/implementations/mock.js +275 -0
  34. package/src/infrastructure/providers/implementations/ollama.js +151 -0
  35. package/src/infrastructure/providers/implementations/openai.js +273 -0
  36. package/src/infrastructure/providers/implementations/vertex.js +438 -0
  37. package/src/infrastructure/providers/provider-management.service.js +415 -0
  38. package/src/infrastructure/providers/provider-manager.service.js +363 -0
  39. package/src/infrastructure/providers/utils/base-provider-helpers.js +660 -0
  40. package/src/infrastructure/providers/utils/model-config.js +610 -0
  41. package/src/infrastructure/providers/utils/provider-utils.js +286 -0
  42. package/src/shared/constants/colors.js +370 -0
  43. package/src/shared/utils/cli-entry-utils.js +525 -0
  44. package/src/shared/utils/error-classes.js +423 -0
  45. package/src/shared/utils/json-utils.js +318 -0
  46. package/src/shared/utils/utils.js +1997 -0
  47. package/types/index.d.ts +464 -0
@@ -0,0 +1,730 @@
1
+ import { GitService } from '../../domains/git/git.service.js';
2
+ import { AIAnalysisService } from '../../domains/ai/ai-analysis.service.js';
3
+ import { ChangelogService } from '../../domains/changelog/changelog.service.js';
4
+ import { AnalysisEngine } from '../../domains/analysis/analysis.engine.js';
5
+ import { ProviderManagerService } from '../../infrastructure/providers/provider-manager.service.js';
6
+ import { InteractiveWorkflowService } from '../../infrastructure/interactive/interactive-workflow.service.js';
7
+ import colors from '../../shared/constants/colors.js';
8
+
9
+ export class ChangelogOrchestrator {
10
+ constructor(configManager, options = {}) {
11
+ this.configManager = configManager;
12
+ this.options = options;
13
+ this.analysisMode = options.analysisMode || 'standard';
14
+ this.metrics = {
15
+ startTime: Date.now(),
16
+ commitsProcessed: 0,
17
+ apiCalls: 0,
18
+ errors: 0,
19
+ batchesProcessed: 0,
20
+ totalTokens: 0,
21
+ ruleBasedFallbacks: 0,
22
+ cacheHits: 0
23
+ };
24
+
25
+ this.initialized = false;
26
+ this.initializationPromise = null;
27
+
28
+ // Start initialization
29
+ this.initializationPromise = this.initializeServices();
30
+ }
31
+
32
+ async ensureInitialized() {
33
+ if (!this.initialized) {
34
+ await this.initializationPromise;
35
+ }
36
+ }
37
+
38
+ async initializeServices() {
39
+ try {
40
+ // Initialize AI provider
41
+ this.providerManager = new ProviderManagerService(this.configManager.getAll(), {
42
+ errorHandler: { logToConsole: true }
43
+ });
44
+ this.aiProvider = this.providerManager.getActiveProvider();
45
+
46
+ // Create lightweight implementations for missing dependencies
47
+ this.gitManager = await this.createGitManager();
48
+ this.tagger = await this.createTagger();
49
+ this.promptEngine = await this.createPromptEngine();
50
+
51
+ // Initialize domain services with proper dependencies
52
+ this.gitService = new GitService(this.gitManager, this.tagger);
53
+ this.aiAnalysisService = new AIAnalysisService(this.aiProvider, this.promptEngine, this.tagger, this.analysisMode);
54
+ this.analysisEngine = new AnalysisEngine(this.gitService, this.aiAnalysisService);
55
+ this.changelogService = new ChangelogService(this.gitService, this.aiAnalysisService, this.analysisEngine, this.configManager);
56
+ this.interactiveService = new InteractiveWorkflowService(this.gitService, this.aiAnalysisService, this.changelogService);
57
+
58
+ // Only log if not in MCP server mode
59
+ if (!process.env.MCP_SERVER_MODE) {
60
+ console.log(colors.successMessage('⚙️ Services initialized'));
61
+ }
62
+ this.initialized = true;
63
+
64
+ } catch (error) {
65
+ console.error(colors.errorMessage('Failed to initialize services:'), error.message);
66
+ throw error;
67
+ }
68
+ }
69
+
70
+ async createGitManager() {
71
+ const { execSync } = await import('child_process');
72
+
73
+ return {
74
+ isGitRepo: (() => {
75
+ try {
76
+ execSync('git rev-parse --git-dir', { stdio: 'ignore' });
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ })(),
82
+
83
+ execGit(command) {
84
+ try {
85
+ return execSync(command, { encoding: 'utf8', stdio: 'pipe' });
86
+ } catch (error) {
87
+ throw new Error(`Git command failed: ${error.message}`);
88
+ }
89
+ },
90
+
91
+ execGitSafe(command) {
92
+ try {
93
+ return execSync(command, { encoding: 'utf8', stdio: 'pipe' });
94
+ } catch {
95
+ return '';
96
+ }
97
+ },
98
+
99
+ execGitShow(command) {
100
+ try {
101
+ return execSync(command, { encoding: 'utf8', stdio: 'pipe' });
102
+ } catch (error) {
103
+ // console.warn(`Git command failed: ${command}`);
104
+ // console.warn(`Error: ${error.message}`);
105
+ return null;
106
+ }
107
+ },
108
+
109
+ validateCommitHash(hash) {
110
+ try {
111
+ execSync(`git cat-file -e ${hash}`, { stdio: 'ignore' });
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ },
117
+
118
+ getAllBranches() {
119
+ try {
120
+ const output = execSync('git branch -a', { encoding: 'utf8' });
121
+ return output.split('\n').filter(Boolean).map(branch => branch.trim().replace(/^\*\s*/, ''));
122
+ } catch {
123
+ return [];
124
+ }
125
+ },
126
+
127
+ getUnmergedCommits() {
128
+ try {
129
+ const output = execSync('git log --oneline --no-merges HEAD ^origin/main', { encoding: 'utf8' });
130
+ return output.split('\n').filter(Boolean);
131
+ } catch {
132
+ return [];
133
+ }
134
+ },
135
+
136
+ getDanglingCommits() {
137
+ try {
138
+ const output = execSync('git fsck --no-reflog | grep "dangling commit"', { encoding: 'utf8' });
139
+ return output.split('\n').filter(Boolean);
140
+ } catch {
141
+ return [];
142
+ }
143
+ },
144
+
145
+ getUntrackedFiles() {
146
+ try {
147
+ const output = execSync('git ls-files --others --exclude-standard', { encoding: 'utf8' });
148
+ return output.split('\n').filter(Boolean);
149
+ } catch {
150
+ return [];
151
+ }
152
+ },
153
+
154
+ getRecentCommits(limit = 10) {
155
+ try {
156
+ const output = execSync(`git log --oneline -${limit}`, { encoding: 'utf8' });
157
+ return output.split('\n').filter(Boolean);
158
+ } catch {
159
+ return [];
160
+ }
161
+ },
162
+
163
+ getComprehensiveAnalysis() {
164
+ return {
165
+ totalCommits: this.getRecentCommits(1000).length,
166
+ branches: this.getAllBranches(),
167
+ untrackedFiles: this.getUntrackedFiles()
168
+ };
169
+ },
170
+
171
+ hasFile(filename) {
172
+ try {
173
+ execSync(`test -f ${filename}`, { stdio: 'ignore' });
174
+ return true;
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+ };
180
+ }
181
+
182
+ async createTagger() {
183
+ const { analyzeSemanticChanges, analyzeFunctionalImpact } = await import('../../shared/utils/utils.js');
184
+
185
+ return {
186
+ analyzeCommit(commit) {
187
+ const semanticChanges = [];
188
+ const breakingChanges = [];
189
+ const categories = [];
190
+ const tags = [];
191
+
192
+ // Basic analysis based on commit message
193
+ const message = commit.message.toLowerCase();
194
+
195
+ if (message.includes('breaking') || message.includes('!:')) {
196
+ breakingChanges.push('Breaking change detected in commit message');
197
+ categories.push('breaking');
198
+ tags.push('breaking');
199
+ }
200
+
201
+ if (message.startsWith('feat')) {
202
+ categories.push('feature');
203
+ tags.push('feature');
204
+ } else if (message.startsWith('fix')) {
205
+ categories.push('fix');
206
+ tags.push('bugfix');
207
+ } else if (message.startsWith('docs')) {
208
+ categories.push('docs');
209
+ tags.push('documentation');
210
+ }
211
+
212
+ // Analyze files if available
213
+ if (commit.files && commit.files.length > 0) {
214
+ commit.files.forEach(file => {
215
+ const semantic = analyzeSemanticChanges('', file.path);
216
+ if (semantic.frameworks) {
217
+ semanticChanges.push(...semantic.frameworks);
218
+ }
219
+ });
220
+ }
221
+
222
+ // Determine importance
223
+ let importance = 'medium';
224
+ if (breakingChanges.length > 0 || commit.files?.length > 10) {
225
+ importance = 'high';
226
+ } else if (categories.includes('docs') || commit.files?.length < 3) {
227
+ importance = 'low';
228
+ }
229
+
230
+ return {
231
+ semanticChanges,
232
+ breakingChanges,
233
+ categories,
234
+ importance,
235
+ tags
236
+ };
237
+ }
238
+ };
239
+ }
240
+
241
+ async createPromptEngine() {
242
+ const { buildEnhancedPrompt } = await import('../../shared/utils/utils.js');
243
+
244
+ return {
245
+ systemPrompts: {
246
+ master: "You are an expert software analyst specializing in code change analysis and changelog generation.",
247
+ standard: "Provide clear, concise analysis focusing on the practical impact of changes.",
248
+ detailed: "Provide comprehensive technical analysis with detailed explanations and implications.",
249
+ enterprise: "Provide enterprise-grade analysis suitable for stakeholder communication and decision-making.",
250
+ changesAnalysis: "You are an expert at analyzing code changes and their business impact."
251
+ },
252
+
253
+ optimizeForProvider(prompt, providerName, capabilities = {}) {
254
+ // Simple optimization - could be enhanced based on provider capabilities
255
+ if (providerName?.toLowerCase().includes('claude')) {
256
+ return `Please analyze this carefully and provide structured output:\n\n${prompt}`;
257
+ } else if (providerName?.toLowerCase().includes('gpt')) {
258
+ return `${prompt}\n\nPlease respond in JSON format as requested.`;
259
+ }
260
+ return prompt;
261
+ },
262
+
263
+ buildRepositoryHealthPrompt(healthData, analysisMode) {
264
+ return `Analyze the health of this repository based on the following data:\n\n${JSON.stringify(healthData, null, 2)}\n\nProvide assessment and recommendations.`;
265
+ }
266
+ };
267
+ }
268
+
269
+ async generateChangelog(version, since) {
270
+ try {
271
+ await this.ensureInitialized();
272
+
273
+ this.metrics.startTime = Date.now();
274
+
275
+ console.log('\n' + colors.processingMessage('🚀 Starting changelog generation...'));
276
+
277
+ // Validate git repository
278
+ if (!this.gitManager.isGitRepo) {
279
+ throw new Error('Not a git repository');
280
+ }
281
+
282
+ // Generate changelog using the service
283
+ const result = await this.changelogService.generateChangelog(version, since);
284
+
285
+ if (!result) {
286
+ console.log(colors.warningMessage('No changelog generated'));
287
+ return null;
288
+ }
289
+
290
+ // Update metrics
291
+ this.updateMetrics(result);
292
+
293
+ // Display results
294
+ this.displayResults(result, version);
295
+
296
+ return result;
297
+
298
+ } catch (error) {
299
+ this.metrics.errors++;
300
+ console.error(colors.errorMessage('Changelog generation failed:'), error.message);
301
+ throw error;
302
+ }
303
+ }
304
+
305
+ async analyzeRepository(options = {}) {
306
+ try {
307
+ await this.ensureInitialized();
308
+
309
+ console.log(colors.processingMessage('🔍 Starting repository analysis...'));
310
+
311
+ const analysisType = options.type || 'changes';
312
+ const result = await this.analysisEngine.analyze(analysisType, options);
313
+
314
+ this.displayAnalysisResults(result, analysisType);
315
+
316
+ return result;
317
+
318
+ } catch (error) {
319
+ this.metrics.errors++;
320
+ console.error(colors.errorMessage('Repository analysis failed:'), error.message);
321
+ throw error;
322
+ }
323
+ }
324
+
325
+ async runInteractive() {
326
+ await this.ensureInitialized();
327
+
328
+ const { runInteractiveMode, selectSpecificCommits } = await import('../../shared/utils/utils.js');
329
+ const { confirm } = await import('@clack/prompts');
330
+
331
+ console.log(colors.processingMessage('🎮 Starting interactive mode...'));
332
+
333
+ let continueSession = true;
334
+
335
+ while (continueSession) {
336
+ try {
337
+ const result = await runInteractiveMode();
338
+
339
+ if (result.action === 'exit') {
340
+ console.log(colors.successMessage('👋 Goodbye!'));
341
+ break;
342
+ }
343
+
344
+ await this.handleInteractiveAction(result.action);
345
+
346
+ // Ask if user wants to continue
347
+ const continueChoice = await confirm({
348
+ message: 'Would you like to perform another action?',
349
+ initialValue: true
350
+ });
351
+
352
+ continueSession = continueChoice;
353
+
354
+ } catch (error) {
355
+ console.error(colors.errorMessage(`Interactive mode error: ${error.message}`));
356
+
357
+ const retryChoice = await confirm({
358
+ message: 'Would you like to try again?',
359
+ initialValue: true
360
+ });
361
+
362
+ continueSession = retryChoice;
363
+ }
364
+ }
365
+
366
+ return { interactive: true, status: 'completed' };
367
+ }
368
+
369
+ async handleInteractiveAction(action) {
370
+
371
+ switch (action) {
372
+ case 'changelog-recent':
373
+ await this.handleRecentChangelogGeneration();
374
+ break;
375
+
376
+ case 'changelog-specific':
377
+ await this.handleSpecificChangelogGeneration();
378
+ break;
379
+
380
+ case 'analyze-workdir':
381
+ await this.generateChangelogFromChanges();
382
+ break;
383
+
384
+ case 'analyze-repo':
385
+ await this.analyzeRepository({ type: 'comprehensive' });
386
+ break;
387
+
388
+ case 'commit-message':
389
+ await this.handleCommitMessageGeneration();
390
+ break;
391
+
392
+ case 'configure-providers':
393
+ await this.handleProviderConfiguration();
394
+ break;
395
+
396
+ case 'validate-config':
397
+ await this.validateConfiguration();
398
+ break;
399
+
400
+ default:
401
+ console.log(colors.warningMessage(`Unknown action: ${action}`));
402
+ }
403
+ }
404
+
405
+ async handleRecentChangelogGeneration() {
406
+ const { text } = await import('@clack/prompts');
407
+
408
+ const commitCountInput = await text({
409
+ message: 'How many recent commits to include?',
410
+ placeholder: '10',
411
+ validate: (value) => {
412
+ const num = parseInt(value);
413
+ if (isNaN(num) || num <= 0 || num > 100) {
414
+ return 'Please enter a number between 1 and 100';
415
+ }
416
+ }
417
+ });
418
+
419
+ const commitCount = parseInt(commitCountInput) || 10;
420
+
421
+ console.log(colors.processingMessage(`📝 Generating changelog for ${commitCount} recent commits...`));
422
+
423
+ const result = await this.generateChangelog({
424
+ version: `Recent-${commitCount}-commits`,
425
+ maxCommits: commitCount
426
+ });
427
+
428
+ if (result?.changelog) {
429
+ console.log(colors.successMessage('✅ Changelog generated successfully!'));
430
+ }
431
+ }
432
+
433
+ async handleSpecificChangelogGeneration() {
434
+ const { selectSpecificCommits } = await import('../../shared/utils/utils.js');
435
+
436
+ console.log(colors.infoMessage('📋 Select specific commits for changelog generation:'));
437
+
438
+ const selectedCommits = await selectSpecificCommits(30);
439
+
440
+ if (selectedCommits.length === 0) {
441
+ console.log(colors.warningMessage('No commits selected.'));
442
+ return;
443
+ }
444
+
445
+ console.log(colors.processingMessage(`📝 Generating changelog for ${selectedCommits.length} selected commits...`));
446
+
447
+ const result = await this.generateChangelogFromCommits(selectedCommits);
448
+
449
+ if (result?.changelog) {
450
+ console.log(colors.successMessage('✅ Changelog generated successfully!'));
451
+ }
452
+ }
453
+
454
+ async handleCommitMessageGeneration() {
455
+ console.log(colors.processingMessage('🤖 Analyzing current changes for commit message suggestions...'));
456
+
457
+ // Use shared utility for getting working directory changes
458
+ const { getWorkingDirectoryChanges } = await import('../../shared/utils/utils.js');
459
+ const changes = getWorkingDirectoryChanges();
460
+
461
+ if (!changes || changes.length === 0) {
462
+ console.log(colors.warningMessage('No uncommitted changes found.'));
463
+ return;
464
+ }
465
+
466
+ const analysis = await this.interactiveService.generateCommitSuggestion();
467
+
468
+ if (analysis.success && analysis.suggestions.length > 0) {
469
+ const { select } = await import('@clack/prompts');
470
+
471
+ const choices = [
472
+ ...analysis.suggestions.map((msg, index) => ({
473
+ value: msg,
474
+ label: `${index + 1}. ${msg}`
475
+ })),
476
+ {
477
+ value: 'CUSTOM',
478
+ label: '✏️ Write custom message'
479
+ }
480
+ ];
481
+
482
+ const selectedMessage = await select({
483
+ message: 'Choose a commit message:',
484
+ options: choices
485
+ });
486
+
487
+ if (selectedMessage === 'CUSTOM') {
488
+ const { text } = await import('@clack/prompts');
489
+
490
+ const customMessage = await text({
491
+ message: 'Enter your commit message:',
492
+ validate: (input) => {
493
+ if (!input || input.trim().length === 0) {
494
+ return 'Commit message cannot be empty';
495
+ }
496
+ }
497
+ });
498
+
499
+ console.log(colors.successMessage(`📝 Custom message: ${customMessage}`));
500
+ } else {
501
+ console.log(colors.successMessage(`📝 Selected: ${selectedMessage}`));
502
+ }
503
+ } else {
504
+ console.log(colors.warningMessage('Could not generate commit message suggestions.'));
505
+ }
506
+ }
507
+
508
+ async handleProviderConfiguration() {
509
+ const { select } = await import('@clack/prompts');
510
+
511
+ const availableProviders = this.providerManager.getAllProviders();
512
+
513
+ const choices = availableProviders.map(p => ({
514
+ value: p.name,
515
+ label: `${p.name} ${p.available ? '✅' : '⚠️ (needs configuration)'}`
516
+ }));
517
+
518
+ const selectedProvider = await select({
519
+ message: 'Select provider to configure:',
520
+ options: choices
521
+ });
522
+
523
+ console.log(colors.infoMessage(`🔧 Configuring ${selectedProvider}...`));
524
+ console.log(colors.infoMessage('Please edit your .env.local file to add the required API keys.'));
525
+ console.log(colors.highlight(`Example for ${selectedProvider.toUpperCase()}:`));
526
+
527
+ switch (selectedProvider) {
528
+ case 'openai':
529
+ console.log(colors.code('OPENAI_API_KEY=your_api_key_here'));
530
+ break;
531
+ case 'anthropic':
532
+ console.log(colors.code('ANTHROPIC_API_KEY=your_api_key_here'));
533
+ break;
534
+ case 'azure':
535
+ console.log(colors.code('AZURE_OPENAI_API_KEY=your_api_key_here'));
536
+ console.log(colors.code('AZURE_OPENAI_ENDPOINT=your_endpoint_here'));
537
+ break;
538
+ case 'google':
539
+ console.log(colors.code('GOOGLE_API_KEY=your_api_key_here'));
540
+ break;
541
+ default:
542
+ console.log(colors.code(`${selectedProvider.toUpperCase()}_API_KEY=your_api_key_here`));
543
+ }
544
+ }
545
+
546
+ async generateChangelogFromChanges(version) {
547
+ try {
548
+ await this.ensureInitialized();
549
+
550
+ console.log(colors.processingMessage('📝 Generating changelog from working directory changes...'));
551
+
552
+ const result = await this.changelogService.generateChangelogFromChanges(version);
553
+
554
+ if (result) {
555
+ console.log(colors.successMessage('✅ Working directory changelog generated'));
556
+ console.log(result.changelog);
557
+ }
558
+
559
+ return result;
560
+
561
+ } catch (error) {
562
+ this.metrics.errors++;
563
+ console.error(colors.errorMessage('Working directory changelog generation failed:'), error.message);
564
+ throw error;
565
+ }
566
+ }
567
+
568
+ updateMetrics(result) {
569
+ if (result.analyzedCommits) {
570
+ this.metrics.commitsProcessed += result.analyzedCommits.length;
571
+ }
572
+
573
+ // Get metrics from AI service
574
+ const aiMetrics = this.aiAnalysisService.getMetrics();
575
+ this.metrics.apiCalls += aiMetrics.apiCalls;
576
+ this.metrics.totalTokens += aiMetrics.totalTokens;
577
+ this.metrics.ruleBasedFallbacks += aiMetrics.ruleBasedFallbacks;
578
+ }
579
+
580
+ displayResults(result, version) {
581
+ const { changelog, insights, analyzedCommits } = result;
582
+
583
+ console.log('\n' + colors.successMessage('✅ Changelog Generation Complete'));
584
+
585
+ if (insights) {
586
+ // Create a clean insights summary
587
+ const insightLines = [
588
+ `${colors.label('Total commits')}: ${colors.number(insights.totalCommits)}`,
589
+ `${colors.label('Complexity')}: ${this.getComplexityColor(insights.complexity)(insights.complexity)}`,
590
+ `${colors.label('Risk level')}: ${this.getRiskColor(insights.riskLevel)(insights.riskLevel)}`
591
+ ];
592
+
593
+ if (insights.breaking) {
594
+ insightLines.push('');
595
+ insightLines.push(colors.warningMessage('⚠️ Contains breaking changes'));
596
+ }
597
+
598
+ if (Object.keys(insights.commitTypes).length > 0) {
599
+ insightLines.push('');
600
+ insightLines.push(colors.dim('Commit types:'));
601
+ Object.entries(insights.commitTypes).forEach(([type, count]) => {
602
+ insightLines.push(` ${colors.commitType(type)}: ${colors.number(count)}`);
603
+ });
604
+ }
605
+
606
+ console.log(colors.box('📊 Release Insights', insightLines.join('\n')));
607
+ }
608
+
609
+ // Don't show changelog content in terminal - it's saved to file
610
+
611
+ this.displayMetrics();
612
+ }
613
+
614
+ getComplexityColor(complexity) {
615
+ const level = complexity?.toLowerCase();
616
+ switch (level) {
617
+ case 'low': return colors.success;
618
+ case 'medium': return colors.warning;
619
+ case 'high': return colors.error;
620
+ default: return colors.highlight;
621
+ }
622
+ }
623
+
624
+ getRiskColor(risk) {
625
+ const level = risk?.toLowerCase();
626
+ switch (level) {
627
+ case 'low': return colors.riskLow;
628
+ case 'medium': return colors.riskMedium;
629
+ case 'high': return colors.riskHigh;
630
+ case 'critical': return colors.riskCritical;
631
+ default: return colors.highlight;
632
+ }
633
+ }
634
+
635
+ displayAnalysisResults(result, type) {
636
+ console.log(colors.successMessage(`\n✅ ${type.charAt(0).toUpperCase() + type.slice(1)} Analysis Complete`));
637
+ console.log(colors.separator());
638
+
639
+ if (result.summary) {
640
+ console.log(colors.sectionHeader('📋 Summary'));
641
+ console.log(result.summary);
642
+ console.log('');
643
+ }
644
+
645
+ if (result.analysis) {
646
+ console.log(colors.sectionHeader('🔍 Analysis Details'));
647
+ if (typeof result.analysis === 'object') {
648
+ Object.entries(result.analysis).forEach(([key, value]) => {
649
+ if (typeof value === 'object') {
650
+ console.log(`${key}: ${JSON.stringify(value, null, 2)}`);
651
+ } else {
652
+ console.log(`${key}: ${colors.highlight(value)}`);
653
+ }
654
+ });
655
+ } else {
656
+ console.log(result.analysis);
657
+ }
658
+ }
659
+
660
+ this.displayMetrics();
661
+ }
662
+
663
+ displayMetrics() {
664
+ const duration = Date.now() - this.metrics.startTime;
665
+
666
+ const metricLines = [
667
+ `${colors.label('Duration')}: ${colors.number(this.formatDuration(duration))}`,
668
+ `${colors.label('Commits processed')}: ${colors.number(this.metrics.commitsProcessed)}`,
669
+ `${colors.label('API calls')}: ${colors.number(this.metrics.apiCalls)}`,
670
+ `${colors.label('Total tokens')}: ${colors.number(this.metrics.totalTokens.toLocaleString())}`
671
+ ];
672
+
673
+ if (this.metrics.ruleBasedFallbacks > 0) {
674
+ metricLines.push('');
675
+ metricLines.push(colors.warning(`⚠️ Rule-based fallbacks: ${this.metrics.ruleBasedFallbacks}`));
676
+ }
677
+
678
+ if (this.metrics.errors > 0) {
679
+ metricLines.push('');
680
+ metricLines.push(colors.error(`❌ Errors: ${this.metrics.errors}`));
681
+ }
682
+
683
+ console.log(colors.box('📈 Performance Metrics', metricLines.join('\n')));
684
+ }
685
+
686
+ formatDuration(ms) {
687
+ if (ms < 1000) return `${ms}ms`;
688
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
689
+ return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
690
+ }
691
+
692
+ // Configuration methods
693
+ setAnalysisMode(mode) {
694
+ this.analysisMode = mode;
695
+ if (this.aiAnalysisService) {
696
+ this.aiAnalysisService.analysisMode = mode;
697
+ }
698
+ }
699
+
700
+ setModelOverride(model) {
701
+ if (this.aiAnalysisService) {
702
+ this.aiAnalysisService.setModelOverride(model);
703
+ }
704
+ }
705
+
706
+ // Metrics methods
707
+ getMetrics() {
708
+ return {
709
+ ...this.metrics,
710
+ aiMetrics: this.aiAnalysisService?.getMetrics() || {}
711
+ };
712
+ }
713
+
714
+ resetMetrics() {
715
+ this.metrics = {
716
+ startTime: Date.now(),
717
+ commitsProcessed: 0,
718
+ apiCalls: 0,
719
+ errors: 0,
720
+ batchesProcessed: 0,
721
+ totalTokens: 0,
722
+ ruleBasedFallbacks: 0,
723
+ cacheHits: 0
724
+ };
725
+
726
+ if (this.aiAnalysisService) {
727
+ this.aiAnalysisService.resetMetrics();
728
+ }
729
+ }
730
+ }