@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
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import { AnalysisEngine } from '../../domains/analysis/analysis.engine.js'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
1
|
+
import process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import { AIAnalysisService } from '../../domains/ai/ai-analysis.service.js'
|
|
4
|
+
import { AnalysisEngine } from '../../domains/analysis/analysis.engine.js'
|
|
5
|
+
import { ChangelogService } from '../../domains/changelog/changelog.service.js'
|
|
6
|
+
import { GitService } from '../../domains/git/git.service.js'
|
|
7
|
+
import { InteractiveStagingService } from '../../infrastructure/interactive/interactive-staging.service.js'
|
|
8
|
+
import { InteractiveWorkflowService } from '../../infrastructure/interactive/interactive-workflow.service.js'
|
|
9
|
+
import { ProviderManagerService } from '../../infrastructure/providers/provider-manager.service.js'
|
|
10
|
+
import { CommitMessageValidationService } from '../../infrastructure/validation/commit-message-validation.service.js'
|
|
11
|
+
import colors from '../../shared/constants/colors.js'
|
|
8
12
|
|
|
9
13
|
export class ChangelogOrchestrator {
|
|
10
14
|
constructor(configManager, options = {}) {
|
|
11
|
-
this.configManager = configManager
|
|
12
|
-
this.options = options
|
|
13
|
-
this.analysisMode = options.analysisMode || 'standard'
|
|
15
|
+
this.configManager = configManager
|
|
16
|
+
this.options = options
|
|
17
|
+
this.analysisMode = options.analysisMode || 'standard'
|
|
14
18
|
this.metrics = {
|
|
15
19
|
startTime: Date.now(),
|
|
16
20
|
commitsProcessed: 0,
|
|
@@ -19,19 +23,30 @@ export class ChangelogOrchestrator {
|
|
|
19
23
|
batchesProcessed: 0,
|
|
20
24
|
totalTokens: 0,
|
|
21
25
|
ruleBasedFallbacks: 0,
|
|
22
|
-
cacheHits: 0
|
|
23
|
-
}
|
|
26
|
+
cacheHits: 0,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.initialized = false
|
|
30
|
+
this.initializationPromise = null
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
this.
|
|
32
|
+
// Cache frequently used imports for performance
|
|
33
|
+
this._importCache = new Map()
|
|
27
34
|
|
|
28
35
|
// Start initialization
|
|
29
|
-
this.initializationPromise = this.initializeServices()
|
|
36
|
+
this.initializationPromise = this.initializeServices()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Cached import helper to avoid repeated dynamic imports
|
|
40
|
+
async _getCachedImport(moduleName) {
|
|
41
|
+
if (!this._importCache.has(moduleName)) {
|
|
42
|
+
this._importCache.set(moduleName, await import(moduleName))
|
|
43
|
+
}
|
|
44
|
+
return this._importCache.get(moduleName)
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
async ensureInitialized() {
|
|
33
48
|
if (!this.initialized) {
|
|
34
|
-
await this.initializationPromise
|
|
49
|
+
await this.initializationPromise
|
|
35
50
|
}
|
|
36
51
|
}
|
|
37
52
|
|
|
@@ -39,124 +54,179 @@ export class ChangelogOrchestrator {
|
|
|
39
54
|
try {
|
|
40
55
|
// Initialize AI provider
|
|
41
56
|
this.providerManager = new ProviderManagerService(this.configManager.getAll(), {
|
|
42
|
-
errorHandler: { logToConsole: true }
|
|
43
|
-
})
|
|
44
|
-
this.aiProvider = this.providerManager.getActiveProvider()
|
|
57
|
+
errorHandler: { logToConsole: true },
|
|
58
|
+
})
|
|
59
|
+
this.aiProvider = this.providerManager.getActiveProvider()
|
|
45
60
|
|
|
46
61
|
// Create lightweight implementations for missing dependencies
|
|
47
|
-
this.gitManager = await this.createGitManager()
|
|
48
|
-
this.tagger = await this.createTagger()
|
|
49
|
-
this.promptEngine = await this.createPromptEngine()
|
|
62
|
+
this.gitManager = await this.createGitManager()
|
|
63
|
+
this.tagger = await this.createTagger()
|
|
64
|
+
this.promptEngine = await this.createPromptEngine()
|
|
50
65
|
|
|
51
66
|
// Initialize domain services with proper dependencies
|
|
52
|
-
this.gitService = new GitService(this.gitManager, this.tagger)
|
|
53
|
-
this.aiAnalysisService = new AIAnalysisService(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
this.gitService = new GitService(this.gitManager, this.tagger)
|
|
68
|
+
this.aiAnalysisService = new AIAnalysisService(
|
|
69
|
+
this.aiProvider,
|
|
70
|
+
this.promptEngine,
|
|
71
|
+
this.tagger,
|
|
72
|
+
this.analysisMode
|
|
73
|
+
)
|
|
74
|
+
this.analysisEngine = new AnalysisEngine(this.gitService, this.aiAnalysisService)
|
|
75
|
+
this.changelogService = new ChangelogService(
|
|
76
|
+
this.gitService,
|
|
77
|
+
this.aiAnalysisService,
|
|
78
|
+
this.analysisEngine,
|
|
79
|
+
this.configManager
|
|
80
|
+
)
|
|
81
|
+
this.interactiveService = new InteractiveWorkflowService(
|
|
82
|
+
this.gitService,
|
|
83
|
+
this.aiAnalysisService,
|
|
84
|
+
this.changelogService
|
|
85
|
+
)
|
|
86
|
+
this.stagingService = new InteractiveStagingService(this.gitManager)
|
|
87
|
+
this.validationService = new CommitMessageValidationService(this.configManager)
|
|
57
88
|
|
|
58
89
|
// Only log if not in MCP server mode
|
|
59
90
|
if (!process.env.MCP_SERVER_MODE) {
|
|
60
|
-
console.log(colors.successMessage('⚙️ Services initialized'))
|
|
91
|
+
console.log(colors.successMessage('⚙️ Services initialized'))
|
|
61
92
|
}
|
|
62
|
-
this.initialized = true
|
|
63
|
-
|
|
93
|
+
this.initialized = true
|
|
64
94
|
} catch (error) {
|
|
65
|
-
|
|
66
|
-
|
|
95
|
+
// Enhanced error handling with recovery suggestions
|
|
96
|
+
let errorMessage = 'Failed to initialize services: '
|
|
97
|
+
const suggestions = []
|
|
98
|
+
|
|
99
|
+
if (error.message.includes('not a git repository')) {
|
|
100
|
+
errorMessage += 'Not in a git repository'
|
|
101
|
+
suggestions.push('Run this command from within a git repository')
|
|
102
|
+
suggestions.push('Initialize a git repository with: git init')
|
|
103
|
+
} else if (error.message.includes('API key') || error.message.includes('provider')) {
|
|
104
|
+
errorMessage += 'AI provider configuration issue'
|
|
105
|
+
suggestions.push('Check your .env.local file for API keys')
|
|
106
|
+
suggestions.push('Run: ai-changelog providers list')
|
|
107
|
+
} else {
|
|
108
|
+
errorMessage += error.message
|
|
109
|
+
suggestions.push('Try running with --debug for more information')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.error(colors.errorMessage(errorMessage))
|
|
113
|
+
if (suggestions.length > 0) {
|
|
114
|
+
console.error(colors.infoMessage('Suggestions:'))
|
|
115
|
+
suggestions.forEach((suggestion) => {
|
|
116
|
+
console.error(colors.dim(` • ${suggestion}`))
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw error
|
|
67
121
|
}
|
|
68
122
|
}
|
|
69
123
|
|
|
70
124
|
async createGitManager() {
|
|
71
|
-
const { execSync } = await
|
|
125
|
+
const { execSync } = await this._getCachedImport('child_process')
|
|
72
126
|
|
|
73
127
|
return {
|
|
74
128
|
isGitRepo: (() => {
|
|
75
129
|
try {
|
|
76
|
-
execSync('git rev-parse --git-dir', { stdio: 'ignore' })
|
|
77
|
-
return true
|
|
130
|
+
execSync('git rev-parse --git-dir', { stdio: 'ignore' })
|
|
131
|
+
return true
|
|
78
132
|
} catch {
|
|
79
|
-
return false
|
|
133
|
+
return false
|
|
80
134
|
}
|
|
81
135
|
})(),
|
|
82
136
|
|
|
83
137
|
execGit(command) {
|
|
84
138
|
try {
|
|
85
|
-
return execSync(command, { encoding: 'utf8', stdio: 'pipe' })
|
|
139
|
+
return execSync(command, { encoding: 'utf8', stdio: 'pipe' })
|
|
86
140
|
} catch (error) {
|
|
87
|
-
|
|
141
|
+
// Enhanced error handling with more specific messages
|
|
142
|
+
if (error.code === 128) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Git repository error: ${error.message.includes('not a git repository') ? 'Not in a git repository' : error.message}`
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
if (error.code === 129) {
|
|
148
|
+
throw new Error(`Git command syntax error: ${command}`)
|
|
149
|
+
}
|
|
150
|
+
throw new Error(`Git command failed (${command}): ${error.message}`)
|
|
88
151
|
}
|
|
89
152
|
},
|
|
90
153
|
|
|
91
154
|
execGitSafe(command) {
|
|
92
155
|
try {
|
|
93
|
-
return execSync(command, { encoding: 'utf8', stdio: 'pipe' })
|
|
156
|
+
return execSync(command, { encoding: 'utf8', stdio: 'pipe' })
|
|
94
157
|
} catch {
|
|
95
|
-
return ''
|
|
158
|
+
return ''
|
|
96
159
|
}
|
|
97
160
|
},
|
|
98
161
|
|
|
99
162
|
execGitShow(command) {
|
|
100
163
|
try {
|
|
101
|
-
return execSync(command, { encoding: 'utf8', stdio: 'pipe' })
|
|
102
|
-
} catch (
|
|
164
|
+
return execSync(command, { encoding: 'utf8', stdio: 'pipe' })
|
|
165
|
+
} catch (_error) {
|
|
103
166
|
// console.warn(`Git command failed: ${command}`);
|
|
104
167
|
// console.warn(`Error: ${error.message}`);
|
|
105
|
-
return null
|
|
168
|
+
return null
|
|
106
169
|
}
|
|
107
170
|
},
|
|
108
171
|
|
|
109
172
|
validateCommitHash(hash) {
|
|
110
173
|
try {
|
|
111
|
-
execSync(`git cat-file -e ${hash}`, { stdio: 'ignore' })
|
|
112
|
-
return true
|
|
174
|
+
execSync(`git cat-file -e ${hash}`, { stdio: 'ignore' })
|
|
175
|
+
return true
|
|
113
176
|
} catch {
|
|
114
|
-
return false
|
|
177
|
+
return false
|
|
115
178
|
}
|
|
116
179
|
},
|
|
117
180
|
|
|
118
181
|
getAllBranches() {
|
|
119
182
|
try {
|
|
120
|
-
const output = execSync('git branch -a', { encoding: 'utf8' })
|
|
121
|
-
return output
|
|
183
|
+
const output = execSync('git branch -a', { encoding: 'utf8' })
|
|
184
|
+
return output
|
|
185
|
+
.split('\n')
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.map((branch) => branch.trim().replace(/^\*\s*/, ''))
|
|
122
188
|
} catch {
|
|
123
|
-
return []
|
|
189
|
+
return []
|
|
124
190
|
}
|
|
125
191
|
},
|
|
126
192
|
|
|
127
193
|
getUnmergedCommits() {
|
|
128
194
|
try {
|
|
129
|
-
const output = execSync('git log --oneline --no-merges HEAD ^origin/main', {
|
|
130
|
-
|
|
195
|
+
const output = execSync('git log --oneline --no-merges HEAD ^origin/main', {
|
|
196
|
+
encoding: 'utf8',
|
|
197
|
+
})
|
|
198
|
+
return output.split('\n').filter(Boolean)
|
|
131
199
|
} catch {
|
|
132
|
-
return []
|
|
200
|
+
return []
|
|
133
201
|
}
|
|
134
202
|
},
|
|
135
203
|
|
|
136
204
|
getDanglingCommits() {
|
|
137
205
|
try {
|
|
138
|
-
const output = execSync('git fsck --no-reflog | grep "dangling commit"', {
|
|
139
|
-
|
|
206
|
+
const output = execSync('git fsck --no-reflog | grep "dangling commit"', {
|
|
207
|
+
encoding: 'utf8',
|
|
208
|
+
})
|
|
209
|
+
return output.split('\n').filter(Boolean)
|
|
140
210
|
} catch {
|
|
141
|
-
return []
|
|
211
|
+
return []
|
|
142
212
|
}
|
|
143
213
|
},
|
|
144
214
|
|
|
145
215
|
getUntrackedFiles() {
|
|
146
216
|
try {
|
|
147
|
-
const output = execSync('git ls-files --others --exclude-standard', { encoding: 'utf8' })
|
|
148
|
-
return output.split('\n').filter(Boolean)
|
|
217
|
+
const output = execSync('git ls-files --others --exclude-standard', { encoding: 'utf8' })
|
|
218
|
+
return output.split('\n').filter(Boolean)
|
|
149
219
|
} catch {
|
|
150
|
-
return []
|
|
220
|
+
return []
|
|
151
221
|
}
|
|
152
222
|
},
|
|
153
223
|
|
|
154
224
|
getRecentCommits(limit = 10) {
|
|
155
225
|
try {
|
|
156
|
-
const output = execSync(`git log --oneline -${limit}`, { encoding: 'utf8' })
|
|
157
|
-
return output.split('\n').filter(Boolean)
|
|
226
|
+
const output = execSync(`git log --oneline -${limit}`, { encoding: 'utf8' })
|
|
227
|
+
return output.split('\n').filter(Boolean)
|
|
158
228
|
} catch {
|
|
159
|
-
return []
|
|
229
|
+
return []
|
|
160
230
|
}
|
|
161
231
|
},
|
|
162
232
|
|
|
@@ -164,67 +234,69 @@ export class ChangelogOrchestrator {
|
|
|
164
234
|
return {
|
|
165
235
|
totalCommits: this.getRecentCommits(1000).length,
|
|
166
236
|
branches: this.getAllBranches(),
|
|
167
|
-
untrackedFiles: this.getUntrackedFiles()
|
|
168
|
-
}
|
|
237
|
+
untrackedFiles: this.getUntrackedFiles(),
|
|
238
|
+
}
|
|
169
239
|
},
|
|
170
240
|
|
|
171
241
|
hasFile(filename) {
|
|
172
242
|
try {
|
|
173
|
-
execSync(`test -f ${filename}`, { stdio: 'ignore' })
|
|
174
|
-
return true
|
|
243
|
+
execSync(`test -f ${filename}`, { stdio: 'ignore' })
|
|
244
|
+
return true
|
|
175
245
|
} catch {
|
|
176
|
-
return false
|
|
246
|
+
return false
|
|
177
247
|
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
248
|
+
},
|
|
249
|
+
}
|
|
180
250
|
}
|
|
181
251
|
|
|
182
252
|
async createTagger() {
|
|
183
|
-
const { analyzeSemanticChanges, analyzeFunctionalImpact } = await import(
|
|
253
|
+
const { analyzeSemanticChanges, analyzeFunctionalImpact } = await import(
|
|
254
|
+
'../../shared/utils/utils.js'
|
|
255
|
+
)
|
|
184
256
|
|
|
185
257
|
return {
|
|
186
258
|
analyzeCommit(commit) {
|
|
187
|
-
const semanticChanges = []
|
|
188
|
-
const breakingChanges = []
|
|
189
|
-
const categories = []
|
|
190
|
-
const tags = []
|
|
259
|
+
const semanticChanges = []
|
|
260
|
+
const breakingChanges = []
|
|
261
|
+
const categories = []
|
|
262
|
+
const tags = []
|
|
191
263
|
|
|
192
264
|
// Basic analysis based on commit message
|
|
193
|
-
const message = commit.message.toLowerCase()
|
|
265
|
+
const message = commit.message.toLowerCase()
|
|
194
266
|
|
|
195
267
|
if (message.includes('breaking') || message.includes('!:')) {
|
|
196
|
-
breakingChanges.push('Breaking change detected in commit message')
|
|
197
|
-
categories.push('breaking')
|
|
198
|
-
tags.push('breaking')
|
|
268
|
+
breakingChanges.push('Breaking change detected in commit message')
|
|
269
|
+
categories.push('breaking')
|
|
270
|
+
tags.push('breaking')
|
|
199
271
|
}
|
|
200
272
|
|
|
201
273
|
if (message.startsWith('feat')) {
|
|
202
|
-
categories.push('feature')
|
|
203
|
-
tags.push('feature')
|
|
274
|
+
categories.push('feature')
|
|
275
|
+
tags.push('feature')
|
|
204
276
|
} else if (message.startsWith('fix')) {
|
|
205
|
-
categories.push('fix')
|
|
206
|
-
tags.push('bugfix')
|
|
277
|
+
categories.push('fix')
|
|
278
|
+
tags.push('bugfix')
|
|
207
279
|
} else if (message.startsWith('docs')) {
|
|
208
|
-
categories.push('docs')
|
|
209
|
-
tags.push('documentation')
|
|
280
|
+
categories.push('docs')
|
|
281
|
+
tags.push('documentation')
|
|
210
282
|
}
|
|
211
283
|
|
|
212
284
|
// Analyze files if available
|
|
213
285
|
if (commit.files && commit.files.length > 0) {
|
|
214
|
-
commit.files.forEach(file => {
|
|
215
|
-
const semantic = analyzeSemanticChanges('', file.path)
|
|
286
|
+
commit.files.forEach((file) => {
|
|
287
|
+
const semantic = analyzeSemanticChanges('', file.path)
|
|
216
288
|
if (semantic.frameworks) {
|
|
217
|
-
semanticChanges.push(...semantic.frameworks)
|
|
289
|
+
semanticChanges.push(...semantic.frameworks)
|
|
218
290
|
}
|
|
219
|
-
})
|
|
291
|
+
})
|
|
220
292
|
}
|
|
221
293
|
|
|
222
294
|
// Determine importance
|
|
223
|
-
let importance = 'medium'
|
|
295
|
+
let importance = 'medium'
|
|
224
296
|
if (breakingChanges.length > 0 || commit.files?.length > 10) {
|
|
225
|
-
importance = 'high'
|
|
297
|
+
importance = 'high'
|
|
226
298
|
} else if (categories.includes('docs') || commit.files?.length < 3) {
|
|
227
|
-
importance = 'low'
|
|
299
|
+
importance = 'low'
|
|
228
300
|
}
|
|
229
301
|
|
|
230
302
|
return {
|
|
@@ -232,474 +304,509 @@ export class ChangelogOrchestrator {
|
|
|
232
304
|
breakingChanges,
|
|
233
305
|
categories,
|
|
234
306
|
importance,
|
|
235
|
-
tags
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
307
|
+
tags,
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
}
|
|
239
311
|
}
|
|
240
312
|
|
|
241
313
|
async createPromptEngine() {
|
|
242
|
-
const { buildEnhancedPrompt } = await import('../../shared/utils/utils.js')
|
|
314
|
+
const { buildEnhancedPrompt } = await import('../../shared/utils/utils.js')
|
|
243
315
|
|
|
244
316
|
return {
|
|
245
317
|
systemPrompts: {
|
|
246
|
-
master:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
318
|
+
master:
|
|
319
|
+
'You are an expert software analyst specializing in code change analysis and changelog generation.',
|
|
320
|
+
standard: 'Provide clear, concise analysis focusing on the practical impact of changes.',
|
|
321
|
+
detailed:
|
|
322
|
+
'Provide comprehensive technical analysis with detailed explanations and implications.',
|
|
323
|
+
enterprise:
|
|
324
|
+
'Provide enterprise-grade analysis suitable for stakeholder communication and decision-making.',
|
|
325
|
+
changesAnalysis: 'You are an expert at analyzing code changes and their business impact.',
|
|
251
326
|
},
|
|
252
327
|
|
|
253
|
-
optimizeForProvider(prompt, providerName,
|
|
328
|
+
optimizeForProvider(prompt, providerName, _capabilities = {}) {
|
|
254
329
|
// Simple optimization - could be enhanced based on provider capabilities
|
|
255
330
|
if (providerName?.toLowerCase().includes('claude')) {
|
|
256
|
-
return `Please analyze this carefully and provide structured output:\n\n${prompt}
|
|
257
|
-
}
|
|
258
|
-
|
|
331
|
+
return `Please analyze this carefully and provide structured output:\n\n${prompt}`
|
|
332
|
+
}
|
|
333
|
+
if (providerName?.toLowerCase().includes('gpt')) {
|
|
334
|
+
return `${prompt}\n\nPlease respond in JSON format as requested.`
|
|
259
335
|
}
|
|
260
|
-
return prompt
|
|
336
|
+
return prompt
|
|
261
337
|
},
|
|
262
338
|
|
|
263
|
-
buildRepositoryHealthPrompt(healthData,
|
|
264
|
-
return `Analyze the health of this repository based on the following data:\n\n${JSON.stringify(healthData, null, 2)}\n\nProvide assessment and recommendations
|
|
265
|
-
}
|
|
266
|
-
}
|
|
339
|
+
buildRepositoryHealthPrompt(healthData, _analysisMode) {
|
|
340
|
+
return `Analyze the health of this repository based on the following data:\n\n${JSON.stringify(healthData, null, 2)}\n\nProvide assessment and recommendations.`
|
|
341
|
+
},
|
|
342
|
+
}
|
|
267
343
|
}
|
|
268
344
|
|
|
269
345
|
async generateChangelog(version, since) {
|
|
270
346
|
try {
|
|
271
|
-
await this.ensureInitialized()
|
|
347
|
+
await this.ensureInitialized()
|
|
272
348
|
|
|
273
|
-
this.metrics.startTime = Date.now()
|
|
349
|
+
this.metrics.startTime = Date.now()
|
|
274
350
|
|
|
275
|
-
console.log(
|
|
351
|
+
console.log(`\n${colors.processingMessage('🚀 Starting changelog generation...')}`)
|
|
276
352
|
|
|
277
353
|
// Validate git repository
|
|
278
354
|
if (!this.gitManager.isGitRepo) {
|
|
279
|
-
throw new Error('Not a git repository')
|
|
355
|
+
throw new Error('Not a git repository')
|
|
280
356
|
}
|
|
281
357
|
|
|
282
358
|
// Generate changelog using the service
|
|
283
|
-
const result = await this.changelogService.generateChangelog(version, since)
|
|
359
|
+
const result = await this.changelogService.generateChangelog(version, since)
|
|
284
360
|
|
|
285
361
|
if (!result) {
|
|
286
|
-
console.log(colors.warningMessage('No changelog generated'))
|
|
287
|
-
return null
|
|
362
|
+
console.log(colors.warningMessage('No changelog generated'))
|
|
363
|
+
return null
|
|
288
364
|
}
|
|
289
365
|
|
|
290
366
|
// Update metrics
|
|
291
|
-
this.updateMetrics(result)
|
|
367
|
+
this.updateMetrics(result)
|
|
292
368
|
|
|
293
369
|
// Display results
|
|
294
|
-
this.displayResults(result, version)
|
|
295
|
-
|
|
296
|
-
return result;
|
|
370
|
+
this.displayResults(result, version)
|
|
297
371
|
|
|
372
|
+
return result
|
|
298
373
|
} catch (error) {
|
|
299
|
-
this.metrics.errors
|
|
300
|
-
console.error(colors.errorMessage('Changelog generation failed:'), error.message)
|
|
301
|
-
throw error
|
|
374
|
+
this.metrics.errors++
|
|
375
|
+
console.error(colors.errorMessage('Changelog generation failed:'), error.message)
|
|
376
|
+
throw error
|
|
302
377
|
}
|
|
303
378
|
}
|
|
304
379
|
|
|
305
380
|
async analyzeRepository(options = {}) {
|
|
306
381
|
try {
|
|
307
|
-
await this.ensureInitialized()
|
|
382
|
+
await this.ensureInitialized()
|
|
308
383
|
|
|
309
|
-
console.log(colors.processingMessage('🔍 Starting repository analysis...'))
|
|
384
|
+
console.log(colors.processingMessage('🔍 Starting repository analysis...'))
|
|
310
385
|
|
|
311
|
-
const analysisType = options.type || 'changes'
|
|
312
|
-
const result = await this.analysisEngine.analyze(analysisType, options)
|
|
386
|
+
const analysisType = options.type || 'changes'
|
|
387
|
+
const result = await this.analysisEngine.analyze(analysisType, options)
|
|
313
388
|
|
|
314
|
-
this.displayAnalysisResults(result, analysisType)
|
|
315
|
-
|
|
316
|
-
return result;
|
|
389
|
+
this.displayAnalysisResults(result, analysisType)
|
|
317
390
|
|
|
391
|
+
return result
|
|
318
392
|
} catch (error) {
|
|
319
|
-
this.metrics.errors
|
|
320
|
-
console.error(colors.errorMessage('Repository analysis failed:'), error.message)
|
|
321
|
-
throw error
|
|
393
|
+
this.metrics.errors++
|
|
394
|
+
console.error(colors.errorMessage('Repository analysis failed:'), error.message)
|
|
395
|
+
throw error
|
|
322
396
|
}
|
|
323
397
|
}
|
|
324
398
|
|
|
325
399
|
async runInteractive() {
|
|
326
|
-
await this.ensureInitialized()
|
|
400
|
+
await this.ensureInitialized()
|
|
327
401
|
|
|
328
|
-
const { runInteractiveMode, selectSpecificCommits } = await import(
|
|
329
|
-
|
|
402
|
+
const { runInteractiveMode, selectSpecificCommits } = await import(
|
|
403
|
+
'../../shared/utils/utils.js'
|
|
404
|
+
)
|
|
405
|
+
const { confirm } = await this._getCachedImport('@clack/prompts')
|
|
330
406
|
|
|
331
|
-
console.log(colors.processingMessage('🎮 Starting interactive mode...'))
|
|
407
|
+
console.log(colors.processingMessage('🎮 Starting interactive mode...'))
|
|
332
408
|
|
|
333
|
-
let continueSession = true
|
|
409
|
+
let continueSession = true
|
|
334
410
|
|
|
335
411
|
while (continueSession) {
|
|
336
412
|
try {
|
|
337
|
-
const result = await runInteractiveMode()
|
|
413
|
+
const result = await runInteractiveMode()
|
|
338
414
|
|
|
339
415
|
if (result.action === 'exit') {
|
|
340
|
-
console.log(colors.successMessage('👋 Goodbye!'))
|
|
341
|
-
break
|
|
416
|
+
console.log(colors.successMessage('👋 Goodbye!'))
|
|
417
|
+
break
|
|
342
418
|
}
|
|
343
419
|
|
|
344
|
-
await this.handleInteractiveAction(result.action)
|
|
420
|
+
await this.handleInteractiveAction(result.action)
|
|
345
421
|
|
|
346
422
|
// Ask if user wants to continue
|
|
347
423
|
const continueChoice = await confirm({
|
|
348
424
|
message: 'Would you like to perform another action?',
|
|
349
|
-
initialValue: true
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
continueSession = continueChoice;
|
|
425
|
+
initialValue: true,
|
|
426
|
+
})
|
|
353
427
|
|
|
428
|
+
continueSession = continueChoice
|
|
354
429
|
} catch (error) {
|
|
355
|
-
console.error(colors.errorMessage(`Interactive mode error: ${error.message}`))
|
|
430
|
+
console.error(colors.errorMessage(`Interactive mode error: ${error.message}`))
|
|
356
431
|
|
|
357
432
|
const retryChoice = await confirm({
|
|
358
433
|
message: 'Would you like to try again?',
|
|
359
|
-
initialValue: true
|
|
360
|
-
})
|
|
434
|
+
initialValue: true,
|
|
435
|
+
})
|
|
361
436
|
|
|
362
|
-
continueSession = retryChoice
|
|
437
|
+
continueSession = retryChoice
|
|
363
438
|
}
|
|
364
439
|
}
|
|
365
440
|
|
|
366
|
-
return { interactive: true, status: 'completed' }
|
|
441
|
+
return { interactive: true, status: 'completed' }
|
|
367
442
|
}
|
|
368
443
|
|
|
369
444
|
async handleInteractiveAction(action) {
|
|
370
|
-
|
|
371
445
|
switch (action) {
|
|
372
446
|
case 'changelog-recent':
|
|
373
|
-
await this.handleRecentChangelogGeneration()
|
|
374
|
-
break
|
|
447
|
+
await this.handleRecentChangelogGeneration()
|
|
448
|
+
break
|
|
375
449
|
|
|
376
450
|
case 'changelog-specific':
|
|
377
|
-
await this.handleSpecificChangelogGeneration()
|
|
378
|
-
break
|
|
451
|
+
await this.handleSpecificChangelogGeneration()
|
|
452
|
+
break
|
|
379
453
|
|
|
380
454
|
case 'analyze-workdir':
|
|
381
|
-
await this.generateChangelogFromChanges()
|
|
382
|
-
break
|
|
455
|
+
await this.generateChangelogFromChanges()
|
|
456
|
+
break
|
|
383
457
|
|
|
384
458
|
case 'analyze-repo':
|
|
385
|
-
await this.analyzeRepository({ type: 'comprehensive' })
|
|
386
|
-
break
|
|
459
|
+
await this.analyzeRepository({ type: 'comprehensive' })
|
|
460
|
+
break
|
|
387
461
|
|
|
388
462
|
case 'commit-message':
|
|
389
|
-
await this.handleCommitMessageGeneration()
|
|
390
|
-
break
|
|
463
|
+
await this.handleCommitMessageGeneration()
|
|
464
|
+
break
|
|
391
465
|
|
|
392
466
|
case 'configure-providers':
|
|
393
|
-
await this.handleProviderConfiguration()
|
|
394
|
-
break
|
|
467
|
+
await this.handleProviderConfiguration()
|
|
468
|
+
break
|
|
395
469
|
|
|
396
470
|
case 'validate-config':
|
|
397
|
-
await this.validateConfiguration()
|
|
398
|
-
break
|
|
471
|
+
await this.validateConfiguration()
|
|
472
|
+
break
|
|
399
473
|
|
|
400
474
|
default:
|
|
401
|
-
console.log(colors.warningMessage(`Unknown action: ${action}`))
|
|
475
|
+
console.log(colors.warningMessage(`Unknown action: ${action}`))
|
|
402
476
|
}
|
|
403
477
|
}
|
|
404
478
|
|
|
405
479
|
async handleRecentChangelogGeneration() {
|
|
406
|
-
const { text } = await import('@clack/prompts')
|
|
480
|
+
const { text } = await import('@clack/prompts')
|
|
407
481
|
|
|
408
482
|
const commitCountInput = await text({
|
|
409
483
|
message: 'How many recent commits to include?',
|
|
410
484
|
placeholder: '10',
|
|
411
485
|
validate: (value) => {
|
|
412
|
-
const num = parseInt(value)
|
|
413
|
-
if (isNaN(num) || num <= 0 || num > 100) {
|
|
414
|
-
return 'Please enter a number between 1 and 100'
|
|
486
|
+
const num = Number.parseInt(value, 10)
|
|
487
|
+
if (Number.isNaN(num) || num <= 0 || num > 100) {
|
|
488
|
+
return 'Please enter a number between 1 and 100'
|
|
415
489
|
}
|
|
416
|
-
}
|
|
417
|
-
})
|
|
490
|
+
},
|
|
491
|
+
})
|
|
418
492
|
|
|
419
|
-
const commitCount = parseInt(commitCountInput) || 10
|
|
493
|
+
const commitCount = Number.parseInt(commitCountInput, 10) || 10
|
|
420
494
|
|
|
421
|
-
console.log(
|
|
495
|
+
console.log(
|
|
496
|
+
colors.processingMessage(`📝 Generating changelog for ${commitCount} recent commits...`)
|
|
497
|
+
)
|
|
422
498
|
|
|
423
499
|
const result = await this.generateChangelog({
|
|
424
500
|
version: `Recent-${commitCount}-commits`,
|
|
425
|
-
maxCommits: commitCount
|
|
426
|
-
})
|
|
501
|
+
maxCommits: commitCount,
|
|
502
|
+
})
|
|
427
503
|
|
|
428
504
|
if (result?.changelog) {
|
|
429
|
-
console.log(colors.successMessage('✅ Changelog generated successfully!'))
|
|
505
|
+
console.log(colors.successMessage('✅ Changelog generated successfully!'))
|
|
430
506
|
}
|
|
431
507
|
}
|
|
432
508
|
|
|
433
509
|
async handleSpecificChangelogGeneration() {
|
|
434
|
-
const { selectSpecificCommits } = await import('../../shared/utils/utils.js')
|
|
510
|
+
const { selectSpecificCommits } = await import('../../shared/utils/utils.js')
|
|
435
511
|
|
|
436
|
-
console.log(colors.infoMessage('📋 Select specific commits for changelog generation:'))
|
|
512
|
+
console.log(colors.infoMessage('📋 Select specific commits for changelog generation:'))
|
|
437
513
|
|
|
438
|
-
const selectedCommits = await selectSpecificCommits(30)
|
|
514
|
+
const selectedCommits = await selectSpecificCommits(30)
|
|
439
515
|
|
|
440
516
|
if (selectedCommits.length === 0) {
|
|
441
|
-
console.log(colors.warningMessage('No commits selected.'))
|
|
442
|
-
return
|
|
517
|
+
console.log(colors.warningMessage('No commits selected.'))
|
|
518
|
+
return
|
|
443
519
|
}
|
|
444
520
|
|
|
445
|
-
console.log(
|
|
521
|
+
console.log(
|
|
522
|
+
colors.processingMessage(
|
|
523
|
+
`📝 Generating changelog for ${selectedCommits.length} selected commits...`
|
|
524
|
+
)
|
|
525
|
+
)
|
|
446
526
|
|
|
447
|
-
const result = await this.generateChangelogFromCommits(selectedCommits)
|
|
527
|
+
const result = await this.generateChangelogFromCommits(selectedCommits)
|
|
448
528
|
|
|
449
529
|
if (result?.changelog) {
|
|
450
|
-
console.log(colors.successMessage('✅ Changelog generated successfully!'))
|
|
530
|
+
console.log(colors.successMessage('✅ Changelog generated successfully!'))
|
|
451
531
|
}
|
|
452
532
|
}
|
|
453
533
|
|
|
454
534
|
async handleCommitMessageGeneration() {
|
|
455
|
-
console.log(
|
|
535
|
+
console.log(
|
|
536
|
+
colors.processingMessage('🤖 Analyzing current changes for commit message suggestions...')
|
|
537
|
+
)
|
|
456
538
|
|
|
457
539
|
// Use shared utility for getting working directory changes
|
|
458
|
-
const { getWorkingDirectoryChanges } = await import('../../shared/utils/utils.js')
|
|
459
|
-
const changes = getWorkingDirectoryChanges()
|
|
540
|
+
const { getWorkingDirectoryChanges } = await import('../../shared/utils/utils.js')
|
|
541
|
+
const changes = getWorkingDirectoryChanges()
|
|
460
542
|
|
|
461
543
|
if (!changes || changes.length === 0) {
|
|
462
|
-
console.log(colors.warningMessage('No uncommitted changes found.'))
|
|
463
|
-
return
|
|
544
|
+
console.log(colors.warningMessage('No uncommitted changes found.'))
|
|
545
|
+
return
|
|
464
546
|
}
|
|
465
547
|
|
|
466
|
-
const analysis = await this.interactiveService.generateCommitSuggestion()
|
|
548
|
+
const analysis = await this.interactiveService.generateCommitSuggestion()
|
|
467
549
|
|
|
468
550
|
if (analysis.success && analysis.suggestions.length > 0) {
|
|
469
|
-
const { select } = await
|
|
551
|
+
const { select } = await this._getCachedImport('@clack/prompts')
|
|
470
552
|
|
|
471
553
|
const choices = [
|
|
472
554
|
...analysis.suggestions.map((msg, index) => ({
|
|
473
555
|
value: msg,
|
|
474
|
-
label: `${index + 1}. ${msg}
|
|
556
|
+
label: `${index + 1}. ${msg}`,
|
|
475
557
|
})),
|
|
476
558
|
{
|
|
477
559
|
value: 'CUSTOM',
|
|
478
|
-
label: '✏️ Write custom message'
|
|
479
|
-
}
|
|
480
|
-
]
|
|
560
|
+
label: '✏️ Write custom message',
|
|
561
|
+
},
|
|
562
|
+
]
|
|
481
563
|
|
|
482
564
|
const selectedMessage = await select({
|
|
483
565
|
message: 'Choose a commit message:',
|
|
484
|
-
options: choices
|
|
485
|
-
})
|
|
566
|
+
options: choices,
|
|
567
|
+
})
|
|
486
568
|
|
|
487
569
|
if (selectedMessage === 'CUSTOM') {
|
|
488
|
-
const { text } = await
|
|
570
|
+
const { text } = await this._getCachedImport('@clack/prompts')
|
|
489
571
|
|
|
490
572
|
const customMessage = await text({
|
|
491
573
|
message: 'Enter your commit message:',
|
|
492
574
|
validate: (input) => {
|
|
493
575
|
if (!input || input.trim().length === 0) {
|
|
494
|
-
return 'Commit message cannot be empty'
|
|
576
|
+
return 'Commit message cannot be empty'
|
|
495
577
|
}
|
|
496
|
-
}
|
|
497
|
-
})
|
|
578
|
+
},
|
|
579
|
+
})
|
|
498
580
|
|
|
499
|
-
console.log(colors.successMessage(`📝 Custom message: ${customMessage}`))
|
|
581
|
+
console.log(colors.successMessage(`📝 Custom message: ${customMessage}`))
|
|
500
582
|
} else {
|
|
501
|
-
console.log(colors.successMessage(`📝 Selected: ${selectedMessage}`))
|
|
583
|
+
console.log(colors.successMessage(`📝 Selected: ${selectedMessage}`))
|
|
502
584
|
}
|
|
503
585
|
} else {
|
|
504
|
-
console.log(colors.warningMessage('Could not generate commit message suggestions.'))
|
|
586
|
+
console.log(colors.warningMessage('Could not generate commit message suggestions.'))
|
|
505
587
|
}
|
|
506
588
|
}
|
|
507
589
|
|
|
508
590
|
async handleProviderConfiguration() {
|
|
509
|
-
const { select } = await
|
|
591
|
+
const { select } = await this._getCachedImport('@clack/prompts')
|
|
510
592
|
|
|
511
|
-
const availableProviders = this.providerManager.getAllProviders()
|
|
593
|
+
const availableProviders = this.providerManager.getAllProviders()
|
|
512
594
|
|
|
513
|
-
const choices = availableProviders.map(p => ({
|
|
595
|
+
const choices = availableProviders.map((p) => ({
|
|
514
596
|
value: p.name,
|
|
515
|
-
label: `${p.name} ${p.available ? '✅' : '⚠️ (needs configuration)'}
|
|
516
|
-
}))
|
|
597
|
+
label: `${p.name} ${p.available ? '✅' : '⚠️ (needs configuration)'}`,
|
|
598
|
+
}))
|
|
517
599
|
|
|
518
600
|
const selectedProvider = await select({
|
|
519
601
|
message: 'Select provider to configure:',
|
|
520
|
-
options: choices
|
|
521
|
-
})
|
|
602
|
+
options: choices,
|
|
603
|
+
})
|
|
522
604
|
|
|
523
|
-
console.log(colors.infoMessage(`🔧 Configuring ${selectedProvider}...`))
|
|
524
|
-
console.log(
|
|
525
|
-
|
|
605
|
+
console.log(colors.infoMessage(`🔧 Configuring ${selectedProvider}...`))
|
|
606
|
+
console.log(
|
|
607
|
+
colors.infoMessage('Please edit your .env.local file to add the required API keys.')
|
|
608
|
+
)
|
|
609
|
+
console.log(colors.highlight(`Example for ${selectedProvider.toUpperCase()}:`))
|
|
526
610
|
|
|
527
611
|
switch (selectedProvider) {
|
|
528
612
|
case 'openai':
|
|
529
|
-
console.log(colors.code('OPENAI_API_KEY=your_api_key_here'))
|
|
530
|
-
break
|
|
613
|
+
console.log(colors.code('OPENAI_API_KEY=your_api_key_here'))
|
|
614
|
+
break
|
|
531
615
|
case 'anthropic':
|
|
532
|
-
console.log(colors.code('ANTHROPIC_API_KEY=your_api_key_here'))
|
|
533
|
-
break
|
|
616
|
+
console.log(colors.code('ANTHROPIC_API_KEY=your_api_key_here'))
|
|
617
|
+
break
|
|
534
618
|
case 'azure':
|
|
535
|
-
console.log(colors.code('AZURE_OPENAI_API_KEY=your_api_key_here'))
|
|
536
|
-
console.log(colors.code('AZURE_OPENAI_ENDPOINT=your_endpoint_here'))
|
|
537
|
-
break
|
|
619
|
+
console.log(colors.code('AZURE_OPENAI_API_KEY=your_api_key_here'))
|
|
620
|
+
console.log(colors.code('AZURE_OPENAI_ENDPOINT=your_endpoint_here'))
|
|
621
|
+
break
|
|
538
622
|
case 'google':
|
|
539
|
-
console.log(colors.code('GOOGLE_API_KEY=your_api_key_here'))
|
|
540
|
-
break
|
|
623
|
+
console.log(colors.code('GOOGLE_API_KEY=your_api_key_here'))
|
|
624
|
+
break
|
|
541
625
|
default:
|
|
542
|
-
console.log(colors.code(`${selectedProvider.toUpperCase()}_API_KEY=your_api_key_here`))
|
|
626
|
+
console.log(colors.code(`${selectedProvider.toUpperCase()}_API_KEY=your_api_key_here`))
|
|
543
627
|
}
|
|
544
628
|
}
|
|
545
629
|
|
|
546
630
|
async generateChangelogFromChanges(version) {
|
|
547
631
|
try {
|
|
548
|
-
await this.ensureInitialized()
|
|
632
|
+
await this.ensureInitialized()
|
|
549
633
|
|
|
550
|
-
console.log(
|
|
634
|
+
console.log(
|
|
635
|
+
colors.processingMessage('📝 Generating changelog from working directory changes...')
|
|
636
|
+
)
|
|
551
637
|
|
|
552
|
-
const result = await this.changelogService.generateChangelogFromChanges(version)
|
|
638
|
+
const result = await this.changelogService.generateChangelogFromChanges(version)
|
|
553
639
|
|
|
554
640
|
if (result) {
|
|
555
|
-
console.log(colors.successMessage('✅ Working directory changelog generated'))
|
|
556
|
-
console.log(result.changelog)
|
|
641
|
+
console.log(colors.successMessage('✅ Working directory changelog generated'))
|
|
642
|
+
console.log(result.changelog)
|
|
557
643
|
}
|
|
558
644
|
|
|
559
|
-
return result
|
|
560
|
-
|
|
645
|
+
return result
|
|
561
646
|
} catch (error) {
|
|
562
|
-
this.metrics.errors
|
|
563
|
-
console.error(
|
|
564
|
-
|
|
647
|
+
this.metrics.errors++
|
|
648
|
+
console.error(
|
|
649
|
+
colors.errorMessage('Working directory changelog generation failed:'),
|
|
650
|
+
error.message
|
|
651
|
+
)
|
|
652
|
+
throw error
|
|
565
653
|
}
|
|
566
654
|
}
|
|
567
655
|
|
|
568
656
|
updateMetrics(result) {
|
|
569
657
|
if (result.analyzedCommits) {
|
|
570
|
-
this.metrics.commitsProcessed += result.analyzedCommits.length
|
|
658
|
+
this.metrics.commitsProcessed += result.analyzedCommits.length
|
|
571
659
|
}
|
|
572
660
|
|
|
573
661
|
// Get metrics from AI service
|
|
574
|
-
const aiMetrics = this.aiAnalysisService.getMetrics()
|
|
575
|
-
this.metrics.apiCalls += aiMetrics.apiCalls
|
|
576
|
-
this.metrics.totalTokens += aiMetrics.totalTokens
|
|
577
|
-
this.metrics.ruleBasedFallbacks += aiMetrics.ruleBasedFallbacks
|
|
662
|
+
const aiMetrics = this.aiAnalysisService.getMetrics()
|
|
663
|
+
this.metrics.apiCalls += aiMetrics.apiCalls
|
|
664
|
+
this.metrics.totalTokens += aiMetrics.totalTokens
|
|
665
|
+
this.metrics.ruleBasedFallbacks += aiMetrics.ruleBasedFallbacks
|
|
578
666
|
}
|
|
579
667
|
|
|
580
|
-
displayResults(result,
|
|
581
|
-
const { changelog, insights, analyzedCommits } = result
|
|
668
|
+
displayResults(result, _version) {
|
|
669
|
+
const { changelog, insights, analyzedCommits } = result
|
|
582
670
|
|
|
583
|
-
console.log(
|
|
671
|
+
console.log(`\n${colors.successMessage('✅ Changelog Generation Complete')}`)
|
|
584
672
|
|
|
585
673
|
if (insights) {
|
|
586
674
|
// Create a clean insights summary
|
|
587
675
|
const insightLines = [
|
|
588
676
|
`${colors.label('Total commits')}: ${colors.number(insights.totalCommits)}`,
|
|
589
677
|
`${colors.label('Complexity')}: ${this.getComplexityColor(insights.complexity)(insights.complexity)}`,
|
|
590
|
-
`${colors.label('Risk level')}: ${this.getRiskColor(insights.riskLevel)(insights.riskLevel)}
|
|
591
|
-
]
|
|
678
|
+
`${colors.label('Risk level')}: ${this.getRiskColor(insights.riskLevel)(insights.riskLevel)}`,
|
|
679
|
+
]
|
|
592
680
|
|
|
593
681
|
if (insights.breaking) {
|
|
594
|
-
insightLines.push('')
|
|
595
|
-
insightLines.push(colors.warningMessage('⚠️ Contains breaking changes'))
|
|
682
|
+
insightLines.push('')
|
|
683
|
+
insightLines.push(colors.warningMessage('⚠️ Contains breaking changes'))
|
|
596
684
|
}
|
|
597
685
|
|
|
598
686
|
if (Object.keys(insights.commitTypes).length > 0) {
|
|
599
|
-
insightLines.push('')
|
|
600
|
-
insightLines.push(colors.dim('Commit types:'))
|
|
687
|
+
insightLines.push('')
|
|
688
|
+
insightLines.push(colors.dim('Commit types:'))
|
|
601
689
|
Object.entries(insights.commitTypes).forEach(([type, count]) => {
|
|
602
|
-
insightLines.push(` ${colors.commitType(type)}: ${colors.number(count)}`)
|
|
603
|
-
})
|
|
690
|
+
insightLines.push(` ${colors.commitType(type)}: ${colors.number(count)}`)
|
|
691
|
+
})
|
|
604
692
|
}
|
|
605
693
|
|
|
606
|
-
console.log(colors.box('📊 Release Insights', insightLines.join('\n')))
|
|
694
|
+
console.log(colors.box('📊 Release Insights', insightLines.join('\n')))
|
|
607
695
|
}
|
|
608
696
|
|
|
609
697
|
// Don't show changelog content in terminal - it's saved to file
|
|
610
698
|
|
|
611
|
-
this.displayMetrics()
|
|
699
|
+
this.displayMetrics()
|
|
612
700
|
}
|
|
613
701
|
|
|
614
702
|
getComplexityColor(complexity) {
|
|
615
|
-
const level = complexity?.toLowerCase()
|
|
703
|
+
const level = complexity?.toLowerCase()
|
|
616
704
|
switch (level) {
|
|
617
|
-
case 'low':
|
|
618
|
-
|
|
619
|
-
case '
|
|
620
|
-
|
|
705
|
+
case 'low':
|
|
706
|
+
return colors.success
|
|
707
|
+
case 'medium':
|
|
708
|
+
return colors.warning
|
|
709
|
+
case 'high':
|
|
710
|
+
return colors.error
|
|
711
|
+
default:
|
|
712
|
+
return colors.highlight
|
|
621
713
|
}
|
|
622
714
|
}
|
|
623
715
|
|
|
624
716
|
getRiskColor(risk) {
|
|
625
|
-
const level = risk?.toLowerCase()
|
|
717
|
+
const level = risk?.toLowerCase()
|
|
626
718
|
switch (level) {
|
|
627
|
-
case 'low':
|
|
628
|
-
|
|
629
|
-
case '
|
|
630
|
-
|
|
631
|
-
|
|
719
|
+
case 'low':
|
|
720
|
+
return colors.riskLow
|
|
721
|
+
case 'medium':
|
|
722
|
+
return colors.riskMedium
|
|
723
|
+
case 'high':
|
|
724
|
+
return colors.riskHigh
|
|
725
|
+
case 'critical':
|
|
726
|
+
return colors.riskCritical
|
|
727
|
+
default:
|
|
728
|
+
return colors.highlight
|
|
632
729
|
}
|
|
633
730
|
}
|
|
634
731
|
|
|
635
732
|
displayAnalysisResults(result, type) {
|
|
636
|
-
console.log(
|
|
637
|
-
|
|
733
|
+
console.log(
|
|
734
|
+
colors.successMessage(
|
|
735
|
+
`\n✅ ${type.charAt(0).toUpperCase() + type.slice(1)} Analysis Complete`
|
|
736
|
+
)
|
|
737
|
+
)
|
|
738
|
+
console.log(colors.separator())
|
|
638
739
|
|
|
639
740
|
if (result.summary) {
|
|
640
|
-
console.log(colors.sectionHeader('📋 Summary'))
|
|
641
|
-
console.log(result.summary)
|
|
642
|
-
console.log('')
|
|
741
|
+
console.log(colors.sectionHeader('📋 Summary'))
|
|
742
|
+
console.log(result.summary)
|
|
743
|
+
console.log('')
|
|
643
744
|
}
|
|
644
745
|
|
|
645
746
|
if (result.analysis) {
|
|
646
|
-
console.log(colors.sectionHeader('🔍 Analysis Details'))
|
|
747
|
+
console.log(colors.sectionHeader('🔍 Analysis Details'))
|
|
647
748
|
if (typeof result.analysis === 'object') {
|
|
648
749
|
Object.entries(result.analysis).forEach(([key, value]) => {
|
|
649
750
|
if (typeof value === 'object') {
|
|
650
|
-
console.log(`${key}: ${JSON.stringify(value, null, 2)}`)
|
|
751
|
+
console.log(`${key}: ${JSON.stringify(value, null, 2)}`)
|
|
651
752
|
} else {
|
|
652
|
-
console.log(`${key}: ${colors.highlight(value)}`)
|
|
753
|
+
console.log(`${key}: ${colors.highlight(value)}`)
|
|
653
754
|
}
|
|
654
|
-
})
|
|
755
|
+
})
|
|
655
756
|
} else {
|
|
656
|
-
console.log(result.analysis)
|
|
757
|
+
console.log(result.analysis)
|
|
657
758
|
}
|
|
658
759
|
}
|
|
659
760
|
|
|
660
|
-
this.displayMetrics()
|
|
761
|
+
this.displayMetrics()
|
|
661
762
|
}
|
|
662
763
|
|
|
663
764
|
displayMetrics() {
|
|
664
|
-
const duration = Date.now() - this.metrics.startTime
|
|
765
|
+
const duration = Date.now() - this.metrics.startTime
|
|
665
766
|
|
|
666
767
|
const metricLines = [
|
|
667
768
|
`${colors.label('Duration')}: ${colors.number(this.formatDuration(duration))}`,
|
|
668
769
|
`${colors.label('Commits processed')}: ${colors.number(this.metrics.commitsProcessed)}`,
|
|
669
770
|
`${colors.label('API calls')}: ${colors.number(this.metrics.apiCalls)}`,
|
|
670
|
-
`${colors.label('Total tokens')}: ${colors.number(this.metrics.totalTokens.toLocaleString())}
|
|
671
|
-
]
|
|
771
|
+
`${colors.label('Total tokens')}: ${colors.number(this.metrics.totalTokens.toLocaleString())}`,
|
|
772
|
+
]
|
|
672
773
|
|
|
673
774
|
if (this.metrics.ruleBasedFallbacks > 0) {
|
|
674
|
-
metricLines.push('')
|
|
675
|
-
metricLines.push(
|
|
775
|
+
metricLines.push('')
|
|
776
|
+
metricLines.push(
|
|
777
|
+
colors.warning(`⚠️ Rule-based fallbacks: ${this.metrics.ruleBasedFallbacks}`)
|
|
778
|
+
)
|
|
676
779
|
}
|
|
677
780
|
|
|
678
781
|
if (this.metrics.errors > 0) {
|
|
679
|
-
metricLines.push('')
|
|
680
|
-
metricLines.push(colors.error(`❌ Errors: ${this.metrics.errors}`))
|
|
782
|
+
metricLines.push('')
|
|
783
|
+
metricLines.push(colors.error(`❌ Errors: ${this.metrics.errors}`))
|
|
681
784
|
}
|
|
682
785
|
|
|
683
|
-
console.log(colors.box('📈 Performance Metrics', metricLines.join('\n')))
|
|
786
|
+
console.log(colors.box('📈 Performance Metrics', metricLines.join('\n')))
|
|
684
787
|
}
|
|
685
788
|
|
|
686
789
|
formatDuration(ms) {
|
|
687
|
-
if (ms < 1000)
|
|
688
|
-
|
|
689
|
-
|
|
790
|
+
if (ms < 1000) {
|
|
791
|
+
return `${ms}ms`
|
|
792
|
+
}
|
|
793
|
+
if (ms < 60000) {
|
|
794
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
795
|
+
}
|
|
796
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`
|
|
690
797
|
}
|
|
691
798
|
|
|
692
799
|
// Configuration methods
|
|
693
800
|
setAnalysisMode(mode) {
|
|
694
|
-
this.analysisMode = mode
|
|
801
|
+
this.analysisMode = mode
|
|
695
802
|
if (this.aiAnalysisService) {
|
|
696
|
-
this.aiAnalysisService.analysisMode = mode
|
|
803
|
+
this.aiAnalysisService.analysisMode = mode
|
|
697
804
|
}
|
|
698
805
|
}
|
|
699
806
|
|
|
700
807
|
setModelOverride(model) {
|
|
701
808
|
if (this.aiAnalysisService) {
|
|
702
|
-
this.aiAnalysisService.setModelOverride(model)
|
|
809
|
+
this.aiAnalysisService.setModelOverride(model)
|
|
703
810
|
}
|
|
704
811
|
}
|
|
705
812
|
|
|
@@ -707,8 +814,8 @@ export class ChangelogOrchestrator {
|
|
|
707
814
|
getMetrics() {
|
|
708
815
|
return {
|
|
709
816
|
...this.metrics,
|
|
710
|
-
aiMetrics: this.aiAnalysisService?.getMetrics() || {}
|
|
711
|
-
}
|
|
817
|
+
aiMetrics: this.aiAnalysisService?.getMetrics() || {},
|
|
818
|
+
}
|
|
712
819
|
}
|
|
713
820
|
|
|
714
821
|
resetMetrics() {
|
|
@@ -720,11 +827,648 @@ export class ChangelogOrchestrator {
|
|
|
720
827
|
batchesProcessed: 0,
|
|
721
828
|
totalTokens: 0,
|
|
722
829
|
ruleBasedFallbacks: 0,
|
|
723
|
-
cacheHits: 0
|
|
724
|
-
}
|
|
830
|
+
cacheHits: 0,
|
|
831
|
+
}
|
|
725
832
|
|
|
726
833
|
if (this.aiAnalysisService) {
|
|
727
|
-
this.aiAnalysisService.resetMetrics()
|
|
834
|
+
this.aiAnalysisService.resetMetrics()
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Interactive commit workflow
|
|
839
|
+
async executeCommitWorkflow(options = {}) {
|
|
840
|
+
await this.ensureInitialized()
|
|
841
|
+
|
|
842
|
+
console.log(colors.header('🚀 Interactive Commit Workflow'))
|
|
843
|
+
|
|
844
|
+
try {
|
|
845
|
+
// Step 1: Show current git status
|
|
846
|
+
const statusResult = await this.stagingService.showGitStatus()
|
|
847
|
+
|
|
848
|
+
// Check if we have any changes at all
|
|
849
|
+
const totalChanges =
|
|
850
|
+
statusResult.staged.length + statusResult.unstaged.length + statusResult.untracked.length
|
|
851
|
+
if (totalChanges === 0) {
|
|
852
|
+
console.log(colors.infoMessage('✨ Working directory clean - no changes to commit.'))
|
|
853
|
+
return { success: false, message: 'No changes to commit' }
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Step 2: Handle staging based on options
|
|
857
|
+
let stagedFiles = []
|
|
858
|
+
|
|
859
|
+
if (options.stageAll) {
|
|
860
|
+
// Auto-stage all changes
|
|
861
|
+
console.log(colors.processingMessage('📦 Staging all changes...'))
|
|
862
|
+
await this.stagingService.stageAllChanges()
|
|
863
|
+
stagedFiles = [...statusResult.unstaged, ...statusResult.untracked]
|
|
864
|
+
} else if (
|
|
865
|
+
options.interactive &&
|
|
866
|
+
(statusResult.unstaged.length > 0 || statusResult.untracked.length > 0)
|
|
867
|
+
) {
|
|
868
|
+
// Interactive staging
|
|
869
|
+
console.log(colors.infoMessage('\n🎯 Interactive staging mode'))
|
|
870
|
+
stagedFiles = await this.stagingService.selectFilesToStage()
|
|
871
|
+
|
|
872
|
+
if (stagedFiles.length === 0 && statusResult.staged.length === 0) {
|
|
873
|
+
console.log(colors.warningMessage('No files staged for commit.'))
|
|
874
|
+
return { success: false, message: 'No files staged' }
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Step 3: Verify we have staged changes
|
|
879
|
+
if (!this.stagingService.hasStagedChanges()) {
|
|
880
|
+
console.log(colors.warningMessage('No staged changes found for commit.'))
|
|
881
|
+
|
|
882
|
+
if (statusResult.unstaged.length > 0 || statusResult.untracked.length > 0) {
|
|
883
|
+
console.log(
|
|
884
|
+
colors.infoMessage(
|
|
885
|
+
'💡 Use --all flag to stage all changes, or run interactively to select files.'
|
|
886
|
+
)
|
|
887
|
+
)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return { success: false, message: 'No staged changes' }
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Step 4: Get final staged changes for analysis
|
|
894
|
+
const finalStatus = this.stagingService.getDetailedStatus()
|
|
895
|
+
console.log(
|
|
896
|
+
colors.successMessage(`\n✅ Ready to commit ${finalStatus.staged.length} staged file(s)`)
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
// Step 5: Branch Intelligence Analysis
|
|
900
|
+
const { analyzeBranchIntelligence, getSuggestedCommitType, generateCommitContextFromBranch } =
|
|
901
|
+
await import('../../shared/utils/utils.js')
|
|
902
|
+
|
|
903
|
+
const branchAnalysis = analyzeBranchIntelligence()
|
|
904
|
+
const suggestedType = getSuggestedCommitType(branchAnalysis, finalStatus.staged)
|
|
905
|
+
const _branchContext = generateCommitContextFromBranch(branchAnalysis, finalStatus.staged)
|
|
906
|
+
|
|
907
|
+
// Display branch intelligence findings
|
|
908
|
+
if (branchAnalysis.confidence > 20) {
|
|
909
|
+
console.log(colors.infoMessage('\n🧠 Branch Intelligence:'))
|
|
910
|
+
console.log(colors.secondary(` Branch: ${branchAnalysis.branch}`))
|
|
911
|
+
|
|
912
|
+
if (branchAnalysis.type) {
|
|
913
|
+
console.log(
|
|
914
|
+
colors.success(
|
|
915
|
+
` 🏷️ Detected type: ${branchAnalysis.type} (${branchAnalysis.confidence}% confidence)`
|
|
916
|
+
)
|
|
917
|
+
)
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (branchAnalysis.ticket) {
|
|
921
|
+
console.log(colors.highlight(` 🎫 Related ticket: ${branchAnalysis.ticket}`))
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (branchAnalysis.description) {
|
|
925
|
+
console.log(colors.dim(` 📝 Description: ${branchAnalysis.description}`))
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
console.log(colors.dim(` 🔍 Patterns: ${branchAnalysis.patterns.join(', ')}`))
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Display suggested commit type
|
|
932
|
+
console.log(
|
|
933
|
+
colors.infoMessage(
|
|
934
|
+
`\n💡 Suggested commit type: ${colors.highlight(suggestedType.type)} (from ${suggestedType.source}, ${suggestedType.confidence}% confidence)`
|
|
935
|
+
)
|
|
936
|
+
)
|
|
937
|
+
|
|
938
|
+
// Step 6: Generate enhanced commit message
|
|
939
|
+
let commitMessage
|
|
940
|
+
|
|
941
|
+
if (options.customMessage) {
|
|
942
|
+
commitMessage = options.customMessage
|
|
943
|
+
} else {
|
|
944
|
+
// Generate AI-enhanced commit message with branch context
|
|
945
|
+
console.log(colors.processingMessage('🤖 Generating AI-powered commit message...'))
|
|
946
|
+
|
|
947
|
+
try {
|
|
948
|
+
commitMessage = await this.generateAICommitMessage(
|
|
949
|
+
branchAnalysis,
|
|
950
|
+
suggestedType,
|
|
951
|
+
finalStatus.staged
|
|
952
|
+
)
|
|
953
|
+
} catch (_error) {
|
|
954
|
+
console.log(colors.warningMessage('⚠️ AI generation failed, using rule-based fallback'))
|
|
955
|
+
commitMessage = this.generateBranchAwareCommitMessage(
|
|
956
|
+
branchAnalysis,
|
|
957
|
+
suggestedType,
|
|
958
|
+
finalStatus.staged
|
|
959
|
+
)
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Step 7: Validate commit message
|
|
964
|
+
console.log(colors.processingMessage('\n🔍 Validating commit message...'))
|
|
965
|
+
|
|
966
|
+
const validationContext = {
|
|
967
|
+
branchAnalysis,
|
|
968
|
+
stagedFiles: finalStatus.staged,
|
|
969
|
+
suggestedType,
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const validationResult = await this.validationService.validateCommitMessage(
|
|
973
|
+
commitMessage,
|
|
974
|
+
validationContext
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
// Display validation results
|
|
978
|
+
const isValid = this.validationService.displayValidationResults(validationResult)
|
|
979
|
+
|
|
980
|
+
// Step 8: Interactive improvement if needed
|
|
981
|
+
if (!isValid || validationResult.warnings.length > 0) {
|
|
982
|
+
const { confirm } = await this._getCachedImport('@clack/prompts')
|
|
983
|
+
|
|
984
|
+
const shouldImprove = await confirm({
|
|
985
|
+
message: 'Would you like to improve the commit message?',
|
|
986
|
+
initialValue: !isValid,
|
|
987
|
+
})
|
|
988
|
+
|
|
989
|
+
if (shouldImprove) {
|
|
990
|
+
commitMessage = await this.handleCommitMessageImprovement(
|
|
991
|
+
commitMessage,
|
|
992
|
+
validationResult,
|
|
993
|
+
validationContext
|
|
994
|
+
)
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if (options.dryRun) {
|
|
999
|
+
console.log(colors.infoMessage('\n📋 Dry-run mode - showing what would be committed:'))
|
|
1000
|
+
console.log(colors.highlight(`Commit message:\n${commitMessage}`))
|
|
1001
|
+
return {
|
|
1002
|
+
success: true,
|
|
1003
|
+
commitMessage,
|
|
1004
|
+
stagedFiles: finalStatus.staged.length,
|
|
1005
|
+
dryRun: true,
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Step 6: Execute the actual commit
|
|
1010
|
+
console.log(colors.processingMessage('\n💾 Executing commit...'))
|
|
1011
|
+
|
|
1012
|
+
try {
|
|
1013
|
+
// Secure commit execution using git commit with stdin to avoid command injection
|
|
1014
|
+
const { execSync } = await this._getCachedImport('child_process')
|
|
1015
|
+
const commitHash = execSync('git commit --file=-', {
|
|
1016
|
+
input: commitMessage,
|
|
1017
|
+
encoding: 'utf8',
|
|
1018
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1019
|
+
}).trim()
|
|
1020
|
+
|
|
1021
|
+
console.log(colors.successMessage('✅ Commit executed successfully!'))
|
|
1022
|
+
console.log(colors.highlight(`📝 Commit hash: ${commitHash.trim()}`))
|
|
1023
|
+
console.log(colors.dim(`📝 Message: ${commitMessage.split('\n')[0]}`))
|
|
1024
|
+
|
|
1025
|
+
return {
|
|
1026
|
+
success: true,
|
|
1027
|
+
commitHash: commitHash.trim(),
|
|
1028
|
+
commitMessage,
|
|
1029
|
+
stagedFiles: finalStatus.staged.length,
|
|
1030
|
+
}
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
console.error(colors.errorMessage(`❌ Failed to execute commit: ${error.message}`))
|
|
1033
|
+
console.log(colors.infoMessage('\n💡 Files remain staged. You can manually commit with:'))
|
|
1034
|
+
console.log(colors.code('git commit --file=- # (and paste your commit message)'))
|
|
1035
|
+
|
|
1036
|
+
return {
|
|
1037
|
+
success: false,
|
|
1038
|
+
error: error.message,
|
|
1039
|
+
commitMessage,
|
|
1040
|
+
stagedFiles: finalStatus.staged.length,
|
|
1041
|
+
filesStaged: true,
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
success: true,
|
|
1047
|
+
commitMessage,
|
|
1048
|
+
stagedFiles: finalStatus.staged.length,
|
|
1049
|
+
phase: 'staging-complete',
|
|
1050
|
+
}
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
console.error(colors.errorMessage(`Commit workflow error: ${error.message}`))
|
|
1053
|
+
throw error
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/**
|
|
1058
|
+
* Generate AI-powered commit message using branch intelligence and file changes
|
|
1059
|
+
*/
|
|
1060
|
+
async generateAICommitMessage(branchAnalysis, suggestedType, stagedFiles) {
|
|
1061
|
+
if (!this.aiAnalysisService?.aiProvider?.isAvailable()) {
|
|
1062
|
+
throw new Error('AI provider not available')
|
|
728
1063
|
}
|
|
1064
|
+
|
|
1065
|
+
// Build context for AI
|
|
1066
|
+
const branchInfo =
|
|
1067
|
+
branchAnalysis?.confidence > 30
|
|
1068
|
+
? `Branch: ${branchAnalysis.branch} (${branchAnalysis.type || 'unknown'} type, ${branchAnalysis.confidence}% confidence)`
|
|
1069
|
+
: `Branch: ${branchAnalysis?.branch || 'unknown'}`
|
|
1070
|
+
|
|
1071
|
+
const fileChanges = stagedFiles.map((f) => `${f.status} ${f.path}`).join('\n')
|
|
1072
|
+
|
|
1073
|
+
// Build detailed context about the changes
|
|
1074
|
+
const changesSummary = {
|
|
1075
|
+
added: stagedFiles.filter((f) => f.status === 'A').length,
|
|
1076
|
+
modified: stagedFiles.filter((f) => f.status === 'M').length,
|
|
1077
|
+
deleted: stagedFiles.filter((f) => f.status === 'D').length,
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const prompt = `Generate a high-quality conventional commit message for the following staged changes:
|
|
1081
|
+
|
|
1082
|
+
${branchInfo}
|
|
1083
|
+
${branchAnalysis?.ticket ? `Related ticket: ${branchAnalysis.ticket}` : ''}
|
|
1084
|
+
|
|
1085
|
+
File changes (${stagedFiles.length} files):
|
|
1086
|
+
${fileChanges}
|
|
1087
|
+
|
|
1088
|
+
Change summary: ${changesSummary.added} added, ${changesSummary.modified} modified, ${changesSummary.deleted} deleted
|
|
1089
|
+
|
|
1090
|
+
Suggested commit type: ${suggestedType.type} (${suggestedType.confidence}% confidence from ${suggestedType.source})
|
|
1091
|
+
|
|
1092
|
+
Requirements:
|
|
1093
|
+
- Use conventional commit format: type(scope): description
|
|
1094
|
+
- Keep subject line under 72 characters
|
|
1095
|
+
- Use imperative mood ("add", not "added")
|
|
1096
|
+
- Be specific and descriptive about what changed
|
|
1097
|
+
- Include a body if the changes are complex
|
|
1098
|
+
- Use the suggested type unless the changes clearly indicate otherwise
|
|
1099
|
+
|
|
1100
|
+
Generate only the commit message, no explanation.`
|
|
1101
|
+
|
|
1102
|
+
try {
|
|
1103
|
+
const response = await this.aiAnalysisService.aiProvider.generateCompletion(
|
|
1104
|
+
[
|
|
1105
|
+
{
|
|
1106
|
+
role: 'user',
|
|
1107
|
+
content: prompt,
|
|
1108
|
+
},
|
|
1109
|
+
],
|
|
1110
|
+
{
|
|
1111
|
+
max_tokens: 200,
|
|
1112
|
+
temperature: 0.3,
|
|
1113
|
+
}
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
const aiCommitMessage = response.content.trim()
|
|
1117
|
+
|
|
1118
|
+
// Validate the AI response has the basic structure
|
|
1119
|
+
if (!aiCommitMessage.includes(':') || aiCommitMessage.length < 10) {
|
|
1120
|
+
throw new Error('AI generated invalid commit message format')
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
return aiCommitMessage
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
throw new Error(`AI commit generation failed: ${error.message}`)
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Generate branch-aware commit message using rules and branch intelligence (fallback)
|
|
1131
|
+
*/
|
|
1132
|
+
generateBranchAwareCommitMessage(branchAnalysis, suggestedType, stagedFiles) {
|
|
1133
|
+
const type = suggestedType.type
|
|
1134
|
+
|
|
1135
|
+
// Build description based on branch intelligence
|
|
1136
|
+
let description = 'implement changes'
|
|
1137
|
+
|
|
1138
|
+
if (branchAnalysis.description && branchAnalysis.confidence > 40) {
|
|
1139
|
+
description = branchAnalysis.description
|
|
1140
|
+
} else {
|
|
1141
|
+
// Generate description from file changes
|
|
1142
|
+
const fileTypes = new Set()
|
|
1143
|
+
stagedFiles.forEach((file) => {
|
|
1144
|
+
const path = file.path
|
|
1145
|
+
if (path.includes('service')) {
|
|
1146
|
+
fileTypes.add('services')
|
|
1147
|
+
}
|
|
1148
|
+
if (path.includes('component')) {
|
|
1149
|
+
fileTypes.add('components')
|
|
1150
|
+
}
|
|
1151
|
+
if (path.includes('utils')) {
|
|
1152
|
+
fileTypes.add('utilities')
|
|
1153
|
+
}
|
|
1154
|
+
if (path.includes('config')) {
|
|
1155
|
+
fileTypes.add('configuration')
|
|
1156
|
+
}
|
|
1157
|
+
if (path.includes('test')) {
|
|
1158
|
+
fileTypes.add('tests')
|
|
1159
|
+
}
|
|
1160
|
+
if (path.includes('doc')) {
|
|
1161
|
+
fileTypes.add('documentation')
|
|
1162
|
+
}
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
if (fileTypes.size > 0) {
|
|
1166
|
+
description = `update ${Array.from(fileTypes).join(', ')}`
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Build commit message
|
|
1171
|
+
let commitMessage = `${type}: ${description}`
|
|
1172
|
+
|
|
1173
|
+
// Add body with details
|
|
1174
|
+
const bodyLines = []
|
|
1175
|
+
|
|
1176
|
+
if (branchAnalysis.ticket) {
|
|
1177
|
+
bodyLines.push(`Related to: ${branchAnalysis.ticket}`)
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Add file summary
|
|
1181
|
+
const addedFiles = stagedFiles.filter((f) => f.status === 'A').length
|
|
1182
|
+
const modifiedFiles = stagedFiles.filter((f) => f.status === 'M').length
|
|
1183
|
+
const deletedFiles = stagedFiles.filter((f) => f.status === 'D').length
|
|
1184
|
+
|
|
1185
|
+
const changes = []
|
|
1186
|
+
if (addedFiles > 0) {
|
|
1187
|
+
changes.push(`${addedFiles} added`)
|
|
1188
|
+
}
|
|
1189
|
+
if (modifiedFiles > 0) {
|
|
1190
|
+
changes.push(`${modifiedFiles} modified`)
|
|
1191
|
+
}
|
|
1192
|
+
if (deletedFiles > 0) {
|
|
1193
|
+
changes.push(`${deletedFiles} deleted`)
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (changes.length > 0) {
|
|
1197
|
+
bodyLines.push(`Files: ${changes.join(', ')}`)
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Add branch context
|
|
1201
|
+
if (branchAnalysis.confidence > 60) {
|
|
1202
|
+
bodyLines.push(`Branch: ${branchAnalysis.branch} (${branchAnalysis.confidence}% confidence)`)
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (bodyLines.length > 0) {
|
|
1206
|
+
commitMessage += `\n\n${bodyLines.join('\n')}`
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
return commitMessage
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/**
|
|
1213
|
+
* Handle interactive commit message improvement
|
|
1214
|
+
*/
|
|
1215
|
+
async handleCommitMessageImprovement(originalMessage, validationResult, context) {
|
|
1216
|
+
const { select, text, confirm } = await import('@clack/prompts')
|
|
1217
|
+
|
|
1218
|
+
console.log(colors.infoMessage('\n🔧 Commit Message Improvement'))
|
|
1219
|
+
|
|
1220
|
+
// Try automatic improvement first
|
|
1221
|
+
const improvementResult = await this.validationService.improveCommitMessage(
|
|
1222
|
+
originalMessage,
|
|
1223
|
+
context
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
const options = [
|
|
1227
|
+
{
|
|
1228
|
+
value: 'auto',
|
|
1229
|
+
label: '🤖 Use automatically improved version',
|
|
1230
|
+
hint: improvementResult.improved
|
|
1231
|
+
? 'AI-suggested improvements applied'
|
|
1232
|
+
: 'Minor fixes applied',
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
value: 'manual',
|
|
1236
|
+
label: '✏️ Edit manually',
|
|
1237
|
+
hint: 'Customize the commit message yourself',
|
|
1238
|
+
},
|
|
1239
|
+
]
|
|
1240
|
+
|
|
1241
|
+
// Add AI suggestions if available
|
|
1242
|
+
if (this.aiAnalysisService.hasAI) {
|
|
1243
|
+
options.unshift({
|
|
1244
|
+
value: 'ai',
|
|
1245
|
+
label: '🧠 Generate AI suggestions',
|
|
1246
|
+
hint: 'Get AI-powered commit message alternatives',
|
|
1247
|
+
})
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const choice = await select({
|
|
1251
|
+
message: 'How would you like to improve the commit message?',
|
|
1252
|
+
options,
|
|
1253
|
+
})
|
|
1254
|
+
|
|
1255
|
+
switch (choice) {
|
|
1256
|
+
case 'ai':
|
|
1257
|
+
return await this.generateAICommitSuggestions(originalMessage, context, validationResult)
|
|
1258
|
+
|
|
1259
|
+
case 'auto':
|
|
1260
|
+
console.log(colors.successMessage('\n✅ Using improved message:'))
|
|
1261
|
+
console.log(colors.highlight(improvementResult.message))
|
|
1262
|
+
return improvementResult.message
|
|
1263
|
+
|
|
1264
|
+
case 'manual':
|
|
1265
|
+
return await this.handleManualCommitEdit(originalMessage, validationResult)
|
|
1266
|
+
|
|
1267
|
+
default:
|
|
1268
|
+
return originalMessage
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Generate AI-powered commit message suggestions
|
|
1274
|
+
*/
|
|
1275
|
+
async generateAICommitSuggestions(originalMessage, context, validationResult) {
|
|
1276
|
+
if (!this.aiAnalysisService?.aiProvider?.isAvailable()) {
|
|
1277
|
+
throw new Error('AI provider not available for suggestions')
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const { select } = await this._getCachedImport('@clack/prompts')
|
|
1281
|
+
|
|
1282
|
+
console.log(colors.processingMessage('🤖 Generating AI suggestions...'))
|
|
1283
|
+
|
|
1284
|
+
try {
|
|
1285
|
+
// Build comprehensive context for AI
|
|
1286
|
+
const branchInfo =
|
|
1287
|
+
context.branchAnalysis?.confidence > 30
|
|
1288
|
+
? `Branch: ${context.branchAnalysis.branch} (${context.branchAnalysis.type || 'unknown'} type)`
|
|
1289
|
+
: ''
|
|
1290
|
+
|
|
1291
|
+
const fileChanges = context.stagedFiles.map((f) => `${f.status} ${f.path}`).join('\n')
|
|
1292
|
+
|
|
1293
|
+
const validationIssues = [...validationResult.errors, ...validationResult.warnings].join('\n')
|
|
1294
|
+
|
|
1295
|
+
const prompt = `Improve this commit message based on the validation feedback and context:
|
|
1296
|
+
|
|
1297
|
+
Original message: "${originalMessage}"
|
|
1298
|
+
|
|
1299
|
+
Validation issues:
|
|
1300
|
+
${validationIssues}
|
|
1301
|
+
|
|
1302
|
+
File changes:
|
|
1303
|
+
${fileChanges}
|
|
1304
|
+
|
|
1305
|
+
${branchInfo}
|
|
1306
|
+
|
|
1307
|
+
Requirements:
|
|
1308
|
+
- Follow conventional commit format
|
|
1309
|
+
- Address all validation issues
|
|
1310
|
+
- Keep subject under 72 characters
|
|
1311
|
+
- Use imperative mood
|
|
1312
|
+
- Be specific and descriptive
|
|
1313
|
+
|
|
1314
|
+
Provide 3 improved alternatives.`
|
|
1315
|
+
|
|
1316
|
+
const response = await this.aiAnalysisService.aiProvider.generateCompletion(
|
|
1317
|
+
[
|
|
1318
|
+
{
|
|
1319
|
+
role: 'user',
|
|
1320
|
+
content: prompt,
|
|
1321
|
+
},
|
|
1322
|
+
],
|
|
1323
|
+
{ max_tokens: 300 }
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
const suggestions = this.parseAICommitSuggestions(response.content)
|
|
1327
|
+
|
|
1328
|
+
if (suggestions.length === 0) {
|
|
1329
|
+
console.log(
|
|
1330
|
+
colors.warningMessage('No AI suggestions generated, falling back to manual edit.')
|
|
1331
|
+
)
|
|
1332
|
+
return await this.handleManualCommitEdit(originalMessage, validationResult)
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// Present suggestions to user
|
|
1336
|
+
const choices = suggestions.map((suggestion, index) => ({
|
|
1337
|
+
value: suggestion,
|
|
1338
|
+
label: `${index + 1}. ${suggestion.split('\n')[0]}`, // First line only
|
|
1339
|
+
hint: suggestion.includes('\n') ? 'Has body content' : 'Subject only',
|
|
1340
|
+
}))
|
|
1341
|
+
|
|
1342
|
+
choices.push({
|
|
1343
|
+
value: 'MANUAL',
|
|
1344
|
+
label: '✏️ Edit manually instead',
|
|
1345
|
+
hint: 'Write your own commit message',
|
|
1346
|
+
})
|
|
1347
|
+
|
|
1348
|
+
const selectedMessage = await select({
|
|
1349
|
+
message: 'Choose an AI-generated commit message:',
|
|
1350
|
+
options: choices,
|
|
1351
|
+
})
|
|
1352
|
+
|
|
1353
|
+
if (selectedMessage === 'MANUAL') {
|
|
1354
|
+
return await this.handleManualCommitEdit(originalMessage, validationResult)
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
console.log(colors.successMessage('\n✅ Selected AI suggestion:'))
|
|
1358
|
+
console.log(colors.highlight(selectedMessage))
|
|
1359
|
+
return selectedMessage
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
console.error(colors.errorMessage(`AI suggestion failed: ${error.message}`))
|
|
1362
|
+
return await this.handleManualCommitEdit(originalMessage, validationResult)
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Handle manual commit message editing
|
|
1368
|
+
*/
|
|
1369
|
+
async handleManualCommitEdit(originalMessage, validationResult) {
|
|
1370
|
+
const { text, confirm } = await import('@clack/prompts')
|
|
1371
|
+
|
|
1372
|
+
console.log(colors.infoMessage('\n✏️ Manual Edit Mode'))
|
|
1373
|
+
console.log(colors.dim('Validation issues to address:'))
|
|
1374
|
+
|
|
1375
|
+
validationResult.errors.forEach((error) => {
|
|
1376
|
+
console.log(colors.error(` • ${error}`))
|
|
1377
|
+
})
|
|
1378
|
+
|
|
1379
|
+
validationResult.warnings.forEach((warning) => {
|
|
1380
|
+
console.log(colors.warning(` • ${warning}`))
|
|
1381
|
+
})
|
|
1382
|
+
|
|
1383
|
+
if (validationResult.suggestions.length > 0) {
|
|
1384
|
+
console.log(colors.dim('\nSuggestions:'))
|
|
1385
|
+
validationResult.suggestions.forEach((suggestion) => {
|
|
1386
|
+
console.log(colors.dim(` • ${suggestion}`))
|
|
1387
|
+
})
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
let improvedMessage
|
|
1391
|
+
let isValid = false
|
|
1392
|
+
let attempts = 0
|
|
1393
|
+
const maxAttempts = 3
|
|
1394
|
+
|
|
1395
|
+
while (!isValid && attempts < maxAttempts) {
|
|
1396
|
+
improvedMessage = await text({
|
|
1397
|
+
message: 'Enter improved commit message:',
|
|
1398
|
+
placeholder: originalMessage,
|
|
1399
|
+
defaultValue: attempts === 0 ? originalMessage : undefined,
|
|
1400
|
+
validate: (input) => {
|
|
1401
|
+
if (!input || input.trim().length === 0) {
|
|
1402
|
+
return 'Commit message cannot be empty'
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
})
|
|
1406
|
+
|
|
1407
|
+
// Validate the improved message
|
|
1408
|
+
const newValidation = await this.validationService.validateCommitMessage(improvedMessage, {
|
|
1409
|
+
branchAnalysis: validationResult.parsed?.branchAnalysis,
|
|
1410
|
+
})
|
|
1411
|
+
|
|
1412
|
+
if (newValidation.valid) {
|
|
1413
|
+
isValid = true
|
|
1414
|
+
console.log(colors.successMessage('✅ Validation passed!'))
|
|
1415
|
+
} else {
|
|
1416
|
+
attempts++
|
|
1417
|
+
console.log(
|
|
1418
|
+
colors.warningMessage(`\n⚠️ Validation failed (attempt ${attempts}/${maxAttempts}):`)
|
|
1419
|
+
)
|
|
1420
|
+
this.validationService.displayValidationResults(newValidation)
|
|
1421
|
+
|
|
1422
|
+
if (attempts < maxAttempts) {
|
|
1423
|
+
const tryAgain = await confirm({
|
|
1424
|
+
message: 'Try again with improvements?',
|
|
1425
|
+
initialValue: true,
|
|
1426
|
+
})
|
|
1427
|
+
|
|
1428
|
+
if (!tryAgain) {
|
|
1429
|
+
break
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
return improvedMessage || originalMessage
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/**
|
|
1439
|
+
* Parse AI-generated commit suggestions
|
|
1440
|
+
*/
|
|
1441
|
+
parseAICommitSuggestions(content) {
|
|
1442
|
+
const suggestions = []
|
|
1443
|
+
const lines = content.split('\n').filter((line) => line.trim())
|
|
1444
|
+
|
|
1445
|
+
let currentSuggestion = ''
|
|
1446
|
+
for (const line of lines) {
|
|
1447
|
+
const trimmed = line.trim()
|
|
1448
|
+
|
|
1449
|
+
// Check if it's a new suggestion (starts with number, bullet, or looks like commit format)
|
|
1450
|
+
if (
|
|
1451
|
+
trimmed.match(
|
|
1452
|
+
/^(\d+[.)]|\*|-|•)|^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.*?\))?:/
|
|
1453
|
+
)
|
|
1454
|
+
) {
|
|
1455
|
+
if (currentSuggestion) {
|
|
1456
|
+
suggestions.push(currentSuggestion.trim())
|
|
1457
|
+
}
|
|
1458
|
+
// Clean up the line (remove numbering/bullets)
|
|
1459
|
+
currentSuggestion = trimmed.replace(/^(\d+[.)]|\*|-|•)\s*/, '')
|
|
1460
|
+
} else if (currentSuggestion && trimmed.length > 0) {
|
|
1461
|
+
// Add to current suggestion (body content)
|
|
1462
|
+
currentSuggestion += `\n${trimmed}`
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// Add the last suggestion
|
|
1467
|
+
if (currentSuggestion) {
|
|
1468
|
+
suggestions.push(currentSuggestion.trim())
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// Filter valid suggestions
|
|
1472
|
+
return suggestions.filter((s) => s.length > 10 && s.includes(':')).slice(0, 3) // Limit to 3 suggestions
|
|
729
1473
|
}
|
|
730
|
-
}
|
|
1474
|
+
}
|