@entro314labs/ai-changelog-generator 3.2.0 → 3.3.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 +41 -10
- package/ai-changelog-mcp.sh +0 -0
- package/ai-changelog.sh +0 -0
- package/bin/ai-changelog-dxt.js +0 -0
- package/package.json +72 -80
- package/src/ai-changelog-generator.js +11 -2
- package/src/application/orchestrators/changelog.orchestrator.js +12 -202
- package/src/cli.js +4 -5
- package/src/domains/ai/ai-analysis.service.js +2 -0
- package/src/domains/analysis/analysis.engine.js +758 -5
- package/src/domains/changelog/changelog.service.js +711 -13
- package/src/domains/changelog/workspace-changelog.service.js +429 -571
- package/src/domains/git/commit-tagger.js +552 -0
- package/src/domains/git/git-manager.js +357 -0
- package/src/domains/git/git.service.js +865 -16
- package/src/infrastructure/cli/cli.controller.js +14 -9
- package/src/infrastructure/config/configuration.manager.js +24 -2
- package/src/infrastructure/interactive/interactive-workflow.service.js +8 -1
- package/src/infrastructure/mcp/mcp-server.service.js +35 -11
- package/src/infrastructure/providers/core/base-provider.js +1 -1
- package/src/infrastructure/providers/implementations/anthropic.js +16 -173
- package/src/infrastructure/providers/implementations/azure.js +16 -63
- package/src/infrastructure/providers/implementations/dummy.js +13 -16
- package/src/infrastructure/providers/implementations/mock.js +13 -26
- package/src/infrastructure/providers/implementations/ollama.js +12 -4
- package/src/infrastructure/providers/implementations/openai.js +13 -165
- package/src/infrastructure/providers/provider-management.service.js +126 -412
- package/src/infrastructure/providers/utils/base-provider-helpers.js +11 -0
- package/src/shared/utils/cli-ui.js +1 -1
- package/src/shared/utils/diff-processor.js +21 -19
- package/src/shared/utils/error-classes.js +33 -0
- package/src/shared/utils/utils.js +65 -60
- package/src/domains/git/git-repository.analyzer.js +0 -678
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
import { GitError } from '../../shared/utils/error-classes.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GitManager - Handles all Git command execution and repository operations
|
|
7
|
+
*
|
|
8
|
+
* This class provides a centralized interface for Git operations with proper
|
|
9
|
+
* error handling, command validation, and consistent output formatting.
|
|
10
|
+
*/
|
|
11
|
+
export class GitManager {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.options = {
|
|
14
|
+
encoding: 'utf8',
|
|
15
|
+
timeout: 30000,
|
|
16
|
+
stdio: 'pipe',
|
|
17
|
+
...options,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Cache git repository status
|
|
21
|
+
this._isGitRepoCache = null
|
|
22
|
+
this._gitDirCache = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if current directory is a Git repository
|
|
27
|
+
* @returns {boolean} True if in a Git repository
|
|
28
|
+
*/
|
|
29
|
+
get isGitRepo() {
|
|
30
|
+
if (this._isGitRepoCache === null) {
|
|
31
|
+
this._isGitRepoCache = this._checkIsGitRepo()
|
|
32
|
+
}
|
|
33
|
+
return this._isGitRepoCache
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the Git directory path
|
|
38
|
+
* @returns {string|null} Path to .git directory or null if not a Git repo
|
|
39
|
+
*/
|
|
40
|
+
get gitDir() {
|
|
41
|
+
if (this._gitDirCache === null && this.isGitRepo) {
|
|
42
|
+
try {
|
|
43
|
+
this._gitDirCache = this.execGitSafe('git rev-parse --git-dir').trim()
|
|
44
|
+
} catch {
|
|
45
|
+
this._gitDirCache = null
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return this._gitDirCache
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Execute a Git command with full error handling
|
|
53
|
+
* @param {string} command - The Git command to execute
|
|
54
|
+
* @returns {string} Command output
|
|
55
|
+
* @throws {GitError} If command fails
|
|
56
|
+
*/
|
|
57
|
+
execGit(command) {
|
|
58
|
+
try {
|
|
59
|
+
return execSync(command, {
|
|
60
|
+
encoding: this.options.encoding,
|
|
61
|
+
stdio: this.options.stdio,
|
|
62
|
+
timeout: this.options.timeout,
|
|
63
|
+
})
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw this._createGitError(command, error)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Execute a Git command safely, returning empty string on failure
|
|
71
|
+
* @param {string} command - The Git command to execute
|
|
72
|
+
* @returns {string} Command output or empty string on failure
|
|
73
|
+
*/
|
|
74
|
+
execGitSafe(command) {
|
|
75
|
+
try {
|
|
76
|
+
return this.execGit(command)
|
|
77
|
+
} catch {
|
|
78
|
+
return ''
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Execute a Git show command with specific error handling for missing files
|
|
84
|
+
* @param {string} command - The Git show command to execute
|
|
85
|
+
* @returns {string|null} Command output or null if file doesn't exist
|
|
86
|
+
*/
|
|
87
|
+
execGitShow(command) {
|
|
88
|
+
try {
|
|
89
|
+
return this.execGit(command)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// Return null for missing files rather than throwing
|
|
92
|
+
if (
|
|
93
|
+
error.message.includes('does not exist') ||
|
|
94
|
+
error.message.includes('bad file') ||
|
|
95
|
+
error.message.includes('not found')
|
|
96
|
+
) {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
throw error
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate a commit hash exists in the repository
|
|
105
|
+
* @param {string} hash - The commit hash to validate
|
|
106
|
+
* @returns {boolean} True if commit exists
|
|
107
|
+
*/
|
|
108
|
+
validateCommitHash(hash) {
|
|
109
|
+
if (!hash || typeof hash !== 'string') {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Basic format validation
|
|
114
|
+
if (!/^[a-f0-9]{6,40}$/i.test(hash)) {
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
this.execGit(`git cat-file -e ${hash}`)
|
|
120
|
+
return true
|
|
121
|
+
} catch {
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the current branch name
|
|
128
|
+
* @returns {string} Current branch name or 'HEAD' if detached
|
|
129
|
+
*/
|
|
130
|
+
getCurrentBranch() {
|
|
131
|
+
try {
|
|
132
|
+
return this.execGitSafe('git branch --show-current').trim() || 'HEAD'
|
|
133
|
+
} catch {
|
|
134
|
+
return 'HEAD'
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get the remote URL for the repository
|
|
140
|
+
* @param {string} remoteName - Name of the remote (default: 'origin')
|
|
141
|
+
* @returns {string|null} Remote URL or null if not found
|
|
142
|
+
*/
|
|
143
|
+
getRemoteUrl(remoteName = 'origin') {
|
|
144
|
+
try {
|
|
145
|
+
return this.execGitSafe(`git remote get-url ${remoteName}`).trim() || null
|
|
146
|
+
} catch {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get repository root directory
|
|
153
|
+
* @returns {string|null} Repository root path or null if not in a Git repo
|
|
154
|
+
*/
|
|
155
|
+
getRepositoryRoot() {
|
|
156
|
+
try {
|
|
157
|
+
return this.execGitSafe('git rev-parse --show-toplevel').trim() || null
|
|
158
|
+
} catch {
|
|
159
|
+
return null
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if working directory is clean (no uncommitted changes)
|
|
165
|
+
* @returns {boolean} True if working directory is clean
|
|
166
|
+
*/
|
|
167
|
+
isWorkingDirectoryClean() {
|
|
168
|
+
try {
|
|
169
|
+
const status = this.execGitSafe('git status --porcelain')
|
|
170
|
+
return status.trim() === ''
|
|
171
|
+
} catch {
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get list of all tags in repository
|
|
178
|
+
* @returns {Array<string>} Array of tag names
|
|
179
|
+
*/
|
|
180
|
+
getAllTags() {
|
|
181
|
+
try {
|
|
182
|
+
const output = this.execGitSafe('git tag -l')
|
|
183
|
+
return output
|
|
184
|
+
.split('\n')
|
|
185
|
+
.filter((tag) => tag.trim())
|
|
186
|
+
.sort()
|
|
187
|
+
} catch {
|
|
188
|
+
return []
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get the latest tag
|
|
194
|
+
* @returns {string|null} Latest tag name or null if no tags
|
|
195
|
+
*/
|
|
196
|
+
getLatestTag() {
|
|
197
|
+
try {
|
|
198
|
+
return this.execGitSafe('git describe --tags --abbrev=0').trim() || null
|
|
199
|
+
} catch {
|
|
200
|
+
return null
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Get commits between two references
|
|
206
|
+
* @param {string} from - Starting reference (commit, tag, branch)
|
|
207
|
+
* @param {string} to - Ending reference (default: 'HEAD')
|
|
208
|
+
* @returns {Array<Object>} Array of commit objects
|
|
209
|
+
*/
|
|
210
|
+
getCommitsBetween(from, to = 'HEAD') {
|
|
211
|
+
try {
|
|
212
|
+
const output = this.execGitSafe(`git log ${from}..${to} --oneline`)
|
|
213
|
+
return output
|
|
214
|
+
.split('\n')
|
|
215
|
+
.filter((line) => line.trim())
|
|
216
|
+
.map((line) => {
|
|
217
|
+
const [hash, ...messageParts] = line.split(' ')
|
|
218
|
+
return {
|
|
219
|
+
hash: hash.trim(),
|
|
220
|
+
message: messageParts.join(' '),
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
} catch {
|
|
224
|
+
return []
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check if a file exists in a specific commit
|
|
230
|
+
* @param {string} commitHash - The commit to check
|
|
231
|
+
* @param {string} filePath - The file path to check
|
|
232
|
+
* @returns {boolean} True if file exists in the commit
|
|
233
|
+
*/
|
|
234
|
+
fileExistsInCommit(commitHash, filePath) {
|
|
235
|
+
try {
|
|
236
|
+
this.execGit(`git cat-file -e ${commitHash}:${filePath}`)
|
|
237
|
+
return true
|
|
238
|
+
} catch {
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get file content from a specific commit
|
|
245
|
+
* @param {string} commitHash - The commit to get the file from
|
|
246
|
+
* @param {string} filePath - The file path to retrieve
|
|
247
|
+
* @returns {string|null} File content or null if file doesn't exist
|
|
248
|
+
*/
|
|
249
|
+
getFileFromCommit(commitHash, filePath) {
|
|
250
|
+
try {
|
|
251
|
+
return this.execGitShow(`git show ${commitHash}:${filePath}`)
|
|
252
|
+
} catch {
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Reset internal caches (call when repository state might have changed)
|
|
259
|
+
*/
|
|
260
|
+
resetCache() {
|
|
261
|
+
this._isGitRepoCache = null
|
|
262
|
+
this._gitDirCache = null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Get detailed repository information
|
|
267
|
+
* @returns {Object} Repository information object
|
|
268
|
+
*/
|
|
269
|
+
getRepositoryInfo() {
|
|
270
|
+
if (!this.isGitRepo) {
|
|
271
|
+
return { isGitRepo: false }
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
isGitRepo: true,
|
|
276
|
+
gitDir: this.gitDir,
|
|
277
|
+
repositoryRoot: this.getRepositoryRoot(),
|
|
278
|
+
currentBranch: this.getCurrentBranch(),
|
|
279
|
+
remoteUrl: this.getRemoteUrl(),
|
|
280
|
+
isClean: this.isWorkingDirectoryClean(),
|
|
281
|
+
latestTag: this.getLatestTag(),
|
|
282
|
+
totalTags: this.getAllTags().length,
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Private methods
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if current directory is a Git repository
|
|
290
|
+
* @private
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
_checkIsGitRepo() {
|
|
294
|
+
try {
|
|
295
|
+
execSync('git rev-parse --git-dir', {
|
|
296
|
+
stdio: 'ignore',
|
|
297
|
+
timeout: 5000,
|
|
298
|
+
})
|
|
299
|
+
return true
|
|
300
|
+
} catch {
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Create a GitError from a command execution error
|
|
307
|
+
* @private
|
|
308
|
+
* @param {string} command - The Git command that failed
|
|
309
|
+
* @param {Error} error - The original error
|
|
310
|
+
* @returns {GitError}
|
|
311
|
+
*/
|
|
312
|
+
_createGitError(command, error) {
|
|
313
|
+
const gitCommand = command.replace(/^git\s+/, '')
|
|
314
|
+
|
|
315
|
+
// Enhanced error handling with more specific messages
|
|
316
|
+
if (error.code === 128) {
|
|
317
|
+
if (error.message.includes('not a git repository')) {
|
|
318
|
+
return GitError.fromCommandFailure(gitCommand, null, null, 'Not in a git repository', error)
|
|
319
|
+
}
|
|
320
|
+
return GitError.fromCommandFailure(
|
|
321
|
+
gitCommand,
|
|
322
|
+
null,
|
|
323
|
+
null,
|
|
324
|
+
`Git repository error: ${error.message}`,
|
|
325
|
+
error
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (error.code === 129) {
|
|
330
|
+
return GitError.fromCommandFailure(
|
|
331
|
+
gitCommand,
|
|
332
|
+
null,
|
|
333
|
+
null,
|
|
334
|
+
`Git command syntax error: ${command}`,
|
|
335
|
+
error
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (error.code === 'ENOENT') {
|
|
340
|
+
return GitError.fromCommandFailure(
|
|
341
|
+
gitCommand,
|
|
342
|
+
null,
|
|
343
|
+
null,
|
|
344
|
+
'Git is not installed or not in PATH',
|
|
345
|
+
error
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return GitError.fromCommandFailure(
|
|
350
|
+
gitCommand,
|
|
351
|
+
null,
|
|
352
|
+
null,
|
|
353
|
+
`Git command failed: ${error.message}`,
|
|
354
|
+
error
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
}
|