bumper-cli
Version:
🚀 A magical release management system with beautiful changelogs and automated workflows
291 lines • 10.7 kB
JavaScript
import chalk from 'chalk';
import inquirer from 'inquirer';
// Conventional commit types and their descriptions
const COMMIT_TYPES = {
feat: { emoji: '✨', description: 'A new feature' },
fix: { emoji: '🐛', description: 'A bug fix' },
docs: { emoji: '📚', description: 'Documentation only changes' },
style: { emoji: '💄', description: 'Changes that do not affect the meaning of the code' },
refactor: { emoji: '♻️', description: 'A code change that neither fixes a bug nor adds a feature' },
perf: { emoji: '⚡', description: 'A code change that improves performance' },
test: { emoji: '✅', description: 'Adding missing tests or correcting existing tests' },
build: { emoji: '📦', description: 'Changes that affect the build system or external dependencies' },
ci: { emoji: '🔧', description: 'Changes to our CI configuration files and scripts' },
chore: { emoji: '🔨', description: 'Other changes that do not modify src or test files' },
revert: { emoji: '⏪', description: 'Reverts a previous commit' },
security: { emoji: '🔒', description: 'Security fixes' },
};
// Common scope suggestions based on common project structures
const COMMON_SCOPES = [
'auth',
'api',
'ui',
'cli',
'docs',
'test',
'build',
'ci',
'deps',
'config',
'types',
'utils',
'core',
'server',
'client',
'database',
'cache',
'logging',
'monitoring',
'security',
];
// Keywords that suggest commit types
const TYPE_KEYWORDS = {
feat: ['add', 'new', 'create', 'implement', 'introduce', 'support', 'enable', 'allow'],
fix: ['fix', 'resolve', 'repair', 'correct', 'solve', 'patch', 'bug', 'issue', 'error'],
docs: ['document', 'readme', 'docs', 'comment', 'example', 'guide', 'tutorial'],
style: ['style', 'format', 'indent', 'whitespace', 'prettier', 'eslint'],
refactor: ['refactor', 'restructure', 'reorganize', 'cleanup', 'simplify', 'extract'],
perf: ['performance', 'optimize', 'speed', 'fast', 'slow', 'cache'],
test: ['test', 'spec', 'coverage', 'mock', 'stub', 'fixture'],
build: ['build', 'compile', 'bundle', 'webpack', 'rollup', 'dependencies'],
ci: ['ci', 'github', 'actions', 'workflow', 'pipeline', 'deploy'],
chore: ['chore', 'maintenance', 'update', 'upgrade', 'bump', 'version'],
revert: ['revert', 'undo', 'rollback', 'backout'],
security: ['security', 'vulnerability', 'cve', 'auth', 'permission', 'access'],
};
// Suggest commit type based on message content
const suggestCommitType = (message) => {
const lowerMessage = message.toLowerCase();
for (const [type, keywords] of Object.entries(TYPE_KEYWORDS)) {
if (keywords.some(keyword => lowerMessage.includes(keyword))) {
return type;
}
}
return 'chore'; // Default fallback
};
// Suggest scope based on message content and common patterns
const suggestScope = (message) => {
const lowerMessage = message.toLowerCase();
// Check for parentheses patterns like "fix (auth): ..." first
const scopeMatch = message.match(/\(([^)]+)\)/);
if (scopeMatch) {
return scopeMatch[1];
}
// Only suggest scope if it's a clear match (exact word boundaries)
for (const scope of COMMON_SCOPES) {
const scopeRegex = new RegExp(`\\b${scope}\\b`, 'i');
if (scopeRegex.test(lowerMessage)) {
return scope;
}
}
return undefined;
};
// Clean and format a message
const cleanMessage = (message) => {
let clean = message.trim();
// Remove trailing period
if (clean.endsWith('.')) {
clean = clean.slice(0, -1);
}
// Convert to lowercase then capitalize first letter
clean = clean.toLowerCase();
clean = clean.charAt(0).toUpperCase() + clean.slice(1);
return clean;
};
// Format a message into conventional commit format
export const formatCommitMessage = (message, type, scope, breaking = false) => {
const cleanMsg = cleanMessage(message);
const suggestedType = type || suggestCommitType(message);
const suggestedScope = scope || suggestScope(message);
let formatted = suggestedType;
if (suggestedScope) {
formatted += `(${suggestedScope})`;
}
if (breaking) {
formatted += '!';
}
formatted += `: ${cleanMsg}`;
return formatted;
};
// Check if message is already in conventional format
const isConventionalFormat = (message) => {
const conventionalRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|security)(\([\w-]+\))?(!)?:\s(.+)$/;
return conventionalRegex.test(message);
};
// Parse existing conventional commit
const parseConventionalCommit = (message) => {
const conventionalRegex = /^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|security)(\([\w-]+\))?(!)?:\s(.+)$/;
const match = message.match(conventionalRegex);
if (!match)
return null;
const [, type, scope, isBreaking, subject] = match;
return {
type: type || 'chore',
scope: scope ? scope.slice(1, -1) : undefined,
breaking: !!isBreaking,
subject: subject?.trim() || '',
};
};
// Generate improvements for a commit message
const generateImprovements = (message) => {
const improvements = [];
if (!isConventionalFormat(message)) {
improvements.push('Convert to conventional commit format');
}
if (message.toLowerCase().includes('breaking') || message.includes('!')) {
improvements.push('Mark as breaking change');
}
if (message.length > 72) {
improvements.push('Shorten message to under 72 characters');
}
if (message.endsWith('.')) {
improvements.push('Remove trailing period');
}
return improvements;
};
// Suggest improvements for an existing commit message
export const suggestCommitFormat = (message) => {
const improvements = generateImprovements(message);
let type = suggestCommitType(message);
let scope = suggestScope(message);
let breaking = false;
// Check if it's already in conventional format
if (isConventionalFormat(message)) {
const parsed = parseConventionalCommit(message);
if (parsed) {
type = parsed.type;
scope = parsed.scope;
breaking = parsed.breaking;
// For existing conventional commits, just clean up the subject
let cleanSubject = parsed.subject;
if (cleanSubject.endsWith('.')) {
cleanSubject = cleanSubject.slice(0, -1);
improvements.push('Remove trailing period');
}
// Capitalize first letter
cleanSubject = cleanSubject.charAt(0).toLowerCase() + cleanSubject.slice(1);
cleanSubject = cleanSubject.charAt(0).toUpperCase() + cleanSubject.slice(1);
let suggested = type;
if (scope) {
suggested += `(${scope})`;
}
if (breaking) {
suggested += '!';
}
suggested += `: ${cleanSubject}`;
return {
original: message,
suggested,
type,
scope,
breaking,
improvements,
};
}
}
// Check for breaking change indicators
if (message.toLowerCase().includes('breaking') || message.includes('!')) {
breaking = true;
}
const suggested = formatCommitMessage(message, type, scope, breaking);
return {
original: message,
suggested,
type,
scope,
breaking,
improvements,
};
};
// Create type choices for interactive prompts
const createTypeChoices = () => Object.entries(COMMIT_TYPES).map(([value, info]) => ({
name: `${info.emoji} ${value}: ${info.description}`,
value,
}));
// Validate description input
const validateDescription = (input) => {
if (!input.trim()) {
return 'Description is required';
}
if (input.length > 72) {
return 'Description should be under 72 characters';
}
return true;
};
// Interactive commit creation
export const createInteractiveCommit = async () => {
console.log(chalk.blue('🎯 Creating a conventional commit...\n'));
// Get commit type
const { type } = await inquirer.prompt([
{
type: 'list',
name: 'type',
message: 'What type of change is this?',
choices: createTypeChoices(),
},
]);
// Get scope (optional)
const { scope } = await inquirer.prompt([
{
type: 'input',
name: 'scope',
message: 'What is the scope of this change? (optional)',
default: '',
},
]);
// Get breaking change
const { breaking } = await inquirer.prompt([
{
type: 'confirm',
name: 'breaking',
message: 'Is this a breaking change?',
default: false,
},
]);
// Get description
const { description } = await inquirer.prompt([
{
type: 'input',
name: 'description',
message: 'Write a short description of the change:',
validate: validateDescription,
},
]);
// Get body (optional)
const { body } = await inquirer.prompt([
{
type: 'input',
name: 'body',
message: 'Write a longer description (optional):',
default: '',
},
]);
// Build the commit message
let commitMessage = formatCommitMessage(description, type, scope || undefined, breaking);
if (body.trim()) {
commitMessage += `\n\n${body.trim()}`;
}
return commitMessage;
};
// Display commit format suggestions
export const displayCommitSuggestions = (message) => {
const suggestion = suggestCommitFormat(message);
console.log(chalk.blue('💡 Commit Message Suggestions\n'));
console.log(chalk.gray('Original:'), suggestion.original);
console.log(chalk.green('Suggested:'), suggestion.suggested);
if (suggestion.improvements.length > 0) {
console.log(chalk.yellow('\nImprovements:'));
for (const improvement of suggestion.improvements) {
console.log(chalk.yellow(` • ${improvement}`));
}
}
console.log(chalk.blue('\nType:'), suggestion.type);
if (suggestion.scope) {
console.log(chalk.blue('Scope:'), suggestion.scope);
}
if (suggestion.breaking) {
console.log(chalk.red('Breaking:'), 'Yes');
}
};
//# sourceMappingURL=commitFormatter.js.map