UNPKG

ai-github-changelog-generator-cli-mcp

Version:

AI-powered changelog generator with MCP server support - works with OpenAI GPT-4.1 and Azure OpenAI with reasoning models

1,366 lines (1,168 loc) โ€ข 111 kB
#!/usr/bin/env node // Load environment variables from .env.local require('dotenv').config({ path: '.env.local' }); const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); // AI Provider (supports OpenAI and Azure OpenAI) const AIProvider = require('./ai-provider'); const GitManager = require('./git-manager'); const ConfigManager = require('./config'); const colors = require('./colors'); // Dynamic import for inquirer since it's an ES module let inquirer; async function getInquirer() { if (!inquirer) { inquirer = await import('inquirer'); } return inquirer.default; } // Configuration const CHANGELOG_FILE = 'AI_CHANGELOG.md'; const COMMIT_TYPES = { feat: '๐Ÿš€ Features', fix: '๐Ÿ› Bug Fixes', docs: '๐Ÿ“š Documentation', style: '๐Ÿ’Ž Styling', refactor: 'โ™ป๏ธ Code Refactoring', perf: 'โšก Performance Improvements', test: '๐Ÿงช Tests', chore: '๐Ÿ”ง Maintenance', ci: '๐Ÿ‘ท CI/CD', build: '๐Ÿ“ฆ Build System', db: '๐Ÿ—„๏ธ Database', api: '๐Ÿ”Œ API Changes', ui: '๐ŸŽจ UI/UX', auth: '๐Ÿ” Authentication', config: 'โš™๏ธ Configuration', security: '๐Ÿ” Security', deps: '๐Ÿ“ฆ Dependencies' }; class AIChangelogGenerator { constructor(options = {}) { this.analysisMode = 'standard'; this.modelOverride = null; // New: Allow model override this.dryRun = options.dryRun || false; // Add dry-run mode this.noColor = options.noColor || false; // Add option to disable colors this.includeAttribution = options.includeAttribution !== false; // Add attribution option (enabled by default) this.configManager = new ConfigManager(); this.metrics = { startTime: Date.now(), commitsProcessed: 0, apiCalls: 0, errors: 0, batchesProcessed: 0, totalTokens: 0, cacheHits: 0 }; // Configure colors if (this.noColor || process.env.NO_COLOR) { colors.disable(); } // Initialize components this.initializeComponents(); } initializeComponents() { try { // Initialize git manager this.gitManager = new GitManager(); this.gitExists = this.gitManager.isGitRepo; // Initialize AI provider this.aiProvider = new AIProvider(); this.hasAI = this.aiProvider.isAvailable; if (!this.hasAI) { console.log(colors.warningMessage('No AI provider configured. Using rule-based analysis...')); console.log(colors.infoMessage('Configure AZURE_OPENAI_* or OPENAI_API_KEY in .env.local for AI-powered analysis')); } else { console.log(colors.aiMessage(`AI Provider: ${colors.highlight(this.aiProvider.activeProvider.toUpperCase())} (${this.aiProvider.getProviderInfo()})`)); // Enhanced: Log model capabilities this.logModelCapabilities(); } } catch (error) { console.error(colors.errorMessage(`Initialization failed: ${error.message}`)); this.gitExists = false; this.hasAI = false; this.metrics.errors++; } } // Enhanced: Log available model capabilities logModelCapabilities() { if (!this.hasAI) return; const defaultModel = this.aiProvider.modelConfig.default; const capabilities = this.aiProvider.getModelCapabilities(defaultModel); const features = []; if (capabilities.reasoning) features.push('๐Ÿง  Advanced Reasoning'); if (capabilities.largeContext) features.push('๐Ÿ“š 1M Token Context'); if (capabilities.promptCaching) features.push('๐Ÿ’ฐ 75% Cost Reduction'); if (capabilities.codingOptimized) features.push('โšก Coding Optimized'); if (features.length > 0) { console.log(colors.infoMessage(`Model Features: ${colors.highlight(features.join(', '))}`)); } } setAnalysisMode(mode) { const validModes = ['standard', 'detailed', 'enterprise']; if (validModes.includes(mode)) { this.analysisMode = mode; console.log(colors.metricsMessage(`Analysis mode set to: ${colors.highlight(mode)}`)); } else { console.warn(colors.warningMessage(`Invalid analysis mode: ${mode}. Using standard mode.`)); this.analysisMode = 'standard'; } } // Enhanced: Smart model selection based on commit complexity async selectOptimalModel(commitAnalysis) { if (!this.hasAI) return null; // Check for model override first if (this.modelOverride) { console.log(`๐ŸŽฏ Using model override: ${this.modelOverride}`); return this.modelOverride; } const { files, diffStats, breaking, semanticAnalysis } = commitAnalysis; const filesCount = files?.length || 0; const linesChanged = (diffStats?.insertions || 0) + (diffStats?.deletions || 0); // Detect complex patterns const hasArchitecturalChanges = semanticAnalysis?.patterns?.includes('refactor') || semanticAnalysis?.patterns?.includes('architecture') || semanticAnalysis?.frameworks?.length > 2; try { // Enhanced: Use AI provider's intelligent model selection const commitInfo = { files: files?.map(f => f.filePath) || [], additions: diffStats?.insertions || 0, deletions: diffStats?.deletions || 0, message: commitAnalysis.subject, breaking, complex: hasArchitecturalChanges }; const optimalModel = await this.aiProvider.selectOptimalModel(commitInfo); if (optimalModel?.model) { console.log(colors.infoMessage(`Selected model: ${colors.highlight(optimalModel.model)} for commit (${colors.number(filesCount)} files, ${colors.number(linesChanged)} lines)`)); if (optimalModel.capabilities?.reasoning) { console.log(colors.aiMessage('Using reasoning model for complex analysis')); } return optimalModel.model; } } catch (error) { console.warn(colors.warningMessage(`Model selection failed: ${error.message}, using default`)); } return this.aiProvider.modelConfig.default; } // commit analysis with comprehensive diff understanding async getCommitAnalysis(commitHash) { try { // Validate commit hash first if (!this.gitManager.validateCommitHash(commitHash)) { console.warn(colors.warningMessage(`Invalid commit hash: ${colors.hash(commitHash)}`)); return null; } // Get comprehensive commit information const commitInfo = this.gitManager.execGit(`git show --pretty=format:"%H|%s|%an|%ad|%B" --no-patch ${commitHash}`); const lines = commitInfo.split('\n'); const [hash, subject, author, date] = lines[0].split('|'); const body = lines.slice(1).join('\n').trim(); // Get files with detailed analysis const filesCommand = `git show --name-status --pretty=format: ${commitHash}`; const filesOutput = this.gitManager.execGitSafe(filesCommand); const files = await Promise.all( filesOutput.split('\n') .filter(Boolean) .map(async (line) => { const parts = line.split('\t'); if (parts.length < 2) return null; const [status, filePath] = parts; return await this.analyzeFileChange(commitHash, status, filePath); }) ); // Filter out null entries const validFiles = files.filter(Boolean); // Get overall diff statistics const diffStats = this.getCommitDiffStats(commitHash); const analysis = { hash: hash.substring(0, 7), fullHash: hash, subject, author, date, body, files: validFiles, diffStats, type: this.extractCommitType(subject), scope: this.extractCommitScope(subject), breaking: this.isBreakingChange(subject, body), semanticAnalysis: this.performSemanticAnalysis(validFiles, subject, body), complexity: this.assessOverallComplexity(validFiles, diffStats), riskAssessment: this.assessRisk(validFiles, diffStats, subject, body), messageQuality: this.assessCommitMessageQuality(subject, body) }; this.metrics.commitsProcessed++; return analysis; } catch (error) { console.error(colors.errorMessage(`Error analyzing commit ${colors.hash(commitHash)}: ${error.message}`)); this.metrics.errors++; return null; } } // Assess overall complexity assessOverallComplexity(files, diffStats) { const filesCount = files.length; const linesChanged = (diffStats.insertions || 0) + (diffStats.deletions || 0); let complexityScore = 0; // File count impact if (filesCount > 50) complexityScore += 5; else if (filesCount > 20) complexityScore += 3; else if (filesCount > 10) complexityScore += 2; else if (filesCount > 5) complexityScore += 1; // Lines changed impact if (linesChanged > 5000) complexityScore += 5; else if (linesChanged > 1000) complexityScore += 3; else if (linesChanged > 500) complexityScore += 2; else if (linesChanged > 100) complexityScore += 1; // File type diversity const categories = new Set(files.map(f => f.category)); if (categories.size > 4) complexityScore += 2; else if (categories.size > 2) complexityScore += 1; // Determine complexity level let level = 'minimal'; if (complexityScore >= 8) level = 'very high'; else if (complexityScore >= 6) level = 'high'; else if (complexityScore >= 4) level = 'medium'; else if (complexityScore >= 2) level = 'low'; return { level, score: complexityScore, factors: { filesCount, linesChanged, categoriesCount: categories.size } }; } // Risk assessment assessRisk(files, diffStats, subject, body) { let riskScore = 0; const riskFactors = []; // Breaking changes if (this.isBreakingChange(subject, body)) { riskScore += 5; riskFactors.push('Breaking changes detected'); } // Database changes if (files.some(f => f.category === 'database')) { riskScore += 3; riskFactors.push('Database schema changes'); } // Configuration changes if (files.some(f => f.category === 'config')) { riskScore += 2; riskFactors.push('Configuration changes'); } // Large scale changes if (files.length > 50 || (diffStats.insertions + diffStats.deletions) > 2000) { riskScore += 2; riskFactors.push('Large scale changes'); } // Security-related changes const securityKeywords = ['auth', 'security', 'password', 'token', 'permission']; if (securityKeywords.some(keyword => subject.toLowerCase().includes(keyword) || body.toLowerCase().includes(keyword))) { riskScore += 3; riskFactors.push('Security-related changes'); } let level = 'low'; if (riskScore >= 8) level = 'critical'; else if (riskScore >= 6) level = 'high'; else if (riskScore >= 4) level = 'medium'; else if (riskScore >= 2) level = 'low-medium'; return { level, score: riskScore, factors: riskFactors }; } // Deep file change analysis async analyzeFileChange(commitHash, status, filePath) { try { // Get file diff with context - improved error handling const diffCommand = `git show ${commitHash} --pretty=format: -U5 -- "${filePath}"`; let diff = ''; // Use the safer git show method that suppresses stderr diff = this.gitManager.execGitShow(diffCommand); if (diff === null) { // File doesn't exist or other git error occurred if (status === 'D') { diff = 'File deleted in this commit'; } else { // For missing files, provide a clean user message without git details console.warn(colors.warningMessage(`โš ๏ธ File ${colors.file(filePath)} not available (renamed, moved, or binary)`)); diff = 'File content unavailable (renamed, moved, or binary)'; } } else if (!diff || diff.trim() === '') { // Empty diff handling if (status === 'A') { diff = 'New file created'; } else { diff = 'No changes detected (binary or empty file)'; } } // Get file content context with better error handling let beforeContent = ''; let afterContent = ''; if (status !== 'A' && !diff.includes('not available')) { const beforeResult = this.gitManager.execGitShow(`git show ${commitHash}~1:"${filePath}"`); beforeContent = beforeResult ? beforeResult.slice(0, 1000) : ''; } if (status !== 'D' && !diff.includes('not available')) { const afterResult = this.gitManager.execGitShow(`git show ${commitHash}:"${filePath}"`); afterContent = afterResult ? afterResult.slice(0, 1000) : ''; } return { status, filePath, diff, beforeContent, afterContent, category: this.categorizeFile(filePath), language: this.detectLanguage(filePath), importance: this.assessFileImportance(filePath, status), complexity: this.assessChangeComplexity(diff), semanticChanges: this.analyzeSemanticChanges(diff, filePath), functionalImpact: this.analyzeFunctionalImpact(diff, filePath, status), businessRelevance: this.assessBusinessRelevance(filePath, diff), fileExists: !diff.includes('not found') && !diff.includes('does not exist') }; } catch (error) { console.error(colors.errorMessage(`Error analyzing file ${colors.file(filePath)}: ${error.message}`)); this.metrics.errors++; // Return minimal file info even on error return { status, filePath, diff: 'Analysis failed', beforeContent: '', afterContent: '', category: this.categorizeFile(filePath), language: this.detectLanguage(filePath), importance: 1, complexity: 1, semanticChanges: { patterns: [], frameworks: [], codeElements: [] }, functionalImpact: null, businessRelevance: 1, fileExists: false, error: error.message }; } } // Extract commit type from conventional commit format extractCommitType(subject) { if (!subject) return 'other'; const match = subject.match(/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert|security|deps|config|ui|api|db|wip)(\(.+\))?!?:/i); return match ? match[1].toLowerCase() : this.inferTypeFromMessage(subject); } inferTypeFromMessage(message) { if (!message) return 'other'; const lowerMessage = message.toLowerCase(); // Common patterns for type inference if (lowerMessage.includes('add') || lowerMessage.includes('new') || lowerMessage.includes('implement')) return 'feat'; if (lowerMessage.includes('fix') || lowerMessage.includes('bug') || lowerMessage.includes('resolve')) return 'fix'; if (lowerMessage.includes('update') || lowerMessage.includes('upgrade') || lowerMessage.includes('improve')) return 'refactor'; if (lowerMessage.includes('remove') || lowerMessage.includes('delete') || lowerMessage.includes('clean')) return 'chore'; if (lowerMessage.includes('test') || lowerMessage.includes('spec')) return 'test'; if (lowerMessage.includes('doc') || lowerMessage.includes('readme')) return 'docs'; if (lowerMessage.includes('style') || lowerMessage.includes('format') || lowerMessage.includes('lint')) return 'style'; if (lowerMessage.includes('performance') || lowerMessage.includes('perf') || lowerMessage.includes('optimize')) return 'perf'; if (lowerMessage.includes('security') || lowerMessage.includes('vulnerability')) return 'security'; if (lowerMessage.includes('config') || lowerMessage.includes('setting')) return 'config'; if (lowerMessage.includes('build') || lowerMessage.includes('deploy') || lowerMessage.includes('release')) return 'build'; if (lowerMessage.includes('ci') || lowerMessage.includes('workflow') || lowerMessage.includes('action')) return 'ci'; if (lowerMessage.includes('depend') || lowerMessage.includes('package') || lowerMessage.includes('version')) return 'deps'; return 'other'; } // Extract scope from conventional commit format extractCommitScope(subject) { if (!subject) return null; const match = subject.match(/^[a-z]+\((.+)\)!?:/i); return match ? match[1] : null; } // Check if commit represents a breaking change isBreakingChange(subject, body) { if (!subject) return false; return subject.includes('!:') || subject.toUpperCase().includes('BREAKING') || body.includes('BREAKING CHANGE:') || body.includes('BREAKING-CHANGE:') || body.toUpperCase().includes('BREAKING CHANGE'); } // Categorize file by type categorizeFile(filePath) { if (!filePath) return 'other'; const pathLower = filePath.toLowerCase(); if (pathLower.match(/\.(ts|tsx|js|jsx)$/)) return 'source'; if (pathLower.match(/\.(css|scss|sass|less|styl)$/)) return 'style'; if (pathLower.match(/\.(json|yaml|yml|toml|ini|xml)$/)) return 'config'; if (pathLower.match(/\.(md|txt|rst|adoc)$/)) return 'docs'; if (pathLower.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/)) return 'test'; if (pathLower.match(/\.(sql|prisma)$/) || pathLower.includes('migration')) return 'database'; if (pathLower.match(/\.(png|jpg|jpeg|gif|svg|webp|ico)$/)) return 'asset'; if (pathLower.match(/\.(sh|bash|ps1|bat)$/)) return 'script'; return 'other'; } // Detect programming language detectLanguage(filePath) { if (!filePath) return 'Unknown'; const ext = filePath.split('.').pop()?.toLowerCase(); const languageMap = { 'ts': 'TypeScript', 'tsx': 'TypeScript React', 'js': 'JavaScript', 'jsx': 'JavaScript React', 'py': 'Python', 'java': 'Java', 'c': 'C', 'cpp': 'C++', 'go': 'Go', 'rs': 'Rust', 'php': 'PHP', 'rb': 'Ruby', 'swift': 'Swift', 'kt': 'Kotlin', 'css': 'CSS', 'scss': 'SCSS', 'sass': 'Sass', 'html': 'HTML', 'json': 'JSON', 'md': 'Markdown', 'sql': 'SQL', 'yml': 'YAML', 'yaml': 'YAML', 'toml': 'TOML', 'sh': 'Shell', 'bash': 'Bash' }; return languageMap[ext] || 'Unknown'; } // Assess file importance assessFileImportance(filePath, status) { if (!filePath) return 1; let score = 0; // Core application files if (filePath.match(/\/(pages|app|src|components)\//)) score += 3; if (filePath.match(/\/(api|server|backend)\//)) score += 3; if (filePath.match(/\/(utils|lib|helpers)\//)) score += 2; // Configuration files if (filePath.match(/^(package\.json|tsconfig|webpack\.config|babel\.config)/)) score += 3; if (filePath.match(/\.env|dockerfile|docker-compose/i)) score += 2; // Database/Schema files if (filePath.match(/\/(migrations|schema|database)\//)) score += 3; // Status impact if (status === 'A') score += 2; // New files are important if (status === 'D') score += 1; // Deleted files are notable return Math.min(score, 5); // Cap at 5 } // Assess change complexity assessChangeComplexity(diff) { if (!diff || diff === 'Binary file or diff unavailable') return 1; const lines = diff.split('\n'); const additions = lines.filter(line => line.startsWith('+')).length; const deletions = lines.filter(line => line.startsWith('-')).length; const total = additions + deletions; if (total < 10) return 1; if (total < 50) return 2; if (total < 100) return 3; if (total < 200) return 4; return 5; } // semantic analysis analyzeSemanticChanges(diff, filePath) { const analysis = { changeType: 'modification', patterns: new Set(), frameworks: new Set(), keywords: new Set(), codeElements: new Set(), apiChanges: [], dataChanges: [] }; if (!diff || diff === 'Binary file or diff unavailable') { return this.convertSetsToArrays(analysis); } const addedLines = diff.split('\n').filter(line => line.startsWith('+') && !line.startsWith('+++')); const removedLines = diff.split('\n').filter(line => line.startsWith('-') && !line.startsWith('---')); // Framework detection if (filePath.includes('database/') || filePath.includes('sql/') || filePath.includes('migrations/')) { analysis.frameworks.add('Database'); if (diff.includes('CREATE TABLE') || diff.includes('ALTER TABLE')) { analysis.changeType = 'schema_change'; analysis.patterns.add('database_schema'); } if (diff.includes('CREATE POLICY') || diff.includes('ALTER POLICY')) { analysis.patterns.add('security_policy'); } } if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) { analysis.frameworks.add('React'); if (diff.includes('useState') || diff.includes('useEffect')) { analysis.patterns.add('react_hooks'); } if (diff.includes('useCallback') || diff.includes('useMemo')) { analysis.patterns.add('performance_optimization'); } } if (filePath.includes('/api/') || filePath.includes('route.')) { analysis.frameworks.add('API'); analysis.changeType = 'api_change'; ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => { if (diff.includes(`export async function ${method}`) || diff.includes(`app.${method.toLowerCase()}`)) { analysis.apiChanges.push(`${method} endpoint`); analysis.patterns.add('api_endpoint'); } }); } // Code element detection const codePatterns = { 'function_definition': /(?:export\s+)?(?:async\s+)?function\s+(\w+)/g, 'component_definition': /(?:export\s+)?(?:const|function)\s+(\w+Component|\w+Page|\w+Layout)/g, 'hook_definition': /(?:export\s+)?(?:const|function)\s+(use\w+)/g, 'type_definition': /(?:export\s+)?(?:type|interface)\s+(\w+)/g, 'constant_definition': /(?:export\s+)?const\s+(\w+)/g }; Object.entries(codePatterns).forEach(([pattern, regex]) => { const matches = [...diff.matchAll(regex)]; if (matches.length > 0) { analysis.patterns.add(pattern); matches.forEach(match => analysis.codeElements.add(match[1])); } }); // Advanced pattern detection const advancedPatterns = { 'error_handling': [/try\s*{/, /catch\s*\(/, /throw\s+/, /Error\(/], 'async_operations': [/async\s+/, /await\s+/, /Promise\./, /\.then\(/], 'data_validation': [/validate/, /schema/, /validation/, /validator/i], 'authentication': [/auth/, /login/, /logout/, /token/, /jwt/, /session/i], 'authorization': [/permission/, /role/, /access/, /policy/, /guard/i], 'caching': [/cache/, /memo/, /useMemo/, /useCallback/i], 'testing': [/test/, /spec/, /mock/, /describe/, /it\(/], 'styling': [/className/, /css/, /styled/, /style/], 'state_management': [/useState/, /useReducer/, /store/, /state/i], 'routing': [/router/, /navigate/, /redirect/, /route/, /Link/], 'data_fetching': [/fetch/, /axios/, /useQuery/, /useMutation/, /api/i] }; Object.entries(advancedPatterns).forEach(([pattern, regexes]) => { if (regexes.some(regex => regex.test(diff))) { analysis.patterns.add(pattern); } }); return this.convertSetsToArrays(analysis); } convertSetsToArrays(analysis) { return { ...analysis, patterns: Array.from(analysis.patterns), frameworks: Array.from(analysis.frameworks), keywords: Array.from(analysis.keywords), codeElements: Array.from(analysis.codeElements) }; } // Analyze functional impact analyzeFunctionalImpact(diff, filePath, status) { const impact = { scope: this.determineChangeScope(filePath), breaking: false, userFacing: false, apiChanges: false, dataChanges: false, securityRelated: false, performanceImpact: false, migrationRequired: false, deploymentImpact: 'low' }; if (!diff || diff === 'Binary file or diff unavailable') { return impact; } // Breaking change detection const breakingPatterns = [ /BREAKING\s*CHANGE/i, /export\s+(?:interface|type)\s+\w+.*{/, /export\s+(?:const|function)\s+\w+/, /DROP\s+TABLE/i, /ALTER\s+TABLE.*DROP/i, /removeField/i, /deleteColumn/i ]; impact.breaking = breakingPatterns.some(pattern => pattern.test(diff)); // User-facing detection const userFacingPaths = [ '/pages/', '/app/', '/src/', '/components/', '/views/', '/templates/', '/public/', '.css', '.scss', '.html' ]; impact.userFacing = userFacingPaths.some(path => filePath.includes(path)); // API changes detection impact.apiChanges = filePath.includes('/api/') || filePath.includes('route.') || diff.includes('export async function') || diff.includes('app.get') || diff.includes('app.post'); // Database changes detection impact.dataChanges = filePath.includes('database/') || filePath.includes('migration') || filePath.includes('schema') || diff.includes('CREATE TABLE') || diff.includes('ALTER TABLE'); // Security-related detection const securityPatterns = [ /auth/i, /security/i, /password/i, /token/i, /jwt/i, /encrypt/i, /decrypt/i, /hash/i, /validate/i, /sanitize/i, /policy/i, /permission/i, /role/i, /access/i, /cors/i ]; impact.securityRelated = securityPatterns.some(pattern => pattern.test(diff)); // Performance impact detection const performancePatterns = [ /lazy/i, /memo/i, /callback/i, /optimize/i, /cache/i, /index/i, /query/i, /batch/i, /concurrent/i, /async/i ]; impact.performanceImpact = performancePatterns.some(pattern => pattern.test(diff)); // Migration requirement detection impact.migrationRequired = impact.breaking || impact.dataChanges || filePath.includes('package.json') || filePath.includes('.env'); // Deployment impact assessment if (impact.breaking || impact.dataChanges) { impact.deploymentImpact = 'high'; } else if (impact.apiChanges || impact.securityRelated || filePath.includes('config')) { impact.deploymentImpact = 'medium'; } return impact; } // Business relevance assessment assessBusinessRelevance(filePath, diff) { const relevance = { priority: 'low', userImpact: 'minimal', businessValue: 'technical', customerFacing: false, revenueImpact: false }; if (!filePath) return relevance; // High priority paths const highPriorityPaths = [ '/dashboard', '/billing', '/auth', '/onboarding', '/checkout', '/payment', '/subscription', '/profile' ]; const mediumPriorityPaths = [ '/app/', '/components/ui', '/components/forms', '/api/auth', '/api/users' ]; if (highPriorityPaths.some(path => filePath.includes(path))) { relevance.priority = 'high'; relevance.customerFacing = true; relevance.businessValue = 'direct'; } else if (mediumPriorityPaths.some(path => filePath.includes(path))) { relevance.priority = 'medium'; relevance.customerFacing = true; relevance.businessValue = 'indirect'; } // Revenue impact detection if (diff) { const revenueKeywords = [ /payment/i, /billing/i, /subscription/i, /checkout/i, /revenue/i, /pricing/i, /plan/i, /upgrade/i, /downgrade/i ]; relevance.revenueImpact = revenueKeywords.some(pattern => pattern.test(diff)); } return relevance; } // Perform semantic analysis performSemanticAnalysis(files, subject, body) { const analysis = { hasApiChanges: false, hasDbChanges: false, hasUiChanges: false, hasConfigChanges: false, affectedFeatures: [], riskLevel: 'low', patterns: new Set(), frameworks: [], codeElements: new Set() }; // Analyze each file for deeper semantic understanding files.forEach(file => { if (file.filePath.includes('/api/')) { analysis.hasApiChanges = true; analysis.frameworks.push('API'); } if (file.filePath.includes('migration') || file.filePath.includes('schema')) { analysis.hasDbChanges = true; analysis.frameworks.push('Database'); } if (file.filePath.match(/\.(tsx|jsx|css|scss)$/)) { analysis.hasUiChanges = true; analysis.frameworks.push('React'); } if (file.filePath.match(/config|\.env/)) { analysis.hasConfigChanges = true; analysis.patterns.add('configuration'); } // Merge file semantic changes if (file.semanticChanges) { if (file.semanticChanges.frameworks) { file.semanticChanges.frameworks.forEach(fw => { if (!analysis.frameworks.includes(fw)) { analysis.frameworks.push(fw); } }); } if (file.semanticChanges.patterns) { file.semanticChanges.patterns.forEach(pattern => { analysis.patterns.add(pattern); }); } if (file.semanticChanges.codeElements) { file.semanticChanges.codeElements.forEach(element => { analysis.codeElements.add(element); }); } } }); // Analyze commit message patterns const message = (subject + ' ' + body).toLowerCase(); if (message.includes('feat') || message.includes('add') || message.includes('new')) { analysis.patterns.add('new_feature'); } if (message.includes('fix') || message.includes('bug') || message.includes('resolve')) { analysis.patterns.add('bug_fix'); } if (message.includes('perf') || message.includes('optim') || message.includes('speed')) { analysis.patterns.add('performance_optimization'); } if (message.includes('security') || message.includes('auth') || message.includes('permission')) { analysis.patterns.add('security_policy'); } // Convert sets to arrays analysis.codeElements = Array.from(analysis.codeElements); analysis.patterns = Array.from(analysis.patterns); // Determine risk level if (analysis.hasApiChanges || analysis.hasDbChanges) { analysis.riskLevel = 'high'; } else if (analysis.hasUiChanges || analysis.hasConfigChanges) { analysis.riskLevel = 'medium'; } return analysis; } // Generate release insights async generateReleaseInsights(analyzedCommits, version) { const insights = { summary: '', totalCommits: analyzedCommits.length, commitTypes: {}, riskLevel: 'low', affectedAreas: new Set(), breaking: false, complexity: 'low', businessImpact: 'minor', deploymentRequirements: [] }; // Count commit types and assess risk analyzedCommits.forEach(commit => { insights.commitTypes[commit.type] = (insights.commitTypes[commit.type] || 0) + 1; if (commit.breaking) insights.breaking = true; if (commit.semanticAnalysis?.riskLevel === 'high') insights.riskLevel = 'high'; commit.files.forEach(file => { insights.affectedAreas.add(file.category); }); // Check for deployment requirements if (commit.semanticAnalysis?.hasDbChanges) { insights.deploymentRequirements.push('Database migration required'); } if (commit.breaking) { insights.deploymentRequirements.push('Breaking changes - review migration notes above.'); } }); insights.affectedAreas = Array.from(insights.affectedAreas); // Assess overall complexity const totalFiles = analyzedCommits.reduce((sum, commit) => sum + commit.files.length, 0); const avgFilesPerCommit = totalFiles / analyzedCommits.length; if (avgFilesPerCommit > 20 || insights.breaking) { insights.complexity = 'high'; } else if (avgFilesPerCommit > 10) { insights.complexity = 'medium'; } // Assess business impact const hasUserFacingChanges = analyzedCommits.some(commit => commit.files.some(file => file.functionalImpact?.userFacing)); const hasRevenueImpact = analyzedCommits.some(commit => commit.files.some(file => file.businessRelevance?.revenueImpact)); if (hasRevenueImpact || insights.breaking) { insights.businessImpact = 'major'; } else if (hasUserFacingChanges) { insights.businessImpact = 'moderate'; } // Generate summary const features = insights.commitTypes.feat || 0; const fixes = insights.commitTypes.fix || 0; const breaking = insights.breaking ? ' with breaking changes' : ''; insights.summary = `Release includes ${features} new features, ${fixes} bug fixes${breaking}`; return insights; } // Batch processing for large repositories async generateChangelogBatch(commitHashes) { const batchSize = 10; // Process in batches to avoid rate limits const results = []; for (let i = 0; i < commitHashes.length; i += batchSize) { const batch = commitHashes.slice(i, i + batchSize); const batchNum = Math.floor(i/batchSize) + 1; const totalBatches = Math.ceil(commitHashes.length/batchSize); console.log(colors.processingMessage(`Processing batch ${colors.highlight(`${batchNum}/${totalBatches}`)} (${colors.number(batch.length)} commits)`)); // Show progress bar console.log(colors.progress(batchNum, totalBatches, 'batches processed')); const batchPromises = batch.map(hash => this.getCommitAnalysis(hash)); const batchResults = await Promise.allSettled(batchPromises); const successfulResults = batchResults .filter(r => r.status === 'fulfilled' && r.value) .map(r => r.value); results.push(...successfulResults); this.metrics.batchesProcessed++; // Rate limiting between batches if (i + batchSize < commitHashes.length && this.hasAI) { await this.sleep(1000); // 1 second between batches } } return results.filter(Boolean); } // AI integration with better error handling and retry logic async generateAISummary(commitAnalysis) { if (!this.hasAI) { return this.generateRuleBasedSummary(commitAnalysis); } // Select optimal model for this commit const selectedModel = await this.selectOptimalModel(commitAnalysis); try { // Validate AI provider before generating const modelCheck = await this.aiProvider.validateModelAvailability(selectedModel || this.aiProvider.modelConfig.default); if (!modelCheck.available) { console.warn(colors.warningMessage('Selected model not available, falling back to rule-based analysis')); return this.generateRuleBasedSummary(commitAnalysis); } const prompt = this.buildEnhancedPrompt(commitAnalysis); const messages = [ { role: "system", content: `You are an expert technical writer and software engineer specializing in changelog generation for modern web applications. You understand various frameworks, databases, and development practices. Your task is to analyze git commits and generate clear, informative changelog entries that balance technical accuracy with user-friendly language. IMPORTANT: Always respond with valid JSON in the exact format requested. Focus on user impact and business value while maintaining technical precision.` }, { role: "user", content: prompt } ]; const settings = { temperature: 0.3, max_tokens: 1000, model: selectedModel }; // Add reasoning effort for reasoning models if (selectedModel && (selectedModel.includes('o3') || selectedModel.includes('o4'))) { settings.reasoning_effort = this.analysisMode === 'enterprise' ? 'high' : 'medium'; } const response = await this.aiProvider.generateCompletion(messages, settings); this.metrics.apiCalls++; if (response.usage) { this.metrics.totalTokens += response.usage.total_tokens || 0; } if (!response.content) { throw new Error('Empty response from AI provider'); } return this.parseAIResponse(response.content, commitAnalysis); } catch (error) { console.error(colors.errorMessage(`AI API error: ${error.message}`)); this.metrics.errors++; return this.generateRuleBasedSummary(commitAnalysis); } } // Build prompt optimized for GPT-4.1 series buildEnhancedPrompt(commitAnalysis) { const { subject, files, semanticAnalysis, diffStats, complexity, riskAssessment } = commitAnalysis; // Build comprehensive context const filesContext = files.map(file => ({ path: file.filePath, status: file.status, category: file.category, language: file.language, complexity: file.complexity, semanticChanges: file.semanticChanges, functionalImpact: file.functionalImpact, businessRelevance: file.businessRelevance, keyChanges: this.extractKeyDiffLines(file.diff) })).slice(0, 15); // Limit for token efficiency but increased for GPT-4.1 // prompt leveraging GPT-4.1's improved instruction following return `<task> Analyze this git commit for changelog generation using your reasoning capabilities. <commit_context> Subject: ${subject} Files changed: ${files.length} Lines: +${diffStats.insertions} -${diffStats.deletions} Frameworks: ${semanticAnalysis.frameworks.join(', ')} Patterns: ${semanticAnalysis.patterns.join(', ')} Complexity: ${complexity.level} (score: ${complexity.score}) Risk Level: ${riskAssessment.level} Risk Factors: ${riskAssessment.factors.join(', ')} </commit_context> <files_analysis> ${JSON.stringify(filesContext, null, 2)} </files_analysis> <analysis_requirements> 1. **Primary Impact**: What does this change do for end users? 2. **Technical Scope**: How does this affect the codebase architecture? 3. **Business Value**: What problem does this solve or feature does this enable? 4. **Risk Assessment**: What are the potential impacts of this change? 5. **Migration Needs**: Are there any breaking changes or upgrade requirements? 6. **Performance Impact**: How might this affect system performance? </analysis_requirements> <response_format> Return ONLY valid JSON in this exact structure: { "summary": "Clear, user-friendly description (1-2 sentences)", "technicalSummary": "Detailed technical description for developers", "category": "feature|fix|improvement|refactor|docs|chore|breaking|security", "impact": "critical|high|medium|low", "scope": "major|minor|patch", "userFacing": true|false, "breaking": true|false, "businessImpact": "How this affects users, business goals, or product value", "technicalImpact": "How this affects codebase, architecture, or development", "highlights": ["key point 1", "key point 2", "key point 3"], "migrationNotes": "Steps needed for upgrade/deployment" or null, "tags": ["tag1", "tag2", "tag3"], "relatedAreas": ["area1", "area2"], "riskLevel": "low|medium|high", "confidence": 0.9 } </response_format> </task>`; } parseAIResponse(content, originalCommit) { try { // Clean the response let jsonStr = content.trim(); // Remove markdown code blocks if (jsonStr.startsWith('```json')) { jsonStr = jsonStr.replace(/^```json\s*/, '').replace(/\s*```$/, ''); } else if (jsonStr.startsWith('```')) { jsonStr = jsonStr.replace(/^```\s*/, '').replace(/\s*```$/, ''); } const parsed = JSON.parse(jsonStr); // Validate and enhance the response const baseResponse = { summary: parsed.summary || originalCommit.subject, technicalSummary: parsed.technicalSummary || '', category: parsed.category || originalCommit.type || 'other', impact: parsed.impact || 'low', scope: parsed.scope || 'patch', userFacing: Boolean(parsed.userFacing), breaking: Boolean(parsed.breaking), businessImpact: parsed.businessImpact || '', technicalImpact: parsed.technicalImpact || '', highlights: Array.isArray(parsed.highlights) ? parsed.highlights : [], migrationNotes: parsed.migrationNotes || null, tags: Array.isArray(parsed.tags) ? parsed.tags : [], relatedAreas: Array.isArray(parsed.relatedAreas) ? parsed.relatedAreas : [], riskLevel: parsed.riskLevel || 'low', confidence: parsed.confidence || 0.8 }; return baseResponse; } catch (error) { console.error(colors.errorMessage(`Error parsing AI response: ${error.message}`)); this.metrics.errors++; return this.generateRuleBasedSummary(originalCommit); } } // rule-based analysis as fallback generateRuleBasedSummary(commitAnalysis) { const { subject, files, semanticAnalysis, diffStats, complexity, riskAssessment, messageQuality } = commitAnalysis; // Check for poor commit message quality and warn users if (messageQuality && messageQuality.level === 'very_poor' || messageQuality.level === 'poor') { console.warn(colors.warningMessage(`โš ๏ธ Poor commit message quality detected: "${subject}"`)); console.warn(colors.warningMessage(` Quality: ${messageQuality.level} (score: ${messageQuality.score}/8)`)); if (messageQuality.issues.length > 0) { console.warn(colors.warningMessage(` Issues: ${messageQuality.issues.join(', ')}`)); } if (messageQuality.suggestions.length > 0) { console.warn(colors.infoMessage(` ๐Ÿ’ก Suggestions: ${messageQuality.suggestions.join(', ')}`)); } } // Determine category and impact let category = commitAnalysis.type || 'other'; let impact = 'low'; let userFacing = false; // Enhanced analysis using complexity and risk assessment if (riskAssessment.level === 'critical' || riskAssessment.level === 'high') { impact = riskAssessment.level; category = 'breaking'; } else if (complexity.level === 'high' || complexity.level === 'very high') { impact = 'medium'; } // Analyze file changes for better categorization const hasUIChanges = files.some(f => f.category === 'source' && f.filePath.match(/\.(tsx|jsx)$/)); const hasDBChanges = files.some(f => f.category === 'database'); const hasAPIChanges = files.some(f => f.functionalImpact?.apiChanges); const hasSecurityChanges = files.some(f => f.functionalImpact?.securityRelated); if (hasSecurityChanges) { impact = 'high'; category = 'security'; } else if (commitAnalysis.breaking) { impact = 'critical'; category = 'breaking'; } else if (hasDBChanges || hasAPIChanges) { impact = 'medium'; } else if (hasUIChanges) { impact = 'medium'; userFacing = true; } // Generate intelligent summary based on message quality let summary = subject; const insights = []; // Use semantic analysis for better insights if (semanticAnalysis.patterns.includes('new_feature')) { insights.push('introduces new functionality'); } if (semanticAnalysis.patterns.includes('bug_fix')) { insights.push('resolves issues'); } if (semanticAnalysis.patterns.includes('performance_optimization')) { insights.push('improves performance'); } if (semanticAnalysis.patterns.includes('security_policy')) { insights.push('enhances security'); } // Framework-specific insights if (semanticAnalysis.frameworks.includes('React')) { insights.push('updates React components'); } if (semanticAnalysis.frameworks.includes('Database')) { insights.push('modifies database layer'); } // Enhance summary if commit message quality is poor if (messageQuality && (messageQuality.level === 'very_poor' || messageQuality.level === 'poor')) { if (insights.length > 0) { summary = `${subject} (${insights.slice(0, 2).join(', ')})`; } else if (files.length > 0) { // Fallback to file-based description when commit message is poor const mainCategory = files[0].category; const fileTypes = [...new Set(files.map(f => f.category))]; if (fileTypes.length === 1) { summary = `${subject} (updates ${mainCategory} files)`; } else { summary = `${subject} (affects ${fileTypes.join(', ')})`; } } } else if (insights.length > 0) { summary = insights.length > 1 ? `${subject} (${insights.slice(0, 2).join(', ')})` : subject; } const highlights = []; if (diffStats.insertions > 100) highlights.push('Significant code additions'); if (files.length > 10) highlights.push('Wide-ranging changes'); if (hasUIChanges) highlights.push('User interface updates'); if (hasDBChanges) highlights.push('Database modifications'); if (complexity.level !== 'minimal') highlights.push(`${complexity.level} complexity changes`); // Add message quality warning to highlights if poor if (messageQuality && (messageQuality.level === 'very_poor' || messageQuality.level === 'poor')) { highlights.unshift(`โš ๏ธ Poor commit message quality (${messageQuality.level})`); } return { summary, technicalSummary: `Modified ${files.length} files with +${diffStats.insertions}/-${diffStats.deletions} lines. Complexity: ${complexity.level}`, category, impact, scope: impact === 'critical' ? 'major' : impact === 'high' ? 'minor' : 'patch', userFacing, breaking: commitAnalysis.breaking, businessImpact: userFacing ? 'Affects user experience' : 'Internal improvements', technicalImpact: `Changes in ${[...new Set(files.map(f => f.category))].join(', ')}`, highlights: highlights.slice(0, 4), migrationNotes: commitAnalysis.breaking ? 'Review breaking changes before deployment' : null, tags: semanticAnalysis.frameworks.concat(semanticAnalysis.patterns.slice(0, 3)), relatedAreas: [...new Set(files.map(f => f.category))].slice(0, 3), riskLevel: riskAssessment.level, confidence: messageQuality ? Math.max(0.3, 0.7 - (8 - messageQuality.score) * 0.05) : 0.7, messageQuality: messageQuality }; } // Build comprehensive changelog from analyzed commits buildChangelog(analyzedCommits, releaseInsights, version) { const currentDate = new Date().toISOString().split('T')[0]; const versionHeader = version || 'Unreleased'; let changelog = `# Changelog\n\n## [${versionHeader}] - ${currentDate}\n\n`; // Add release summary with business impact if (releaseInsights.summary) { changelog += `### ๐Ÿ“‹ Release Summary\n${releaseInsights.summary}\n\n`; changelog += `**Business Impact**: ${releaseInsights.businessImpact}\n`; changelog += `**Complexity**: ${releaseInsights.complexity}\n`; if (releaseInsights.deploymentRequirements.length > 0) { changelog += `**Deployment Requirements**: ${releaseInsights.deploymentRequirements.join(', ')}\n`; } changelog += '\n'; } // Group commits by type const commitsByType = { feat: [], fix: [], security: [], breaking: [], docs: [], style: [], refactor: [], perf: [], test: [], chore: [], deps: [], other: [] }; analyzedCommits.forEach(commit => { const type = commit.breaking ? 'breaking' : (commit.type || 'other'); if (commitsByType[type]) { commitsByType[type].push(commit); } else { commitsByType.other.push(commit); } }); // Add sections for each commit type Object.entries(commitsByType).forEach(([type, commits]) => { if (commits.length > 0) { changelog += `${COMMIT_TYPES[type] || type}\n\n`; commits.forEach(commit => { const summary = commit.aiSummary?.summary || commit.subject; const confidence = commit.aiSummary?.confidence ? ` (${Math.round(commit.aiSummary.confidence * 100)}%)` : ''; changelog += `- **${commit.scope ? `${commit.scope}: ` : ''}${summary}**`; if (commit.breaking || commit.aiSummary?.breaking) { changelog += ` โš ๏ธ BREAKING CHANGE`; } if (commit.aiSummary?.impact === 'critical' || commit.aiSummary?.impact === 'high') { changelog += ` ๐Ÿ”ฅ`; } changelog += ` (${commit.hash})${confidence}\n`; if (commit.aiSummary?.technicalSummary) { changelog += ` - ${commit.aiSummary.technicalSummary}\n`; } if (commit.aiSummary?.highlights?.length > 0) { commit.aiSummary.highlights.forEach(highlight => { changelog += ` - ${highlight}\n`; }); } if (commit.aiSummary?.migrationNotes) { changelog += ` - **Migration**: ${commit.aiSummary.migrationNotes}\n`; } changelog += '\n'; }); } }); // Add detailed risk assessment if needed if (releaseInsights.riskLevel !== 'low' || releaseInsights.breaking) { changelog += `### โš ๏ธ Risk Assessment\n`; changelog += `**Risk Level:** ${releaseInsights.riskLevel.toUpperCase()}\n\n`; if (releaseInsights.breaking) { changelog += `๐Ÿšจ **Breaking Changes**: This release contains breaking changes. Please review migration notes above.\n\n`; } if (releaseInsights.deploymentRequirements.length > 0) { changelog += `๐Ÿ“‹ **Deployment Requirements**:\n`; releaseInsights.deploymentRequirements.forEach(req => { changelog += `