ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
401 lines • 14.5 kB
JavaScript
/**
* File Change Monitor
* Monitors file system changes during development with automatic screenshot capture
* Core utility for Code Quality Integration
*/
import * as fs from 'fs';
import * as path from 'path';
import { nanoid } from 'nanoid';
export class FileChangeMonitor {
isMonitoring = false;
monitorId;
config;
changes = [];
screenshots = [];
startTime;
watchers = [];
/**
* Start monitoring file changes
*/
async startMonitoring(config) {
if (this.isMonitoring) {
throw new Error('File monitoring is already active');
}
this.config = config;
this.monitorId = nanoid();
this.startTime = Date.now();
this.isMonitoring = true;
this.changes = [];
this.screenshots = [];
// Get list of files to watch
const filesToWatch = await this.getFilesToWatch(config.projectPath, config.filePatterns);
// Setup file watchers
for (const filePath of filesToWatch) {
try {
const watcher = fs.watch(filePath, (eventType, filename) => {
this.handleFileChange(filePath, eventType);
});
this.watchers.push(watcher);
}
catch (error) {
// File might not exist or be accessible - continue with others
console.warn(`Could not watch file: ${filePath}`);
}
}
// Setup directory watchers for new files
await this.setupDirectoryWatchers(config.projectPath, config.filePatterns);
console.log(`🔍 File change monitoring started: ${this.monitorId}`);
console.log(`📁 Watching ${filesToWatch.length} files in ${config.projectPath}`);
return {
monitorId: this.monitorId,
watchedFiles: filesToWatch.length
};
}
/**
* Stop monitoring and generate report
*/
async stopMonitoring(generateReport = true) {
if (!this.isMonitoring || !this.config || !this.startTime) {
return null;
}
// Close all watchers
this.watchers.forEach(watcher => {
try {
watcher.close();
}
catch (error) {
// Ignore close errors
}
});
this.watchers = [];
const duration = this.formatDuration(Date.now() - this.startTime);
// Group changes by file
const fileMap = new Map();
this.changes.forEach(change => {
const existing = fileMap.get(change.path);
if (existing) {
existing.changeCount++;
existing.lastChanged = Math.max(existing.lastChanged, change.timestamp);
}
else {
fileMap.set(change.path, {
path: change.path,
changeCount: 1,
lastChanged: change.timestamp
});
}
});
const report = {
duration,
changedFiles: Array.from(fileMap.values()),
totalChanges: this.changes.length,
screenshotsCaptured: this.screenshots.length,
uiStabilityScore: this.calculateUIStabilityScore()
};
if (generateReport) {
report.detailedReport = await this.generateDetailedReport();
}
// Reset state
this.isMonitoring = false;
this.config = undefined;
this.monitorId = undefined;
this.startTime = undefined;
console.log(`🏁 File change monitoring stopped. Report generated.`);
return report;
}
/**
* Get change impact analysis for a specific change
*/
async getChangeImpactAnalysis(options) {
const change = options.changeId
? this.changes.find(c => c.id === options.changeId)
: this.changes[this.changes.length - 1]; // Latest change
if (!change) {
return {
change: null,
uiImpact: 'none',
functionalityImpact: 'none',
performanceImpact: 'neutral',
recommendations: ['No recent changes detected']
};
}
const analysis = {
change,
uiImpact: this.assessUIImpact(change),
functionalityImpact: this.assessFunctionalityImpact(change),
performanceImpact: this.assessPerformanceImpact(change),
recommendations: this.generateRecommendations(change)
};
// Add visual diff analysis if requested
if (options.includeVisualDiff) {
analysis.visualDiff = await this.performVisualDiffAnalysis(change);
}
// Add performance analysis if requested
if (options.includePerformanceImpact) {
analysis.performanceAnalysis = await this.performPerformanceAnalysis(change);
}
return analysis;
}
/**
* Handle individual file changes
*/
async handleFileChange(filePath, eventType) {
if (!this.config || !this.isMonitoring)
return;
try {
const stats = fs.statSync(filePath);
const change = {
id: nanoid(),
timestamp: Date.now(),
path: filePath,
type: eventType === 'rename' ? 'created' : 'modified',
size: stats.size,
linesChanged: await this.countLinesChanged(filePath)
};
this.changes.push(change);
console.log(`📝 File changed: ${path.relative(this.config.projectPath, filePath)}`);
// Capture screenshot if enabled
if (this.config.screenshotOnChange && this.config.session) {
await this.captureScreenshotForChange(change);
}
}
catch (error) {
// File might have been deleted or is temporarily inaccessible
const change = {
id: nanoid(),
timestamp: Date.now(),
path: filePath,
type: 'deleted',
size: 0
};
this.changes.push(change);
}
}
/**
* Get files to watch based on patterns
*/
async getFilesToWatch(projectPath, patterns) {
const files = [];
const scanDirectory = (dir) => {
try {
const entries = fs.readdirSync(dir);
for (const entry of entries) {
const fullPath = path.join(dir, entry);
const stats = fs.statSync(fullPath);
if (stats.isDirectory()) {
// Skip node_modules and other common directories
if (!['node_modules', '.git', 'dist', 'build', '.next'].includes(entry)) {
scanDirectory(fullPath);
}
}
else if (stats.isFile()) {
// Check if file matches any pattern
const matchesPattern = patterns.some(pattern => {
const regex = new RegExp(pattern.replace('*', '.*'));
return regex.test(entry);
});
if (matchesPattern) {
files.push(fullPath);
}
}
}
}
catch (error) {
// Directory might not be accessible - continue
}
};
scanDirectory(projectPath);
return files;
}
/**
* Setup directory watchers for new files
*/
async setupDirectoryWatchers(projectPath, patterns) {
const watchDirectory = (dir) => {
try {
const watcher = fs.watch(dir, { recursive: true }, (eventType, filename) => {
if (filename && patterns.some(pattern => {
const regex = new RegExp(pattern.replace('*', '.*'));
return regex.test(filename);
})) {
const fullPath = path.join(dir, filename);
this.handleFileChange(fullPath, eventType);
}
});
this.watchers.push(watcher);
}
catch (error) {
// Directory might not support recursive watching
console.warn(`Could not setup recursive watcher for: ${dir}`);
}
};
watchDirectory(projectPath);
}
/**
* Count lines changed in a file (simplified implementation)
*/
async countLinesChanged(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
return content.split('\n').length;
}
catch (error) {
return 0;
}
}
/**
* Capture screenshot when file changes
*/
async captureScreenshotForChange(change) {
if (!this.config?.session?.page)
return;
try {
const screenshotPath = `screenshot_${change.id}_${Date.now()}.png`;
await this.config.session.page.screenshot({
path: screenshotPath,
fullPage: true
});
this.screenshots.push(screenshotPath);
console.log(`📸 Screenshot captured: ${screenshotPath}`);
}
catch (error) {
console.warn(`Failed to capture screenshot: ${error}`);
}
}
/**
* Assess UI impact of a change
*/
assessUIImpact(change) {
const filePath = change.path.toLowerCase();
if (filePath.includes('.css') || filePath.includes('.scss') || filePath.includes('.less')) {
return 'high';
}
if (filePath.includes('component') || filePath.includes('.tsx') || filePath.includes('.jsx')) {
return 'medium';
}
if (filePath.includes('.ts') || filePath.includes('.js')) {
return 'low';
}
return 'none';
}
/**
* Assess functionality impact of a change
*/
assessFunctionalityImpact(change) {
const filePath = change.path.toLowerCase();
if (filePath.includes('api') || filePath.includes('service') || filePath.includes('util')) {
return 'high';
}
if (filePath.includes('component') || filePath.includes('hook')) {
return 'medium';
}
return 'low';
}
/**
* Assess performance impact of a change
*/
assessPerformanceImpact(change) {
// Simplified logic - could be enhanced with actual performance analysis
if (change.size > 10000) { // Large files might impact performance
return 'negative';
}
if (change.path.includes('optimization') || change.path.includes('cache')) {
return 'positive';
}
return 'neutral';
}
/**
* Generate recommendations based on change analysis
*/
generateRecommendations(change) {
const recommendations = [];
if (change.linesChanged && change.linesChanged > 300) {
recommendations.push('Consider breaking down large files for better maintainability');
}
if (change.path.includes('component') && !change.path.includes('test')) {
recommendations.push('Consider adding or updating tests for modified components');
}
if (change.path.includes('.css') || change.path.includes('.scss')) {
recommendations.push('Verify visual consistency across different screen sizes');
}
return recommendations;
}
/**
* Perform visual diff analysis
*/
async performVisualDiffAnalysis(change) {
// Simplified implementation - would integrate with actual visual comparison tools
return {
screenshotsCompared: 2,
similarity: Math.random() * 20 + 80, // 80-100% similarity
changesDetected: Math.floor(Math.random() * 5)
};
}
/**
* Perform performance analysis
*/
async performPerformanceAnalysis(change) {
// Simplified implementation - would integrate with actual performance monitoring
return {
loadTimeChange: Math.floor(Math.random() * 200 - 100), // -100 to +100ms
bundleSizeChange: Math.floor(Math.random() * 20 - 10), // -10 to +10KB
memoryImpact: Math.floor(Math.random() * 10 - 5) // -5 to +5MB
};
}
/**
* Calculate UI stability score
*/
calculateUIStabilityScore() {
if (this.changes.length === 0)
return 100;
const uiChanges = this.changes.filter(change => this.assessUIImpact(change) !== 'none').length;
const score = Math.max(0, 100 - (uiChanges * 10));
return Math.min(100, score);
}
/**
* Generate detailed report
*/
async generateDetailedReport() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const reportPath = `file_change_report_${timestamp}.json`;
const report = {
monitorId: this.monitorId,
startTime: this.startTime,
endTime: Date.now(),
config: this.config,
changes: this.changes,
screenshots: this.screenshots,
summary: {
totalChanges: this.changes.length,
screenshotsCaptured: this.screenshots.length,
uiStabilityScore: this.calculateUIStabilityScore()
}
};
try {
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
return reportPath;
}
catch (error) {
console.warn(`Failed to write detailed report: ${error}`);
return 'Report generation failed';
}
}
/**
* Format duration string
*/
formatDuration(ms) {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
if (hours > 0) {
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
}
else if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
else {
return `${seconds}s`;
}
}
}
//# sourceMappingURL=file-change-monitor.js.map