@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +41 -10
  2. package/ai-changelog-mcp.sh +0 -0
  3. package/ai-changelog.sh +0 -0
  4. package/bin/ai-changelog-dxt.js +0 -0
  5. package/package.json +72 -80
  6. package/src/ai-changelog-generator.js +11 -2
  7. package/src/application/orchestrators/changelog.orchestrator.js +12 -202
  8. package/src/cli.js +4 -5
  9. package/src/domains/ai/ai-analysis.service.js +2 -0
  10. package/src/domains/analysis/analysis.engine.js +758 -5
  11. package/src/domains/changelog/changelog.service.js +711 -13
  12. package/src/domains/changelog/workspace-changelog.service.js +429 -571
  13. package/src/domains/git/commit-tagger.js +552 -0
  14. package/src/domains/git/git-manager.js +357 -0
  15. package/src/domains/git/git.service.js +865 -16
  16. package/src/infrastructure/cli/cli.controller.js +14 -9
  17. package/src/infrastructure/config/configuration.manager.js +24 -2
  18. package/src/infrastructure/interactive/interactive-workflow.service.js +8 -1
  19. package/src/infrastructure/mcp/mcp-server.service.js +35 -11
  20. package/src/infrastructure/providers/core/base-provider.js +1 -1
  21. package/src/infrastructure/providers/implementations/anthropic.js +16 -173
  22. package/src/infrastructure/providers/implementations/azure.js +16 -63
  23. package/src/infrastructure/providers/implementations/dummy.js +13 -16
  24. package/src/infrastructure/providers/implementations/mock.js +13 -26
  25. package/src/infrastructure/providers/implementations/ollama.js +12 -4
  26. package/src/infrastructure/providers/implementations/openai.js +13 -165
  27. package/src/infrastructure/providers/provider-management.service.js +126 -412
  28. package/src/infrastructure/providers/utils/base-provider-helpers.js +11 -0
  29. package/src/shared/utils/cli-ui.js +1 -1
  30. package/src/shared/utils/diff-processor.js +21 -19
  31. package/src/shared/utils/error-classes.js +33 -0
  32. package/src/shared/utils/utils.js +65 -60
  33. 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
+ }