@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,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
+ }