figma-restoration-mcp-vue-tools
Version:
Professional Figma Component Restoration Kit - MCP tools with snapDOM-powered high-quality screenshots, intelligent shadow detection, and advanced diff analysis for Vue component restoration. Features enhanced figma_compare with color-coded region analysi
859 lines (734 loc) • 26.3 kB
JavaScript
/**
* README Synchronizer
* Updates README with benchmark results while preserving existing content
*/
import fs from 'fs';
import path from 'path';
import { BENCHMARK_CONFIG } from './config.js';
import { fileExists, logWithTimestamp } from './utils.js';
export class READMESynchronizer {
constructor(readmePath = './README.md') {
this.readmePath = path.resolve(readmePath);
this.backupPath = this.readmePath + BENCHMARK_CONFIG.REPORT.BACKUP_SUFFIX;
this.sectionMarker = BENCHMARK_CONFIG.REPORT.README_SECTION_MARKER;
}
/**
* Update README with benchmark content
* @param {string} markdownContent - New benchmark section content
* @returns {Object} Update results
*/
updateREADME(markdownContent) {
logWithTimestamp(`Updating README at: ${this.readmePath}`);
const updateResult = {
success: false,
backupCreated: false,
originalSize: 0,
newSize: 0,
sectionFound: false,
sectionReplaced: false,
error: null
};
try {
// Check if README exists
if (!fileExists(this.readmePath)) {
throw new Error(`README file not found: ${this.readmePath}`);
}
// Read current README content
const originalContent = fs.readFileSync(this.readmePath, 'utf8');
updateResult.originalSize = originalContent.length;
// Create backup
this.createBackup(originalContent);
updateResult.backupCreated = true;
// Find and replace benchmark section
const sectionInfo = this.findBenchmarkSection(originalContent);
updateResult.sectionFound = sectionInfo.found;
let newContent;
if (sectionInfo.found) {
// Replace existing section
newContent = this.replaceBenchmarkSection(originalContent, markdownContent, sectionInfo);
updateResult.sectionReplaced = true;
logWithTimestamp('Existing benchmark section replaced');
} else {
// Add new section
newContent = this.addBenchmarkSection(originalContent, markdownContent);
logWithTimestamp('New benchmark section added');
}
// Validate new content
const validation = this.validateNewContent(originalContent, newContent);
if (!validation.isValid) {
throw new Error(`Content validation failed: ${validation.errors.join(', ')}`);
}
// Write updated content
fs.writeFileSync(this.readmePath, newContent, 'utf8');
updateResult.newSize = newContent.length;
updateResult.success = true;
logWithTimestamp(`README updated successfully (${updateResult.originalSize} → ${updateResult.newSize} chars)`);
return updateResult;
} catch (error) {
updateResult.error = error.message;
logWithTimestamp(`Error updating README: ${error.message}`, 'error');
// Attempt to restore from backup if it was created
if (updateResult.backupCreated) {
this.restoreFromBackup();
}
return updateResult;
}
}
/**
* Find benchmark section in README content
* @param {string} content - README content
* @returns {Object} Section information
*/
findBenchmarkSection(content) {
const lines = content.split('\n');
let startIndex = -1;
let endIndex = -1;
// Find start of benchmark section
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim() === this.sectionMarker.trim()) {
startIndex = i;
break;
}
}
if (startIndex === -1) {
return {
found: false,
start: -1,
end: -1,
startLine: -1,
endLine: -1
};
}
// Find end of benchmark section (next ## heading or end of file)
for (let i = startIndex + 1; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('## ') && !line.includes('📊')) {
endIndex = i - 1;
break;
}
}
// If no next section found, section goes to end of file
if (endIndex === -1) {
endIndex = lines.length - 1;
}
return {
found: true,
start: startIndex,
end: endIndex,
startLine: startIndex + 1,
endLine: endIndex + 1,
content: lines.slice(startIndex, endIndex + 1).join('\n')
};
}
/**
* Replace existing benchmark section
* @param {string} originalContent - Original README content
* @param {string} newSectionContent - New benchmark section content
* @param {Object} sectionInfo - Section location information
* @returns {string} Updated content
*/
replaceBenchmarkSection(originalContent, newSectionContent, sectionInfo) {
const lines = originalContent.split('\n');
// Remove old section
const beforeSection = lines.slice(0, sectionInfo.start);
const afterSection = lines.slice(sectionInfo.end + 1);
// Insert new section
const newSectionLines = newSectionContent.trim().split('\n');
return [
...beforeSection,
...newSectionLines,
...afterSection
].join('\n');
}
/**
* Add new benchmark section to README
* @param {string} originalContent - Original README content
* @param {string} newSectionContent - New benchmark section content
* @returns {string} Updated content
*/
addBenchmarkSection(originalContent, newSectionContent) {
// Try to find a good place to insert the section
const insertionPoint = this.findInsertionPoint(originalContent);
if (insertionPoint.found) {
const lines = originalContent.split('\n');
const beforeInsertion = lines.slice(0, insertionPoint.index);
const afterInsertion = lines.slice(insertionPoint.index);
const newSectionLines = newSectionContent.trim().split('\n');
return [
...beforeInsertion,
'', // Empty line before section
...newSectionLines,
'', // Empty line after section
...afterInsertion
].join('\n');
} else {
// Append to end of file
return originalContent.trim() + '\n\n' + newSectionContent.trim() + '\n';
}
}
/**
* Find appropriate insertion point for new benchmark section
* @param {string} content - README content
* @returns {Object} Insertion point information
*/
findInsertionPoint(content) {
const lines = content.split('\n');
// Look for common section patterns where benchmark should be inserted
const preferredSections = [
'## Features',
'## Usage',
'## Installation',
'## Getting Started',
'## Documentation'
];
// Find the first preferred section and insert after it
for (const section of preferredSections) {
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim().toLowerCase().startsWith(section.toLowerCase())) {
// Find the end of this section
let sectionEnd = i + 1;
while (sectionEnd < lines.length && !lines[sectionEnd].trim().startsWith('## ')) {
sectionEnd++;
}
return {
found: true,
index: sectionEnd,
reason: `After ${section} section`
};
}
}
}
// If no preferred section found, look for any ## section and insert before the last one
const sectionIndices = [];
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim().startsWith('## ')) {
sectionIndices.push(i);
}
}
if (sectionIndices.length > 0) {
// Insert before the last section (usually License, Contributing, etc.)
return {
found: true,
index: sectionIndices[sectionIndices.length - 1],
reason: 'Before last section'
};
}
return {
found: false,
index: -1,
reason: 'No suitable insertion point found'
};
}
/**
* Preserve existing content while updating benchmark section
* @param {string} originalContent - Original content
* @param {string} newSectionContent - New section content
* @returns {string} Preserved content
*/
preserveExistingContent(originalContent, newSectionContent) {
// This method ensures that non-benchmark content is preserved
const sectionInfo = this.findBenchmarkSection(originalContent);
if (sectionInfo.found) {
return this.replaceBenchmarkSection(originalContent, newSectionContent, sectionInfo);
} else {
return this.addBenchmarkSection(originalContent, newSectionContent);
}
}
/**
* Create backup of current README
* @param {string} content - Content to backup
*/
createBackup(content) {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const timestampedBackupPath = this.readmePath + `.backup-${timestamp}`;
// Create timestamped backup
fs.writeFileSync(timestampedBackupPath, content, 'utf8');
// Create/update latest backup
fs.writeFileSync(this.backupPath, content, 'utf8');
logWithTimestamp(`Backup created: ${timestampedBackupPath}`);
} catch (error) {
logWithTimestamp(`Warning: Could not create backup: ${error.message}`, 'warn');
}
}
/**
* Restore README from backup
*/
restoreFromBackup() {
try {
if (fileExists(this.backupPath)) {
const backupContent = fs.readFileSync(this.backupPath, 'utf8');
fs.writeFileSync(this.readmePath, backupContent, 'utf8');
logWithTimestamp('README restored from backup');
return true;
} else {
logWithTimestamp('No backup file found for restoration', 'warn');
return false;
}
} catch (error) {
logWithTimestamp(`Error restoring from backup: ${error.message}`, 'error');
return false;
}
}
/**
* Validate new content before writing
* @param {string} originalContent - Original content
* @param {string} newContent - New content
* @returns {Object} Validation results
*/
validateNewContent(originalContent, newContent) {
const validation = {
isValid: true,
errors: [],
warnings: []
};
// Check if content is not empty
if (!newContent || newContent.trim().length === 0) {
validation.isValid = false;
validation.errors.push('New content is empty');
return validation;
}
// Check if content is significantly shorter (possible data loss)
const originalLength = originalContent.length;
const newLength = newContent.length;
const reductionRatio = (originalLength - newLength) / originalLength;
if (reductionRatio > 0.5) {
validation.isValid = false;
validation.errors.push(`Content reduced by ${(reductionRatio * 100).toFixed(1)}% - possible data loss`);
} else if (reductionRatio > 0.2) {
validation.warnings.push(`Content reduced by ${(reductionRatio * 100).toFixed(1)}%`);
}
// Check if benchmark section exists in new content
if (!newContent.includes(this.sectionMarker)) {
validation.warnings.push('Benchmark section marker not found in new content');
}
// Check for basic markdown structure
const hasHeadings = /^#{1,6}\s+.+$/m.test(newContent);
if (!hasHeadings) {
validation.warnings.push('No markdown headings found in new content');
}
return validation;
}
/**
* Get README statistics
* @returns {Object} README statistics
*/
getREADMEStats() {
if (!fileExists(this.readmePath)) {
return {
exists: false,
size: 0,
lines: 0,
hasBenchmarkSection: false
};
}
try {
const content = fs.readFileSync(this.readmePath, 'utf8');
const lines = content.split('\n');
const sectionInfo = this.findBenchmarkSection(content);
return {
exists: true,
size: content.length,
lines: lines.length,
hasBenchmarkSection: sectionInfo.found,
benchmarkSectionLines: sectionInfo.found ? (sectionInfo.end - sectionInfo.start + 1) : 0,
lastModified: fs.statSync(this.readmePath).mtime
};
} catch (error) {
logWithTimestamp(`Error getting README stats: ${error.message}`, 'error');
return {
exists: true,
error: error.message
};
}
}
/**
* Preview changes without writing to file
* @param {string} markdownContent - New benchmark section content
* @returns {Object} Preview information
*/
previewChanges(markdownContent) {
if (!fileExists(this.readmePath)) {
return {
error: 'README file not found',
canPreview: false
};
}
try {
const originalContent = fs.readFileSync(this.readmePath, 'utf8');
const sectionInfo = this.findBenchmarkSection(originalContent);
let newContent;
if (sectionInfo.found) {
newContent = this.replaceBenchmarkSection(originalContent, markdownContent, sectionInfo);
} else {
newContent = this.addBenchmarkSection(originalContent, markdownContent);
}
return {
canPreview: true,
changes: {
originalSize: originalContent.length,
newSize: newContent.length,
sizeDifference: newContent.length - originalContent.length,
sectionExists: sectionInfo.found,
operation: sectionInfo.found ? 'replace' : 'add'
},
preview: {
first100Chars: newContent.substring(0, 100) + '...',
benchmarkSectionPreview: markdownContent.substring(0, 200) + '...'
}
};
} catch (error) {
return {
error: error.message,
canPreview: false
};
}
}
/**
* Clean up old backup files
* @param {number} maxBackups - Maximum number of backups to keep
*/
cleanupBackups(maxBackups = 5) {
try {
const dir = path.dirname(this.readmePath);
const baseName = path.basename(this.readmePath);
const files = fs.readdirSync(dir);
// Find backup files
const backupFiles = files
.filter(file => file.startsWith(baseName + '.backup-'))
.map(file => ({
name: file,
path: path.join(dir, file),
stat: fs.statSync(path.join(dir, file))
}))
.sort((a, b) => b.stat.mtime - a.stat.mtime); // Sort by modification time, newest first
// Remove old backups
if (backupFiles.length > maxBackups) {
const filesToDelete = backupFiles.slice(maxBackups);
for (const file of filesToDelete) {
fs.unlinkSync(file.path);
logWithTimestamp(`Deleted old backup: ${file.name}`);
}
}
} catch (error) {
logWithTimestamp(`Error cleaning up backups: ${error.message}`, 'warn');
}
}
/**
* Advanced backup management with versioning
* @param {string} content - Content to backup
* @param {string} operation - Operation being performed
* @returns {Object} Backup information
*/
createVersionedBackup(content, operation = 'update') {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupInfo = {
timestamp,
operation,
size: content.length,
path: null,
success: false
};
try {
const dir = path.dirname(this.readmePath);
const baseName = path.basename(this.readmePath, '.md');
// Create versioned backup with operation info
const versionedBackupPath = path.join(dir, `${baseName}.backup-${operation}-${timestamp}.md`);
// Add metadata header to backup
const backupContent = `<!-- Backup created: ${new Date().toISOString()} -->\n` +
`<!-- Operation: ${operation} -->\n` +
`<!-- Original size: ${content.length} chars -->\n\n` +
content;
fs.writeFileSync(versionedBackupPath, backupContent, 'utf8');
// Update latest backup
fs.writeFileSync(this.backupPath, content, 'utf8');
backupInfo.path = versionedBackupPath;
backupInfo.success = true;
logWithTimestamp(`Versioned backup created: ${path.basename(versionedBackupPath)}`);
// Clean up old backups
this.cleanupBackups();
return backupInfo;
} catch (error) {
backupInfo.error = error.message;
logWithTimestamp(`Error creating versioned backup: ${error.message}`, 'error');
return backupInfo;
}
}
/**
* List all available backups
* @returns {Object[]} Array of backup information
*/
listBackups() {
try {
const dir = path.dirname(this.readmePath);
const baseName = path.basename(this.readmePath);
const files = fs.readdirSync(dir);
const backups = files
.filter(file => file.startsWith(baseName + '.backup'))
.map(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
// Try to extract operation from filename
const operationMatch = file.match(/\.backup-([^-]+)-/);
const operation = operationMatch ? operationMatch[1] : 'unknown';
return {
filename: file,
path: filePath,
size: stat.size,
created: stat.mtime,
operation,
isLatest: file === path.basename(this.backupPath)
};
})
.sort((a, b) => b.created - a.created);
return backups;
} catch (error) {
logWithTimestamp(`Error listing backups: ${error.message}`, 'error');
return [];
}
}
/**
* Restore from specific backup
* @param {string} backupPath - Path to backup file
* @returns {Object} Restoration results
*/
restoreFromSpecificBackup(backupPath) {
const restoreResult = {
success: false,
backupUsed: backupPath,
originalSize: 0,
restoredSize: 0,
error: null
};
try {
if (!fileExists(backupPath)) {
throw new Error(`Backup file not found: ${backupPath}`);
}
// Read current README size for comparison
if (fileExists(this.readmePath)) {
const currentContent = fs.readFileSync(this.readmePath, 'utf8');
restoreResult.originalSize = currentContent.length;
}
// Read backup content
let backupContent = fs.readFileSync(backupPath, 'utf8');
// Remove backup metadata if present
backupContent = this.cleanBackupMetadata(backupContent);
restoreResult.restoredSize = backupContent.length;
// Create a backup of current state before restoring
if (fileExists(this.readmePath)) {
const currentContent = fs.readFileSync(this.readmePath, 'utf8');
this.createVersionedBackup(currentContent, 'pre-restore');
}
// Restore from backup
fs.writeFileSync(this.readmePath, backupContent, 'utf8');
restoreResult.success = true;
logWithTimestamp(`README restored from: ${path.basename(backupPath)}`);
return restoreResult;
} catch (error) {
restoreResult.error = error.message;
logWithTimestamp(`Error restoring from backup: ${error.message}`, 'error');
return restoreResult;
}
}
/**
* Clean backup metadata from content
* @param {string} content - Backup content
* @returns {string} Cleaned content
*/
cleanBackupMetadata(content) {
// Remove backup metadata comments
const lines = content.split('\n');
const cleanedLines = [];
let skipMetadata = false;
for (const line of lines) {
if (line.trim().startsWith('<!-- Backup created:') ||
line.trim().startsWith('<!-- Operation:') ||
line.trim().startsWith('<!-- Original size:')) {
skipMetadata = true;
continue;
}
if (skipMetadata && line.trim() === '') {
skipMetadata = false;
continue;
}
if (!skipMetadata) {
cleanedLines.push(line);
}
}
return cleanedLines.join('\n');
}
/**
* Validate README integrity after operations
* @returns {Object} Integrity check results
*/
validateIntegrity() {
const integrity = {
isValid: true,
errors: [],
warnings: [],
stats: null
};
try {
if (!fileExists(this.readmePath)) {
integrity.isValid = false;
integrity.errors.push('README file does not exist');
return integrity;
}
const content = fs.readFileSync(this.readmePath, 'utf8');
const stats = this.getREADMEStats();
integrity.stats = stats;
// Check for basic markdown structure
if (!content.includes('#')) {
integrity.warnings.push('No markdown headings found');
}
// Check for minimum content length
if (content.length < 100) {
integrity.warnings.push('README content is very short');
}
// Check for benchmark section
const sectionInfo = this.findBenchmarkSection(content);
if (!sectionInfo.found) {
integrity.warnings.push('Benchmark section not found');
}
// Check for common README sections
const commonSections = ['# ', '## Installation', '## Usage', '## Features'];
const missingSections = commonSections.filter(section =>
!content.toLowerCase().includes(section.toLowerCase())
);
if (missingSections.length > 0) {
integrity.warnings.push(`Missing common sections: ${missingSections.join(', ')}`);
}
// Check for broken markdown syntax
const brokenLinks = content.match(/\[([^\]]+)\]\(\s*\)/g);
if (brokenLinks) {
integrity.warnings.push(`Found ${brokenLinks.length} broken link(s)`);
}
logWithTimestamp(`README integrity check completed: ${integrity.errors.length} errors, ${integrity.warnings.length} warnings`);
return integrity;
} catch (error) {
integrity.isValid = false;
integrity.errors.push(`Integrity check failed: ${error.message}`);
return integrity;
}
}
/**
* Create rollback point before major operations
* @param {string} operationName - Name of the operation
* @returns {Object} Rollback point information
*/
createRollbackPoint(operationName) {
const rollbackInfo = {
success: false,
operationName,
timestamp: new Date().toISOString(),
rollbackId: null,
path: null
};
try {
if (!fileExists(this.readmePath)) {
throw new Error('README file does not exist');
}
const content = fs.readFileSync(this.readmePath, 'utf8');
const rollbackId = `rollback-${operationName}-${Date.now()}`;
const rollbackPath = path.join(
path.dirname(this.readmePath),
`${path.basename(this.readmePath, '.md')}.${rollbackId}.md`
);
// Create rollback file with metadata
const rollbackContent = `<!-- Rollback Point -->\n` +
`<!-- Operation: ${operationName} -->\n` +
`<!-- Created: ${rollbackInfo.timestamp} -->\n` +
`<!-- Rollback ID: ${rollbackId} -->\n\n` +
content;
fs.writeFileSync(rollbackPath, rollbackContent, 'utf8');
rollbackInfo.success = true;
rollbackInfo.rollbackId = rollbackId;
rollbackInfo.path = rollbackPath;
logWithTimestamp(`Rollback point created: ${rollbackId}`);
return rollbackInfo;
} catch (error) {
rollbackInfo.error = error.message;
logWithTimestamp(`Error creating rollback point: ${error.message}`, 'error');
return rollbackInfo;
}
}
/**
* Execute rollback to specific point
* @param {string} rollbackId - Rollback point ID
* @returns {Object} Rollback results
*/
executeRollback(rollbackId) {
const rollbackResult = {
success: false,
rollbackId,
error: null
};
try {
const rollbackPath = path.join(
path.dirname(this.readmePath),
`${path.basename(this.readmePath, '.md')}.${rollbackId}.md`
);
if (!fileExists(rollbackPath)) {
throw new Error(`Rollback point not found: ${rollbackId}`);
}
// Create backup of current state
if (fileExists(this.readmePath)) {
const currentContent = fs.readFileSync(this.readmePath, 'utf8');
this.createVersionedBackup(currentContent, 'pre-rollback');
}
// Restore from rollback point
const rollbackContent = fs.readFileSync(rollbackPath, 'utf8');
const cleanedContent = this.cleanBackupMetadata(rollbackContent);
fs.writeFileSync(this.readmePath, cleanedContent, 'utf8');
rollbackResult.success = true;
logWithTimestamp(`Rollback executed: ${rollbackId}`);
return rollbackResult;
} catch (error) {
rollbackResult.error = error.message;
logWithTimestamp(`Error executing rollback: ${error.message}`, 'error');
return rollbackResult;
}
}
/**
* Get backup and recovery status
* @returns {Object} Status information
*/
getBackupStatus() {
const status = {
hasLatestBackup: fileExists(this.backupPath),
backupCount: 0,
totalBackupSize: 0,
oldestBackup: null,
newestBackup: null,
rollbackPoints: []
};
try {
const backups = this.listBackups();
status.backupCount = backups.length;
status.totalBackupSize = backups.reduce((sum, backup) => sum + backup.size, 0);
if (backups.length > 0) {
status.oldestBackup = backups[backups.length - 1];
status.newestBackup = backups[0];
}
// Find rollback points
const dir = path.dirname(this.readmePath);
const files = fs.readdirSync(dir);
const baseName = path.basename(this.readmePath, '.md');
status.rollbackPoints = files
.filter(file => file.includes(`${baseName}.rollback-`))
.map(file => {
const rollbackId = file.replace(`${baseName}.`, '').replace('.md', '');
return {
id: rollbackId,
filename: file,
created: fs.statSync(path.join(dir, file)).mtime
};
})
.sort((a, b) => b.created - a.created);
return status;
} catch (error) {
status.error = error.message;
return status;
}
}
}