@entro314labs/ai-changelog-generator 3.1.1 → 3.2.0

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 (51) hide show
  1. package/CHANGELOG.md +383 -877
  2. package/README.md +8 -3
  3. package/ai-changelog-mcp.sh +0 -0
  4. package/ai-changelog.sh +0 -0
  5. package/bin/ai-changelog-dxt.js +9 -9
  6. package/bin/ai-changelog-mcp.js +19 -17
  7. package/bin/ai-changelog.js +6 -6
  8. package/package.json +80 -48
  9. package/src/ai-changelog-generator.js +83 -81
  10. package/src/application/orchestrators/changelog.orchestrator.js +791 -516
  11. package/src/application/services/application.service.js +137 -128
  12. package/src/cli.js +76 -57
  13. package/src/domains/ai/ai-analysis.service.js +289 -209
  14. package/src/domains/analysis/analysis.engine.js +253 -193
  15. package/src/domains/changelog/changelog.service.js +1062 -784
  16. package/src/domains/changelog/workspace-changelog.service.js +420 -249
  17. package/src/domains/git/git-repository.analyzer.js +348 -258
  18. package/src/domains/git/git.service.js +132 -112
  19. package/src/infrastructure/cli/cli.controller.js +390 -274
  20. package/src/infrastructure/config/configuration.manager.js +220 -190
  21. package/src/infrastructure/interactive/interactive-staging.service.js +154 -135
  22. package/src/infrastructure/interactive/interactive-workflow.service.js +200 -159
  23. package/src/infrastructure/mcp/mcp-server.service.js +208 -207
  24. package/src/infrastructure/metrics/metrics.collector.js +140 -123
  25. package/src/infrastructure/providers/core/base-provider.js +87 -40
  26. package/src/infrastructure/providers/implementations/anthropic.js +101 -99
  27. package/src/infrastructure/providers/implementations/azure.js +124 -101
  28. package/src/infrastructure/providers/implementations/bedrock.js +136 -126
  29. package/src/infrastructure/providers/implementations/dummy.js +23 -23
  30. package/src/infrastructure/providers/implementations/google.js +123 -114
  31. package/src/infrastructure/providers/implementations/huggingface.js +94 -87
  32. package/src/infrastructure/providers/implementations/lmstudio.js +75 -60
  33. package/src/infrastructure/providers/implementations/mock.js +69 -73
  34. package/src/infrastructure/providers/implementations/ollama.js +89 -66
  35. package/src/infrastructure/providers/implementations/openai.js +88 -89
  36. package/src/infrastructure/providers/implementations/vertex.js +227 -197
  37. package/src/infrastructure/providers/provider-management.service.js +245 -207
  38. package/src/infrastructure/providers/provider-manager.service.js +145 -125
  39. package/src/infrastructure/providers/utils/base-provider-helpers.js +308 -302
  40. package/src/infrastructure/providers/utils/model-config.js +220 -195
  41. package/src/infrastructure/providers/utils/provider-utils.js +105 -100
  42. package/src/infrastructure/validation/commit-message-validation.service.js +259 -161
  43. package/src/shared/constants/colors.js +453 -180
  44. package/src/shared/utils/cli-demo.js +285 -0
  45. package/src/shared/utils/cli-entry-utils.js +257 -249
  46. package/src/shared/utils/cli-ui.js +447 -0
  47. package/src/shared/utils/diff-processor.js +513 -0
  48. package/src/shared/utils/error-classes.js +125 -156
  49. package/src/shared/utils/json-utils.js +93 -89
  50. package/src/shared/utils/utils.js +1117 -945
  51. package/types/index.d.ts +353 -344
@@ -1,56 +1,68 @@
1
- import { categorizeFile, detectLanguage, assessFileImportance, assessChangeComplexity, analyzeSemanticChanges, analyzeFunctionalImpact } from '../../shared/utils/utils.js';
2
- import colors from '../../shared/constants/colors.js';
3
- import { GitError } from '../../shared/utils/error-classes.js';
1
+ import colors from '../../shared/constants/colors.js'
2
+ import { GitError } from '../../shared/utils/error-classes.js'
3
+ import {
4
+ analyzeFunctionalImpact,
5
+ analyzeSemanticChanges,
6
+ assessChangeComplexity,
7
+ assessFileImportance,
8
+ categorizeFile,
9
+ detectLanguage,
10
+ } from '../../shared/utils/utils.js'
4
11
 
5
12
  export class GitService {
6
13
  constructor(gitManager, tagger) {
7
- this.gitManager = gitManager;
8
- this.tagger = tagger;
14
+ this.gitManager = gitManager
15
+ this.tagger = tagger
9
16
  }
10
17
 
11
18
  async getCommitAnalysis(commitHash) {
12
19
  try {
13
20
  // Validate commit hash first
14
21
  if (!this.gitManager.validateCommitHash(commitHash)) {
15
- console.warn(colors.warningMessage(`Invalid commit hash: ${colors.hash(commitHash)}`));
16
- return null;
22
+ console.warn(colors.warningMessage(`Invalid commit hash: ${colors.hash(commitHash)}`))
23
+ return null
17
24
  }
18
25
 
19
26
  // Get comprehensive commit information
20
- const commitInfo = this.gitManager.execGit(`git show --pretty=format:"%H|%s|%an|%ad|%B" --no-patch ${commitHash}`);
21
- const lines = commitInfo.split('\n');
22
- const [hash, subject, author, date] = lines[0].split('|');
23
- const body = lines.slice(1).join('\n').trim();
27
+ const commitInfo = this.gitManager.execGit(
28
+ `git show --pretty=format:"%H|%s|%an|%ad|%B" --no-patch ${commitHash}`
29
+ )
30
+ const lines = commitInfo.split('\n')
31
+ const [hash, subject, author, date] = lines[0].split('|')
32
+ const body = lines.slice(1).join('\n').trim()
24
33
 
25
34
  // Get files with detailed analysis
26
- const filesCommand = `git show --name-status --pretty=format: ${commitHash}`;
27
- const filesOutput = this.gitManager.execGitSafe(filesCommand);
35
+ const filesCommand = `git show --name-status --pretty=format: ${commitHash}`
36
+ const filesOutput = this.gitManager.execGitSafe(filesCommand)
28
37
  const files = await Promise.all(
29
- filesOutput.split('\n')
38
+ filesOutput
39
+ .split('\n')
30
40
  .filter(Boolean)
31
41
  .map(async (line) => {
32
- const parts = line.split('\t');
33
- if (parts.length < 2) return null;
34
- const [status, filePath] = parts;
35
- return await this.analyzeFileChange(commitHash, status, filePath);
42
+ const parts = line.split('\t')
43
+ if (parts.length < 2) {
44
+ return null
45
+ }
46
+ const [status, filePath] = parts
47
+ return await this.analyzeFileChange(commitHash, status, filePath)
36
48
  })
37
- );
49
+ )
38
50
 
39
51
  // Filter out null entries
40
- const validFiles = files.filter(Boolean);
52
+ const validFiles = files.filter(Boolean)
41
53
 
42
54
  // Get overall diff statistics
43
- const diffStats = this.getCommitDiffStats(commitHash);
55
+ const diffStats = this.getCommitDiffStats(commitHash)
44
56
 
45
57
  // Use intelligent tagging system
46
58
  const commitForTagging = {
47
59
  hash: hash.substring(0, 7),
48
60
  message: subject,
49
- files: validFiles.map(f => ({ path: f.filePath })),
50
- stats: diffStats
51
- };
61
+ files: validFiles.map((f) => ({ path: f.filePath })),
62
+ stats: diffStats,
63
+ }
52
64
 
53
- const taggingAnalysis = this.tagger.analyzeCommit(commitForTagging);
65
+ const taggingAnalysis = this.tagger.analyzeCommit(commitForTagging)
54
66
 
55
67
  const analysis = {
56
68
  hash: hash.substring(0, 7),
@@ -65,58 +77,62 @@ export class GitService {
65
77
  breakingChanges: taggingAnalysis.breakingChanges || [],
66
78
  categories: taggingAnalysis.categories || [],
67
79
  importance: taggingAnalysis.importance || 'medium',
68
- tags: taggingAnalysis.tags || []
69
- };
80
+ tags: taggingAnalysis.tags || [],
81
+ }
70
82
 
71
- return analysis;
83
+ return analysis
72
84
  } catch (error) {
73
- const gitError = GitError.fromCommandFailure('show', null, null, error.message, error);
74
- console.error(colors.errorMessage(`Error analyzing commit ${commitHash}:`), gitError.message);
75
- return null;
85
+ const gitError = GitError.fromCommandFailure('show', null, null, error.message, error)
86
+ console.error(colors.errorMessage(`Error analyzing commit ${commitHash}:`), gitError.message)
87
+ return null
76
88
  }
77
89
  }
78
90
 
79
91
  async analyzeFileChange(commitHash, status, filePath) {
80
92
  try {
81
93
  // Get file diff with context
82
- const diffCommand = `git show ${commitHash} --pretty=format: -U5 -- "${filePath}"`;
83
- let diff = '';
94
+ const diffCommand = `git show ${commitHash} --pretty=format: -U5 -- "${filePath}"`
95
+ let diff = ''
84
96
 
85
- diff = this.gitManager.execGitShow(diffCommand);
97
+ diff = this.gitManager.execGitShow(diffCommand)
86
98
 
87
99
  if (diff === null) {
88
100
  if (status === 'D') {
89
- diff = 'File deleted in this commit';
101
+ diff = 'File deleted in this commit'
90
102
  } else {
91
- console.warn(colors.warningMessage(`⚠️ File ${colors.file(filePath)} diff failed for commit ${commitHash}`));
92
- diff = 'File content unavailable (git show failed)';
103
+ console.warn(
104
+ colors.warningMessage(
105
+ `⚠️ File ${colors.file(filePath)} diff failed for commit ${commitHash}`
106
+ )
107
+ )
108
+ diff = 'File content unavailable (git show failed)'
93
109
  }
94
110
  } else if (!diff || diff.trim() === '') {
95
111
  if (status === 'A') {
96
112
  // For new files, try to get the content directly
97
- const newFileContent = this.gitManager.execGitShow(`git show ${commitHash}:"${filePath}"`);
113
+ const newFileContent = this.gitManager.execGitShow(`git show ${commitHash}:"${filePath}"`)
98
114
  if (newFileContent && newFileContent.length > 0) {
99
- diff = `New file created with content:\n${newFileContent.slice(0, 1000)}${newFileContent.length > 1000 ? '\n...' : ''}`;
115
+ diff = `New file created with content:\n${newFileContent.slice(0, 1000)}${newFileContent.length > 1000 ? '\n...' : ''}`
100
116
  } else {
101
- diff = 'New file created (content unavailable)';
117
+ diff = 'New file created (content unavailable)'
102
118
  }
103
119
  } else {
104
- diff = 'No changes detected (binary or empty file)';
120
+ diff = 'No changes detected (binary or empty file)'
105
121
  }
106
122
  }
107
123
 
108
124
  // Get file content context
109
- let beforeContent = '';
110
- let afterContent = '';
125
+ let beforeContent = ''
126
+ let afterContent = ''
111
127
 
112
128
  if (status !== 'A' && !diff.includes('not available')) {
113
- const beforeResult = this.gitManager.execGitShow(`git show ${commitHash}~1:"${filePath}"`);
114
- beforeContent = beforeResult ? beforeResult.slice(0, 1000) : '';
129
+ const beforeResult = this.gitManager.execGitShow(`git show ${commitHash}~1:"${filePath}"`)
130
+ beforeContent = beforeResult ? beforeResult.slice(0, 1000) : ''
115
131
  }
116
132
 
117
133
  if (status !== 'D' && !diff.includes('not available')) {
118
- const afterResult = this.gitManager.execGitShow(`git show ${commitHash}:"${filePath}"`);
119
- afterContent = afterResult ? afterResult.slice(0, 1000) : '';
134
+ const afterResult = this.gitManager.execGitShow(`git show ${commitHash}:"${filePath}"`)
135
+ afterContent = afterResult ? afterResult.slice(0, 1000) : ''
120
136
  }
121
137
 
122
138
  return {
@@ -130,96 +146,96 @@ export class GitService {
130
146
  importance: assessFileImportance(filePath, status),
131
147
  complexity: assessChangeComplexity(diff),
132
148
  semanticChanges: analyzeSemanticChanges(diff, filePath),
133
- functionalImpact: analyzeFunctionalImpact(diff, filePath, status)
134
- };
149
+ functionalImpact: analyzeFunctionalImpact(diff, filePath, status),
150
+ }
135
151
  } catch (error) {
136
- console.error(colors.errorMessage(`Error analyzing file change ${filePath}:`), error.message);
137
- return null;
152
+ console.error(colors.errorMessage(`Error analyzing file change ${filePath}:`), error.message)
153
+ return null
138
154
  }
139
155
  }
140
156
 
141
157
  async analyzeWorkingDirectoryFileChange(status, filePath) {
142
158
  try {
143
- let diff = '';
159
+ let diff = ''
144
160
 
145
161
  // Get working directory diff based on status
146
162
  if (status === 'A' || status === '??') {
147
163
  // New/untracked file - show entire content (first 50 lines)
148
164
  try {
149
- const content = this.gitManager.execGitSafe(`cat "${filePath}"`);
150
- if (content && content.trim()) {
151
- const lines = content.split('\n').slice(0, 50);
152
- diff = `New file created with ${content.split('\n').length} lines\n\nContent preview:\n${lines.join('\n')}${content.split('\n').length > 50 ? '\n... (truncated)' : ''}`;
165
+ const content = this.gitManager.execGitSafe(`cat "${filePath}"`)
166
+ if (content?.trim()) {
167
+ const lines = content.split('\n').slice(0, 50)
168
+ diff = `New file created with ${content.split('\n').length} lines\n\nContent preview:\n${lines.join('\n')}${content.split('\n').length > 50 ? '\n... (truncated)' : ''}`
153
169
  } else {
154
- diff = 'New empty file created';
170
+ diff = 'New empty file created'
155
171
  }
156
172
  } catch {
157
- diff = 'New file created (binary or inaccessible)';
173
+ diff = 'New file created (binary or inaccessible)'
158
174
  }
159
175
  } else if (status === 'D') {
160
176
  // Deleted file - get the content that was removed for better analysis
161
177
  try {
162
- const headContent = this.gitManager.execGitSafe(`git show HEAD:"${filePath}"`);
163
- if (headContent && headContent.trim()) {
178
+ const headContent = this.gitManager.execGitSafe(`git show HEAD:"${filePath}"`)
179
+ if (headContent?.trim()) {
164
180
  // Get first 30 lines to understand what was removed
165
- const lines = headContent.split('\n').slice(0, 30);
166
- diff = `File deleted from working directory\n\nRemoved content preview:\n${lines.join('\n')}${headContent.split('\n').length > 30 ? '\n... (truncated)' : ''}`;
181
+ const lines = headContent.split('\n').slice(0, 30)
182
+ diff = `File deleted from working directory\n\nRemoved content preview:\n${lines.join('\n')}${headContent.split('\n').length > 30 ? '\n... (truncated)' : ''}`
167
183
  } else {
168
- diff = 'File deleted from working directory (content was empty)';
184
+ diff = 'File deleted from working directory (content was empty)'
169
185
  }
170
186
  } catch {
171
- diff = 'File deleted from working directory (content unavailable)';
187
+ diff = 'File deleted from working directory (content unavailable)'
172
188
  }
173
189
  } else if (status === 'M' || status.includes('M')) {
174
190
  // Modified file - get actual diff
175
191
  try {
176
- const diffCommand = `git diff HEAD -- "${filePath}"`;
177
- diff = this.gitManager.execGitSafe(diffCommand);
192
+ const diffCommand = `git diff HEAD -- "${filePath}"`
193
+ diff = this.gitManager.execGitSafe(diffCommand)
178
194
  if (!diff || diff.trim() === '') {
179
195
  // Try staged diff if no working directory diff
180
- const stagedDiff = this.gitManager.execGitSafe(`git diff --cached -- "${filePath}"`);
181
- diff = stagedDiff || 'No diff available (binary or identical)';
196
+ const stagedDiff = this.gitManager.execGitSafe(`git diff --cached -- "${filePath}"`)
197
+ diff = stagedDiff || 'No diff available (binary or identical)'
182
198
  }
183
199
  } catch {
184
- diff = 'Modified file (diff unavailable)';
200
+ diff = 'Modified file (diff unavailable)'
185
201
  }
186
202
  } else if (status === 'R') {
187
203
  // Renamed file
188
- diff = 'File renamed in working directory';
204
+ diff = 'File renamed in working directory'
189
205
  } else {
190
206
  // Other status
191
- diff = `File status: ${status}`;
207
+ diff = `File status: ${status}`
192
208
  }
193
209
 
194
210
  // Get file content context
195
- let beforeContent = '';
196
- let afterContent = '';
211
+ let beforeContent = ''
212
+ let afterContent = ''
197
213
 
198
214
  if (status === 'D') {
199
215
  // For deleted files, get the content that was removed
200
216
  try {
201
- const headResult = this.gitManager.execGitSafe(`git show HEAD:"${filePath}"`);
202
- beforeContent = headResult ? headResult.slice(0, 1000) : '';
217
+ const headResult = this.gitManager.execGitSafe(`git show HEAD:"${filePath}"`)
218
+ beforeContent = headResult ? headResult.slice(0, 1000) : ''
203
219
  } catch {
204
- beforeContent = '';
220
+ beforeContent = ''
205
221
  }
206
222
  // afterContent stays empty for deleted files
207
223
  } else if (status !== 'A' && status !== '??') {
208
224
  // For modified files, get both before and after
209
225
  try {
210
226
  // Get HEAD version
211
- const headResult = this.gitManager.execGitSafe(`git show HEAD:"${filePath}"`);
212
- beforeContent = headResult ? headResult.slice(0, 1000) : '';
227
+ const headResult = this.gitManager.execGitSafe(`git show HEAD:"${filePath}"`)
228
+ beforeContent = headResult ? headResult.slice(0, 1000) : ''
213
229
  } catch {
214
- beforeContent = '';
230
+ beforeContent = ''
215
231
  }
216
232
 
217
233
  try {
218
234
  // Get current working directory version
219
- const currentResult = this.gitManager.execGitSafe(`cat "${filePath}"`);
220
- afterContent = currentResult ? currentResult.slice(0, 1000) : '';
235
+ const currentResult = this.gitManager.execGitSafe(`cat "${filePath}"`)
236
+ afterContent = currentResult ? currentResult.slice(0, 1000) : ''
221
237
  } catch {
222
- afterContent = '';
238
+ afterContent = ''
223
239
  }
224
240
  }
225
241
 
@@ -234,10 +250,13 @@ export class GitService {
234
250
  importance: assessFileImportance(filePath, status),
235
251
  complexity: assessChangeComplexity(diff),
236
252
  semanticChanges: analyzeSemanticChanges(diff, filePath),
237
- functionalImpact: analyzeFunctionalImpact(diff, filePath, status)
238
- };
253
+ functionalImpact: analyzeFunctionalImpact(diff, filePath, status),
254
+ }
239
255
  } catch (error) {
240
- console.error(colors.errorMessage(`Error analyzing working directory file change ${filePath}:`), error.message);
256
+ console.error(
257
+ colors.errorMessage(`Error analyzing working directory file change ${filePath}:`),
258
+ error.message
259
+ )
241
260
  return {
242
261
  status,
243
262
  filePath,
@@ -249,54 +268,55 @@ export class GitService {
249
268
  importance: 'medium',
250
269
  complexity: { score: 1 },
251
270
  semanticChanges: { changeType: 'unknown', patterns: [], frameworks: [] },
252
- functionalImpact: { scope: 'local', severity: 'low' }
253
- };
271
+ functionalImpact: { scope: 'local', severity: 'low' },
272
+ }
254
273
  }
255
274
  }
256
275
 
257
276
  getCommitDiffStats(commitHash) {
258
277
  try {
259
- const command = `git show --stat --pretty=format: ${commitHash}`;
260
- const output = this.gitManager.execGitSafe(command);
261
- const lines = output.split('\n').filter(Boolean);
262
- const summary = lines[lines.length - 1];
278
+ const command = `git show --stat --pretty=format: ${commitHash}`
279
+ const output = this.gitManager.execGitSafe(command)
280
+ const lines = output.split('\n').filter(Boolean)
281
+ const summary = lines.at(-1)
263
282
 
264
- if (summary && summary.includes('changed')) {
265
- const match = summary.match(/(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/);
283
+ if (summary?.includes('changed')) {
284
+ const match = summary.match(
285
+ /(\d+) files? changed(?:, (\d+) insertions?\(\+\))?(?:, (\d+) deletions?\(-\))?/
286
+ )
266
287
  if (match) {
267
288
  return {
268
- files: parseInt(match[1]),
269
- insertions: parseInt(match[2] || 0),
270
- deletions: parseInt(match[3] || 0)
271
- };
289
+ files: Number.parseInt(match[1], 10),
290
+ insertions: Number.parseInt(match[2] || 0, 10),
291
+ deletions: Number.parseInt(match[3] || 0, 10),
292
+ }
272
293
  }
273
294
  }
274
295
 
275
- return { files: 0, insertions: 0, deletions: 0 };
296
+ return { files: 0, insertions: 0, deletions: 0 }
276
297
  } catch {
277
- return { files: 0, insertions: 0, deletions: 0 };
298
+ return { files: 0, insertions: 0, deletions: 0 }
278
299
  }
279
300
  }
280
301
 
281
302
  async getCommitsSince(since) {
282
303
  try {
283
- const command = since
284
- ? `git log --oneline --since="${since}"`
285
- : 'git log --oneline -10';
304
+ const command = since ? `git log --oneline --since="${since}"` : 'git log --oneline -10'
286
305
 
287
- const output = this.gitManager.execGitSafe(command);
288
- return output.split('\n')
306
+ const output = this.gitManager.execGitSafe(command)
307
+ return output
308
+ .split('\n')
289
309
  .filter(Boolean)
290
- .map(line => {
291
- const [hash, ...messageParts] = line.split(' ');
310
+ .map((line) => {
311
+ const [hash, ...messageParts] = line.split(' ')
292
312
  return {
293
313
  hash: hash.substring(0, 7),
294
- message: messageParts.join(' ')
295
- };
296
- });
314
+ message: messageParts.join(' '),
315
+ }
316
+ })
297
317
  } catch (error) {
298
- console.error(colors.errorMessage('Error getting commits since:'), error.message);
299
- return [];
318
+ console.error(colors.errorMessage('Error getting commits since:'), error.message)
319
+ return []
300
320
  }
301
321
  }
302
- }
322
+ }