@m3hti/commit-genie 1.0.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +430 -0
  3. package/dist/commands/config.d.ts +3 -0
  4. package/dist/commands/config.d.ts.map +1 -0
  5. package/dist/commands/config.js +31 -0
  6. package/dist/commands/config.js.map +1 -0
  7. package/dist/commands/generate.d.ts +10 -0
  8. package/dist/commands/generate.d.ts.map +1 -0
  9. package/dist/commands/generate.js +313 -0
  10. package/dist/commands/generate.js.map +1 -0
  11. package/dist/commands/generate.test.d.ts +2 -0
  12. package/dist/commands/generate.test.d.ts.map +1 -0
  13. package/dist/commands/generate.test.js +168 -0
  14. package/dist/commands/generate.test.js.map +1 -0
  15. package/dist/commands/hook.d.ts +4 -0
  16. package/dist/commands/hook.d.ts.map +1 -0
  17. package/dist/commands/hook.js +62 -0
  18. package/dist/commands/hook.js.map +1 -0
  19. package/dist/commands/stats.d.ts +6 -0
  20. package/dist/commands/stats.d.ts.map +1 -0
  21. package/dist/commands/stats.js +39 -0
  22. package/dist/commands/stats.js.map +1 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +77 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/services/aiService.d.ts +38 -0
  28. package/dist/services/aiService.d.ts.map +1 -0
  29. package/dist/services/aiService.js +187 -0
  30. package/dist/services/aiService.js.map +1 -0
  31. package/dist/services/analyzerService.d.ts +60 -0
  32. package/dist/services/analyzerService.d.ts.map +1 -0
  33. package/dist/services/analyzerService.js +832 -0
  34. package/dist/services/analyzerService.js.map +1 -0
  35. package/dist/services/analyzerService.test.d.ts +2 -0
  36. package/dist/services/analyzerService.test.d.ts.map +1 -0
  37. package/dist/services/analyzerService.test.js +323 -0
  38. package/dist/services/analyzerService.test.js.map +1 -0
  39. package/dist/services/configService.d.ts +25 -0
  40. package/dist/services/configService.d.ts.map +1 -0
  41. package/dist/services/configService.js +207 -0
  42. package/dist/services/configService.js.map +1 -0
  43. package/dist/services/configService.test.d.ts +2 -0
  44. package/dist/services/configService.test.d.ts.map +1 -0
  45. package/dist/services/configService.test.js +165 -0
  46. package/dist/services/configService.test.js.map +1 -0
  47. package/dist/services/gitService.d.ts +44 -0
  48. package/dist/services/gitService.d.ts.map +1 -0
  49. package/dist/services/gitService.js +217 -0
  50. package/dist/services/gitService.js.map +1 -0
  51. package/dist/services/gitService.test.d.ts +2 -0
  52. package/dist/services/gitService.test.d.ts.map +1 -0
  53. package/dist/services/gitService.test.js +140 -0
  54. package/dist/services/gitService.test.js.map +1 -0
  55. package/dist/services/historyService.d.ts +39 -0
  56. package/dist/services/historyService.d.ts.map +1 -0
  57. package/dist/services/historyService.js +195 -0
  58. package/dist/services/historyService.js.map +1 -0
  59. package/dist/services/historyService.test.d.ts +2 -0
  60. package/dist/services/historyService.test.d.ts.map +1 -0
  61. package/dist/services/historyService.test.js +157 -0
  62. package/dist/services/historyService.test.js.map +1 -0
  63. package/dist/services/hookService.d.ts +29 -0
  64. package/dist/services/hookService.d.ts.map +1 -0
  65. package/dist/services/hookService.js +164 -0
  66. package/dist/services/hookService.js.map +1 -0
  67. package/dist/services/statsService.d.ts +28 -0
  68. package/dist/services/statsService.d.ts.map +1 -0
  69. package/dist/services/statsService.js +204 -0
  70. package/dist/services/statsService.js.map +1 -0
  71. package/dist/types/index.d.ts +134 -0
  72. package/dist/types/index.d.ts.map +1 -0
  73. package/dist/types/index.js +3 -0
  74. package/dist/types/index.js.map +1 -0
  75. package/dist/utils/filePatterns.d.ts +5 -0
  76. package/dist/utils/filePatterns.d.ts.map +1 -0
  77. package/dist/utils/filePatterns.js +77 -0
  78. package/dist/utils/filePatterns.js.map +1 -0
  79. package/dist/utils/filePatterns.test.d.ts +2 -0
  80. package/dist/utils/filePatterns.test.d.ts.map +1 -0
  81. package/dist/utils/filePatterns.test.js +51 -0
  82. package/dist/utils/filePatterns.test.js.map +1 -0
  83. package/dist/utils/prompt.d.ts +4 -0
  84. package/dist/utils/prompt.d.ts.map +1 -0
  85. package/dist/utils/prompt.js +60 -0
  86. package/dist/utils/prompt.js.map +1 -0
  87. package/package.json +47 -0
@@ -0,0 +1,832 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AnalyzerService = void 0;
4
+ const gitService_1 = require("./gitService");
5
+ const configService_1 = require("./configService");
6
+ const historyService_1 = require("./historyService");
7
+ const aiService_1 = require("./aiService");
8
+ const filePatterns_1 = require("../utils/filePatterns");
9
+ const COMMIT_EMOJIS = {
10
+ feat: '✨',
11
+ fix: '🐛',
12
+ docs: '📚',
13
+ style: '💄',
14
+ refactor: '♻️',
15
+ test: '🧪',
16
+ chore: '🔧',
17
+ perf: '⚡',
18
+ };
19
+ // Default keywords that indicate breaking changes
20
+ const DEFAULT_BREAKING_KEYWORDS = [
21
+ 'breaking',
22
+ 'breaking change',
23
+ 'breaking-change',
24
+ 'removed',
25
+ 'deprecated',
26
+ 'incompatible',
27
+ ];
28
+ // Patterns that indicate breaking changes in code
29
+ const BREAKING_PATTERNS = [
30
+ // Removed exports
31
+ /^-\s*export\s+(function|class|const|let|var|interface|type|enum)\s+(\w+)/gm,
32
+ // Removed function parameters
33
+ /^-\s*(public|private|protected)?\s*(async\s+)?function\s+\w+\s*\([^)]+\)/gm,
34
+ // Changed function signatures (removed parameters)
35
+ /^-\s*\w+\s*\([^)]+\)\s*[:{]/gm,
36
+ // Removed class methods
37
+ /^-\s*(public|private|protected)\s+(static\s+)?(async\s+)?\w+\s*\(/gm,
38
+ // Removed interface/type properties
39
+ /^-\s+\w+\s*[?:]?\s*:/gm,
40
+ // Major version bump in package.json
41
+ /^-\s*"version":\s*"\d+/gm,
42
+ ];
43
+ class AnalyzerService {
44
+ /**
45
+ * Analyze staged changes and return structured analysis
46
+ */
47
+ static analyzeChanges() {
48
+ const stagedFiles = gitService_1.GitService.getStagedFiles();
49
+ const diff = gitService_1.GitService.getDiff();
50
+ const stats = gitService_1.GitService.getDiffStats();
51
+ const filesAffected = {
52
+ test: 0,
53
+ docs: 0,
54
+ config: 0,
55
+ source: 0,
56
+ };
57
+ const fileChanges = {
58
+ added: [],
59
+ modified: [],
60
+ deleted: [],
61
+ renamed: [],
62
+ };
63
+ // Analyze file types and statuses
64
+ for (const file of stagedFiles) {
65
+ const fileType = (0, filePatterns_1.detectFileType)(file.path);
66
+ filesAffected[fileType]++;
67
+ const fileName = this.getFileName(file.path);
68
+ switch (file.status) {
69
+ case 'A':
70
+ fileChanges.added.push(fileName);
71
+ break;
72
+ case 'M':
73
+ fileChanges.modified.push(fileName);
74
+ break;
75
+ case 'D':
76
+ fileChanges.deleted.push(fileName);
77
+ break;
78
+ case 'R':
79
+ fileChanges.renamed.push(fileName);
80
+ break;
81
+ }
82
+ }
83
+ // Determine if this is a large change (3+ files or 100+ lines changed)
84
+ const totalChanges = stats.insertions + stats.deletions;
85
+ const isLargeChange = stagedFiles.length >= 3 || totalChanges >= 100;
86
+ // Determine commit type based on file types and changes
87
+ const commitType = this.determineCommitType(filesAffected, diff, stagedFiles);
88
+ // Generate description
89
+ const description = this.generateDescription(filesAffected, {
90
+ added: fileChanges.added.length,
91
+ modified: fileChanges.modified.length,
92
+ deleted: fileChanges.deleted.length,
93
+ renamed: fileChanges.renamed.length
94
+ }, stagedFiles, diff);
95
+ // Determine scope if applicable
96
+ const scope = this.determineScope(stagedFiles);
97
+ // Detect breaking changes
98
+ const { isBreaking, reasons } = this.detectBreakingChanges(diff, stagedFiles);
99
+ return {
100
+ commitType,
101
+ scope,
102
+ description,
103
+ filesAffected,
104
+ fileChanges,
105
+ isLargeChange,
106
+ isBreakingChange: isBreaking,
107
+ breakingChangeReasons: reasons,
108
+ };
109
+ }
110
+ /**
111
+ * Detect breaking changes from diff content and file changes
112
+ */
113
+ static detectBreakingChanges(diff, stagedFiles) {
114
+ const config = configService_1.ConfigService.getConfig();
115
+ const breakingConfig = config.breakingChangeDetection;
116
+ // Check if breaking change detection is disabled
117
+ if (breakingConfig?.enabled === false) {
118
+ return { isBreaking: false, reasons: [] };
119
+ }
120
+ const reasons = [];
121
+ const diffLower = diff.toLowerCase();
122
+ // Check for keyword-based breaking changes
123
+ const keywords = breakingConfig?.keywords || DEFAULT_BREAKING_KEYWORDS;
124
+ for (const keyword of keywords) {
125
+ if (diffLower.includes(keyword.toLowerCase())) {
126
+ reasons.push(`Contains "${keyword}" keyword`);
127
+ }
128
+ }
129
+ // Check for deleted source files (potentially breaking)
130
+ const deletedSourceFiles = stagedFiles.filter(f => f.status === 'D' && (0, filePatterns_1.detectFileType)(f.path) === 'source');
131
+ if (deletedSourceFiles.length > 0) {
132
+ const fileNames = deletedSourceFiles.map(f => this.getFileName(f.path)).join(', ');
133
+ reasons.push(`Deleted source files: ${fileNames}`);
134
+ }
135
+ // Check for pattern-based breaking changes in diff
136
+ for (const pattern of BREAKING_PATTERNS) {
137
+ pattern.lastIndex = 0; // Reset regex state
138
+ const matches = diff.match(pattern);
139
+ if (matches && matches.length > 0) {
140
+ // Identify what type of breaking change
141
+ if (pattern.source.includes('export')) {
142
+ reasons.push('Removed exported members');
143
+ }
144
+ else if (pattern.source.includes('function')) {
145
+ reasons.push('Changed function signatures');
146
+ }
147
+ else if (pattern.source.includes('public|private|protected')) {
148
+ reasons.push('Removed class methods');
149
+ }
150
+ else if (pattern.source.includes('version')) {
151
+ reasons.push('Major version change detected');
152
+ }
153
+ break; // Only add one pattern-based reason
154
+ }
155
+ }
156
+ // Check for renamed files that might break imports
157
+ const renamedFiles = stagedFiles.filter(f => f.status === 'R');
158
+ if (renamedFiles.length > 0) {
159
+ const sourceRenames = renamedFiles.filter(f => (0, filePatterns_1.detectFileType)(f.path) === 'source');
160
+ if (sourceRenames.length > 0) {
161
+ reasons.push('Renamed source files (may break imports)');
162
+ }
163
+ }
164
+ // Deduplicate reasons
165
+ const uniqueReasons = [...new Set(reasons)];
166
+ return {
167
+ isBreaking: uniqueReasons.length > 0,
168
+ reasons: uniqueReasons,
169
+ };
170
+ }
171
+ /**
172
+ * Determine the commit type based on analysis
173
+ */
174
+ static determineCommitType(filesAffected, diff, stagedFiles) {
175
+ const diffLower = diff.toLowerCase();
176
+ const filePaths = stagedFiles.map((f) => f.path.toLowerCase());
177
+ // === FILE TYPE BASED DETECTION (highest priority) ===
178
+ // If only test files changed
179
+ if (filesAffected.test > 0 &&
180
+ filesAffected.source === 0 &&
181
+ filesAffected.docs === 0) {
182
+ return 'test';
183
+ }
184
+ // If only docs changed
185
+ if (filesAffected.docs > 0 &&
186
+ filesAffected.source === 0 &&
187
+ filesAffected.test === 0) {
188
+ return 'docs';
189
+ }
190
+ // If only config files changed
191
+ if (filesAffected.config > 0 &&
192
+ filesAffected.source === 0 &&
193
+ filesAffected.test === 0 &&
194
+ filesAffected.docs === 0) {
195
+ return 'chore';
196
+ }
197
+ // === STYLE DETECTION ===
198
+ // Check for style/formatting files
199
+ const isStyleChange = filePaths.some(p => p.endsWith('.css') ||
200
+ p.endsWith('.scss') ||
201
+ p.endsWith('.sass') ||
202
+ p.endsWith('.less') ||
203
+ p.endsWith('.styl') ||
204
+ p.includes('.style') ||
205
+ p.includes('styles/'));
206
+ // Check for formatting-only changes (whitespace, semicolons, quotes)
207
+ const formattingPatterns = [
208
+ /^[+-]\s*$/gm, // Only whitespace changes
209
+ /^[+-]\s*['"`];?\s*$/gm, // Quote changes
210
+ /^[+-].*;\s*$/gm, // Semicolon additions/removals
211
+ ];
212
+ const isFormattingChange = formattingPatterns.some(p => p.test(diff));
213
+ if (isStyleChange || (isFormattingChange && !this.hasLogicChanges(diff))) {
214
+ return 'style';
215
+ }
216
+ // === PERFORMANCE DETECTION ===
217
+ const perfPatterns = [
218
+ /\bperformance\b/i,
219
+ /\boptimiz(e|ation|ing)\b/i,
220
+ /\bfaster\b/i,
221
+ /\bspeed\s*(up|improvement)\b/i,
222
+ /\bcach(e|ing)\b/i,
223
+ /\bmemoiz(e|ation)\b/i,
224
+ /\blazy\s*load/i,
225
+ /\basync\b.*\bawait\b/i,
226
+ /\bparallel\b/i,
227
+ /\bbatch(ing)?\b/i,
228
+ ];
229
+ if (perfPatterns.some(p => p.test(diffLower))) {
230
+ return 'perf';
231
+ }
232
+ // === FIX DETECTION ===
233
+ // Check if this is adding validation/guards to existing code (defensive coding = fix)
234
+ const hasOnlyModifications = stagedFiles.every((f) => f.status === 'M');
235
+ const validationPatterns = [
236
+ /^\+.*\btypeof\s+\w+\s*===?\s*['"`]\w+['"`]/m, // typeof checks
237
+ /^\+.*\binstanceof\s+\w+/m, // instanceof checks
238
+ /^\+.*\bArray\.isArray\s*\(/m, // Array.isArray checks
239
+ /^\+.*\b(Number|String|Boolean)\.is\w+\s*\(/m, // Number.isNaN, etc.
240
+ /^\+.*\bif\s*\(\s*typeof\b/m, // if (typeof ...
241
+ /^\+.*\bif\s*\(\s*!\w+\s*\)/m, // if (!var) guards
242
+ /^\+.*\bif\s*\(\s*\w+\s*(===?|!==?)\s*(null|undefined)\s*\)/m, // null/undefined checks
243
+ /^\+.*\bif\s*\(\s*(null|undefined)\s*(===?|!==?)\s*\w+\s*\)/m, // null/undefined checks (reversed)
244
+ /^\+.*\?\?/m, // Nullish coalescing
245
+ /^\+.*\?\./m, // Optional chaining
246
+ /^\+.*\|\|/m, // Default value patterns (when combined with guards)
247
+ ];
248
+ // If only modifying files and adding validation patterns, it's likely a fix
249
+ if (hasOnlyModifications && validationPatterns.some(p => p.test(diff))) {
250
+ return 'fix';
251
+ }
252
+ const fixPatterns = [
253
+ /\bfix(es|ed|ing)?\s*(the\s*)?(bug|issue|error|problem|crash)/i,
254
+ /\bfix(es|ed|ing)?\b/i, // Simple "fix" or "fixed" alone
255
+ /\bbug\s*fix/i,
256
+ /\bBUG:/i, // Bug comment markers
257
+ /\bhotfix\b/i,
258
+ /\bpatch(es|ed|ing)?\b/i,
259
+ /\bresolv(e|es|ed|ing)\s*(the\s*)?(issue|bug|error)/i,
260
+ /\bcorrect(s|ed|ing)?\s*(the\s*)?(bug|issue|error|problem)/i,
261
+ /\brepair(s|ed|ing)?\b/i,
262
+ /\bhandle\s*(error|exception|null|undefined)/i,
263
+ /\bnull\s*check/i,
264
+ /\bundefined\s*check/i,
265
+ /\btry\s*{\s*.*\s*}\s*catch/i,
266
+ /\bif\s*\(\s*!\s*\w+\s*\)/, // Null/undefined guards
267
+ /\bwas\s*broken\b/i, // "was broken" indicates fixing
268
+ /\bbroken\b.*\bfix/i, // broken...fix pattern
269
+ ];
270
+ if (fixPatterns.some(p => p.test(diff))) {
271
+ return 'fix';
272
+ }
273
+ // === DETECT NEW FUNCTIONALITY ===
274
+ const hasNewFiles = stagedFiles.some((f) => f.status === 'A');
275
+ // Comprehensive new code detection
276
+ const hasNewExports = /^\+\s*export\s+(function|class|const|let|var|interface|type|default)/m.test(diff);
277
+ const hasNewFunctions = /^\+\s*(async\s+)?function\s+\w+/m.test(diff);
278
+ const hasNewArrowFunctions = /^\+\s*(export\s+)?(const|let|var)\s+\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>/m.test(diff);
279
+ const hasNewClasses = /^\+\s*(export\s+)?(class|abstract\s+class)\s+\w+/m.test(diff);
280
+ const hasNewMethods = /^\+\s+(async\s+)?\w+\s*\([^)]*\)\s*{/m.test(diff);
281
+ const hasNewInterfaces = /^\+\s*(export\s+)?(interface|type)\s+\w+/m.test(diff);
282
+ const hasNewComponents = /^\+\s*(export\s+)?(const|function)\s+[A-Z]\w+/m.test(diff); // React components start with capital
283
+ const hasNewImports = /^\+\s*import\s+/m.test(diff);
284
+ // Count significant additions vs removals
285
+ const addedLines = (diff.match(/^\+[^+]/gm) || []).length;
286
+ const removedLines = (diff.match(/^-[^-]/gm) || []).length;
287
+ const netNewLines = addedLines - removedLines;
288
+ // Detect if we're adding new functionality
289
+ const hasNewFunctionality = hasNewExports || hasNewFunctions || hasNewArrowFunctions ||
290
+ hasNewClasses || hasNewMethods || hasNewInterfaces || hasNewComponents;
291
+ // === FEAT DETECTION (new functionality) - CHECK EARLY ===
292
+ if (hasNewFiles) {
293
+ return 'feat';
294
+ }
295
+ // New functions, classes, components = feat
296
+ if (hasNewFunctionality) {
297
+ return 'feat';
298
+ }
299
+ // Significant net additions with new imports often means new feature
300
+ if (hasNewImports && netNewLines > 5) {
301
+ return 'feat';
302
+ }
303
+ // Check for new feature indicators in comments/strings
304
+ const featPatterns = [
305
+ /\badd(s|ed|ing)?\s+(new\s+)?(feature|support|ability|option|functionality)/i,
306
+ /\bimplement(s|ed|ing)?\s+\w+/i,
307
+ /\bintroduc(e|es|ed|ing)\s+(new\s+)?\w+/i,
308
+ /\benable(s|d|ing)?\s+(new\s+)?\w+/i,
309
+ /\bnew\s+(feature|function|method|api|endpoint)/i,
310
+ ];
311
+ if (featPatterns.some(p => p.test(diff))) {
312
+ return 'feat';
313
+ }
314
+ // === REFACTOR DETECTION ===
315
+ const refactorPatterns = [
316
+ /\brefactor(s|ed|ing)?\b/i,
317
+ /\brestructur(e|es|ed|ing)\b/i,
318
+ /\bclean\s*up\b/i,
319
+ /\bsimplif(y|ies|ied|ying)\b/i,
320
+ /\brenam(e|es|ed|ing)\b/i,
321
+ /\bmov(e|es|ed|ing)\s*(to|from|into)\b/i,
322
+ /\bextract(s|ed|ing)?\s*(function|method|class|component)/i,
323
+ /\binline(s|d|ing)?\b/i,
324
+ /\bdedup(licate)?\b/i,
325
+ /\bDRY\b/,
326
+ /\breorganiz(e|es|ed|ing)\b/i,
327
+ ];
328
+ if (refactorPatterns.some(p => p.test(diff))) {
329
+ return 'refactor';
330
+ }
331
+ // If modifications with balanced adds/removes and no new functionality, likely refactor
332
+ if (hasOnlyModifications && !hasNewFunctionality) {
333
+ // If roughly equal adds and removes, it's likely refactoring
334
+ if (addedLines > 0 && removedLines > 0) {
335
+ const ratio = Math.min(addedLines, removedLines) / Math.max(addedLines, removedLines);
336
+ if (ratio > 0.5) { // More strict ratio - must be very balanced
337
+ return 'refactor';
338
+ }
339
+ }
340
+ }
341
+ // === CHORE DETECTION ===
342
+ const chorePatterns = [
343
+ /\bdependenc(y|ies)\b/i,
344
+ /\bupgrade\b/i,
345
+ /\bupdate\s*(version|dep)/i,
346
+ /\bbump\b/i,
347
+ /\bpackage\.json\b/i,
348
+ /\bpackage-lock\.json\b/i,
349
+ /\byarn\.lock\b/i,
350
+ /\b\.gitignore\b/i,
351
+ /\bci\b.*\b(config|setup)\b/i,
352
+ /\blint(er|ing)?\b/i,
353
+ ];
354
+ if (chorePatterns.some(p => p.test(diff)) || chorePatterns.some(p => filePaths.some(f => p.test(f)))) {
355
+ return 'chore';
356
+ }
357
+ // === FALLBACK ===
358
+ // If we have more additions than removals, lean towards feat
359
+ if (filesAffected.source > 0) {
360
+ if (netNewLines > 10) {
361
+ return 'feat'; // Significant new code added
362
+ }
363
+ if (hasOnlyModifications) {
364
+ return 'refactor'; // Modifications without clear new functionality
365
+ }
366
+ return 'feat'; // Default for source changes with new files
367
+ }
368
+ return 'chore';
369
+ }
370
+ /**
371
+ * Check if diff contains actual logic changes (not just formatting)
372
+ */
373
+ static hasLogicChanges(diff) {
374
+ // Remove formatting-only changes and check if there's real code
375
+ const lines = diff.split('\n').filter(line => (line.startsWith('+') || line.startsWith('-')) &&
376
+ !line.startsWith('+++') &&
377
+ !line.startsWith('---'));
378
+ for (const line of lines) {
379
+ const content = line.substring(1).trim();
380
+ // Skip empty lines, comments, and whitespace-only
381
+ if (content.length === 0 ||
382
+ content.startsWith('//') ||
383
+ content.startsWith('/*') ||
384
+ content.startsWith('*') ||
385
+ content === '{' ||
386
+ content === '}' ||
387
+ content === ';') {
388
+ continue;
389
+ }
390
+ // Has actual code change
391
+ return true;
392
+ }
393
+ return false;
394
+ }
395
+ /**
396
+ * Generate a descriptive commit message
397
+ */
398
+ static generateDescription(filesAffected, fileStatuses, stagedFiles, diff) {
399
+ // Single file changes
400
+ if (stagedFiles.length === 1) {
401
+ const file = stagedFiles[0];
402
+ const fileName = this.getFileName(file.path);
403
+ if (file.status === 'A') {
404
+ return `add ${fileName}`;
405
+ }
406
+ else if (file.status === 'D') {
407
+ return `remove ${fileName}`;
408
+ }
409
+ else if (file.status === 'M') {
410
+ return `update ${fileName}`;
411
+ }
412
+ else if (file.status === 'R') {
413
+ return `rename ${fileName}`;
414
+ }
415
+ }
416
+ // Multiple files of the same type
417
+ if (filesAffected.test > 0 && filesAffected.source === 0) {
418
+ return `update test files`;
419
+ }
420
+ if (filesAffected.docs > 0 && filesAffected.source === 0) {
421
+ return `update documentation`;
422
+ }
423
+ if (filesAffected.config > 0 && filesAffected.source === 0) {
424
+ return `update configuration`;
425
+ }
426
+ // Mixed changes - try to be descriptive
427
+ const parts = [];
428
+ if (fileStatuses.added > 0) {
429
+ parts.push(`add ${fileStatuses.added} file${fileStatuses.added > 1 ? 's' : ''}`);
430
+ }
431
+ if (fileStatuses.modified > 0) {
432
+ if (parts.length === 0) {
433
+ parts.push(`update ${fileStatuses.modified} file${fileStatuses.modified > 1 ? 's' : ''}`);
434
+ }
435
+ }
436
+ if (fileStatuses.deleted > 0) {
437
+ parts.push(`remove ${fileStatuses.deleted} file${fileStatuses.deleted > 1 ? 's' : ''}`);
438
+ }
439
+ if (parts.length > 0) {
440
+ return parts.join(' and ');
441
+ }
442
+ // Fallback
443
+ return `update ${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''}`;
444
+ }
445
+ /**
446
+ * Determine scope from file paths
447
+ */
448
+ static determineScope(stagedFiles) {
449
+ if (stagedFiles.length === 0)
450
+ return undefined;
451
+ const config = configService_1.ConfigService.getConfig();
452
+ const paths = stagedFiles.map((f) => f.path);
453
+ // Check config-based scope mappings first
454
+ if (config.scopes && config.scopes.length > 0) {
455
+ for (const mapping of config.scopes) {
456
+ const matchingFiles = paths.filter((p) => p.includes(mapping.pattern));
457
+ if (matchingFiles.length === paths.length) {
458
+ return mapping.scope;
459
+ }
460
+ }
461
+ // If most files match a pattern, use that scope
462
+ for (const mapping of config.scopes) {
463
+ const matchingFiles = paths.filter((p) => p.includes(mapping.pattern));
464
+ if (matchingFiles.length > paths.length / 2) {
465
+ return mapping.scope;
466
+ }
467
+ }
468
+ }
469
+ // Fallback to default heuristic
470
+ const firstPath = paths[0];
471
+ const parts = firstPath.split('/');
472
+ if (parts.length > 1) {
473
+ const potentialScope = parts[0];
474
+ // Common scope names to look for
475
+ const validScopes = [
476
+ 'api',
477
+ 'ui',
478
+ 'auth',
479
+ 'db',
480
+ 'core',
481
+ 'utils',
482
+ 'components',
483
+ 'services',
484
+ ];
485
+ if (validScopes.includes(potentialScope.toLowerCase())) {
486
+ return potentialScope;
487
+ }
488
+ }
489
+ return undefined;
490
+ }
491
+ /**
492
+ * Extract file name from path
493
+ */
494
+ static getFileName(path) {
495
+ const parts = path.split('/');
496
+ return parts[parts.length - 1];
497
+ }
498
+ /**
499
+ * Generate commit body for larger changes
500
+ */
501
+ static generateBody(analysis) {
502
+ if (!analysis.isLargeChange) {
503
+ return undefined;
504
+ }
505
+ const lines = [];
506
+ if (analysis.fileChanges.added.length > 0) {
507
+ lines.push(`- Add ${analysis.fileChanges.added.join(', ')}`);
508
+ }
509
+ if (analysis.fileChanges.modified.length > 0) {
510
+ const files = analysis.fileChanges.modified.slice(0, 5);
511
+ const suffix = analysis.fileChanges.modified.length > 5
512
+ ? ` and ${analysis.fileChanges.modified.length - 5} more`
513
+ : '';
514
+ lines.push(`- Update ${files.join(', ')}${suffix}`);
515
+ }
516
+ if (analysis.fileChanges.deleted.length > 0) {
517
+ lines.push(`- Remove ${analysis.fileChanges.deleted.join(', ')}`);
518
+ }
519
+ if (analysis.fileChanges.renamed.length > 0) {
520
+ lines.push(`- Rename ${analysis.fileChanges.renamed.join(', ')}`);
521
+ }
522
+ return lines.length > 0 ? lines.join('\n') : undefined;
523
+ }
524
+ /**
525
+ * Apply a template to build the commit message subject line
526
+ */
527
+ static applyTemplate(template, type, scope, description, includeEmoji, isBreaking) {
528
+ const emoji = includeEmoji ? COMMIT_EMOJIS[type] : '';
529
+ const breakingIndicator = isBreaking ? '!' : '';
530
+ let result = template
531
+ .replace('{emoji}', emoji)
532
+ .replace('{type}', type + breakingIndicator)
533
+ .replace('{description}', description);
534
+ // Handle scope - if no scope, use noScope template or remove scope placeholder
535
+ if (scope) {
536
+ result = result.replace('{scope}', scope);
537
+ }
538
+ else {
539
+ // Remove scope and parentheses if no scope
540
+ result = result.replace('({scope})', '').replace('{scope}', '');
541
+ }
542
+ // Clean up extra spaces
543
+ result = result.replace(/\s+/g, ' ').trim();
544
+ return result;
545
+ }
546
+ /**
547
+ * Build full commit message string
548
+ */
549
+ static buildFullMessage(type, scope, description, body, includeEmoji, ticketInfo, isBreaking, breakingReasons) {
550
+ const config = configService_1.ConfigService.getConfig();
551
+ const includeBreakingFooter = config.breakingChangeDetection?.includeFooter !== false;
552
+ const templates = config.templates;
553
+ let full = '';
554
+ // Use template if available
555
+ if (templates) {
556
+ const template = scope
557
+ ? (templates.default || '{emoji} {type}({scope}): {description}')
558
+ : (templates.noScope || '{emoji} {type}: {description}');
559
+ full = this.applyTemplate(template, type, scope, description, includeEmoji, isBreaking);
560
+ }
561
+ else {
562
+ // Fallback to original logic
563
+ if (includeEmoji) {
564
+ full += `${COMMIT_EMOJIS[type]} `;
565
+ }
566
+ full += type;
567
+ if (scope) {
568
+ full += `(${scope})`;
569
+ }
570
+ // Add breaking change indicator
571
+ if (isBreaking) {
572
+ full += '!';
573
+ }
574
+ full += `: ${description}`;
575
+ }
576
+ if (body) {
577
+ full += `\n\n${body}`;
578
+ }
579
+ // Add BREAKING CHANGE footer if enabled and breaking
580
+ if (isBreaking && includeBreakingFooter && breakingReasons && breakingReasons.length > 0) {
581
+ full += '\n\nBREAKING CHANGE: ' + breakingReasons[0];
582
+ if (breakingReasons.length > 1) {
583
+ for (let i = 1; i < breakingReasons.length; i++) {
584
+ full += `\n- ${breakingReasons[i]}`;
585
+ }
586
+ }
587
+ }
588
+ // Add ticket reference as footer
589
+ if (ticketInfo) {
590
+ const prefix = ticketInfo.prefix || 'Refs:';
591
+ full += `\n\n${prefix} ${ticketInfo.id}`;
592
+ }
593
+ return full;
594
+ }
595
+ /**
596
+ * Generate the final commit message
597
+ */
598
+ static generateCommitMessage() {
599
+ const analysis = this.analyzeChanges();
600
+ const config = configService_1.ConfigService.getConfig();
601
+ // Determine emoji usage: config overrides, then history learning
602
+ let includeEmoji = config.includeEmoji;
603
+ if (includeEmoji === undefined) {
604
+ includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
605
+ }
606
+ const body = this.generateBody(analysis);
607
+ const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
608
+ const full = this.buildFullMessage(analysis.commitType, analysis.scope, analysis.description, body, includeEmoji, ticketInfo, analysis.isBreakingChange, analysis.breakingChangeReasons);
609
+ return {
610
+ type: analysis.commitType,
611
+ scope: analysis.scope,
612
+ description: analysis.description,
613
+ body,
614
+ full,
615
+ isBreaking: analysis.isBreakingChange,
616
+ };
617
+ }
618
+ /**
619
+ * Generate multiple message suggestions
620
+ */
621
+ static generateMultipleSuggestions() {
622
+ const analysis = this.analyzeChanges();
623
+ const config = configService_1.ConfigService.getConfig();
624
+ // Determine emoji usage: config overrides, then history learning
625
+ let includeEmoji = config.includeEmoji;
626
+ if (includeEmoji === undefined) {
627
+ includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
628
+ }
629
+ const suggestions = [];
630
+ const body = this.generateBody(analysis);
631
+ const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
632
+ const { isBreakingChange, breakingChangeReasons } = analysis;
633
+ // Try to get a better scope from history if none detected
634
+ let scope = analysis.scope;
635
+ if (!scope) {
636
+ const stagedFiles = gitService_1.GitService.getStagedFiles();
637
+ const filePaths = stagedFiles.map(f => f.path);
638
+ scope = historyService_1.HistoryService.getSuggestedScope(filePaths);
639
+ }
640
+ // Suggestion 1: Default (with scope if detected, with ticket, with breaking change)
641
+ const defaultFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
642
+ suggestions.push({
643
+ id: 1,
644
+ label: isBreakingChange ? 'Breaking Change' : 'Recommended',
645
+ message: {
646
+ type: analysis.commitType,
647
+ scope: scope,
648
+ description: analysis.description,
649
+ body,
650
+ full: defaultFull,
651
+ isBreaking: isBreakingChange,
652
+ },
653
+ });
654
+ // Suggestion 2: Without scope (more concise)
655
+ if (scope) {
656
+ const noScopeFull = this.buildFullMessage(analysis.commitType, undefined, analysis.description, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
657
+ suggestions.push({
658
+ id: 2,
659
+ label: 'Concise',
660
+ message: {
661
+ type: analysis.commitType,
662
+ scope: undefined,
663
+ description: analysis.description,
664
+ body,
665
+ full: noScopeFull,
666
+ isBreaking: isBreakingChange,
667
+ },
668
+ });
669
+ }
670
+ // Suggestion 3: Alternative description style
671
+ const altDescription = this.generateAlternativeDescription(analysis);
672
+ if (altDescription && altDescription !== analysis.description) {
673
+ const altFull = this.buildFullMessage(analysis.commitType, scope, altDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
674
+ suggestions.push({
675
+ id: suggestions.length + 1,
676
+ label: 'Detailed',
677
+ message: {
678
+ type: analysis.commitType,
679
+ scope: scope,
680
+ description: altDescription,
681
+ body,
682
+ full: altFull,
683
+ isBreaking: isBreakingChange,
684
+ },
685
+ });
686
+ }
687
+ // Suggestion 4: Without body (compact) - only if body exists
688
+ if (body) {
689
+ const compactFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, undefined, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
690
+ suggestions.push({
691
+ id: suggestions.length + 1,
692
+ label: 'Compact',
693
+ message: {
694
+ type: analysis.commitType,
695
+ scope: scope,
696
+ description: analysis.description,
697
+ body: undefined,
698
+ full: compactFull,
699
+ isBreaking: isBreakingChange,
700
+ },
701
+ });
702
+ }
703
+ // Suggestion 5: Without ticket reference (if ticket was detected)
704
+ if (ticketInfo) {
705
+ const noTicketFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, null, isBreakingChange, breakingChangeReasons);
706
+ suggestions.push({
707
+ id: suggestions.length + 1,
708
+ label: 'No Ticket',
709
+ message: {
710
+ type: analysis.commitType,
711
+ scope: scope,
712
+ description: analysis.description,
713
+ body,
714
+ full: noTicketFull,
715
+ isBreaking: isBreakingChange,
716
+ },
717
+ });
718
+ }
719
+ // Suggestion 6: Without breaking change indicator (if breaking change detected)
720
+ if (isBreakingChange) {
721
+ const noBreakingFull = this.buildFullMessage(analysis.commitType, scope, analysis.description, body, includeEmoji, ticketInfo, false, []);
722
+ suggestions.push({
723
+ id: suggestions.length + 1,
724
+ label: 'No Breaking Flag',
725
+ message: {
726
+ type: analysis.commitType,
727
+ scope: scope,
728
+ description: analysis.description,
729
+ body,
730
+ full: noBreakingFull,
731
+ isBreaking: false,
732
+ },
733
+ });
734
+ }
735
+ return suggestions;
736
+ }
737
+ /**
738
+ * Generate suggestions with optional AI enhancement
739
+ */
740
+ static async generateSuggestionsWithAI(useAI = false) {
741
+ const suggestions = this.generateMultipleSuggestions();
742
+ // If AI is not requested or not enabled, return regular suggestions
743
+ if (!useAI || !aiService_1.AIService.isEnabled()) {
744
+ return suggestions;
745
+ }
746
+ try {
747
+ const analysis = this.analyzeChanges();
748
+ const diff = gitService_1.GitService.getDiff();
749
+ const config = configService_1.ConfigService.getConfig();
750
+ // Determine emoji usage
751
+ let includeEmoji = config.includeEmoji;
752
+ if (includeEmoji === undefined) {
753
+ includeEmoji = historyService_1.HistoryService.projectUsesEmojis();
754
+ }
755
+ const body = this.generateBody(analysis);
756
+ const ticketInfo = historyService_1.HistoryService.detectTicketFromBranch();
757
+ const { isBreakingChange, breakingChangeReasons } = analysis;
758
+ // Get scope
759
+ let scope = analysis.scope;
760
+ if (!scope) {
761
+ const stagedFiles = gitService_1.GitService.getStagedFiles();
762
+ const filePaths = stagedFiles.map(f => f.path);
763
+ scope = historyService_1.HistoryService.getSuggestedScope(filePaths);
764
+ }
765
+ // Get AI-enhanced description
766
+ const aiResponse = await aiService_1.AIService.generateDescription(analysis, diff);
767
+ if (aiResponse && aiResponse.description) {
768
+ const aiDescription = aiResponse.description;
769
+ const aiFull = this.buildFullMessage(analysis.commitType, scope, aiDescription, body, includeEmoji, ticketInfo, isBreakingChange, breakingChangeReasons);
770
+ // Insert AI suggestion at the beginning
771
+ suggestions.unshift({
772
+ id: 0,
773
+ label: 'AI Enhanced',
774
+ message: {
775
+ type: analysis.commitType,
776
+ scope: scope,
777
+ description: aiDescription,
778
+ body,
779
+ full: aiFull,
780
+ isBreaking: isBreakingChange,
781
+ },
782
+ });
783
+ // Re-number suggestions
784
+ suggestions.forEach((s, i) => {
785
+ s.id = i + 1;
786
+ });
787
+ }
788
+ }
789
+ catch (error) {
790
+ // AI failed - error already logged by AIService
791
+ console.log('Falling back to rule-based suggestions.\n');
792
+ }
793
+ return suggestions;
794
+ }
795
+ /**
796
+ * Generate an alternative description style
797
+ */
798
+ static generateAlternativeDescription(analysis) {
799
+ const { fileChanges, filesAffected } = analysis;
800
+ const totalFiles = fileChanges.added.length + fileChanges.modified.length +
801
+ fileChanges.deleted.length + fileChanges.renamed.length;
802
+ // For single file, provide more detail
803
+ if (totalFiles === 1) {
804
+ if (fileChanges.added.length === 1) {
805
+ return `implement ${fileChanges.added[0]}`;
806
+ }
807
+ if (fileChanges.modified.length === 1) {
808
+ return `improve ${fileChanges.modified[0]}`;
809
+ }
810
+ }
811
+ // For multiple files, be more descriptive about categories
812
+ const parts = [];
813
+ if (filesAffected.source > 0) {
814
+ parts.push(`${filesAffected.source} source file${filesAffected.source > 1 ? 's' : ''}`);
815
+ }
816
+ if (filesAffected.test > 0) {
817
+ parts.push(`${filesAffected.test} test${filesAffected.test > 1 ? 's' : ''}`);
818
+ }
819
+ if (filesAffected.docs > 0) {
820
+ parts.push(`${filesAffected.docs} doc${filesAffected.docs > 1 ? 's' : ''}`);
821
+ }
822
+ if (filesAffected.config > 0) {
823
+ parts.push(`${filesAffected.config} config${filesAffected.config > 1 ? 's' : ''}`);
824
+ }
825
+ if (parts.length > 0) {
826
+ return `update ${parts.join(', ')}`;
827
+ }
828
+ return analysis.description;
829
+ }
830
+ }
831
+ exports.AnalyzerService = AnalyzerService;
832
+ //# sourceMappingURL=analyzerService.js.map