@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,108 +1,108 @@
1
- import { execSync } from 'child_process';
2
- import colors from '../../shared/constants/colors.js';
1
+ import { execSync } from 'node:child_process'
2
+
3
+ import colors from '../../shared/constants/colors.js'
3
4
 
4
5
  /**
5
6
  * Interactive Staging Service
6
- *
7
+ *
7
8
  * Provides interactive git staging functionality similar to better-commits
8
9
  * Handles file selection, staging, and unstaging operations
9
10
  */
10
11
  export class InteractiveStagingService {
11
12
  constructor(gitManager) {
12
- this.gitManager = gitManager;
13
+ this.gitManager = gitManager
13
14
  }
14
15
 
15
16
  /**
16
17
  * Show git status with staged and unstaged changes
17
18
  */
18
19
  async showGitStatus() {
19
- console.log(colors.processingMessage(' Checking Git Status '));
20
+ console.log(colors.processingMessage(' Checking Git Status '))
21
+
22
+ const statusResult = this.getDetailedStatus()
20
23
 
21
- const statusResult = this.getDetailedStatus();
22
-
23
24
  if (statusResult.staged.length > 0) {
24
- console.log(colors.successMessage('Changes to be committed:'));
25
- statusResult.staged.forEach(file => {
26
- const icon = this.getStatusIcon(file.status);
27
- console.log(colors.success(` ${icon} ${file.status} ${file.path}`));
28
- });
25
+ console.log(colors.successMessage('Changes to be committed:'))
26
+ statusResult.staged.forEach((file) => {
27
+ const icon = this.getStatusIcon(file.status)
28
+ console.log(colors.success(` ${icon} ${file.status} ${file.path}`))
29
+ })
29
30
  }
30
31
 
31
32
  if (statusResult.unstaged.length > 0) {
32
- console.log(colors.warningMessage('\nChanges not staged for commit:'));
33
- statusResult.unstaged.forEach(file => {
34
- const icon = this.getStatusIcon(file.status);
35
- console.log(colors.warning(` ${icon} ${file.status} ${file.path}`));
36
- });
33
+ console.log(colors.warningMessage('\nChanges not staged for commit:'))
34
+ statusResult.unstaged.forEach((file) => {
35
+ const icon = this.getStatusIcon(file.status)
36
+ console.log(colors.warning(` ${icon} ${file.status} ${file.path}`))
37
+ })
37
38
  }
38
39
 
39
40
  if (statusResult.untracked.length > 0) {
40
- console.log(colors.infoMessage('\nUntracked files:'));
41
- statusResult.untracked.forEach(file => {
42
- console.log(colors.dim(` ✨ ?? ${file.path}`));
43
- });
41
+ console.log(colors.infoMessage('\nUntracked files:'))
42
+ statusResult.untracked.forEach((file) => {
43
+ console.log(colors.dim(` ✨ ?? ${file.path}`))
44
+ })
44
45
  }
45
46
 
46
- return statusResult;
47
+ return statusResult
47
48
  }
48
49
 
49
50
  /**
50
51
  * Interactive file selection for staging
51
52
  */
52
53
  async selectFilesToStage(files = null) {
53
- const { multiselect, confirm } = await import('@clack/prompts');
54
+ const { multiselect, confirm } = await import('@clack/prompts')
54
55
 
55
56
  // Get current status if files not provided
56
57
  if (!files) {
57
- const status = this.getDetailedStatus();
58
- files = [...status.unstaged, ...status.untracked];
58
+ const status = this.getDetailedStatus()
59
+ files = [...status.unstaged, ...status.untracked]
59
60
  }
60
61
 
61
62
  if (files.length === 0) {
62
- console.log(colors.infoMessage('No files available for staging.'));
63
- return [];
63
+ console.log(colors.infoMessage('No files available for staging.'))
64
+ return []
64
65
  }
65
66
 
66
67
  // Create choices for the multiselect prompt
67
- const choices = files.map(file => ({
68
+ const choices = files.map((file) => ({
68
69
  value: file.path,
69
70
  label: `${this.getStatusIcon(file.status)} ${file.status} ${file.path}`,
70
- hint: this.getStatusDescription(file.status)
71
- }));
71
+ hint: this.getStatusDescription(file.status),
72
+ }))
72
73
 
73
74
  try {
74
75
  const selectedFiles = await multiselect({
75
76
  message: 'Select files to stage for commit:',
76
77
  options: choices,
77
- required: false
78
- });
78
+ required: false,
79
+ })
79
80
 
80
81
  if (!selectedFiles || selectedFiles.length === 0) {
81
- console.log(colors.infoMessage('No files selected for staging.'));
82
- return [];
82
+ console.log(colors.infoMessage('No files selected for staging.'))
83
+ return []
83
84
  }
84
85
 
85
86
  // Confirm the selection
86
87
  const shouldStage = await confirm({
87
88
  message: `Stage ${selectedFiles.length} file(s)?`,
88
- initialValue: true
89
- });
89
+ initialValue: true,
90
+ })
90
91
 
91
92
  if (shouldStage) {
92
- await this.stageFiles(selectedFiles);
93
- console.log(colors.successMessage(`✅ Staged ${selectedFiles.length} file(s)`));
94
- return selectedFiles;
95
- } else {
96
- console.log(colors.infoMessage('Staging cancelled.'));
97
- return [];
93
+ await this.stageFiles(selectedFiles)
94
+ console.log(colors.successMessage(`✅ Staged ${selectedFiles.length} file(s)`))
95
+ return selectedFiles
98
96
  }
97
+ console.log(colors.infoMessage('Staging cancelled.'))
98
+ return []
99
99
  } catch (error) {
100
100
  if (error.message.includes('cancelled')) {
101
- console.log(colors.infoMessage('File selection cancelled.'));
101
+ console.log(colors.infoMessage('File selection cancelled.'))
102
102
  } else {
103
- console.error(colors.errorMessage(`Error during file selection: ${error.message}`));
103
+ console.error(colors.errorMessage(`Error during file selection: ${error.message}`))
104
104
  }
105
- return [];
105
+ return []
106
106
  }
107
107
  }
108
108
 
@@ -110,54 +110,53 @@ export class InteractiveStagingService {
110
110
  * Interactive unstaging of files
111
111
  */
112
112
  async selectFilesToUnstage() {
113
- const { multiselect, confirm } = await import('@clack/prompts');
113
+ const { multiselect, confirm } = await import('@clack/prompts')
114
114
 
115
- const status = this.getDetailedStatus();
116
- const stagedFiles = status.staged;
115
+ const status = this.getDetailedStatus()
116
+ const stagedFiles = status.staged
117
117
 
118
118
  if (stagedFiles.length === 0) {
119
- console.log(colors.infoMessage('No staged files to unstage.'));
120
- return [];
119
+ console.log(colors.infoMessage('No staged files to unstage.'))
120
+ return []
121
121
  }
122
122
 
123
- const choices = stagedFiles.map(file => ({
123
+ const choices = stagedFiles.map((file) => ({
124
124
  value: file.path,
125
125
  label: `${this.getStatusIcon(file.status)} ${file.status} ${file.path}`,
126
- hint: 'Remove from staging area'
127
- }));
126
+ hint: 'Remove from staging area',
127
+ }))
128
128
 
129
129
  try {
130
130
  const selectedFiles = await multiselect({
131
131
  message: 'Select files to unstage:',
132
132
  options: choices,
133
- required: false
134
- });
133
+ required: false,
134
+ })
135
135
 
136
136
  if (!selectedFiles || selectedFiles.length === 0) {
137
- console.log(colors.infoMessage('No files selected for unstaging.'));
138
- return [];
137
+ console.log(colors.infoMessage('No files selected for unstaging.'))
138
+ return []
139
139
  }
140
140
 
141
141
  const shouldUnstage = await confirm({
142
142
  message: `Unstage ${selectedFiles.length} file(s)?`,
143
- initialValue: true
144
- });
143
+ initialValue: true,
144
+ })
145
145
 
146
146
  if (shouldUnstage) {
147
- await this.unstageFiles(selectedFiles);
148
- console.log(colors.successMessage(`✅ Unstaged ${selectedFiles.length} file(s)`));
149
- return selectedFiles;
150
- } else {
151
- console.log(colors.infoMessage('Unstaging cancelled.'));
152
- return [];
147
+ await this.unstageFiles(selectedFiles)
148
+ console.log(colors.successMessage(`✅ Unstaged ${selectedFiles.length} file(s)`))
149
+ return selectedFiles
153
150
  }
151
+ console.log(colors.infoMessage('Unstaging cancelled.'))
152
+ return []
154
153
  } catch (error) {
155
154
  if (error.message.includes('cancelled')) {
156
- console.log(colors.infoMessage('File unstaging cancelled.'));
155
+ console.log(colors.infoMessage('File unstaging cancelled.'))
157
156
  } else {
158
- console.error(colors.errorMessage(`Error during file unstaging: ${error.message}`));
157
+ console.error(colors.errorMessage(`Error during file unstaging: ${error.message}`))
159
158
  }
160
- return [];
159
+ return []
161
160
  }
162
161
  }
163
162
 
@@ -166,13 +165,13 @@ export class InteractiveStagingService {
166
165
  */
167
166
  async stageAllChanges() {
168
167
  try {
169
- console.log(colors.processingMessage('Staging all changes...'));
170
- execSync('git add .', { stdio: 'pipe' });
171
- console.log(colors.successMessage('✅ All changes staged'));
172
- return true;
168
+ console.log(colors.processingMessage('Staging all changes...'))
169
+ execSync('git add .', { stdio: 'pipe' })
170
+ console.log(colors.successMessage('✅ All changes staged'))
171
+ return true
173
172
  } catch (error) {
174
- console.error(colors.errorMessage(`Error staging all changes: ${error.message}`));
175
- return false;
173
+ console.error(colors.errorMessage(`Error staging all changes: ${error.message}`))
174
+ return false
176
175
  }
177
176
  }
178
177
 
@@ -181,16 +180,25 @@ export class InteractiveStagingService {
181
180
  */
182
181
  async stageFiles(filePaths) {
183
182
  try {
184
- const files = Array.isArray(filePaths) ? filePaths : [filePaths];
185
-
186
- for (const file of files) {
187
- execSync(`git add "${file}"`, { stdio: 'pipe' });
183
+ const files = Array.isArray(filePaths) ? filePaths : [filePaths]
184
+
185
+ // Use spawn with argument array to avoid command injection
186
+ if (files.length > 0) {
187
+ const { spawnSync } = await import('node:child_process')
188
+ const result = spawnSync('git', ['add', ...files], {
189
+ stdio: 'pipe',
190
+ encoding: 'utf8',
191
+ })
192
+
193
+ if (result.status !== 0) {
194
+ throw new Error(`git add failed: ${result.stderr}`)
195
+ }
188
196
  }
189
-
190
- return true;
197
+
198
+ return true
191
199
  } catch (error) {
192
- console.error(colors.errorMessage(`Error staging files: ${error.message}`));
193
- return false;
200
+ console.error(colors.errorMessage(`Error staging files: ${error.message}`))
201
+ return false
194
202
  }
195
203
  }
196
204
 
@@ -199,16 +207,25 @@ export class InteractiveStagingService {
199
207
  */
200
208
  async unstageFiles(filePaths) {
201
209
  try {
202
- const files = Array.isArray(filePaths) ? filePaths : [filePaths];
203
-
204
- for (const file of files) {
205
- execSync(`git reset HEAD "${file}"`, { stdio: 'pipe' });
210
+ const files = Array.isArray(filePaths) ? filePaths : [filePaths]
211
+
212
+ // Use spawn with argument array to avoid command injection
213
+ if (files.length > 0) {
214
+ const { spawnSync } = await import('node:child_process')
215
+ const result = spawnSync('git', ['reset', 'HEAD', ...files], {
216
+ stdio: 'pipe',
217
+ encoding: 'utf8',
218
+ })
219
+
220
+ if (result.status !== 0) {
221
+ throw new Error(`git reset failed: ${result.stderr}`)
222
+ }
206
223
  }
207
-
208
- return true;
224
+
225
+ return true
209
226
  } catch (error) {
210
- console.error(colors.errorMessage(`Error unstaging files: ${error.message}`));
211
- return false;
227
+ console.error(colors.errorMessage(`Error unstaging files: ${error.message}`))
228
+ return false
212
229
  }
213
230
  }
214
231
 
@@ -217,41 +234,43 @@ export class InteractiveStagingService {
217
234
  */
218
235
  getDetailedStatus() {
219
236
  try {
220
- const output = execSync('git status --porcelain', { encoding: 'utf8' });
221
- const lines = output.split('\n').filter(Boolean);
237
+ const output = execSync('git status --porcelain', { encoding: 'utf8' })
238
+ const lines = output.split('\n').filter(Boolean)
222
239
 
223
- const staged = [];
224
- const unstaged = [];
225
- const untracked = [];
240
+ const staged = []
241
+ const unstaged = []
242
+ const untracked = []
226
243
 
227
- lines.forEach(line => {
228
- if (line.length < 3) return;
244
+ lines.forEach((line) => {
245
+ if (line.length < 3) {
246
+ return
247
+ }
229
248
 
230
- const indexStatus = line.charAt(0);
231
- const workTreeStatus = line.charAt(1);
232
- const filePath = line.substring(3).trim();
249
+ const indexStatus = line.charAt(0)
250
+ const workTreeStatus = line.charAt(1)
251
+ const filePath = line.substring(3).trim()
233
252
 
234
253
  // Handle different status combinations
235
254
  if (indexStatus === '?' && workTreeStatus === '?') {
236
255
  // Untracked file
237
- untracked.push({ status: '??', path: filePath });
256
+ untracked.push({ status: '??', path: filePath })
238
257
  } else {
239
258
  // Staged changes (index status)
240
259
  if (indexStatus !== ' ') {
241
- staged.push({ status: indexStatus, path: filePath });
260
+ staged.push({ status: indexStatus, path: filePath })
242
261
  }
243
-
262
+
244
263
  // Unstaged changes (work tree status)
245
264
  if (workTreeStatus !== ' ') {
246
- unstaged.push({ status: workTreeStatus, path: filePath });
265
+ unstaged.push({ status: workTreeStatus, path: filePath })
247
266
  }
248
267
  }
249
- });
268
+ })
250
269
 
251
- return { staged, unstaged, untracked };
270
+ return { staged, unstaged, untracked }
252
271
  } catch (error) {
253
- console.error(colors.errorMessage(`Error getting git status: ${error.message}`));
254
- return { staged: [], unstaged: [], untracked: [] };
272
+ console.error(colors.errorMessage(`Error getting git status: ${error.message}`))
273
+ return { staged: [], unstaged: [], untracked: [] }
255
274
  }
256
275
  }
257
276
 
@@ -260,15 +279,15 @@ export class InteractiveStagingService {
260
279
  */
261
280
  getStatusIcon(status) {
262
281
  const icons = {
263
- 'M': '📝', // Modified
264
- 'A': '✨', // Added
265
- 'D': '🗑️', // Deleted
266
- 'R': '🔄', // Renamed
267
- 'C': '📋', // Copied
268
- 'U': '⚠️', // Unmerged
269
- '??': '✨' // Untracked
270
- };
271
- return icons[status] || '📄';
282
+ M: '📝', // Modified
283
+ A: '✨', // Added
284
+ D: '🗑️', // Deleted
285
+ R: '🔄', // Renamed
286
+ C: '📋', // Copied
287
+ U: '⚠️', // Unmerged
288
+ '??': '✨', // Untracked
289
+ }
290
+ return icons[status] || '📄'
272
291
  }
273
292
 
274
293
  /**
@@ -276,15 +295,15 @@ export class InteractiveStagingService {
276
295
  */
277
296
  getStatusDescription(status) {
278
297
  const descriptions = {
279
- 'M': 'Modified file',
280
- 'A': 'New file',
281
- 'D': 'Deleted file',
282
- 'R': 'Renamed file',
283
- 'C': 'Copied file',
284
- 'U': 'Unmerged file',
285
- '??': 'Untracked file'
286
- };
287
- return descriptions[status] || 'Changed file';
298
+ M: 'Modified file',
299
+ A: 'New file',
300
+ D: 'Deleted file',
301
+ R: 'Renamed file',
302
+ C: 'Copied file',
303
+ U: 'Unmerged file',
304
+ '??': 'Untracked file',
305
+ }
306
+ return descriptions[status] || 'Changed file'
288
307
  }
289
308
 
290
309
  /**
@@ -292,10 +311,10 @@ export class InteractiveStagingService {
292
311
  */
293
312
  hasStagedChanges() {
294
313
  try {
295
- const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
296
- return output.trim().length > 0;
297
- } catch (error) {
298
- return false;
314
+ const output = execSync('git diff --cached --name-only', { encoding: 'utf8' })
315
+ return output.trim().length > 0
316
+ } catch (_error) {
317
+ return false
299
318
  }
300
319
  }
301
320
 
@@ -304,10 +323,10 @@ export class InteractiveStagingService {
304
323
  */
305
324
  hasUnstagedChanges() {
306
325
  try {
307
- const output = execSync('git diff --name-only', { encoding: 'utf8' });
308
- return output.trim().length > 0;
309
- } catch (error) {
310
- return false;
326
+ const output = execSync('git diff --name-only', { encoding: 'utf8' })
327
+ return output.trim().length > 0
328
+ } catch (_error) {
329
+ return false
311
330
  }
312
331
  }
313
- }
332
+ }