@entro314labs/ai-changelog-generator 3.7.1 → 3.8.1
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/bin/ai-changelog.js +5 -1
- package/package.json +12 -9
- package/src/application/orchestrators/changelog.orchestrator.js +16 -10
- package/src/application/services/application.service.js +36 -20
- package/src/domains/analysis/analysis.engine.js +26 -22
- package/src/domains/changelog/changelog.service.js +66 -8
- package/src/domains/git/git-manager.js +15 -7
- package/src/domains/git/git.service.js +36 -13
- package/src/infrastructure/cli/cli.controller.js +61 -42
- package/src/infrastructure/config/configuration.manager.js +6 -4
- package/src/infrastructure/interactive/interactive-workflow.service.js +119 -12
- package/src/infrastructure/mcp/mcp-server.service.js +13 -10
- package/src/infrastructure/providers/core/base-provider.js +2 -1
- package/src/infrastructure/providers/utils/model-config.js +3 -1
- package/src/infrastructure/validation/commit-message-validation.service.js +1 -1
- package/src/shared/constants/colors.js +2 -1
- package/src/shared/utils/cli-entry-utils.js +3 -1
- package/src/shared/utils/cli-ui.js +17 -14
- package/src/shared/utils/diff-processor.js +3 -13
- package/src/shared/utils/json-utils.js +6 -2
- package/src/shared/utils/utils.js +22 -10
|
@@ -41,7 +41,8 @@ export class GitService {
|
|
|
41
41
|
.trim()
|
|
42
42
|
.split(' ')
|
|
43
43
|
isMergeCommit = parents.length > 1
|
|
44
|
-
} catch {
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.warn(colors.warningMessage(`Failed to check parent commits: ${error.message}`))
|
|
45
46
|
isMergeCommit = false
|
|
46
47
|
}
|
|
47
48
|
}
|
|
@@ -203,7 +204,8 @@ export class GitService {
|
|
|
203
204
|
} else {
|
|
204
205
|
diff = 'New empty file created'
|
|
205
206
|
}
|
|
206
|
-
} catch {
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.warn(colors.warningMessage(`Failed to read new file content: ${error.message}`))
|
|
207
209
|
diff = 'New file created (binary or inaccessible)'
|
|
208
210
|
}
|
|
209
211
|
} else if (status === 'D') {
|
|
@@ -217,7 +219,10 @@ export class GitService {
|
|
|
217
219
|
} else {
|
|
218
220
|
diff = 'File deleted from working directory (content was empty)'
|
|
219
221
|
}
|
|
220
|
-
} catch {
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.warn(
|
|
224
|
+
colors.warningMessage(`Failed to read deleted file content: ${error.message}`)
|
|
225
|
+
)
|
|
221
226
|
diff = 'File deleted from working directory (content unavailable)'
|
|
222
227
|
}
|
|
223
228
|
} else if (status === 'M' || status.includes('M')) {
|
|
@@ -230,7 +235,8 @@ export class GitService {
|
|
|
230
235
|
const stagedDiff = this.gitManager.execGitSafe(`git diff --cached -- "${filePath}"`)
|
|
231
236
|
diff = stagedDiff || 'No diff available (binary or identical)'
|
|
232
237
|
}
|
|
233
|
-
} catch {
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.warn(colors.warningMessage(`Failed to generate diff: ${error.message}`))
|
|
234
240
|
diff = 'Modified file (diff unavailable)'
|
|
235
241
|
}
|
|
236
242
|
} else if (status === 'R') {
|
|
@@ -250,7 +256,8 @@ export class GitService {
|
|
|
250
256
|
try {
|
|
251
257
|
const headResult = this.gitManager.execGitSafe(`git show HEAD:"${filePath}"`)
|
|
252
258
|
beforeContent = headResult ? headResult.slice(0, 1000) : ''
|
|
253
|
-
} catch {
|
|
259
|
+
} catch (error) {
|
|
260
|
+
console.warn(colors.warningMessage(`Failed to get before content: ${error.message}`))
|
|
254
261
|
beforeContent = ''
|
|
255
262
|
}
|
|
256
263
|
// afterContent stays empty for deleted files
|
|
@@ -260,7 +267,10 @@ export class GitService {
|
|
|
260
267
|
// Get HEAD version
|
|
261
268
|
const headResult = this.gitManager.execGitSafe(`git show HEAD:"${filePath}"`)
|
|
262
269
|
beforeContent = headResult ? headResult.slice(0, 1000) : ''
|
|
263
|
-
} catch {
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.warn(
|
|
272
|
+
colors.warningMessage(`Failed to get before content (modified): ${error.message}`)
|
|
273
|
+
)
|
|
264
274
|
beforeContent = ''
|
|
265
275
|
}
|
|
266
276
|
|
|
@@ -268,7 +278,8 @@ export class GitService {
|
|
|
268
278
|
// Get current working directory version
|
|
269
279
|
const currentResult = this.gitManager.execGitSafe(`cat "${filePath}"`)
|
|
270
280
|
afterContent = currentResult ? currentResult.slice(0, 1000) : ''
|
|
271
|
-
} catch {
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.warn(colors.warningMessage(`Failed to get after content: ${error.message}`))
|
|
272
283
|
afterContent = ''
|
|
273
284
|
}
|
|
274
285
|
}
|
|
@@ -301,7 +312,11 @@ export class GitService {
|
|
|
301
312
|
language: 'unknown',
|
|
302
313
|
importance: 'medium',
|
|
303
314
|
complexity: { score: 1 },
|
|
304
|
-
semanticChanges: {
|
|
315
|
+
semanticChanges: {
|
|
316
|
+
changeType: 'unknown',
|
|
317
|
+
patterns: [],
|
|
318
|
+
frameworks: [],
|
|
319
|
+
},
|
|
305
320
|
functionalImpact: { scope: 'local', severity: 'low' },
|
|
306
321
|
}
|
|
307
322
|
}
|
|
@@ -328,7 +343,8 @@ export class GitService {
|
|
|
328
343
|
}
|
|
329
344
|
|
|
330
345
|
return { files: 0, insertions: 0, deletions: 0 }
|
|
331
|
-
} catch {
|
|
346
|
+
} catch (error) {
|
|
347
|
+
console.warn(colors.warningMessage(`Failed to get diff stats: ${error.message}`))
|
|
332
348
|
return { files: 0, insertions: 0, deletions: 0 }
|
|
333
349
|
}
|
|
334
350
|
}
|
|
@@ -420,7 +436,9 @@ export class GitService {
|
|
|
420
436
|
category: categorizeFile(filePath),
|
|
421
437
|
language: detectLanguage(filePath),
|
|
422
438
|
importance: assessFileImportance(filePath, status),
|
|
423
|
-
complexity: {
|
|
439
|
+
complexity: {
|
|
440
|
+
level: changes > 100 ? 'high' : changes > 20 ? 'medium' : 'low',
|
|
441
|
+
},
|
|
424
442
|
semanticChanges: { patterns: ['merge-commit'] },
|
|
425
443
|
functionalImpact: { level: changes > 50 ? 'high' : 'medium' },
|
|
426
444
|
isMergeCommit: true,
|
|
@@ -662,7 +680,9 @@ export class GitService {
|
|
|
662
680
|
details.push(techDetail)
|
|
663
681
|
}
|
|
664
682
|
}
|
|
665
|
-
} catch (
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.warn(colors.warningMessage(`Failed to extract technical details: ${error.message}`))
|
|
685
|
+
}
|
|
666
686
|
}
|
|
667
687
|
|
|
668
688
|
return details
|
|
@@ -830,7 +850,10 @@ export class GitService {
|
|
|
830
850
|
.filter((line) => line.trim())
|
|
831
851
|
.map((line) => {
|
|
832
852
|
const parts = line.split(' ')
|
|
833
|
-
return {
|
|
853
|
+
return {
|
|
854
|
+
size: parseInt(parts[1], 10),
|
|
855
|
+
path: parts.slice(2).join(' '),
|
|
856
|
+
}
|
|
834
857
|
})
|
|
835
858
|
.filter((file) => file.size > 10 * 1024 * 1024) // 10MB threshold
|
|
836
859
|
|
|
@@ -1101,7 +1124,7 @@ ${
|
|
|
1101
1124
|
)
|
|
1102
1125
|
if (firstCommitOutput.trim()) {
|
|
1103
1126
|
const firstCommitDate = new Date(firstCommitOutput.trim())
|
|
1104
|
-
const ageInDays = Math.floor((
|
|
1127
|
+
const ageInDays = Math.floor((Date.now() - firstCommitDate) / (1000 * 60 * 60 * 24))
|
|
1105
1128
|
analysis.metrics.repositoryAgeInDays = ageInDays
|
|
1106
1129
|
}
|
|
1107
1130
|
} catch (error) {
|
|
@@ -306,7 +306,10 @@ export class CLIController {
|
|
|
306
306
|
.option('output', { alias: 'o', type: 'string', description: 'Output file path' })
|
|
307
307
|
.option('since', { type: 'string', description: 'Analyze changes since this git ref' })
|
|
308
308
|
.option('author', { alias: 'a', type: 'string', description: 'Filter commits by author' })
|
|
309
|
-
.option('tag-range', {
|
|
309
|
+
.option('tag-range', {
|
|
310
|
+
type: 'string',
|
|
311
|
+
description: 'Generate changelog between tags (v1.0..v2.0)',
|
|
312
|
+
})
|
|
310
313
|
.option('silent', { type: 'boolean', description: 'Suppress non-essential output' })
|
|
311
314
|
.option('dry-run', { type: 'boolean', description: 'Preview without writing files' })
|
|
312
315
|
.option('detailed', { type: 'boolean', description: 'Use detailed analysis mode' })
|
|
@@ -544,35 +547,33 @@ class WorkingDirCommand extends BaseCommand {
|
|
|
544
547
|
class FromCommitsCommand extends BaseCommand {
|
|
545
548
|
async execute(argv, appService) {
|
|
546
549
|
const _config = this.processStandardFlags(argv, appService)
|
|
547
|
-
|
|
550
|
+
EnhancedConsole.processing(`Generating changelog from commits: ${argv.commits.join(', ')}`)
|
|
548
551
|
|
|
549
552
|
try {
|
|
550
553
|
const result = await appService.generateChangelogFromCommits(argv.commits)
|
|
551
554
|
|
|
552
555
|
if (result?.changelog) {
|
|
553
|
-
|
|
554
|
-
|
|
556
|
+
EnhancedConsole.success('Changelog generated successfully!')
|
|
557
|
+
EnhancedConsole.divider()
|
|
555
558
|
console.log(result.changelog)
|
|
556
559
|
} else {
|
|
557
|
-
|
|
560
|
+
EnhancedConsole.warn('No changelog could be generated from the specified commits.')
|
|
558
561
|
}
|
|
559
562
|
} catch (error) {
|
|
560
|
-
|
|
563
|
+
EnhancedConsole.error(`Error generating changelog: ${error.message}`)
|
|
561
564
|
}
|
|
562
565
|
}
|
|
563
566
|
}
|
|
564
567
|
|
|
565
568
|
class CommitMessageCommand extends BaseCommand {
|
|
566
569
|
async execute(_argv, appService) {
|
|
567
|
-
|
|
568
|
-
colors.processingMessage('🤖 Analyzing current changes for commit message suggestions...')
|
|
569
|
-
)
|
|
570
|
+
EnhancedConsole.processing('Analyzing current changes for commit message suggestions...')
|
|
570
571
|
|
|
571
572
|
try {
|
|
572
573
|
const result = await appService.generateCommitMessage()
|
|
573
574
|
|
|
574
575
|
if (result?.suggestions && result.suggestions.length > 0) {
|
|
575
|
-
|
|
576
|
+
EnhancedConsole.success('Generated commit message suggestions:')
|
|
576
577
|
result.suggestions.forEach((suggestion, index) => {
|
|
577
578
|
console.log(`${colors.number(index + 1)}. ${colors.highlight(suggestion)}`)
|
|
578
579
|
})
|
|
@@ -581,18 +582,18 @@ class CommitMessageCommand extends BaseCommand {
|
|
|
581
582
|
console.log(colors.dim(`\nContext: ${result.context}`))
|
|
582
583
|
}
|
|
583
584
|
} else {
|
|
584
|
-
|
|
585
|
-
|
|
585
|
+
EnhancedConsole.warn('No commit message suggestions could be generated.')
|
|
586
|
+
EnhancedConsole.info('Make sure you have uncommitted changes.')
|
|
586
587
|
}
|
|
587
588
|
} catch (error) {
|
|
588
|
-
|
|
589
|
+
EnhancedConsole.error(`Error generating commit message: ${error.message}`)
|
|
589
590
|
}
|
|
590
591
|
}
|
|
591
592
|
}
|
|
592
593
|
|
|
593
594
|
class CommitCommand extends BaseCommand {
|
|
594
595
|
async execute(argv, appService) {
|
|
595
|
-
|
|
596
|
+
EnhancedConsole.processing('Starting interactive commit workflow...')
|
|
596
597
|
|
|
597
598
|
try {
|
|
598
599
|
// Process flags and model override
|
|
@@ -611,29 +612,25 @@ class CommitCommand extends BaseCommand {
|
|
|
611
612
|
|
|
612
613
|
if (result?.success) {
|
|
613
614
|
if (argv.dryRun) {
|
|
614
|
-
|
|
615
|
+
EnhancedConsole.success('Commit workflow completed (dry-run mode)')
|
|
615
616
|
console.log(colors.highlight(`Proposed commit message:\n${result.commitMessage}`))
|
|
616
617
|
} else {
|
|
617
|
-
|
|
618
|
+
EnhancedConsole.success('Changes committed successfully!')
|
|
618
619
|
console.log(colors.highlight(`Commit: ${result.commitHash}`))
|
|
619
620
|
console.log(colors.dim(`Message: ${result.commitMessage}`))
|
|
620
621
|
}
|
|
621
622
|
} else {
|
|
622
|
-
|
|
623
|
+
EnhancedConsole.warn('Commit workflow cancelled or no changes to commit.')
|
|
623
624
|
}
|
|
624
625
|
} catch (error) {
|
|
625
|
-
|
|
626
|
+
EnhancedConsole.error(`Commit workflow failed: ${error.message}`)
|
|
626
627
|
|
|
627
628
|
// Provide helpful suggestions based on error type
|
|
628
629
|
if (error.message.includes('No changes')) {
|
|
629
|
-
|
|
630
|
-
colors.infoMessage('💡 Try making some changes first, then run the commit command.')
|
|
631
|
-
)
|
|
630
|
+
EnhancedConsole.info('Try making some changes first, then run the commit command.')
|
|
632
631
|
} else if (error.message.includes('git')) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
'💡 Make sure you are in a git repository and git is properly configured.'
|
|
636
|
-
)
|
|
632
|
+
EnhancedConsole.info(
|
|
633
|
+
'Make sure you have uncommitted changes.'
|
|
637
634
|
)
|
|
638
635
|
}
|
|
639
636
|
}
|
|
@@ -664,8 +661,8 @@ class ProvidersCommand extends BaseCommand {
|
|
|
664
661
|
await this.listModels(appService, argv.provider)
|
|
665
662
|
break
|
|
666
663
|
default:
|
|
667
|
-
|
|
668
|
-
|
|
664
|
+
EnhancedConsole.error('Unknown provider subcommand')
|
|
665
|
+
EnhancedConsole.info('Available subcommands: list, switch, configure, validate, status, models')
|
|
669
666
|
}
|
|
670
667
|
}
|
|
671
668
|
|
|
@@ -702,8 +699,8 @@ class ProvidersCommand extends BaseCommand {
|
|
|
702
699
|
|
|
703
700
|
async switchProvider(appService, providerName) {
|
|
704
701
|
if (!providerName) {
|
|
705
|
-
|
|
706
|
-
|
|
702
|
+
EnhancedConsole.error('Please specify a provider name')
|
|
703
|
+
EnhancedConsole.info('Usage: ai-changelog providers switch <provider>')
|
|
707
704
|
return
|
|
708
705
|
}
|
|
709
706
|
|
|
@@ -711,12 +708,10 @@ class ProvidersCommand extends BaseCommand {
|
|
|
711
708
|
const result = await appService.switchProvider(providerName)
|
|
712
709
|
|
|
713
710
|
if (result.success) {
|
|
714
|
-
|
|
711
|
+
EnhancedConsole.success(`Switched to ${providerName} provider`)
|
|
715
712
|
} else {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
colors.infoMessage('Use "ai-changelog providers list" to see available providers')
|
|
719
|
-
)
|
|
713
|
+
EnhancedConsole.error(`Failed to switch provider: ${result.error}`)
|
|
714
|
+
EnhancedConsole.info('Use "ai-changelog providers list" to see available providers')
|
|
720
715
|
}
|
|
721
716
|
} catch (error) {
|
|
722
717
|
EnhancedConsole.error(`Error switching provider: ${error.message}`)
|
|
@@ -743,7 +738,7 @@ class ProvidersCommand extends BaseCommand {
|
|
|
743
738
|
}
|
|
744
739
|
|
|
745
740
|
console.log(colors.header(`\n🔧 Configuring ${providerName.toUpperCase()} Provider`))
|
|
746
|
-
|
|
741
|
+
EnhancedConsole.info('Please add the following to your .env.local file:\n')
|
|
747
742
|
|
|
748
743
|
switch (providerName.toLowerCase()) {
|
|
749
744
|
case 'openai':
|
|
@@ -776,7 +771,7 @@ class ProvidersCommand extends BaseCommand {
|
|
|
776
771
|
console.log(colors.code(`${providerName.toUpperCase()}_API_KEY=your_api_key_here`))
|
|
777
772
|
}
|
|
778
773
|
|
|
779
|
-
|
|
774
|
+
EnhancedConsole.info('\nAfter adding the configuration, run:')
|
|
780
775
|
console.log(colors.highlight(`ai-changelog providers validate ${providerName}`))
|
|
781
776
|
} catch (error) {
|
|
782
777
|
EnhancedConsole.error(`Error configuring provider: ${error.message}`)
|
|
@@ -895,7 +890,9 @@ class ProvidersCommand extends BaseCommand {
|
|
|
895
890
|
|
|
896
891
|
// Summary
|
|
897
892
|
const healthy = healthResults.filter((r) => r.status === 'healthy').length
|
|
898
|
-
const unhealthy = healthResults.filter((r) =>
|
|
893
|
+
const unhealthy = healthResults.filter((r) =>
|
|
894
|
+
['unhealthy', 'error'].includes(r.status)
|
|
895
|
+
).length
|
|
899
896
|
const unconfigured = healthResults.filter((r) => r.status === 'unconfigured').length
|
|
900
897
|
|
|
901
898
|
console.log(colors.dim('─'.repeat(40)))
|
|
@@ -918,7 +915,9 @@ class ProvidersCommand extends BaseCommand {
|
|
|
918
915
|
const provider = providers.find((p) => p.name.toLowerCase() === providerName.toLowerCase())
|
|
919
916
|
if (!provider) {
|
|
920
917
|
console.log(colors.errorMessage(`Provider '${providerName}' not found.`))
|
|
921
|
-
console.log(
|
|
918
|
+
console.log(
|
|
919
|
+
colors.infoMessage('Use "ai-changelog providers list" to see available providers.')
|
|
920
|
+
)
|
|
922
921
|
return
|
|
923
922
|
}
|
|
924
923
|
|
|
@@ -974,13 +973,31 @@ class ProvidersCommand extends BaseCommand {
|
|
|
974
973
|
|
|
975
974
|
getKnownModelsForProvider(providerName) {
|
|
976
975
|
const knownModels = {
|
|
977
|
-
openai: [
|
|
978
|
-
|
|
976
|
+
openai: [
|
|
977
|
+
'gpt-4o',
|
|
978
|
+
'gpt-4o-mini',
|
|
979
|
+
'gpt-4-turbo',
|
|
980
|
+
'gpt-4',
|
|
981
|
+
'gpt-3.5-turbo',
|
|
982
|
+
'o1',
|
|
983
|
+
'o1-mini',
|
|
984
|
+
'o3-mini',
|
|
985
|
+
],
|
|
986
|
+
anthropic: [
|
|
987
|
+
'claude-sonnet-4-20250514',
|
|
988
|
+
'claude-3-5-sonnet-20241022',
|
|
989
|
+
'claude-3-opus-20240229',
|
|
990
|
+
'claude-3-haiku-20240307',
|
|
991
|
+
],
|
|
979
992
|
google: ['gemini-2.0-flash-exp', 'gemini-1.5-pro', 'gemini-1.5-flash', 'gemini-pro'],
|
|
980
993
|
azure: ['gpt-4o', 'gpt-4-turbo', 'gpt-35-turbo'],
|
|
981
994
|
ollama: ['llama3.2', 'llama3.1', 'mistral', 'codellama', 'deepseek-coder'],
|
|
982
995
|
lmstudio: ['local-model'],
|
|
983
|
-
bedrock: [
|
|
996
|
+
bedrock: [
|
|
997
|
+
'anthropic.claude-3-sonnet',
|
|
998
|
+
'anthropic.claude-3-haiku',
|
|
999
|
+
'amazon.titan-text-express',
|
|
1000
|
+
],
|
|
984
1001
|
vertex: ['gemini-1.5-pro', 'gemini-1.5-flash'],
|
|
985
1002
|
huggingface: ['mistralai/Mistral-7B-Instruct-v0.2', 'google/flan-t5-xxl'],
|
|
986
1003
|
}
|
|
@@ -1109,7 +1126,9 @@ class StashCommand extends BaseCommand {
|
|
|
1109
1126
|
console.log(colors.header('\n📋 Stash Changelog:\n'))
|
|
1110
1127
|
console.log(`## Stashed Changes\n`)
|
|
1111
1128
|
console.log(`**Message:** ${details.message}`)
|
|
1112
|
-
console.log(
|
|
1129
|
+
console.log(
|
|
1130
|
+
`**Stats:** ${details.stats.filesChanged} files, +${details.stats.insertions}/-${details.stats.deletions}`
|
|
1131
|
+
)
|
|
1113
1132
|
|
|
1114
1133
|
console.log(`\n**Files affected:**`)
|
|
1115
1134
|
details.files.forEach((f) => {
|
|
@@ -111,9 +111,11 @@ export class ConfigurationManager {
|
|
|
111
111
|
const content = fs.readFileSync(this.configPath, 'utf8')
|
|
112
112
|
const envVars = this.parseEnvFile(content)
|
|
113
113
|
Object.assign(defaults, envVars)
|
|
114
|
-
} catch (
|
|
114
|
+
} catch (error) {
|
|
115
115
|
console.warn(
|
|
116
|
-
colors.warningMessage(
|
|
116
|
+
colors.warningMessage(
|
|
117
|
+
`Warning: Could not load config from ${this.configPath}: ${error.message}`
|
|
118
|
+
)
|
|
117
119
|
)
|
|
118
120
|
}
|
|
119
121
|
}
|
|
@@ -179,10 +181,10 @@ export class ConfigurationManager {
|
|
|
179
181
|
|
|
180
182
|
// Deep merge with defaults
|
|
181
183
|
return this.deepMergeConfig(defaultConfig, yamlConfig)
|
|
182
|
-
} catch (
|
|
184
|
+
} catch (error) {
|
|
183
185
|
console.warn(
|
|
184
186
|
colors.warningMessage(
|
|
185
|
-
`Warning: Could not load changelog config from ${this.changelogConfigPath}, using defaults`
|
|
187
|
+
`Warning: Could not load changelog config from ${this.changelogConfigPath}: ${error.message}, using defaults`
|
|
186
188
|
)
|
|
187
189
|
)
|
|
188
190
|
return defaultConfig
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
|
+
import { execSync } from 'node:child_process'
|
|
2
3
|
|
|
3
4
|
import colors from '../../shared/constants/colors.js'
|
|
4
5
|
import { EnhancedConsole, SimpleSpinner } from '../../shared/utils/cli-ui.js'
|
|
@@ -101,28 +102,40 @@ export class InteractiveWorkflowService {
|
|
|
101
102
|
// Use AI to suggest better commit message
|
|
102
103
|
if (this.aiAnalysisService.hasAI) {
|
|
103
104
|
const prompt = message
|
|
104
|
-
? `Improve this commit message following conventional commit format
|
|
105
|
+
? `Improve this commit message following conventional commit format.
|
|
105
106
|
|
|
106
107
|
Original: "${message}"
|
|
107
108
|
|
|
109
|
+
Provide EXACTLY 3 alternative commit messages, one per line, numbered 1-3.
|
|
110
|
+
Do NOT include any other text, explanations, or conversational phrases.
|
|
111
|
+
|
|
108
112
|
Requirements:
|
|
109
|
-
- Use conventional commit format
|
|
113
|
+
- Use conventional commit format: type(scope): description
|
|
110
114
|
- Keep first line under 72 characters
|
|
111
115
|
- Use imperative mood
|
|
112
116
|
- Be specific and descriptive
|
|
113
117
|
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
Example output format:
|
|
119
|
+
1. feat(auth): add OAuth2 authentication support
|
|
120
|
+
2. fix(api): resolve timeout in user endpoint
|
|
121
|
+
3. refactor(db): optimize query performance`
|
|
122
|
+
: `Generate commit messages for these changes:
|
|
116
123
|
|
|
117
124
|
${analysisContext}
|
|
118
125
|
|
|
126
|
+
Provide EXACTLY 3 commit message suggestions, one per line, numbered 1-3.
|
|
127
|
+
Do NOT include any other text, explanations, or conversational phrases.
|
|
128
|
+
|
|
119
129
|
Requirements:
|
|
120
|
-
- Use conventional commit format
|
|
130
|
+
- Use conventional commit format: type(scope): description
|
|
121
131
|
- Keep first line under 72 characters
|
|
122
132
|
- Use imperative mood
|
|
123
133
|
- Be specific and descriptive
|
|
124
134
|
|
|
125
|
-
|
|
135
|
+
Example output format:
|
|
136
|
+
1. feat(auth): add OAuth2 authentication support
|
|
137
|
+
2. fix(api): resolve timeout in user endpoint
|
|
138
|
+
3. refactor(db): optimize query performance`
|
|
126
139
|
|
|
127
140
|
const response = await this.aiAnalysisService.aiProvider.generateCompletion(
|
|
128
141
|
[
|
|
@@ -195,6 +208,71 @@ Provide 3 suggestions.`
|
|
|
195
208
|
summary += ` (${details.join(', ')})`
|
|
196
209
|
}
|
|
197
210
|
|
|
211
|
+
// Add actual change summaries from git diff for better context
|
|
212
|
+
summary += '\n\nKey changes:\n'
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Get a concise summary of actual changes, limiting to top 10 files
|
|
216
|
+
const filesToAnalyze = changes.slice(0, 10)
|
|
217
|
+
|
|
218
|
+
filesToAnalyze.forEach((change) => {
|
|
219
|
+
const filePath = change.filePath || change.path
|
|
220
|
+
const status = change.status
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
if (status === 'M' || status === 'MM') {
|
|
224
|
+
// For modified files, get a brief diff stat
|
|
225
|
+
const diffStat = execSync(`git diff HEAD -- "${filePath}" | head -50`, {
|
|
226
|
+
encoding: 'utf8',
|
|
227
|
+
maxBuffer: 1024 * 100,
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
if (diffStat) {
|
|
231
|
+
// Extract some meaningful info from diff
|
|
232
|
+
const addedLines = (diffStat.match(/^\+(?!\+)/gm) || []).length
|
|
233
|
+
const removedLines = (diffStat.match(/^-(?!-)/gm) || []).length
|
|
234
|
+
|
|
235
|
+
// Try to extract function/class names or key changes
|
|
236
|
+
const meaningfulLines = diffStat
|
|
237
|
+
.split('\n')
|
|
238
|
+
.filter(
|
|
239
|
+
(line) =>
|
|
240
|
+
line.startsWith('+') &&
|
|
241
|
+
(line.includes('function') ||
|
|
242
|
+
line.includes('class') ||
|
|
243
|
+
line.includes('const') ||
|
|
244
|
+
line.includes('export') ||
|
|
245
|
+
line.includes('import') ||
|
|
246
|
+
line.includes('async') ||
|
|
247
|
+
line.includes('//'))
|
|
248
|
+
)
|
|
249
|
+
.slice(0, 2)
|
|
250
|
+
.map((line) => line.substring(1).trim())
|
|
251
|
+
|
|
252
|
+
if (meaningfulLines.length > 0) {
|
|
253
|
+
summary += `- ${filePath}: +${addedLines}/-${removedLines} lines (${meaningfulLines[0].substring(0, 60)}...)\n`
|
|
254
|
+
} else {
|
|
255
|
+
summary += `- ${filePath}: +${addedLines}/-${removedLines} lines\n`
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else if (status === 'A' || status === '??') {
|
|
259
|
+
summary += `- ${filePath}: New file\n`
|
|
260
|
+
} else if (status === 'D') {
|
|
261
|
+
summary += `- ${filePath}: Deleted\n`
|
|
262
|
+
}
|
|
263
|
+
} catch (fileError) {
|
|
264
|
+
// Skip files that can't be diffed
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
if (changes.length > 10) {
|
|
269
|
+
summary += `... and ${changes.length - 10} more files\n`
|
|
270
|
+
}
|
|
271
|
+
} catch (error) {
|
|
272
|
+
// If diff analysis fails, fall back to basic summary
|
|
273
|
+
console.warn('Could not analyze diffs:', error.message)
|
|
274
|
+
}
|
|
275
|
+
|
|
198
276
|
return summary
|
|
199
277
|
}
|
|
200
278
|
|
|
@@ -380,17 +458,46 @@ Provide 3 suggestions.`
|
|
|
380
458
|
}
|
|
381
459
|
|
|
382
460
|
parseCommitSuggestions(content) {
|
|
383
|
-
// Parse AI response to extract suggestions
|
|
461
|
+
// Parse AI response to extract valid commit message suggestions
|
|
384
462
|
const lines = content.split('\n').filter((line) => line.trim())
|
|
385
463
|
const suggestions = []
|
|
386
464
|
|
|
465
|
+
// Valid conventional commit types
|
|
466
|
+
const validTypes = [
|
|
467
|
+
'feat',
|
|
468
|
+
'fix',
|
|
469
|
+
'docs',
|
|
470
|
+
'style',
|
|
471
|
+
'refactor',
|
|
472
|
+
'test',
|
|
473
|
+
'chore',
|
|
474
|
+
'perf',
|
|
475
|
+
'ci',
|
|
476
|
+
'build',
|
|
477
|
+
'revert',
|
|
478
|
+
]
|
|
479
|
+
|
|
387
480
|
lines.forEach((line) => {
|
|
388
481
|
// Remove numbering and clean up
|
|
389
|
-
const cleaned = line
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
482
|
+
const cleaned = line.replace(/^\d+\.\s*/, '').replace(/^-\s*/, '').trim()
|
|
483
|
+
|
|
484
|
+
// Must be a reasonable length
|
|
485
|
+
if (!cleaned || cleaned.length < 10 || cleaned.length > 100) {
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Must have a colon (conventional format)
|
|
490
|
+
if (!cleaned.includes(':')) {
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Must start with a valid conventional commit type
|
|
495
|
+
const startsWithValidType = validTypes.some((type) => {
|
|
496
|
+
const pattern = new RegExp(`^${type}(\\(|:)`, 'i')
|
|
497
|
+
return pattern.test(cleaned)
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
if (startsWithValidType) {
|
|
394
501
|
suggestions.push(cleaned)
|
|
395
502
|
}
|
|
396
503
|
})
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* Provides Model Context Protocol interface for changelog generation
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { exec } from 'node:child_process'
|
|
9
|
+
import { promisify } from 'node:util'
|
|
9
10
|
import fs from 'node:fs'
|
|
10
11
|
import path, { dirname } from 'node:path'
|
|
11
12
|
import process from 'node:process'
|
|
@@ -18,8 +19,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|
|
18
19
|
import { ChangelogOrchestrator } from '../../application/orchestrators/changelog.orchestrator.js'
|
|
19
20
|
// Import application services
|
|
20
21
|
import { ApplicationService } from '../../application/services/application.service.js'
|
|
21
|
-
|
|
22
|
-
import { GitService } from '../../domains/git/git.service.js'
|
|
22
|
+
|
|
23
23
|
import { ConfigurationManager } from '../config/configuration.manager.js'
|
|
24
24
|
import { ProviderManagerService } from '../providers/provider-manager.service.js'
|
|
25
25
|
|
|
@@ -312,7 +312,7 @@ class AIChangelogMCPServer {
|
|
|
312
312
|
|
|
313
313
|
let result
|
|
314
314
|
|
|
315
|
-
if (source === 'working-dir' || (source === 'auto' && this.hasWorkingDirectoryChanges())) {
|
|
315
|
+
if (source === 'working-dir' || (source === 'auto' && (await this.hasWorkingDirectoryChanges()))) {
|
|
316
316
|
// Generate from working directory changes using ChangelogService
|
|
317
317
|
// Access the changelogService through the orchestrator
|
|
318
318
|
await this.changelogOrchestrator.ensureInitialized()
|
|
@@ -324,7 +324,7 @@ class AIChangelogMCPServer {
|
|
|
324
324
|
})
|
|
325
325
|
|
|
326
326
|
// Format result to match expected structure
|
|
327
|
-
if (result
|
|
327
|
+
if (result?.changelog) {
|
|
328
328
|
result = {
|
|
329
329
|
content: result.changelog,
|
|
330
330
|
metadata: {
|
|
@@ -522,12 +522,15 @@ class AIChangelogMCPServer {
|
|
|
522
522
|
}
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
-
hasWorkingDirectoryChanges() {
|
|
525
|
+
async hasWorkingDirectoryChanges() {
|
|
526
526
|
try {
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
return
|
|
530
|
-
} catch (
|
|
527
|
+
const execAsync = promisify(exec)
|
|
528
|
+
const { stdout } = await execAsync('git status --porcelain', { encoding: 'utf8' })
|
|
529
|
+
return stdout.trim().length > 0
|
|
530
|
+
} catch (error) {
|
|
531
|
+
// If git is not installed or not a git repo, we can assume no changes for our purpose
|
|
532
|
+
// but we should log the error for debugging
|
|
533
|
+
console.warn(`[MCP] Git check warning: ${error.message}`)
|
|
531
534
|
return false
|
|
532
535
|
}
|
|
533
536
|
}
|
|
@@ -87,7 +87,8 @@ export class BaseProvider {
|
|
|
87
87
|
async selectOptimalModel(commitInfo) {
|
|
88
88
|
try {
|
|
89
89
|
return this.getModelRecommendation(commitInfo)
|
|
90
|
-
} catch (
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.warn(`Warning: Model selection failed: ${error.message}. Using default model.`)
|
|
91
92
|
return { model: this.getDefaultModel(), reason: 'fallback' }
|
|
92
93
|
}
|
|
93
94
|
}
|
|
@@ -14,7 +14,9 @@ function addWarningToCache(key) {
|
|
|
14
14
|
warningCache.clear()
|
|
15
15
|
// Keep last 80% of entries
|
|
16
16
|
const keepCount = Math.floor(MAX_WARNING_CACHE_SIZE * 0.8)
|
|
17
|
-
entries.slice(-keepCount).forEach((entry) =>
|
|
17
|
+
entries.slice(-keepCount).forEach((entry) => {
|
|
18
|
+
warningCache.add(entry)
|
|
19
|
+
})
|
|
18
20
|
}
|
|
19
21
|
warningCache.add(key)
|
|
20
22
|
}
|
|
@@ -483,7 +483,7 @@ export class CommitMessageValidationService {
|
|
|
483
483
|
* Display validation results with colors
|
|
484
484
|
*/
|
|
485
485
|
displayValidationResults(validationResult) {
|
|
486
|
-
const { valid, errors, warnings, suggestions,
|
|
486
|
+
const { valid, errors, warnings, suggestions, summary } = validationResult
|
|
487
487
|
|
|
488
488
|
EnhancedConsole.section('🔍 Commit Message Validation')
|
|
489
489
|
console.log(colors.secondary(`${summary}`))
|