@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.
- package/LICENSE +21 -0
- package/README.md +430 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +31 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/generate.d.ts +10 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +313 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/generate.test.d.ts +2 -0
- package/dist/commands/generate.test.d.ts.map +1 -0
- package/dist/commands/generate.test.js +168 -0
- package/dist/commands/generate.test.js.map +1 -0
- package/dist/commands/hook.d.ts +4 -0
- package/dist/commands/hook.d.ts.map +1 -0
- package/dist/commands/hook.js +62 -0
- package/dist/commands/hook.js.map +1 -0
- package/dist/commands/stats.d.ts +6 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +39 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/services/aiService.d.ts +38 -0
- package/dist/services/aiService.d.ts.map +1 -0
- package/dist/services/aiService.js +187 -0
- package/dist/services/aiService.js.map +1 -0
- package/dist/services/analyzerService.d.ts +60 -0
- package/dist/services/analyzerService.d.ts.map +1 -0
- package/dist/services/analyzerService.js +832 -0
- package/dist/services/analyzerService.js.map +1 -0
- package/dist/services/analyzerService.test.d.ts +2 -0
- package/dist/services/analyzerService.test.d.ts.map +1 -0
- package/dist/services/analyzerService.test.js +323 -0
- package/dist/services/analyzerService.test.js.map +1 -0
- package/dist/services/configService.d.ts +25 -0
- package/dist/services/configService.d.ts.map +1 -0
- package/dist/services/configService.js +207 -0
- package/dist/services/configService.js.map +1 -0
- package/dist/services/configService.test.d.ts +2 -0
- package/dist/services/configService.test.d.ts.map +1 -0
- package/dist/services/configService.test.js +165 -0
- package/dist/services/configService.test.js.map +1 -0
- package/dist/services/gitService.d.ts +44 -0
- package/dist/services/gitService.d.ts.map +1 -0
- package/dist/services/gitService.js +217 -0
- package/dist/services/gitService.js.map +1 -0
- package/dist/services/gitService.test.d.ts +2 -0
- package/dist/services/gitService.test.d.ts.map +1 -0
- package/dist/services/gitService.test.js +140 -0
- package/dist/services/gitService.test.js.map +1 -0
- package/dist/services/historyService.d.ts +39 -0
- package/dist/services/historyService.d.ts.map +1 -0
- package/dist/services/historyService.js +195 -0
- package/dist/services/historyService.js.map +1 -0
- package/dist/services/historyService.test.d.ts +2 -0
- package/dist/services/historyService.test.d.ts.map +1 -0
- package/dist/services/historyService.test.js +157 -0
- package/dist/services/historyService.test.js.map +1 -0
- package/dist/services/hookService.d.ts +29 -0
- package/dist/services/hookService.d.ts.map +1 -0
- package/dist/services/hookService.js +164 -0
- package/dist/services/hookService.js.map +1 -0
- package/dist/services/statsService.d.ts +28 -0
- package/dist/services/statsService.d.ts.map +1 -0
- package/dist/services/statsService.js +204 -0
- package/dist/services/statsService.js.map +1 -0
- package/dist/types/index.d.ts +134 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/filePatterns.d.ts +5 -0
- package/dist/utils/filePatterns.d.ts.map +1 -0
- package/dist/utils/filePatterns.js +77 -0
- package/dist/utils/filePatterns.js.map +1 -0
- package/dist/utils/filePatterns.test.d.ts +2 -0
- package/dist/utils/filePatterns.test.d.ts.map +1 -0
- package/dist/utils/filePatterns.test.js +51 -0
- package/dist/utils/filePatterns.test.js.map +1 -0
- package/dist/utils/prompt.d.ts +4 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +60 -0
- package/dist/utils/prompt.js.map +1 -0
- 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
|