@entro314labs/ai-changelog-generator 3.0.5 → 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.
- package/CHANGELOG.md +383 -785
- package/README.md +30 -3
- package/ai-changelog-mcp.sh +0 -0
- package/ai-changelog.sh +0 -0
- package/bin/ai-changelog-dxt.js +9 -9
- package/bin/ai-changelog-mcp.js +19 -17
- package/bin/ai-changelog.js +6 -6
- package/package.json +84 -52
- package/src/ai-changelog-generator.js +83 -81
- package/src/application/orchestrators/changelog.orchestrator.js +1040 -296
- package/src/application/services/application.service.js +145 -123
- package/src/cli.js +76 -57
- package/src/domains/ai/ai-analysis.service.js +289 -209
- package/src/domains/analysis/analysis.engine.js +253 -193
- package/src/domains/changelog/changelog.service.js +1062 -784
- package/src/domains/changelog/workspace-changelog.service.js +420 -249
- package/src/domains/git/git-repository.analyzer.js +348 -258
- package/src/domains/git/git.service.js +132 -112
- package/src/infrastructure/cli/cli.controller.js +415 -247
- package/src/infrastructure/config/configuration.manager.js +220 -190
- package/src/infrastructure/interactive/interactive-staging.service.js +332 -0
- package/src/infrastructure/interactive/interactive-workflow.service.js +200 -159
- package/src/infrastructure/mcp/mcp-server.service.js +208 -207
- package/src/infrastructure/metrics/metrics.collector.js +140 -123
- package/src/infrastructure/providers/core/base-provider.js +87 -40
- package/src/infrastructure/providers/implementations/anthropic.js +101 -99
- package/src/infrastructure/providers/implementations/azure.js +124 -101
- package/src/infrastructure/providers/implementations/bedrock.js +136 -126
- package/src/infrastructure/providers/implementations/dummy.js +23 -23
- package/src/infrastructure/providers/implementations/google.js +123 -114
- package/src/infrastructure/providers/implementations/huggingface.js +94 -87
- package/src/infrastructure/providers/implementations/lmstudio.js +75 -60
- package/src/infrastructure/providers/implementations/mock.js +69 -73
- package/src/infrastructure/providers/implementations/ollama.js +89 -66
- package/src/infrastructure/providers/implementations/openai.js +88 -89
- package/src/infrastructure/providers/implementations/vertex.js +227 -197
- package/src/infrastructure/providers/provider-management.service.js +245 -207
- package/src/infrastructure/providers/provider-manager.service.js +145 -125
- package/src/infrastructure/providers/utils/base-provider-helpers.js +308 -302
- package/src/infrastructure/providers/utils/model-config.js +220 -195
- package/src/infrastructure/providers/utils/provider-utils.js +105 -100
- package/src/infrastructure/validation/commit-message-validation.service.js +556 -0
- package/src/shared/constants/colors.js +467 -172
- package/src/shared/utils/cli-demo.js +285 -0
- package/src/shared/utils/cli-entry-utils.js +257 -249
- package/src/shared/utils/cli-ui.js +447 -0
- package/src/shared/utils/diff-processor.js +513 -0
- package/src/shared/utils/error-classes.js +125 -156
- package/src/shared/utils/json-utils.js +93 -89
- package/src/shared/utils/utils.js +1299 -775
- package/types/index.d.ts +353 -344
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
import colors from '../../shared/constants/colors.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Interactive Staging Service
|
|
7
|
+
*
|
|
8
|
+
* Provides interactive git staging functionality similar to better-commits
|
|
9
|
+
* Handles file selection, staging, and unstaging operations
|
|
10
|
+
*/
|
|
11
|
+
export class InteractiveStagingService {
|
|
12
|
+
constructor(gitManager) {
|
|
13
|
+
this.gitManager = gitManager
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Show git status with staged and unstaged changes
|
|
18
|
+
*/
|
|
19
|
+
async showGitStatus() {
|
|
20
|
+
console.log(colors.processingMessage(' Checking Git Status '))
|
|
21
|
+
|
|
22
|
+
const statusResult = this.getDetailedStatus()
|
|
23
|
+
|
|
24
|
+
if (statusResult.staged.length > 0) {
|
|
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
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (statusResult.unstaged.length > 0) {
|
|
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
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (statusResult.untracked.length > 0) {
|
|
41
|
+
console.log(colors.infoMessage('\nUntracked files:'))
|
|
42
|
+
statusResult.untracked.forEach((file) => {
|
|
43
|
+
console.log(colors.dim(` ✨ ?? ${file.path}`))
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return statusResult
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Interactive file selection for staging
|
|
52
|
+
*/
|
|
53
|
+
async selectFilesToStage(files = null) {
|
|
54
|
+
const { multiselect, confirm } = await import('@clack/prompts')
|
|
55
|
+
|
|
56
|
+
// Get current status if files not provided
|
|
57
|
+
if (!files) {
|
|
58
|
+
const status = this.getDetailedStatus()
|
|
59
|
+
files = [...status.unstaged, ...status.untracked]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (files.length === 0) {
|
|
63
|
+
console.log(colors.infoMessage('No files available for staging.'))
|
|
64
|
+
return []
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create choices for the multiselect prompt
|
|
68
|
+
const choices = files.map((file) => ({
|
|
69
|
+
value: file.path,
|
|
70
|
+
label: `${this.getStatusIcon(file.status)} ${file.status} ${file.path}`,
|
|
71
|
+
hint: this.getStatusDescription(file.status),
|
|
72
|
+
}))
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const selectedFiles = await multiselect({
|
|
76
|
+
message: 'Select files to stage for commit:',
|
|
77
|
+
options: choices,
|
|
78
|
+
required: false,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (!selectedFiles || selectedFiles.length === 0) {
|
|
82
|
+
console.log(colors.infoMessage('No files selected for staging.'))
|
|
83
|
+
return []
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Confirm the selection
|
|
87
|
+
const shouldStage = await confirm({
|
|
88
|
+
message: `Stage ${selectedFiles.length} file(s)?`,
|
|
89
|
+
initialValue: true,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (shouldStage) {
|
|
93
|
+
await this.stageFiles(selectedFiles)
|
|
94
|
+
console.log(colors.successMessage(`✅ Staged ${selectedFiles.length} file(s)`))
|
|
95
|
+
return selectedFiles
|
|
96
|
+
}
|
|
97
|
+
console.log(colors.infoMessage('Staging cancelled.'))
|
|
98
|
+
return []
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.message.includes('cancelled')) {
|
|
101
|
+
console.log(colors.infoMessage('File selection cancelled.'))
|
|
102
|
+
} else {
|
|
103
|
+
console.error(colors.errorMessage(`Error during file selection: ${error.message}`))
|
|
104
|
+
}
|
|
105
|
+
return []
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Interactive unstaging of files
|
|
111
|
+
*/
|
|
112
|
+
async selectFilesToUnstage() {
|
|
113
|
+
const { multiselect, confirm } = await import('@clack/prompts')
|
|
114
|
+
|
|
115
|
+
const status = this.getDetailedStatus()
|
|
116
|
+
const stagedFiles = status.staged
|
|
117
|
+
|
|
118
|
+
if (stagedFiles.length === 0) {
|
|
119
|
+
console.log(colors.infoMessage('No staged files to unstage.'))
|
|
120
|
+
return []
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const choices = stagedFiles.map((file) => ({
|
|
124
|
+
value: file.path,
|
|
125
|
+
label: `${this.getStatusIcon(file.status)} ${file.status} ${file.path}`,
|
|
126
|
+
hint: 'Remove from staging area',
|
|
127
|
+
}))
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const selectedFiles = await multiselect({
|
|
131
|
+
message: 'Select files to unstage:',
|
|
132
|
+
options: choices,
|
|
133
|
+
required: false,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
if (!selectedFiles || selectedFiles.length === 0) {
|
|
137
|
+
console.log(colors.infoMessage('No files selected for unstaging.'))
|
|
138
|
+
return []
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const shouldUnstage = await confirm({
|
|
142
|
+
message: `Unstage ${selectedFiles.length} file(s)?`,
|
|
143
|
+
initialValue: true,
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
if (shouldUnstage) {
|
|
147
|
+
await this.unstageFiles(selectedFiles)
|
|
148
|
+
console.log(colors.successMessage(`✅ Unstaged ${selectedFiles.length} file(s)`))
|
|
149
|
+
return selectedFiles
|
|
150
|
+
}
|
|
151
|
+
console.log(colors.infoMessage('Unstaging cancelled.'))
|
|
152
|
+
return []
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error.message.includes('cancelled')) {
|
|
155
|
+
console.log(colors.infoMessage('File unstaging cancelled.'))
|
|
156
|
+
} else {
|
|
157
|
+
console.error(colors.errorMessage(`Error during file unstaging: ${error.message}`))
|
|
158
|
+
}
|
|
159
|
+
return []
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Stage all changes
|
|
165
|
+
*/
|
|
166
|
+
async stageAllChanges() {
|
|
167
|
+
try {
|
|
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
|
|
172
|
+
} catch (error) {
|
|
173
|
+
console.error(colors.errorMessage(`Error staging all changes: ${error.message}`))
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Stage specific files
|
|
180
|
+
*/
|
|
181
|
+
async stageFiles(filePaths) {
|
|
182
|
+
try {
|
|
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
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return true
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(colors.errorMessage(`Error staging files: ${error.message}`))
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Unstage specific files
|
|
207
|
+
*/
|
|
208
|
+
async unstageFiles(filePaths) {
|
|
209
|
+
try {
|
|
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
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return true
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error(colors.errorMessage(`Error unstaging files: ${error.message}`))
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get detailed git status (staged, unstaged, untracked)
|
|
234
|
+
*/
|
|
235
|
+
getDetailedStatus() {
|
|
236
|
+
try {
|
|
237
|
+
const output = execSync('git status --porcelain', { encoding: 'utf8' })
|
|
238
|
+
const lines = output.split('\n').filter(Boolean)
|
|
239
|
+
|
|
240
|
+
const staged = []
|
|
241
|
+
const unstaged = []
|
|
242
|
+
const untracked = []
|
|
243
|
+
|
|
244
|
+
lines.forEach((line) => {
|
|
245
|
+
if (line.length < 3) {
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const indexStatus = line.charAt(0)
|
|
250
|
+
const workTreeStatus = line.charAt(1)
|
|
251
|
+
const filePath = line.substring(3).trim()
|
|
252
|
+
|
|
253
|
+
// Handle different status combinations
|
|
254
|
+
if (indexStatus === '?' && workTreeStatus === '?') {
|
|
255
|
+
// Untracked file
|
|
256
|
+
untracked.push({ status: '??', path: filePath })
|
|
257
|
+
} else {
|
|
258
|
+
// Staged changes (index status)
|
|
259
|
+
if (indexStatus !== ' ') {
|
|
260
|
+
staged.push({ status: indexStatus, path: filePath })
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Unstaged changes (work tree status)
|
|
264
|
+
if (workTreeStatus !== ' ') {
|
|
265
|
+
unstaged.push({ status: workTreeStatus, path: filePath })
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
return { staged, unstaged, untracked }
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error(colors.errorMessage(`Error getting git status: ${error.message}`))
|
|
273
|
+
return { staged: [], unstaged: [], untracked: [] }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get icon for status
|
|
279
|
+
*/
|
|
280
|
+
getStatusIcon(status) {
|
|
281
|
+
const icons = {
|
|
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] || '📄'
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Get description for status
|
|
295
|
+
*/
|
|
296
|
+
getStatusDescription(status) {
|
|
297
|
+
const descriptions = {
|
|
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'
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Check if there are any staged changes
|
|
311
|
+
*/
|
|
312
|
+
hasStagedChanges() {
|
|
313
|
+
try {
|
|
314
|
+
const output = execSync('git diff --cached --name-only', { encoding: 'utf8' })
|
|
315
|
+
return output.trim().length > 0
|
|
316
|
+
} catch (_error) {
|
|
317
|
+
return false
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Check if there are any unstaged changes
|
|
323
|
+
*/
|
|
324
|
+
hasUnstagedChanges() {
|
|
325
|
+
try {
|
|
326
|
+
const output = execSync('git diff --name-only', { encoding: 'utf8' })
|
|
327
|
+
return output.trim().length > 0
|
|
328
|
+
} catch (_error) {
|
|
329
|
+
return false
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|