UNPKG

@re-shell/cli

Version:

Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja

683 lines (682 loc) 26 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.configDiffer = exports.MergeStrategies = exports.ConfigDiffer = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const yaml = __importStar(require("yaml")); const chalk_1 = __importDefault(require("chalk")); const error_handler_1 = require("./error-handler"); // Configuration differ and merger class ConfigDiffer { constructor(options = {}) { this.options = { ignoreOrder: false, ignorePaths: [], includeMetadata: true, deepComparison: true, ...options }; } // Compare two configuration objects async diff(left, right, leftSource = 'left', rightSource = 'right') { const changes = []; const startTime = new Date().toISOString(); // Perform deep comparison this.deepDiff(left, right, '', changes); // Calculate summary const summary = this.calculateSummary(changes); return { summary, changes, metadata: { comparedAt: startTime, leftSource, rightSource, algorithm: 'deep-recursive' } }; } // Compare configuration files async diffFiles(leftPath, rightPath) { try { const leftContent = await fs.readFile(leftPath, 'utf8'); const rightContent = await fs.readFile(rightPath, 'utf8'); const left = yaml.parse(leftContent); const right = yaml.parse(rightContent); return this.diff(left, right, leftPath, rightPath); } catch (error) { throw new error_handler_1.ValidationError(`Failed to diff files: ${error.message}`); } } // Merge two configuration objects async merge(left, right, strategy, leftSource = 'left', rightSource = 'right') { const conflicts = []; const warnings = []; const startTime = new Date().toISOString(); const merged = this.deepMerge(left, right, '', strategy, conflicts, warnings); return { merged, conflicts, warnings, metadata: { mergedAt: startTime, strategy, leftSource, rightSource } }; } // Merge configuration files async mergeFiles(leftPath, rightPath, outputPath, strategy) { try { const leftContent = await fs.readFile(leftPath, 'utf8'); const rightContent = await fs.readFile(rightPath, 'utf8'); const left = yaml.parse(leftContent); const right = yaml.parse(rightContent); const result = await this.merge(left, right, strategy, leftPath, rightPath); // Write merged result await fs.ensureDir(path.dirname(outputPath)); const mergedContent = yaml.stringify(result.merged); await fs.writeFile(outputPath, mergedContent, 'utf8'); return result; } catch (error) { throw new error_handler_1.ValidationError(`Failed to merge files: ${error.message}`); } } // Apply a diff to a configuration async applyDiff(base, diff) { const result = JSON.parse(JSON.stringify(base)); // Deep clone for (const change of diff.changes) { switch (change.operation) { case 'add': this.setValueAtPath(result, change.path, change.newValue); break; case 'remove': this.removeValueAtPath(result, change.path); break; case 'change': this.setValueAtPath(result, change.path, change.newValue); break; // Note: 'move' operations would require more complex handling } } return result; } // Generate a human-readable diff report generateDiffReport(diff, format = 'text') { switch (format) { case 'json': return JSON.stringify(diff, null, 2); case 'html': return this.generateHtmlReport(diff); case 'text': default: return this.generateTextReport(diff); } } // Deep comparison implementation deepDiff(left, right, currentPath, changes) { // Skip ignored paths if (this.options.ignorePaths?.some(ignorePath => currentPath.startsWith(ignorePath))) { return; } const leftType = this.getValueType(left); const rightType = this.getValueType(right); // Handle null/undefined cases if (left === undefined && right !== undefined) { changes.push({ operation: 'add', path: currentPath, newValue: right, type: rightType, description: `Added ${rightType} value`, severity: 'medium', category: 'structure' }); return; } if (left !== undefined && right === undefined) { changes.push({ operation: 'remove', path: currentPath, oldValue: left, type: leftType, description: `Removed ${leftType} value`, severity: 'medium', category: 'structure' }); return; } if (left === undefined && right === undefined) { return; } // Type change detection if (leftType !== rightType) { changes.push({ operation: 'change', path: currentPath, oldValue: left, newValue: right, type: `${leftType}${rightType}`, description: `Type changed from ${leftType} to ${rightType}`, severity: 'high', category: 'type' }); return; } // Handle different types if (leftType === 'object') { this.diffObjects(left, right, currentPath, changes); } else if (leftType === 'array') { this.diffArrays(left, right, currentPath, changes); } else { // Primitive comparison if (left !== right) { changes.push({ operation: 'change', path: currentPath, oldValue: left, newValue: right, type: leftType, description: `Value changed from ${this.formatValue(left)} to ${this.formatValue(right)}`, severity: this.getSeverityForPath(currentPath), category: 'value' }); } } } // Compare objects diffObjects(left, right, currentPath, changes) { const leftKeys = new Set(Object.keys(left)); const rightKeys = new Set(Object.keys(right)); const allKeys = new Set([...leftKeys, ...rightKeys]); for (const key of allKeys) { const newPath = currentPath ? `${currentPath}.${key}` : key; if (leftKeys.has(key) && rightKeys.has(key)) { // Key exists in both this.deepDiff(left[key], right[key], newPath, changes); } else if (leftKeys.has(key)) { // Key only in left (removed) changes.push({ operation: 'remove', path: newPath, oldValue: left[key], type: this.getValueType(left[key]), description: `Removed property '${key}'`, severity: 'medium', category: 'structure' }); } else { // Key only in right (added) changes.push({ operation: 'add', path: newPath, newValue: right[key], type: this.getValueType(right[key]), description: `Added property '${key}'`, severity: 'medium', category: 'structure' }); } } } // Compare arrays diffArrays(left, right, currentPath, changes) { if (this.options.ignoreOrder) { // Order-independent comparison this.diffArraysIgnoreOrder(left, right, currentPath, changes); } else { // Order-dependent comparison this.diffArraysWithOrder(left, right, currentPath, changes); } } // Array comparison ignoring order diffArraysIgnoreOrder(left, right, currentPath, changes) { const leftSet = new Set(left.map(item => JSON.stringify(item))); const rightSet = new Set(right.map(item => JSON.stringify(item))); // Find added items for (const item of right) { const itemStr = JSON.stringify(item); if (!leftSet.has(itemStr)) { changes.push({ operation: 'add', path: `${currentPath}[*]`, newValue: item, type: this.getValueType(item), description: `Added array item`, severity: 'low', category: 'value' }); } } // Find removed items for (const item of left) { const itemStr = JSON.stringify(item); if (!rightSet.has(itemStr)) { changes.push({ operation: 'remove', path: `${currentPath}[*]`, oldValue: item, type: this.getValueType(item), description: `Removed array item`, severity: 'low', category: 'value' }); } } } // Array comparison preserving order diffArraysWithOrder(left, right, currentPath, changes) { const maxLength = Math.max(left.length, right.length); for (let i = 0; i < maxLength; i++) { const newPath = `${currentPath}[${i}]`; if (i < left.length && i < right.length) { // Both have element at index i this.deepDiff(left[i], right[i], newPath, changes); } else if (i < left.length) { // Left has more elements changes.push({ operation: 'remove', path: newPath, oldValue: left[i], type: this.getValueType(left[i]), description: `Removed array element at index ${i}`, severity: 'low', category: 'structure' }); } else { // Right has more elements changes.push({ operation: 'add', path: newPath, newValue: right[i], type: this.getValueType(right[i]), description: `Added array element at index ${i}`, severity: 'low', category: 'structure' }); } } } // Deep merge implementation deepMerge(left, right, currentPath, strategy, conflicts, warnings) { // Handle null/undefined cases if (left === undefined || left === null) return right; if (right === undefined || right === null) return left; const leftType = this.getValueType(left); const rightType = this.getValueType(right); // Type mismatch - create conflict if (leftType !== rightType) { const conflict = { path: currentPath, leftValue: left, rightValue: right, resolution: strategy.conflictResolution === 'interactive' ? 'unresolved' : strategy.conflictResolution, reason: `Type mismatch: ${leftType} vs ${rightType}` }; if (strategy.conflictResolution === 'left') { conflict.resolvedValue = left; } else if (strategy.conflictResolution === 'right') { conflict.resolvedValue = right; } else if (strategy.customResolver) { conflict.resolvedValue = strategy.customResolver(left, right, currentPath); conflict.resolution = 'custom'; } conflicts.push(conflict); return conflict.resolvedValue !== undefined ? conflict.resolvedValue : left; } // Handle different types if (leftType === 'object') { return this.mergeObjects(left, right, currentPath, strategy, conflicts, warnings); } else if (leftType === 'array') { return this.mergeArrays(left, right, currentPath, strategy, conflicts, warnings); } else { // Primitive values - check for conflicts if (left !== right) { const conflict = { path: currentPath, leftValue: left, rightValue: right, resolution: strategy.conflictResolution === 'interactive' ? 'unresolved' : strategy.conflictResolution, reason: 'Value conflict' }; if (strategy.conflictResolution === 'left') { conflict.resolvedValue = left; } else if (strategy.conflictResolution === 'right') { conflict.resolvedValue = right; } else if (strategy.customResolver) { conflict.resolvedValue = strategy.customResolver(left, right, currentPath); conflict.resolution = 'custom'; } conflicts.push(conflict); return conflict.resolvedValue !== undefined ? conflict.resolvedValue : right; } return left; // No conflict } } // Merge objects mergeObjects(left, right, currentPath, strategy, conflicts, warnings) { const result = { ...left }; for (const [key, rightValue] of Object.entries(right)) { const newPath = currentPath ? `${currentPath}.${key}` : key; if (key in left) { result[key] = this.deepMerge(left[key], rightValue, newPath, strategy, conflicts, warnings); } else { result[key] = rightValue; } } return result; } // Merge arrays mergeArrays(left, right, currentPath, strategy, conflicts, warnings) { switch (strategy.arrayMerge) { case 'replace': return right; case 'concat': return [...left, ...right]; case 'union': const unionSet = new Set([...left, ...right].map(item => JSON.stringify(item))); return Array.from(unionSet).map(item => JSON.parse(item)); case 'intersect': const leftSet = new Set(left.map(item => JSON.stringify(item))); return right.filter(item => leftSet.has(JSON.stringify(item))); case 'custom': if (strategy.customResolver) { return strategy.customResolver(left, right, currentPath); } warnings.push(`Custom array merge resolver not provided for ${currentPath}, using concat`); return [...left, ...right]; default: return right; } } // Utility methods getValueType(value) { if (value === null) return 'null'; if (value === undefined) return 'undefined'; if (Array.isArray(value)) return 'array'; return typeof value; } formatValue(value) { if (typeof value === 'string') return `"${value}"`; if (value === null) return 'null'; if (value === undefined) return 'undefined'; return String(value); } getSeverityForPath(path) { // Define critical paths that should have high severity const criticalPaths = ['name', 'version', 'packageManager']; const highPaths = ['framework', 'template', 'type']; const mediumPaths = ['build', 'dev', 'quality']; const pathParts = path.split('.'); const firstPart = pathParts[0]; if (criticalPaths.includes(firstPart)) return 'critical'; if (highPaths.includes(firstPart)) return 'high'; if (mediumPaths.includes(firstPart)) return 'medium'; return 'low'; } calculateSummary(changes) { const summary = { total: changes.length, added: 0, removed: 0, changed: 0, moved: 0, unchanged: 0 }; for (const change of changes) { switch (change.operation) { case 'add': summary.added++; break; case 'remove': summary.removed++; break; case 'change': summary.changed++; break; case 'move': summary.moved++; break; case 'no-change': summary.unchanged++; break; } } return summary; } setValueAtPath(obj, path, value) { const keys = path.split('.'); const lastKey = keys.pop(); let current = obj; for (const key of keys) { if (!(key in current)) { current[key] = {}; } current = current[key]; } current[lastKey] = value; } removeValueAtPath(obj, path) { const keys = path.split('.'); const lastKey = keys.pop(); let current = obj; for (const key of keys) { if (!(key in current)) return; current = current[key]; } delete current[lastKey]; } generateTextReport(diff) { const lines = []; lines.push(chalk_1.default.cyan('Configuration Diff Report')); lines.push(chalk_1.default.gray('='.repeat(40))); lines.push(''); lines.push(chalk_1.default.blue('Summary:')); lines.push(` Total changes: ${diff.summary.total}`); lines.push(` Added: ${chalk_1.default.green(diff.summary.added)}`); lines.push(` Removed: ${chalk_1.default.red(diff.summary.removed)}`); lines.push(` Changed: ${chalk_1.default.yellow(diff.summary.changed)}`); lines.push(` Moved: ${chalk_1.default.blue(diff.summary.moved)}`); lines.push(''); if (diff.changes.length > 0) { lines.push(chalk_1.default.blue('Changes:')); for (const change of diff.changes) { const severity = change.severity || 'low'; const severityColor = { low: chalk_1.default.gray, medium: chalk_1.default.yellow, high: chalk_1.default.red, critical: chalk_1.default.bgRed.white }[severity]; let symbol = ''; let color = chalk_1.default.white; switch (change.operation) { case 'add': symbol = '+'; color = chalk_1.default.green; break; case 'remove': symbol = '-'; color = chalk_1.default.red; break; case 'change': symbol = '~'; color = chalk_1.default.yellow; break; case 'move': symbol = '→'; color = chalk_1.default.blue; break; } lines.push(` ${color(symbol)} ${change.path} ${severityColor(`[${severity}]`)}`); if (change.description) { lines.push(` ${chalk_1.default.gray(change.description)}`); } if (change.operation === 'change' && change.oldValue !== undefined && change.newValue !== undefined) { lines.push(` ${chalk_1.default.red(`- ${this.formatValue(change.oldValue)}`)}`); lines.push(` ${chalk_1.default.green(`+ ${this.formatValue(change.newValue)}`)}`); } else if (change.operation === 'add' && change.newValue !== undefined) { lines.push(` ${chalk_1.default.green(`+ ${this.formatValue(change.newValue)}`)}`); } else if (change.operation === 'remove' && change.oldValue !== undefined) { lines.push(` ${chalk_1.default.red(`- ${this.formatValue(change.oldValue)}`)}`); } lines.push(''); } } lines.push(chalk_1.default.gray(`Generated at: ${diff.metadata.comparedAt}`)); lines.push(chalk_1.default.gray(`Algorithm: ${diff.metadata.algorithm}`)); return lines.join('\n'); } generateHtmlReport(diff) { // Basic HTML report implementation return ` <!DOCTYPE html> <html> <head> <title>Configuration Diff Report</title> <style> body { font-family: monospace; margin: 20px; } .summary { background: #f5f5f5; padding: 10px; margin: 10px 0; } .change { margin: 5px 0; padding: 5px; } .add { background: #e8f5e8; } .remove { background: #ffe8e8; } .change-op { background: #fff8e8; } .critical { border-left: 5px solid red; } .high { border-left: 5px solid orange; } .medium { border-left: 5px solid yellow; } .low { border-left: 5px solid gray; } </style> </head> <body> <h1>Configuration Diff Report</h1> <div class="summary"> <h2>Summary</h2> <p>Total changes: ${diff.summary.total}</p> <p>Added: ${diff.summary.added}</p> <p>Removed: ${diff.summary.removed}</p> <p>Changed: ${diff.summary.changed}</p> </div> <div class="changes"> <h2>Changes</h2> ${diff.changes.map(change => ` <div class="change ${change.operation} ${change.severity || 'low'}"> <strong>${change.path}</strong> - ${change.operation} <br><small>${change.description || ''}</small> </div> `).join('')} </div> <footer> <p>Generated at: ${diff.metadata.comparedAt}</p> </footer> </body> </html>`; } } exports.ConfigDiffer = ConfigDiffer; // Default merge strategies exports.MergeStrategies = { // Prefer left (base) configuration leftWins: () => ({ arrayMerge: 'replace', conflictResolution: 'left', preserveComments: true, preserveOrder: true }), // Prefer right (incoming) configuration rightWins: () => ({ arrayMerge: 'replace', conflictResolution: 'right', preserveComments: true, preserveOrder: true }), // Smart merge with array concatenation smartMerge: () => ({ arrayMerge: 'union', conflictResolution: 'right', preserveComments: true, preserveOrder: false }), // Conservative merge (preserve existing) conservative: () => ({ arrayMerge: 'concat', conflictResolution: 'left', preserveComments: true, preserveOrder: true }), // Interactive merge (requires user input) interactive: () => ({ arrayMerge: 'union', conflictResolution: 'interactive', preserveComments: true, preserveOrder: false }) }; // Export singleton instance exports.configDiffer = new ConfigDiffer();