@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
JavaScript
;
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();