Skip to main content
Glama
conventional-commits.ts14.9 kB
/** * Conventional Commits Automation for CodeRide MCP * Implements Conventional Commits 1.0.0 specification */ import { spawn } from 'child_process'; import { logger } from './logger.js'; import { InputValidator } from './input-validator.js'; /** * Conventional commit types following the specification */ export type ConventionalCommitType = | 'feat' // New feature | 'fix' // Bug fix | 'docs' // Documentation changes | 'style' // Code style changes (formatting, etc.) | 'refactor' // Code refactoring | 'perf' // Performance improvements | 'test' // Adding or updating tests | 'build' // Build system changes | 'ci' // CI configuration changes | 'chore' // Other changes (maintenance, etc.) | 'revert'; // Reverting previous commits /** * Conventional commit configuration */ export interface ConventionalCommitConfig { type: ConventionalCommitType; scope?: string; description: string; body?: string; footer?: string; breakingChange?: boolean; breakingChangeDescription?: string; } /** * Git operation result */ export interface GitOperationResult { success: boolean; output: string; error?: string; commitHash?: string; } /** * Semantic version impact based on conventional commits */ export type SemVerImpact = 'major' | 'minor' | 'patch' | 'none'; /** * Conventional Commits Generator and Git Automation */ export class ConventionalCommitsManager { private static instance: ConventionalCommitsManager; private constructor() {} /** * Get singleton instance */ static getInstance(): ConventionalCommitsManager { if (!ConventionalCommitsManager.instance) { ConventionalCommitsManager.instance = new ConventionalCommitsManager(); } return ConventionalCommitsManager.instance; } /** * Generate conventional commit message from configuration */ generateCommitMessage(config: ConventionalCommitConfig): string { // Validate configuration this.validateCommitConfig(config); // Build the commit message according to Conventional Commits 1.0.0 let message = ''; // Type and scope if (config.scope) { message += `${config.type}(${config.scope})`; } else { message += config.type; } // Breaking change indicator if (config.breakingChange) { message += '!'; } // Description message += `: ${config.description}`; // Body (optional) if (config.body) { message += `\n\n${config.body}`; } // Footer (optional) if (config.footer) { message += `\n\n${config.footer}`; } // Breaking change footer (if not already indicated with !) if (config.breakingChange && config.breakingChangeDescription) { if (!config.footer) { message += '\n\n'; } else { message += '\n'; } message += `BREAKING CHANGE: ${config.breakingChangeDescription}`; } logger.debug('Generated conventional commit message:', { message }); return message; } /** * Validate commit configuration */ private validateCommitConfig(config: ConventionalCommitConfig): void { // Validate type const validTypes: ConventionalCommitType[] = [ 'feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert' ]; if (!validTypes.includes(config.type)) { throw new Error(`Invalid commit type: ${config.type}. Must be one of: ${validTypes.join(', ')}`); } // Validate description if (!config.description || config.description.trim().length === 0) { throw new Error('Commit description is required'); } if (config.description.length > 100) { throw new Error('Commit description should not exceed 100 characters'); } // Validate scope (if provided) if (config.scope) { if (config.scope.length > 50) { throw new Error('Commit scope should not exceed 50 characters'); } // Scope should not contain special characters if (!/^[a-zA-Z0-9-_]+$/.test(config.scope)) { throw new Error('Commit scope should only contain alphanumeric characters, hyphens, and underscores'); } } // Validate body (if provided) if (config.body && config.body.length > 1000) { throw new Error('Commit body should not exceed 1000 characters'); } // Validate footer (if provided) if (config.footer && config.footer.length > 500) { throw new Error('Commit footer should not exceed 500 characters'); } } /** * Determine commit type based on tool and action context */ determineCommitType(toolName: string, input: any, result: any): ConventionalCommitType { // Task-related commits if (toolName === 'update_task') { if (input.status === 'completed') { return 'feat'; // Task completion is a feature } else if (input.status === 'in-progress') { return 'chore'; // Status updates are maintenance } else if (input.description) { return 'docs'; // Description updates are documentation } return 'chore'; } // Project-related commits if (toolName === 'update_project') { if (input.project_knowledge) { return 'docs'; // Knowledge updates are documentation } if (input.project_diagram) { return 'docs'; // Diagram updates are documentation } return 'docs'; } // Default mapping for other tools const toolTypeMapping: Record<string, ConventionalCommitType> = { 'get_project': 'chore', 'get_task': 'chore', 'get_prompt': 'chore', 'list_projects': 'chore', 'list_tasks': 'chore', 'next_task': 'chore', 'start_project': 'feat' }; return toolTypeMapping[toolName] || 'chore'; } /** * Determine commit scope based on input context */ determineCommitScope(toolName: string, input: any): string | undefined { // Extract project slug or task number as scope if (input.slug) { return input.slug.toUpperCase(); } if (input.number) { // Extract project part from task number (e.g., CRD-1 -> CRD) const projectPart = input.number.split('-')[0]; return projectPart.toUpperCase(); } // Tool-based scopes if (toolName.includes('project')) { return 'project'; } if (toolName.includes('task')) { return 'task'; } return undefined; } /** * Generate commit description based on tool action */ generateCommitDescription(toolName: string, input: any, result: any): string { switch (toolName) { case 'update_task': if (input.status === 'completed') { return `complete task ${input.number}`; } else if (input.status === 'in-progress') { return `start working on task ${input.number}`; } else if (input.description) { return `update task ${input.number} description`; } return `update task ${input.number}`; case 'update_project': const updates: string[] = []; if (input.project_knowledge) updates.push('knowledge'); if (input.project_diagram) updates.push('diagram'); return `update project ${input.slug} ${updates.join(' and ')}`; case 'start_project': return `initialize project ${result.project?.slug || 'setup'}`; default: return `${toolName.replace('_', ' ')} operation`; } } /** * Determine semantic version impact */ determineSemVerImpact(config: ConventionalCommitConfig): SemVerImpact { // Breaking changes = MAJOR if (config.breakingChange) { return 'major'; } // Features = MINOR if (config.type === 'feat') { return 'minor'; } // Bug fixes = PATCH if (config.type === 'fix') { return 'patch'; } // Everything else = no version impact return 'none'; } /** * Execute git command safely */ private async executeGitCommand(command: string, args: string[]): Promise<GitOperationResult> { return new Promise((resolve) => { // Validate git command for security const allowedCommands = ['status', 'add', 'commit', 'diff', 'log']; if (!allowedCommands.includes(command)) { resolve({ success: false, output: '', error: `Git command not allowed: ${command}` }); return; } // Validate arguments for (const arg of args) { if (typeof arg !== 'string') { resolve({ success: false, output: '', error: 'Git arguments must be strings' }); return; } } const child = spawn('git', [command, ...args], { cwd: process.cwd(), stdio: ['pipe', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { const success = code === 0; // Extract commit hash from output if it's a commit command let commitHash: string | undefined; if (success && command === 'commit') { const hashMatch = stdout.match(/\[[\w-]+\s+([a-f0-9]{7,})\]/); if (hashMatch) { commitHash = hashMatch[1]; } } resolve({ success, output: stdout, error: success ? undefined : stderr, commitHash }); }); child.on('error', (error) => { resolve({ success: false, output: '', error: `Git process error: ${error.message}` }); }); // Set timeout for git operations setTimeout(() => { child.kill('SIGTERM'); resolve({ success: false, output: '', error: 'Git operation timed out' }); }, 30000); // 30 second timeout }); } /** * Check git status */ async getGitStatus(): Promise<GitOperationResult> { logger.debug('Checking git status'); return this.executeGitCommand('status', ['--porcelain']); } /** * Stage files for commit */ async stageFiles(files: string[] = ['.']): Promise<GitOperationResult> { logger.debug('Staging files for commit:', { files }); // Validate file paths for (const file of files) { if (file.includes('..') || file.startsWith('/')) { return { success: false, output: '', error: `Invalid file path: ${file}` }; } } return this.executeGitCommand('add', files); } /** * Create conventional commit */ async createCommit(config: ConventionalCommitConfig): Promise<GitOperationResult> { try { // Generate commit message const message = this.generateCommitMessage(config); logger.info('Creating conventional commit:', { type: config.type, scope: config.scope, description: config.description }); // Execute git commit const result = await this.executeGitCommand('commit', ['-m', message]); if (result.success) { logger.info(`✅ Commit created successfully: ${result.commitHash || 'unknown hash'}`); } else { logger.error('❌ Commit failed:', undefined, { error: result.error || 'Unknown error' }); } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('Error creating commit:', error instanceof Error ? error : undefined, { message: errorMessage }); return { success: false, output: '', error: errorMessage }; } } /** * Generate commit configuration from tool context */ generateCommitConfig(toolName: string, input: any, result: any): ConventionalCommitConfig { const type = this.determineCommitType(toolName, input, result); const scope = this.determineCommitScope(toolName, input); const description = this.generateCommitDescription(toolName, input, result); // Determine if this is a breaking change const breakingChange = this.isBreakingChange(toolName, input, result); const config: ConventionalCommitConfig = { type, scope, description, breakingChange }; // Add body for significant changes if (type === 'feat' && input.status === 'completed') { config.body = `Task ${input.number} has been completed successfully.`; } return config; } /** * Determine if change is breaking */ private isBreakingChange(toolName: string, input: any, result: any): boolean { // For now, we'll be conservative and not automatically mark things as breaking // This can be enhanced based on specific project needs return false; } /** * Generate git automation instructions for agent */ generateGitInstructions(toolName: string, input: any, result: any): { gitCommitRequired: boolean; commitConfig: ConventionalCommitConfig; commitMessage: string; filesToStage: string[]; preCommitChecks: string[]; semanticVersionImpact: SemVerImpact; } { const commitConfig = this.generateCommitConfig(toolName, input, result); const commitMessage = this.generateCommitMessage(commitConfig); const semanticVersionImpact = this.determineSemVerImpact(commitConfig); return { gitCommitRequired: true, commitConfig, commitMessage, filesToStage: ['.'], // Stage all changes by default preCommitChecks: [ 'git status', 'git diff --staged' ], semanticVersionImpact }; } } /** * Convenience function to generate commit message */ export function generateCommitMessage(config: ConventionalCommitConfig): string { return ConventionalCommitsManager.getInstance().generateCommitMessage(config); } /** * Convenience function to create commit */ export async function createCommit(config: ConventionalCommitConfig): Promise<GitOperationResult> { return ConventionalCommitsManager.getInstance().createCommit(config); } /** * Convenience function to generate git instructions */ export function generateGitInstructions(toolName: string, input: any, result: any) { return ConventionalCommitsManager.getInstance().generateGitInstructions(toolName, input, result); } /** * Convenience function to get git status */ export async function getGitStatus(): Promise<GitOperationResult> { return ConventionalCommitsManager.getInstance().getGitStatus(); } /** * Convenience function to stage files */ export async function stageFiles(files?: string[]): Promise<GitOperationResult> { return ConventionalCommitsManager.getInstance().stageFiles(files); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/PixdataOrg/coderide-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server