@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +383 -785
  2. package/README.md +30 -3
  3. package/ai-changelog-mcp.sh +0 -0
  4. package/ai-changelog.sh +0 -0
  5. package/bin/ai-changelog-dxt.js +9 -9
  6. package/bin/ai-changelog-mcp.js +19 -17
  7. package/bin/ai-changelog.js +6 -6
  8. package/package.json +84 -52
  9. package/src/ai-changelog-generator.js +83 -81
  10. package/src/application/orchestrators/changelog.orchestrator.js +1040 -296
  11. package/src/application/services/application.service.js +145 -123
  12. package/src/cli.js +76 -57
  13. package/src/domains/ai/ai-analysis.service.js +289 -209
  14. package/src/domains/analysis/analysis.engine.js +253 -193
  15. package/src/domains/changelog/changelog.service.js +1062 -784
  16. package/src/domains/changelog/workspace-changelog.service.js +420 -249
  17. package/src/domains/git/git-repository.analyzer.js +348 -258
  18. package/src/domains/git/git.service.js +132 -112
  19. package/src/infrastructure/cli/cli.controller.js +415 -247
  20. package/src/infrastructure/config/configuration.manager.js +220 -190
  21. package/src/infrastructure/interactive/interactive-staging.service.js +332 -0
  22. package/src/infrastructure/interactive/interactive-workflow.service.js +200 -159
  23. package/src/infrastructure/mcp/mcp-server.service.js +208 -207
  24. package/src/infrastructure/metrics/metrics.collector.js +140 -123
  25. package/src/infrastructure/providers/core/base-provider.js +87 -40
  26. package/src/infrastructure/providers/implementations/anthropic.js +101 -99
  27. package/src/infrastructure/providers/implementations/azure.js +124 -101
  28. package/src/infrastructure/providers/implementations/bedrock.js +136 -126
  29. package/src/infrastructure/providers/implementations/dummy.js +23 -23
  30. package/src/infrastructure/providers/implementations/google.js +123 -114
  31. package/src/infrastructure/providers/implementations/huggingface.js +94 -87
  32. package/src/infrastructure/providers/implementations/lmstudio.js +75 -60
  33. package/src/infrastructure/providers/implementations/mock.js +69 -73
  34. package/src/infrastructure/providers/implementations/ollama.js +89 -66
  35. package/src/infrastructure/providers/implementations/openai.js +88 -89
  36. package/src/infrastructure/providers/implementations/vertex.js +227 -197
  37. package/src/infrastructure/providers/provider-management.service.js +245 -207
  38. package/src/infrastructure/providers/provider-manager.service.js +145 -125
  39. package/src/infrastructure/providers/utils/base-provider-helpers.js +308 -302
  40. package/src/infrastructure/providers/utils/model-config.js +220 -195
  41. package/src/infrastructure/providers/utils/provider-utils.js +105 -100
  42. package/src/infrastructure/validation/commit-message-validation.service.js +556 -0
  43. package/src/shared/constants/colors.js +467 -172
  44. package/src/shared/utils/cli-demo.js +285 -0
  45. package/src/shared/utils/cli-entry-utils.js +257 -249
  46. package/src/shared/utils/cli-ui.js +447 -0
  47. package/src/shared/utils/diff-processor.js +513 -0
  48. package/src/shared/utils/error-classes.js +125 -156
  49. package/src/shared/utils/json-utils.js +93 -89
  50. package/src/shared/utils/utils.js +1299 -775
  51. package/types/index.d.ts +353 -344
@@ -1,16 +1,20 @@
1
- import { GitService } from '../../domains/git/git.service.js';
2
- import { AIAnalysisService } from '../../domains/ai/ai-analysis.service.js';
3
- import { ChangelogService } from '../../domains/changelog/changelog.service.js';
4
- import { AnalysisEngine } from '../../domains/analysis/analysis.engine.js';
5
- import { ProviderManagerService } from '../../infrastructure/providers/provider-manager.service.js';
6
- import { InteractiveWorkflowService } from '../../infrastructure/interactive/interactive-workflow.service.js';
7
- import colors from '../../shared/constants/colors.js';
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
- this.initialized = false;
26
- this.initializationPromise = null;
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(this.aiProvider, this.promptEngine, this.tagger, this.analysisMode);
54
- this.analysisEngine = new AnalysisEngine(this.gitService, this.aiAnalysisService);
55
- this.changelogService = new ChangelogService(this.gitService, this.aiAnalysisService, this.analysisEngine, this.configManager);
56
- this.interactiveService = new InteractiveWorkflowService(this.gitService, this.aiAnalysisService, this.changelogService);
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
- console.error(colors.errorMessage('Failed to initialize services:'), error.message);
66
- throw error;
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 import('child_process');
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
- throw new Error(`Git command failed: ${error.message}`);
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 (error) {
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.split('\n').filter(Boolean).map(branch => branch.trim().replace(/^\*\s*/, ''));
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', { encoding: 'utf8' });
130
- return output.split('\n').filter(Boolean);
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"', { encoding: 'utf8' });
139
- return output.split('\n').filter(Boolean);
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('../../shared/utils/utils.js');
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: "You are an expert software analyst specializing in code change analysis and changelog generation.",
247
- standard: "Provide clear, concise analysis focusing on the practical impact of changes.",
248
- detailed: "Provide comprehensive technical analysis with detailed explanations and implications.",
249
- enterprise: "Provide enterprise-grade analysis suitable for stakeholder communication and decision-making.",
250
- changesAnalysis: "You are an expert at analyzing code changes and their business impact."
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, capabilities = {}) {
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
- } else if (providerName?.toLowerCase().includes('gpt')) {
258
- return `${prompt}\n\nPlease respond in JSON format as requested.`;
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, analysisMode) {
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('\n' + colors.processingMessage('🚀 Starting changelog generation...'));
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('../../shared/utils/utils.js');
329
- const { confirm } = await import('@clack/prompts');
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(colors.processingMessage(`📝 Generating changelog for ${commitCount} recent commits...`));
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(colors.processingMessage(`📝 Generating changelog for ${selectedCommits.length} selected commits...`));
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(colors.processingMessage('🤖 Analyzing current changes for commit message suggestions...'));
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 import('@clack/prompts');
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 import('@clack/prompts');
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 import('@clack/prompts');
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(colors.infoMessage('Please edit your .env.local file to add the required API keys.'));
525
- console.log(colors.highlight(`Example for ${selectedProvider.toUpperCase()}:`));
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(colors.processingMessage('📝 Generating changelog from working directory changes...'));
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(colors.errorMessage('Working directory changelog generation failed:'), error.message);
564
- throw error;
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, version) {
581
- const { changelog, insights, analyzedCommits } = result;
668
+ displayResults(result, _version) {
669
+ const { changelog, insights, analyzedCommits } = result
582
670
 
583
- console.log('\n' + colors.successMessage('✅ Changelog Generation Complete'));
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': return colors.success;
618
- case 'medium': return colors.warning;
619
- case 'high': return colors.error;
620
- default: return colors.highlight;
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': return colors.riskLow;
628
- case 'medium': return colors.riskMedium;
629
- case 'high': return colors.riskHigh;
630
- case 'critical': return colors.riskCritical;
631
- default: return colors.highlight;
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(colors.successMessage(`\n✅ ${type.charAt(0).toUpperCase() + type.slice(1)} Analysis Complete`));
637
- console.log(colors.separator());
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(colors.warning(`⚠️ Rule-based fallbacks: ${this.metrics.ruleBasedFallbacks}`));
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) return `${ms}ms`;
688
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
689
- return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
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
+ }