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