@entro314labs/ai-changelog-generator 3.0.5 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +22 -0
- package/package.json +10 -10
- package/src/application/orchestrators/changelog.orchestrator.js +469 -0
- package/src/application/services/application.service.js +13 -0
- package/src/infrastructure/cli/cli.controller.js +52 -0
- package/src/infrastructure/interactive/interactive-staging.service.js +313 -0
- package/src/infrastructure/validation/commit-message-validation.service.js +458 -0
- package/src/shared/constants/colors.js +28 -6
- package/src/shared/utils/utils.js +352 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import colors from '../../shared/constants/colors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Commit Message Validation Service
|
|
5
|
+
*
|
|
6
|
+
* Provides comprehensive commit message validation based on:
|
|
7
|
+
* - Conventional Commits specification
|
|
8
|
+
* - Configuration-based rules
|
|
9
|
+
* - Branch intelligence context
|
|
10
|
+
* - Best practices for readability and maintainability
|
|
11
|
+
*/
|
|
12
|
+
export class CommitMessageValidationService {
|
|
13
|
+
constructor(configManager) {
|
|
14
|
+
this.configManager = configManager;
|
|
15
|
+
this.config = this.loadValidationConfig();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load validation configuration from config files
|
|
20
|
+
*/
|
|
21
|
+
loadValidationConfig() {
|
|
22
|
+
const config = this.configManager.getAll();
|
|
23
|
+
|
|
24
|
+
// Default validation rules
|
|
25
|
+
const defaults = {
|
|
26
|
+
commitTypes: ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'],
|
|
27
|
+
commitScopes: [], // Empty means any scope is allowed
|
|
28
|
+
maxSubjectLength: 72,
|
|
29
|
+
minSubjectLength: 10,
|
|
30
|
+
requireScope: false,
|
|
31
|
+
requireBody: false,
|
|
32
|
+
requireFooter: false,
|
|
33
|
+
allowBreakingChanges: true,
|
|
34
|
+
subjectCase: 'lower', // 'lower', 'sentence', 'any'
|
|
35
|
+
subjectEndPunctuation: false, // Don't allow period at end
|
|
36
|
+
bodyLineLength: 100,
|
|
37
|
+
footerFormat: 'conventional' // 'conventional', 'any'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Merge with config from ai-changelog.config.yaml
|
|
41
|
+
const yamlConfig = config.convention || {};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...defaults,
|
|
45
|
+
...yamlConfig,
|
|
46
|
+
// Override with specific validation settings if they exist
|
|
47
|
+
commitTypes: yamlConfig.commitTypes || defaults.commitTypes,
|
|
48
|
+
commitScopes: yamlConfig.commitScopes || defaults.commitScopes
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Comprehensive commit message validation
|
|
54
|
+
*/
|
|
55
|
+
async validateCommitMessage(message, context = {}) {
|
|
56
|
+
if (!message || typeof message !== 'string') {
|
|
57
|
+
return this.createValidationResult(false, ['Commit message is required'], []);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const trimmedMessage = message.trim();
|
|
61
|
+
if (trimmedMessage.length === 0) {
|
|
62
|
+
return this.createValidationResult(false, ['Commit message cannot be empty'], []);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lines = trimmedMessage.split('\n');
|
|
66
|
+
const subject = lines[0];
|
|
67
|
+
const body = lines.slice(2).join('\n').trim(); // Skip blank line after subject
|
|
68
|
+
const hasBlankLineAfterSubject = lines.length > 1 && lines[1].trim() === '';
|
|
69
|
+
|
|
70
|
+
const errors = [];
|
|
71
|
+
const warnings = [];
|
|
72
|
+
const suggestions = [];
|
|
73
|
+
|
|
74
|
+
// Parse conventional commit format
|
|
75
|
+
const conventionalCommit = this.parseConventionalCommit(subject);
|
|
76
|
+
|
|
77
|
+
// Subject validation
|
|
78
|
+
this.validateSubject(subject, conventionalCommit, errors, warnings, suggestions, context);
|
|
79
|
+
|
|
80
|
+
// Body validation
|
|
81
|
+
if (lines.length > 1) {
|
|
82
|
+
this.validateBody(body, hasBlankLineAfterSubject, lines, errors, warnings, suggestions);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Footer validation
|
|
86
|
+
const footerLines = this.extractFooterLines(lines);
|
|
87
|
+
if (footerLines.length > 0) {
|
|
88
|
+
this.validateFooter(footerLines, errors, warnings, suggestions);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Context-based validation (branch intelligence)
|
|
92
|
+
if (context.branchAnalysis) {
|
|
93
|
+
this.validateAgainstBranchContext(conventionalCommit, context.branchAnalysis, warnings, suggestions);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Configuration-based validation
|
|
97
|
+
this.validateAgainstConfig(conventionalCommit, errors, warnings, suggestions);
|
|
98
|
+
|
|
99
|
+
const isValid = errors.length === 0;
|
|
100
|
+
const score = this.calculateValidationScore(errors, warnings, suggestions);
|
|
101
|
+
|
|
102
|
+
return this.createValidationResult(isValid, errors, warnings, suggestions, score, conventionalCommit);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse conventional commit format
|
|
107
|
+
*/
|
|
108
|
+
parseConventionalCommit(subject) {
|
|
109
|
+
// Enhanced pattern to capture all parts
|
|
110
|
+
const conventionalPattern = /^([a-z]+)(\(([^)]+)\))?(!)?: (.+)$/;
|
|
111
|
+
const match = subject.match(conventionalPattern);
|
|
112
|
+
|
|
113
|
+
if (!match) {
|
|
114
|
+
return {
|
|
115
|
+
type: null,
|
|
116
|
+
scope: null,
|
|
117
|
+
breaking: false,
|
|
118
|
+
description: subject,
|
|
119
|
+
isConventional: false
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
type: match[1],
|
|
125
|
+
scope: match[3] || null,
|
|
126
|
+
breaking: !!match[4], // Breaking change indicator (!)
|
|
127
|
+
description: match[5],
|
|
128
|
+
isConventional: true
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validate subject line
|
|
134
|
+
*/
|
|
135
|
+
validateSubject(subject, parsed, errors, warnings, suggestions, context) {
|
|
136
|
+
// Length validation
|
|
137
|
+
if (subject.length < this.config.minSubjectLength) {
|
|
138
|
+
errors.push(`Subject too short (${subject.length} chars, minimum ${this.config.minSubjectLength})`);
|
|
139
|
+
suggestions.push('Add more detail about what was changed');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (subject.length > this.config.maxSubjectLength) {
|
|
143
|
+
errors.push(`Subject too long (${subject.length} chars, maximum ${this.config.maxSubjectLength})`);
|
|
144
|
+
suggestions.push('Move additional details to the commit body');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Conventional commit format validation
|
|
148
|
+
if (!parsed.isConventional) {
|
|
149
|
+
errors.push('Subject does not follow conventional commit format');
|
|
150
|
+
suggestions.push('Use format: type(scope): description (e.g., "feat: add new feature")');
|
|
151
|
+
return; // Skip further validation if not conventional
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Type validation
|
|
155
|
+
if (!this.config.commitTypes.includes(parsed.type)) {
|
|
156
|
+
errors.push(`Invalid commit type: "${parsed.type}"`);
|
|
157
|
+
suggestions.push(`Use one of: ${this.config.commitTypes.join(', ')}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Scope validation
|
|
161
|
+
if (this.config.commitScopes.length > 0 && parsed.scope && !this.config.commitScopes.includes(parsed.scope)) {
|
|
162
|
+
warnings.push(`Unexpected scope: "${parsed.scope}"`);
|
|
163
|
+
suggestions.push(`Suggested scopes: ${this.config.commitScopes.join(', ')}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this.config.requireScope && !parsed.scope) {
|
|
167
|
+
errors.push('Scope is required for this repository');
|
|
168
|
+
suggestions.push('Add scope in parentheses: type(scope): description');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Description validation
|
|
172
|
+
if (!parsed.description || parsed.description.trim().length === 0) {
|
|
173
|
+
errors.push('Description is required');
|
|
174
|
+
suggestions.push('Add a clear description of what was changed');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Case validation
|
|
178
|
+
if (this.config.subjectCase === 'lower' && parsed.description) {
|
|
179
|
+
const firstChar = parsed.description.charAt(0);
|
|
180
|
+
if (firstChar !== firstChar.toLowerCase()) {
|
|
181
|
+
warnings.push('Description should start with lowercase letter');
|
|
182
|
+
suggestions.push(`Change "${firstChar}" to "${firstChar.toLowerCase()}"`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// End punctuation validation
|
|
187
|
+
if (!this.config.subjectEndPunctuation && parsed.description && parsed.description.endsWith('.')) {
|
|
188
|
+
warnings.push('Subject should not end with a period');
|
|
189
|
+
suggestions.push('Remove the trailing period');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Imperative mood validation
|
|
193
|
+
if (parsed.description) {
|
|
194
|
+
this.validateImperativeMood(parsed.description, warnings, suggestions);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Validate imperative mood
|
|
200
|
+
*/
|
|
201
|
+
validateImperativeMood(description, warnings, suggestions) {
|
|
202
|
+
const imperativeVerbs = [
|
|
203
|
+
'add', 'remove', 'fix', 'update', 'create', 'delete', 'implement', 'refactor',
|
|
204
|
+
'improve', 'enhance', 'optimize', 'change', 'move', 'rename', 'replace',
|
|
205
|
+
'upgrade', 'downgrade', 'install', 'uninstall', 'configure', 'setup',
|
|
206
|
+
'initialize', 'clean', 'format', 'lint', 'test', 'document'
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
const nonImperativeIndicators = [
|
|
210
|
+
'added', 'removed', 'fixed', 'updated', 'created', 'deleted', 'implemented',
|
|
211
|
+
'improved', 'enhanced', 'optimized', 'changed', 'moved', 'renamed', 'replaced',
|
|
212
|
+
'upgraded', 'downgraded', 'installed', 'uninstalled', 'configured',
|
|
213
|
+
'initialized', 'cleaned', 'formatted', 'linted', 'tested', 'documented'
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
const firstWord = description.split(' ')[0].toLowerCase();
|
|
217
|
+
|
|
218
|
+
if (nonImperativeIndicators.includes(firstWord)) {
|
|
219
|
+
warnings.push('Use imperative mood in description');
|
|
220
|
+
// Try to suggest imperative form
|
|
221
|
+
const imperative = firstWord.replace(/ed$/, '').replace(/d$/, '');
|
|
222
|
+
if (imperativeVerbs.includes(imperative)) {
|
|
223
|
+
suggestions.push(`Change "${firstWord}" to "${imperative}"`);
|
|
224
|
+
} else {
|
|
225
|
+
suggestions.push('Use imperative mood (e.g., "fix bug" not "fixed bug")');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Validate body
|
|
232
|
+
*/
|
|
233
|
+
validateBody(body, hasBlankLine, lines, errors, warnings, suggestions) {
|
|
234
|
+
// Blank line separation
|
|
235
|
+
if (!hasBlankLine) {
|
|
236
|
+
errors.push('Missing blank line between subject and body');
|
|
237
|
+
suggestions.push('Add a blank line after the subject');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Body line length
|
|
241
|
+
if (body) {
|
|
242
|
+
const bodyLines = body.split('\n');
|
|
243
|
+
bodyLines.forEach((line, index) => {
|
|
244
|
+
if (line.length > this.config.bodyLineLength) {
|
|
245
|
+
warnings.push(`Body line ${index + 1} too long (${line.length} chars, recommended max ${this.config.bodyLineLength})`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Required body
|
|
251
|
+
if (this.config.requireBody && (!body || body.trim().length === 0)) {
|
|
252
|
+
errors.push('Commit body is required');
|
|
253
|
+
suggestions.push('Add details about the changes in the commit body');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Extract footer lines (last paragraph that contains key-value pairs)
|
|
259
|
+
*/
|
|
260
|
+
extractFooterLines(lines) {
|
|
261
|
+
if (lines.length < 3) return [];
|
|
262
|
+
|
|
263
|
+
const footerLines = [];
|
|
264
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
265
|
+
const line = lines[i].trim();
|
|
266
|
+
if (line === '') {
|
|
267
|
+
break; // Empty line indicates end of footer
|
|
268
|
+
}
|
|
269
|
+
if (line.includes(':') || line.match(/^(BREAKING CHANGE|Closes?|Fixes?|Refs?)/i)) {
|
|
270
|
+
footerLines.unshift(line);
|
|
271
|
+
} else {
|
|
272
|
+
break; // Non-footer line
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return footerLines;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Validate footer
|
|
281
|
+
*/
|
|
282
|
+
validateFooter(footerLines, errors, warnings, suggestions) {
|
|
283
|
+
footerLines.forEach(line => {
|
|
284
|
+
// Validate footer format
|
|
285
|
+
if (this.config.footerFormat === 'conventional') {
|
|
286
|
+
if (!line.match(/^[A-Za-z-]+: .+/) && !line.match(/^BREAKING CHANGE: .+/)) {
|
|
287
|
+
warnings.push(`Footer line doesn't follow conventional format: "${line}"`);
|
|
288
|
+
suggestions.push('Use format: "Key: value" or "BREAKING CHANGE: description"');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Validate breaking changes
|
|
293
|
+
if (line.startsWith('BREAKING CHANGE:')) {
|
|
294
|
+
if (!this.config.allowBreakingChanges) {
|
|
295
|
+
errors.push('Breaking changes are not allowed in this repository');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Validate against branch context
|
|
303
|
+
*/
|
|
304
|
+
validateAgainstBranchContext(parsed, branchAnalysis, warnings, suggestions) {
|
|
305
|
+
if (!branchAnalysis || branchAnalysis.confidence < 50) return;
|
|
306
|
+
|
|
307
|
+
// Type mismatch
|
|
308
|
+
if (branchAnalysis.type && parsed.type && branchAnalysis.type !== parsed.type) {
|
|
309
|
+
warnings.push(`Commit type "${parsed.type}" doesn't match branch type "${branchAnalysis.type}"`);
|
|
310
|
+
suggestions.push(`Consider using type "${branchAnalysis.type}" based on branch name`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Missing ticket reference
|
|
314
|
+
if (branchAnalysis.ticket && !this.containsTicketReference(parsed, branchAnalysis.ticket)) {
|
|
315
|
+
suggestions.push(`Consider adding ticket reference: ${branchAnalysis.ticket}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Check if commit message contains ticket reference
|
|
321
|
+
*/
|
|
322
|
+
containsTicketReference(parsed, ticket) {
|
|
323
|
+
const fullMessage = `${parsed.type}${parsed.scope ? `(${parsed.scope})` : ''}: ${parsed.description}`;
|
|
324
|
+
return fullMessage.includes(ticket);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Validate against configuration
|
|
329
|
+
*/
|
|
330
|
+
validateAgainstConfig(parsed, errors, warnings, suggestions) {
|
|
331
|
+
// This is handled in other validation methods
|
|
332
|
+
// Additional config-specific validations can be added here
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Calculate validation score
|
|
337
|
+
*/
|
|
338
|
+
calculateValidationScore(errors, warnings, suggestions) {
|
|
339
|
+
let score = 100;
|
|
340
|
+
score -= errors.length * 25; // Major issues
|
|
341
|
+
score -= warnings.length * 10; // Minor issues
|
|
342
|
+
score -= suggestions.length * 5; // Improvements
|
|
343
|
+
return Math.max(0, score);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Create validation result object
|
|
348
|
+
*/
|
|
349
|
+
createValidationResult(isValid, errors = [], warnings = [], suggestions = [], score = 0, parsed = null) {
|
|
350
|
+
return {
|
|
351
|
+
valid: isValid,
|
|
352
|
+
errors,
|
|
353
|
+
warnings,
|
|
354
|
+
suggestions,
|
|
355
|
+
score,
|
|
356
|
+
parsed,
|
|
357
|
+
summary: this.generateValidationSummary(isValid, errors, warnings, suggestions, score)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Generate human-readable validation summary
|
|
363
|
+
*/
|
|
364
|
+
generateValidationSummary(isValid, errors, warnings, suggestions, score) {
|
|
365
|
+
if (isValid && warnings.length === 0 && suggestions.length === 0) {
|
|
366
|
+
return '✅ Perfect commit message!';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const parts = [];
|
|
370
|
+
|
|
371
|
+
if (errors.length > 0) {
|
|
372
|
+
parts.push(`${errors.length} error${errors.length === 1 ? '' : 's'}`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (warnings.length > 0) {
|
|
376
|
+
parts.push(`${warnings.length} warning${warnings.length === 1 ? '' : 's'}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (suggestions.length > 0) {
|
|
380
|
+
parts.push(`${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const status = isValid ? '✅' : '❌';
|
|
384
|
+
return `${status} ${parts.join(', ')} (Score: ${score}/100)`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Display validation results with colors
|
|
389
|
+
*/
|
|
390
|
+
displayValidationResults(validationResult) {
|
|
391
|
+
const { valid, errors, warnings, suggestions, score, summary } = validationResult;
|
|
392
|
+
|
|
393
|
+
console.log(colors.infoMessage(`\n🔍 Commit Message Validation:`));
|
|
394
|
+
console.log(colors.secondary(`${summary}`));
|
|
395
|
+
|
|
396
|
+
if (errors.length > 0) {
|
|
397
|
+
console.log(colors.errorMessage('\n❌ Errors (must fix):'));
|
|
398
|
+
errors.forEach(error => {
|
|
399
|
+
console.log(colors.error(` • ${error}`));
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (warnings.length > 0) {
|
|
404
|
+
console.log(colors.warningMessage('\n⚠️ Warnings (recommended to fix):'));
|
|
405
|
+
warnings.forEach(warning => {
|
|
406
|
+
console.log(colors.warning(` • ${warning}`));
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (suggestions.length > 0) {
|
|
411
|
+
console.log(colors.infoMessage('\n💡 Suggestions (optional improvements):'));
|
|
412
|
+
suggestions.forEach(suggestion => {
|
|
413
|
+
console.log(colors.dim(` • ${suggestion}`));
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return valid;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Interactive commit message improvement
|
|
422
|
+
*/
|
|
423
|
+
async improveCommitMessage(message, context = {}) {
|
|
424
|
+
const validation = await this.validateCommitMessage(message, context);
|
|
425
|
+
|
|
426
|
+
if (validation.valid && validation.warnings.length === 0) {
|
|
427
|
+
return { improved: false, message, validation };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Generate improved message based on validation results
|
|
431
|
+
let improved = message;
|
|
432
|
+
|
|
433
|
+
// Fix common issues automatically
|
|
434
|
+
if (validation.parsed) {
|
|
435
|
+
const { type, scope, description, breaking } = validation.parsed;
|
|
436
|
+
|
|
437
|
+
// Fix case issues
|
|
438
|
+
if (description) {
|
|
439
|
+
let fixedDescription = description;
|
|
440
|
+
|
|
441
|
+
// Fix capitalization
|
|
442
|
+
if (this.config.subjectCase === 'lower') {
|
|
443
|
+
fixedDescription = fixedDescription.charAt(0).toLowerCase() + fixedDescription.slice(1);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Remove trailing period
|
|
447
|
+
if (!this.config.subjectEndPunctuation && fixedDescription.endsWith('.')) {
|
|
448
|
+
fixedDescription = fixedDescription.slice(0, -1);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Reconstruct subject
|
|
452
|
+
improved = `${type}${scope ? `(${scope})` : ''}${breaking ? '!' : ''}: ${fixedDescription}`;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return { improved: improved !== message, message: improved, validation };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
@@ -271,15 +271,37 @@ class Colors {
|
|
|
271
271
|
return `${this.info(`[${bar}]`)} ${this.percentage(`${percentage}%`)} ${label}`;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
//
|
|
275
|
-
|
|
274
|
+
// Helper to get actual visible length of string (without ANSI codes)
|
|
275
|
+
getVisibleLength(str) {
|
|
276
|
+
// Remove ANSI escape sequences to get actual visible length
|
|
277
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Box drawing for sections with dynamic width calculation
|
|
281
|
+
box(title, content, minWidth = 60) {
|
|
282
|
+
const lines = content.split('\n');
|
|
283
|
+
const titleVisibleLength = this.getVisibleLength(title);
|
|
284
|
+
|
|
285
|
+
// Calculate required width based on content
|
|
286
|
+
const maxContentLength = Math.max(
|
|
287
|
+
titleVisibleLength + 4, // title + padding
|
|
288
|
+
...lines.map(line => this.getVisibleLength(line) + 4), // content + padding
|
|
289
|
+
minWidth
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const width = Math.min(maxContentLength, 80); // Cap at 80 chars for readability
|
|
293
|
+
|
|
276
294
|
const topBorder = '┌' + '─'.repeat(width - 2) + '┐';
|
|
277
295
|
const bottomBorder = '└' + '─'.repeat(width - 2) + '┘';
|
|
278
|
-
const titleLine = `│ ${this.header(title)}${' '.repeat(Math.max(0, width - title.length - 3))}│`;
|
|
279
296
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
297
|
+
// Title line with proper padding accounting for ANSI codes
|
|
298
|
+
const titlePadding = width - titleVisibleLength - 3;
|
|
299
|
+
const titleLine = `│ ${this.header(title)}${' '.repeat(Math.max(0, titlePadding))}│`;
|
|
300
|
+
|
|
301
|
+
const contentLines = lines.map(line => {
|
|
302
|
+
const visibleLength = this.getVisibleLength(line);
|
|
303
|
+
const padding = width - visibleLength - 4;
|
|
304
|
+
return `│ ${line}${' '.repeat(Math.max(0, padding))} │`;
|
|
283
305
|
});
|
|
284
306
|
|
|
285
307
|
return [
|