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
JavaScript
#!/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 += `