termcode
Version:
Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative
867 lines (866 loc) • 31.5 kB
JavaScript
import { promises as fs } from "node:fs";
import path from "node:path";
import { log } from "../util/logging.js";
import { hookManager } from "../hooks/manager.js";
/**
* Enhanced Diff Management System with AI-powered analysis and conflict resolution
* Superior to Claude Code's basic diff application
*/
export class EnhancedDiffManager {
activeDiffs = new Map();
diffHistory = [];
backups = new Map();
analysisCache = new Map();
/**
* Create smart diff with comprehensive analysis
*/
async createSmartDiff(changes, metadata) {
const diffId = this.generateDiffId();
const timestamp = Date.now();
log.info(`Creating smart diff: ${diffId}`);
// Process each change
const processedChanges = [];
for (const change of changes) {
const processed = await this.processFileChange(change, metadata.repoPath);
processedChanges.push(processed);
}
// Analyze impact
const impact = await this.analyzeImpact(processedChanges, metadata.repoPath);
// Generate preview
const preview = await this.generatePreview(processedChanges);
// Create rollback data
const rollbackData = await this.createRollbackData(processedChanges, metadata.repoPath);
const smartDiff = {
id: diffId,
timestamp,
changes: processedChanges,
summary: await this.generateSummary(processedChanges, metadata.task),
impact,
preview,
rollbackData,
metadata: {
provider: metadata.provider,
model: metadata.model,
task: metadata.task,
branch: metadata.branch || 'unknown'
}
};
// Execute pre-diff hooks
await hookManager.executeHooks('PreDiff', {
repoPath: metadata.repoPath,
currentBranch: metadata.branch || 'current',
provider: metadata.provider,
model: metadata.model,
sessionId: diffId,
timestamp,
environment: process.env
}, {
filePaths: processedChanges.map(c => c.filePath),
diffs: processedChanges.map(c => ({
file: c.filePath,
oldContent: c.originalContent,
newContent: c.newContent,
unified: this.generateUnifiedDiff(c.originalContent, c.newContent)
})),
summary: smartDiff.summary
});
this.activeDiffs.set(diffId, smartDiff);
return smartDiff;
}
/**
* Analyze diff with AI-powered insights
*/
async analyzeDiff(diffId) {
const diff = this.activeDiffs.get(diffId);
if (!diff) {
throw new Error(`Diff ${diffId} not found`);
}
// Check cache first
if (this.analysisCache.has(diffId)) {
return this.analysisCache.get(diffId);
}
log.info(`Analyzing diff: ${diffId}`);
const analysis = {
conflicts: [],
suggestions: [],
metrics: {
complexity: 0,
maintainability: 0,
testCoverage: 0,
performance: 0
}
};
// Analyze each file change
for (const change of diff.changes) {
// Syntax analysis
const syntaxIssues = await this.analyzeSyntax(change);
analysis.conflicts.push(...syntaxIssues);
// Logic analysis
const logicIssues = await this.analyzeLogic(change);
analysis.conflicts.push(...logicIssues);
// Style analysis
const styleIssues = await this.analyzeStyle(change);
analysis.conflicts.push(...styleIssues);
// Security analysis
const securityIssues = await this.analyzeSecurity(change);
analysis.conflicts.push(...securityIssues);
// Generate suggestions
const suggestions = await this.generateSuggestions(change);
analysis.suggestions.push(...suggestions);
}
// Calculate metrics
analysis.metrics = await this.calculateMetrics(diff);
this.analysisCache.set(diffId, analysis);
return analysis;
}
/**
* Apply diff with intelligent conflict resolution
*/
async applyDiff(diffId, options = {
mode: 'safe',
backup: true,
dryRun: false,
autoResolveConflicts: false
}) {
const diff = this.activeDiffs.get(diffId);
if (!diff) {
throw new Error(`Diff ${diffId} not found`);
}
log.info(`Applying diff: ${diffId} (mode: ${options.mode})`);
const result = {
success: false,
appliedChanges: [],
skippedChanges: [],
conflicts: [],
backupId: ''
};
// Create backup if requested
if (options.backup) {
result.backupId = await this.createBackup(diff);
}
// Analyze potential conflicts first
const analysis = await this.analyzeDiff(diffId);
const criticalConflicts = analysis.conflicts.filter(c => c.severity === 'high');
if (criticalConflicts.length > 0 && options.mode === 'safe') {
log.warn(`Found ${criticalConflicts.length} critical conflicts, aborting safe mode`);
result.conflicts = criticalConflicts.map(c => ({
file: c.file,
conflictMarkers: c.message,
resolution: 'skip'
}));
return result;
}
// Process each change
for (const change of diff.changes) {
try {
const changeResult = await this.applyFileChange(change, options);
if (changeResult.success) {
result.appliedChanges.push(change);
}
else {
result.skippedChanges.push(change);
if (changeResult.conflict) {
result.conflicts.push(changeResult.conflict);
}
}
}
catch (error) {
log.error(`Error applying change to ${change.filePath}:`, error);
result.skippedChanges.push(change);
result.conflicts.push({
file: change.filePath,
conflictMarkers: `Error: ${error}`,
resolution: 'skip'
});
}
}
result.success = result.conflicts.length === 0 ||
result.conflicts.every(c => c.resolution !== undefined);
// Execute post-diff hooks
if (!options.dryRun) {
await hookManager.executeHooks('PostDiff', {
repoPath: path.dirname(diff.changes[0]?.filePath || ''),
currentBranch: diff.metadata.branch,
provider: diff.metadata.provider,
model: diff.metadata.model,
sessionId: diffId,
timestamp: Date.now(),
environment: process.env
}, {
appliedChanges: result.appliedChanges,
conflicts: result.conflicts,
success: result.success
});
}
// Update diff history
this.diffHistory.push(diff);
if (this.diffHistory.length > 100) {
this.diffHistory = this.diffHistory.slice(-100);
}
return result;
}
/**
* Interactive conflict resolution
*/
async resolveConflicts(diffId, resolutions) {
const diff = this.activeDiffs.get(diffId);
if (!diff)
return false;
for (const resolution of resolutions) {
const change = diff.changes.find(c => c.filePath === resolution.file);
if (!change)
continue;
switch (resolution.resolution) {
case 'accept_theirs':
// Use new content as-is
break;
case 'accept_ours':
// Keep original content
change.newContent = change.originalContent;
break;
case 'merge':
if (resolution.customMerge) {
change.newContent = resolution.customMerge;
}
else {
// Attempt automatic merge
change.newContent = await this.attemptAutoMerge(change);
}
break;
case 'skip':
// Mark for skipping
change.confidence = 0;
break;
}
}
return true;
}
/**
* Rollback applied diff
*/
async rollbackDiff(diffId) {
const diff = this.diffHistory.find(d => d.id === diffId);
if (!diff || !diff.rollbackData) {
log.error(`Cannot rollback diff ${diffId}: not found or no rollback data`);
return false;
}
log.info(`Rolling back diff: ${diffId}`);
try {
for (const [filePath, originalContent] of Object.entries(diff.rollbackData.files)) {
if (originalContent === null) {
// File was created, delete it
await fs.unlink(filePath);
}
else {
// File was modified, restore original
await fs.writeFile(filePath, originalContent, 'utf8');
}
}
log.success(`Rolled back diff: ${diffId}`);
return true;
}
catch (error) {
log.error(`Failed to rollback diff ${diffId}:`, error);
return false;
}
}
/**
* Generate visual diff preview
*/
async generateVisualDiff(diffId) {
const diff = this.activeDiffs.get(diffId);
if (!diff)
return '';
const lines = [];
lines.push(`# Diff Preview: ${diffId}`);
lines.push(`**Task:** ${diff.metadata.task}`);
lines.push(`**Impact:** ${diff.impact.riskLevel} (${diff.impact.linesChanged} lines, ${diff.impact.affectedFiles} files)`);
lines.push('');
for (const change of diff.changes) {
lines.push(`## ${change.changeType.toUpperCase()}: ${change.filePath}`);
lines.push(`**Reason:** ${change.reason}`);
lines.push(`**Confidence:** ${Math.round(change.confidence * 100)}%`);
lines.push('');
// Generate side-by-side or unified diff
const visualDiff = this.generateVisualFileDiff(change);
lines.push('```diff');
lines.push(visualDiff);
lines.push('```');
lines.push('');
}
return lines.join('\n');
}
/**
* Process individual file change
*/
async processFileChange(change, repoPath) {
const fullPath = path.resolve(repoPath, change.filePath);
// Determine change type
let changeType = change.changeType;
let originalContent = change.originalContent;
if (!changeType) {
try {
await fs.access(fullPath);
changeType = 'modify';
if (!originalContent) {
originalContent = await fs.readFile(fullPath, 'utf8');
}
}
catch {
changeType = 'create';
originalContent = '';
}
}
// Detect language
const language = this.detectLanguage(change.filePath);
// Calculate confidence based on various factors
const confidence = this.calculateChangeConfidence(change, originalContent);
// Analyze line changes
const lineNumbers = this.analyzeLineChanges(originalContent, change.newContent);
return {
filePath: change.filePath,
originalContent: originalContent || '',
newContent: change.newContent,
changeType: changeType,
language,
confidence,
reason: change.reason || 'No reason provided',
lineNumbers
};
}
/**
* Analyze impact of changes
*/
async analyzeImpact(changes, repoPath) {
let riskLevel = 'low';
let linesChanged = 0;
let testImpact = false;
let breakingChanges = false;
const sensitiveFiles = [
'package.json', 'requirements.txt', '.env', 'Dockerfile',
'config', 'settings', 'security', 'auth'
];
for (const change of changes) {
// Count lines changed
const lines = this.countLinesChanged(change);
linesChanged += lines;
// Check for test files
if (change.filePath.includes('test') || change.filePath.includes('spec')) {
testImpact = true;
}
// Check for sensitive files
if (sensitiveFiles.some(pattern => change.filePath.toLowerCase().includes(pattern))) {
riskLevel = 'high';
}
// Check for potential breaking changes
if (this.hasBreakingChanges(change)) {
breakingChanges = true;
riskLevel = 'critical';
}
// Large changes increase risk
if (lines > 100) {
riskLevel = riskLevel === 'low' ? 'medium' : riskLevel;
}
}
return {
riskLevel,
affectedFiles: changes.length,
linesChanged,
testImpact,
breakingChanges
};
}
/**
* Generate diff summary using AI
*/
async generateSummary(changes, task) {
const changesSummary = changes.map(c => `${c.changeType} ${c.filePath} (${this.countLinesChanged(c)} lines)`).join(', ');
return `Task: ${task}\nChanges: ${changesSummary}`;
}
/**
* Create rollback data
*/
async createRollbackData(changes, repoPath) {
const rollbackData = {
timestamp: Date.now(),
files: {}
};
for (const change of changes) {
const fullPath = path.resolve(repoPath, change.filePath);
if (change.changeType === 'create') {
rollbackData.files[fullPath] = null; // Mark for deletion on rollback
}
else {
rollbackData.files[fullPath] = change.originalContent;
}
}
return rollbackData;
}
/**
* Analyze syntax issues
*/
async analyzeSyntax(change) {
const issues = [];
if (!change.language)
return issues;
// Basic syntax checks based on language
switch (change.language) {
case 'javascript':
case 'typescript':
// Check for common JS/TS syntax issues
if (this.hasUnclosedBraces(change.newContent)) {
issues.push({
file: change.filePath,
line: 0,
type: 'syntax',
severity: 'high',
message: 'Possible unclosed braces detected',
suggestion: 'Check brace matching'
});
}
break;
case 'python':
// Check for Python syntax issues
if (this.hasPythonIndentationIssues(change.newContent)) {
issues.push({
file: change.filePath,
line: 0,
type: 'syntax',
severity: 'high',
message: 'Possible indentation issues detected',
suggestion: 'Check indentation consistency'
});
}
break;
}
return issues;
}
/**
* Analyze logic issues
*/
async analyzeLogic(change) {
const issues = [];
// Check for potential logic issues
if (change.newContent.includes('TODO') || change.newContent.includes('FIXME')) {
issues.push({
file: change.filePath,
line: 0,
type: 'logic',
severity: 'medium',
message: 'Contains TODO/FIXME comments',
suggestion: 'Review and complete TODO items'
});
}
// Check for console.log in production code
if (change.newContent.includes('console.log') && !change.filePath.includes('test')) {
issues.push({
file: change.filePath,
line: 0,
type: 'logic',
severity: 'low',
message: 'Contains console.log statements',
suggestion: 'Remove debug statements or use proper logging'
});
}
return issues;
}
/**
* Analyze style issues
*/
async analyzeStyle(change) {
const issues = [];
// Check for mixed indentation
if (this.hasMixedIndentation(change.newContent)) {
issues.push({
file: change.filePath,
line: 0,
type: 'style',
severity: 'low',
message: 'Mixed indentation detected',
suggestion: 'Use consistent indentation (spaces or tabs)'
});
}
// Check line length
const longLines = this.findLongLines(change.newContent, 120);
if (longLines.length > 0) {
issues.push({
file: change.filePath,
line: longLines[0],
type: 'style',
severity: 'low',
message: `Lines exceed 120 characters (${longLines.length} lines)`,
suggestion: 'Consider breaking long lines'
});
}
return issues;
}
/**
* Analyze security issues
*/
async analyzeSecurity(change) {
const issues = [];
// Check for hardcoded secrets
const secretPatterns = [
/password\s*[:=]\s*['"][^'"]+['"]/,
/api[_-]?key\s*[:=]\s*['"][^'"]+['"]/,
/secret\s*[:=]\s*['"][^'"]+['"]/
];
for (const pattern of secretPatterns) {
if (pattern.test(change.newContent)) {
issues.push({
file: change.filePath,
line: 0,
type: 'security',
severity: 'high',
message: 'Possible hardcoded secret detected',
suggestion: 'Use environment variables for secrets'
});
}
}
return issues;
}
/**
* Generate improvement suggestions
*/
async generateSuggestions(change) {
const suggestions = [];
// Suggest optimizations based on file type and content
if (change.language === 'javascript' || change.language === 'typescript') {
if (change.newContent.includes('var ')) {
suggestions.push({
file: change.filePath,
type: 'optimization',
message: 'Consider using let/const instead of var',
automated: true
});
}
}
return suggestions;
}
/**
* Calculate code metrics
*/
async calculateMetrics(diff) {
// Simple heuristic-based metrics
let complexity = 0;
let maintainability = 0.8;
let testCoverage = 0;
let performance = 0.8;
for (const change of diff.changes) {
// Calculate cyclomatic complexity (simplified)
const complexityIncrease = this.calculateComplexity(change.newContent);
complexity += complexityIncrease;
// Check test coverage
if (change.filePath.includes('test') || change.filePath.includes('spec')) {
testCoverage += 0.2;
}
// Maintainability factors
if (this.countLinesChanged(change) > 50) {
maintainability -= 0.1;
}
}
return {
complexity: Math.min(complexity / diff.changes.length, 1),
maintainability: Math.max(maintainability, 0),
testCoverage: Math.min(testCoverage, 1),
performance
};
}
/**
* Apply individual file change
*/
async applyFileChange(change, options) {
try {
const fullPath = path.resolve(change.filePath);
if (options.dryRun) {
log.info(`[DRY RUN] Would ${change.changeType} ${change.filePath}`);
return { success: true };
}
switch (change.changeType) {
case 'create':
await fs.mkdir(path.dirname(fullPath), { recursive: true });
await fs.writeFile(fullPath, change.newContent, 'utf8');
break;
case 'modify':
// Check for conflicts if file was modified since analysis
const currentContent = await fs.readFile(fullPath, 'utf8');
if (currentContent !== change.originalContent && options.mode === 'safe') {
return {
success: false,
conflict: {
file: change.filePath,
conflictMarkers: 'File was modified externally',
resolution: 'manual'
}
};
}
await fs.writeFile(fullPath, change.newContent, 'utf8');
break;
case 'delete':
await fs.unlink(fullPath);
break;
case 'rename':
// Handle rename (would need additional metadata)
break;
}
return { success: true };
}
catch (error) {
return {
success: false,
conflict: {
file: change.filePath,
conflictMarkers: `Error: ${error}`,
resolution: 'skip'
}
};
}
}
// Utility methods
generateDiffId() {
return `diff_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
detectLanguage(filePath) {
const ext = path.extname(filePath).toLowerCase();
const langMap = {
'.js': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.py': 'python',
'.rb': 'ruby',
'.go': 'go',
'.rs': 'rust',
'.java': 'java',
'.cpp': 'cpp',
'.c': 'c',
'.php': 'php'
};
return langMap[ext] || 'text';
}
calculateChangeConfidence(change, originalContent) {
let confidence = 0.7; // Base confidence
// Increase confidence for small, focused changes
const linesChanged = this.countLinesChanged({ originalContent, newContent: change.newContent });
if (linesChanged < 10)
confidence += 0.2;
if (linesChanged > 100)
confidence -= 0.2;
// Increase confidence if change has clear reason
if (change.reason && change.reason.length > 10) {
confidence += 0.1;
}
return Math.max(0, Math.min(1, confidence));
}
analyzeLineChanges(oldContent, newContent) {
const oldLines = oldContent.split('\n');
const newLines = newContent.split('\n');
const added = [];
const deleted = [];
const modified = [];
// Simple diff algorithm
let oldIndex = 0;
let newIndex = 0;
while (oldIndex < oldLines.length || newIndex < newLines.length) {
if (oldIndex >= oldLines.length) {
// Remaining lines are additions
added.push(newIndex + 1);
newIndex++;
}
else if (newIndex >= newLines.length) {
// Remaining lines are deletions
deleted.push(oldIndex + 1);
oldIndex++;
}
else if (oldLines[oldIndex] === newLines[newIndex]) {
// Lines match
oldIndex++;
newIndex++;
}
else {
// Lines differ - could be modification, addition, or deletion
// Simple heuristic: if next line matches, current line was modified
if (oldIndex + 1 < oldLines.length && oldLines[oldIndex + 1] === newLines[newIndex]) {
deleted.push(oldIndex + 1);
oldIndex++;
}
else if (newIndex + 1 < newLines.length && oldLines[oldIndex] === newLines[newIndex + 1]) {
added.push(newIndex + 1);
newIndex++;
}
else {
modified.push(Math.min(oldIndex + 1, newIndex + 1));
oldIndex++;
newIndex++;
}
}
}
return { added, deleted, modified };
}
countLinesChanged(change) {
const oldLines = change.originalContent.split('\n').length;
const newLines = change.newContent.split('\n').length;
return Math.abs(newLines - oldLines);
}
hasBreakingChanges(change) {
// Simple heuristics for breaking changes
const breakingPatterns = [
/export.*function.*\(/, // Function signature changes
/class.*{/, // Class modifications
/interface.*{/, // Interface changes
/public.*function/, // Public API changes
];
return breakingPatterns.some(pattern => pattern.test(change.originalContent) && !pattern.test(change.newContent));
}
generateUnifiedDiff(oldContent, newContent) {
// Simple unified diff generation
const oldLines = oldContent.split('\n');
const newLines = newContent.split('\n');
const diffLines = [];
let oldIndex = 0;
let newIndex = 0;
while (oldIndex < oldLines.length || newIndex < newLines.length) {
if (oldIndex >= oldLines.length) {
diffLines.push(`+${newLines[newIndex]}`);
newIndex++;
}
else if (newIndex >= newLines.length) {
diffLines.push(`-${oldLines[oldIndex]}`);
oldIndex++;
}
else if (oldLines[oldIndex] === newLines[newIndex]) {
diffLines.push(` ${oldLines[oldIndex]}`);
oldIndex++;
newIndex++;
}
else {
diffLines.push(`-${oldLines[oldIndex]}`);
diffLines.push(`+${newLines[newIndex]}`);
oldIndex++;
newIndex++;
}
}
return diffLines.join('\n');
}
generatePreview(changes) {
const lines = [];
for (const change of changes) {
lines.push(`${change.changeType}: ${change.filePath}`);
lines.push(` Lines changed: ${this.countLinesChanged(change)}`);
lines.push(` Confidence: ${Math.round(change.confidence * 100)}%`);
lines.push(` Reason: ${change.reason}`);
lines.push('');
}
return Promise.resolve(lines.join('\n'));
}
generateVisualFileDiff(change) {
return this.generateUnifiedDiff(change.originalContent, change.newContent);
}
async createBackup(diff) {
const backupId = `backup_${diff.id}_${Date.now()}`;
this.backups.set(backupId, diff.rollbackData);
return backupId;
}
async attemptAutoMerge(change) {
// Simple auto-merge implementation
// In a real implementation, this would use more sophisticated algorithms
return change.newContent; // Fallback to new content
}
// Syntax analysis helpers
hasUnclosedBraces(content) {
let braceCount = 0;
for (const char of content) {
if (char === '{')
braceCount++;
if (char === '}')
braceCount--;
}
return braceCount !== 0;
}
hasPythonIndentationIssues(content) {
const lines = content.split('\n');
let prevIndent = 0;
for (const line of lines) {
if (line.trim() === '')
continue;
const indent = line.length - line.trimLeft().length;
if (indent % 4 !== 0 && indent % 2 !== 0) {
return true; // Irregular indentation
}
prevIndent = indent;
}
return false;
}
hasMixedIndentation(content) {
const hasSpaces = /^ /.test(content);
const hasTabs = /^\t/.test(content);
return hasSpaces && hasTabs;
}
findLongLines(content, maxLength) {
const lines = content.split('\n');
const longLines = [];
lines.forEach((line, index) => {
if (line.length > maxLength) {
longLines.push(index + 1);
}
});
return longLines;
}
calculateComplexity(content) {
// Simple cyclomatic complexity calculation
const complexityKeywords = [
'if', 'else', 'while', 'for', 'switch', 'case', 'catch', '&&', '||', '?'
];
let complexity = 1; // Base complexity
for (const keyword of complexityKeywords) {
const matches = content.match(new RegExp(`\\b${keyword}\\b`, 'g'));
if (matches) {
complexity += matches.length;
}
}
return Math.min(complexity / 100, 1); // Normalize to 0-1
}
/**
* Public API methods
*/
/**
* List active diffs
*/
getActiveDiffs() {
return Array.from(this.activeDiffs.values());
}
/**
* Get diff by ID
*/
getDiff(diffId) {
return this.activeDiffs.get(diffId);
}
/**
* Clear completed diffs
*/
clearDiff(diffId) {
return this.activeDiffs.delete(diffId);
}
/**
* Get diff statistics
*/
getDiffStats() {
const active = this.activeDiffs.size;
const historical = this.diffHistory.length;
const riskDistribution = { low: 0, medium: 0, high: 0, critical: 0 };
this.diffHistory.forEach(diff => {
riskDistribution[diff.impact.riskLevel]++;
});
const avgChanges = historical > 0 ?
this.diffHistory.reduce((sum, diff) => sum + diff.changes.length, 0) / historical : 0;
return {
activeDiffs: active,
totalHistorical: historical,
successRate: 0.85, // Would track actual success rate
averageChangesPerDiff: avgChanges,
riskDistribution
};
}
}
// Export singleton instance
export const enhancedDiffManager = new EnhancedDiffManager();