@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
684 lines (683 loc) ⢠27.3 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.manageConfigDiff = manageConfigDiff;
const chalk_1 = __importDefault(require("chalk"));
const prompts_1 = __importDefault(require("prompts"));
const path = __importStar(require("path"));
const fs = __importStar(require("fs-extra"));
const config_diff_1 = require("../utils/config-diff");
const config_1 = require("../utils/config");
const error_handler_1 = require("../utils/error-handler");
async function manageConfigDiff(options = {}) {
const { spinner, verbose, json } = options;
try {
if (options.diff) {
await performDiff(options, spinner);
return;
}
if (options.merge) {
await performMerge(options, spinner);
return;
}
if (options.apply && options.left) {
await applyDiff(options, spinner);
return;
}
if (options.interactive) {
await interactiveDiffMerge(options, spinner);
return;
}
// Default: show help or current configuration status
await showConfigStatus(options, spinner);
}
catch (error) {
if (spinner)
spinner.fail(chalk_1.default.red('Configuration diff/merge operation failed'));
throw error;
}
}
async function performDiff(options, spinner) {
if (!options.left || !options.right) {
throw new error_handler_1.ValidationError('Both --left and --right configuration sources are required for diff');
}
if (spinner)
spinner.setText('Comparing configurations...');
// Load configurations
const { leftConfig, rightConfig, leftSource, rightSource } = await loadConfigSources(options.left, options.right);
// Configure differ
const differ = new (require('../utils/config-diff').ConfigDiffer)({
ignoreOrder: options.ignoreOrder,
ignorePaths: options.ignorePaths ? options.ignorePaths.split(',') : [],
includeMetadata: true,
deepComparison: true
});
// Perform diff
const diff = await differ.diff(leftConfig, rightConfig, leftSource, rightSource);
if (spinner)
spinner.stop();
// Output results
if (options.json) {
console.log(JSON.stringify(diff, null, 2));
}
else {
const report = differ.generateDiffReport(diff, options.format || 'text');
if (options.format === 'html' && options.output) {
await fs.writeFile(options.output, report);
console.log(chalk_1.default.green(`ā
HTML diff report saved to: ${options.output}`));
}
else {
console.log(report);
}
// Show summary
console.log(chalk_1.default.cyan('\\nš Diff Summary:'));
console.log(`Total changes: ${diff.summary.total}`);
if (diff.summary.total > 0) {
console.log(`⢠Added: ${chalk_1.default.green(diff.summary.added)} items`);
console.log(`⢠Removed: ${chalk_1.default.red(diff.summary.removed)} items`);
console.log(`⢠Changed: ${chalk_1.default.yellow(diff.summary.changed)} items`);
console.log(`⢠Moved: ${chalk_1.default.blue(diff.summary.moved)} items`);
}
else {
console.log(chalk_1.default.green('š Configurations are identical!'));
}
}
}
async function performMerge(options, spinner) {
if (!options.left || !options.right) {
throw new error_handler_1.ValidationError('Both --left and --right configuration sources are required for merge');
}
if (spinner)
spinner.setText('Merging configurations...');
// Load configurations
const { leftConfig, rightConfig, leftSource, rightSource } = await loadConfigSources(options.left, options.right);
// Get merge strategy
let strategy;
if (options.strategy) {
strategy = getMergeStrategy(options.strategy);
}
else if (options.interactive) {
strategy = await getInteractiveMergeStrategy();
}
else {
strategy = config_diff_1.MergeStrategies.smartMerge();
}
// Perform merge
const result = await config_diff_1.configDiffer.merge(leftConfig, rightConfig, strategy, leftSource, rightSource);
if (spinner)
spinner.stop();
// Handle conflicts if any
if (result.conflicts.length > 0 && strategy.conflictResolution === 'interactive') {
await resolveConflictsInteractively(result);
}
// Output results
if (options.output) {
const outputPath = path.resolve(options.output);
if (outputPath.endsWith('.json')) {
await fs.writeFile(outputPath, JSON.stringify(result.merged, null, 2));
}
else {
const yaml = require('yaml');
await fs.writeFile(outputPath, yaml.stringify(result.merged));
}
console.log(chalk_1.default.green(`ā
Merged configuration saved to: ${outputPath}`));
}
else if (options.json) {
console.log(JSON.stringify(result.merged, null, 2));
}
else {
const yaml = require('yaml');
console.log(chalk_1.default.cyan('\\nš Merged Configuration:'));
console.log(chalk_1.default.gray('ā'.repeat(40)));
console.log(yaml.stringify(result.merged));
}
// Show merge summary
console.log(chalk_1.default.cyan('\\nš Merge Summary:'));
if (result.conflicts.length > 0) {
console.log(`Conflicts: ${chalk_1.default.red(result.conflicts.length)}`);
result.conflicts.forEach((conflict, index) => {
console.log(` ${index + 1}. ${conflict.path}: ${conflict.reason} (${conflict.resolution})`);
});
}
else {
console.log(chalk_1.default.green('ā
No conflicts detected'));
}
if (result.warnings.length > 0) {
console.log(`Warnings: ${chalk_1.default.yellow(result.warnings.length)}`);
result.warnings.forEach((warning, index) => {
console.log(` ${index + 1}. ${warning}`);
});
}
}
async function applyDiff(options, spinner) {
if (!options.left || !options.right) {
throw new error_handler_1.ValidationError('Both --left (base config) and --right (diff file) are required for apply');
}
if (spinner)
spinner.setText('Applying diff...');
// Load base configuration
const baseConfig = await loadConfigFromSource(options.left);
// Load diff from file
const diffContent = await fs.readFile(options.right, 'utf8');
const diff = JSON.parse(diffContent);
// Apply diff
const result = await config_diff_1.configDiffer.applyDiff(baseConfig, diff);
if (spinner)
spinner.stop();
// Output results
if (options.output) {
const outputPath = path.resolve(options.output);
if (outputPath.endsWith('.json')) {
await fs.writeFile(outputPath, JSON.stringify(result, null, 2));
}
else {
const yaml = require('yaml');
await fs.writeFile(outputPath, yaml.stringify(result));
}
console.log(chalk_1.default.green(`ā
Configuration with applied diff saved to: ${outputPath}`));
}
else if (options.json) {
console.log(JSON.stringify(result, null, 2));
}
else {
const yaml = require('yaml');
console.log(chalk_1.default.cyan('\\nš Configuration with Applied Diff:'));
console.log(chalk_1.default.gray('ā'.repeat(40)));
console.log(yaml.stringify(result));
}
console.log(chalk_1.default.green(`\\nā
Applied ${diff.summary.total} changes from diff`));
}
async function interactiveDiffMerge(options, spinner) {
if (spinner)
spinner.stop();
const response = await (0, prompts_1.default)([
{
type: 'select',
name: 'operation',
message: 'What would you like to do?',
choices: [
{ title: 'š Compare two configurations (diff)', value: 'diff' },
{ title: 'š Merge two configurations', value: 'merge' },
{ title: 'š Apply diff to configuration', value: 'apply' },
{ title: 'š Compare configuration levels', value: 'levels' },
{ title: 'š Create configuration patch', value: 'patch' }
]
}
]);
if (!response.operation)
return;
switch (response.operation) {
case 'diff':
await interactiveDiff();
break;
case 'merge':
await interactiveMerge();
break;
case 'apply':
await interactiveApply();
break;
case 'levels':
await compareLevels();
break;
case 'patch':
await createPatch();
break;
}
}
async function interactiveDiff() {
const response = await (0, prompts_1.default)([
{
type: 'text',
name: 'left',
message: 'Left configuration (file path or source):',
validate: (value) => value.trim() ? true : 'Left configuration is required'
},
{
type: 'text',
name: 'right',
message: 'Right configuration (file path or source):',
validate: (value) => value.trim() ? true : 'Right configuration is required'
},
{
type: 'select',
name: 'format',
message: 'Output format:',
choices: [
{ title: 'Text (console)', value: 'text' },
{ title: 'HTML report', value: 'html' },
{ title: 'JSON data', value: 'json' }
]
},
{
type: 'toggle',
name: 'ignoreOrder',
message: 'Ignore array order?',
initial: false,
active: 'yes',
inactive: 'no'
},
{
type: 'text',
name: 'output',
message: 'Output file (optional):',
validate: (value, answers) => {
if (!value)
return true;
if (answers.format === 'html' && !value.endsWith('.html')) {
return 'HTML format requires .html extension';
}
if (answers.format === 'json' && !value.endsWith('.json')) {
return 'JSON format requires .json extension';
}
return true;
}
}
]);
if (!response.left || !response.right)
return;
await performDiff({
diff: true,
left: response.left,
right: response.right,
format: response.format,
ignoreOrder: response.ignoreOrder,
output: response.output
});
}
async function interactiveMerge() {
const response = await (0, prompts_1.default)([
{
type: 'text',
name: 'left',
message: 'Base configuration (file path or source):',
validate: (value) => value.trim() ? true : 'Base configuration is required'
},
{
type: 'text',
name: 'right',
message: 'Incoming configuration (file path or source):',
validate: (value) => value.trim() ? true : 'Incoming configuration is required'
},
{
type: 'select',
name: 'strategy',
message: 'Merge strategy:',
choices: [
{ title: 'š§ Smart merge (recommended)', value: 'smart' },
{ title: 'ā¬
ļø Prefer base (left wins)', value: 'left' },
{ title: 'ā”ļø Prefer incoming (right wins)', value: 'right' },
{ title: 'š”ļø Conservative merge', value: 'conservative' },
{ title: 'š¤ Interactive resolution', value: 'interactive' }
]
},
{
type: 'text',
name: 'output',
message: 'Output file path:',
validate: (value) => value.trim() ? true : 'Output file is required for merge'
}
]);
if (!response.left || !response.right || !response.output)
return;
await performMerge({
merge: true,
left: response.left,
right: response.right,
strategy: response.strategy,
output: response.output,
interactive: response.strategy === 'interactive'
});
}
async function interactiveApply() {
const response = await (0, prompts_1.default)([
{
type: 'text',
name: 'config',
message: 'Base configuration file:',
validate: (value) => value.trim() ? true : 'Configuration file is required'
},
{
type: 'text',
name: 'diff',
message: 'Diff file (JSON format):',
validate: (value) => {
if (!value.trim())
return 'Diff file is required';
if (!value.endsWith('.json'))
return 'Diff file must be JSON format';
return true;
}
},
{
type: 'text',
name: 'output',
message: 'Output file path:',
validate: (value) => value.trim() ? true : 'Output file is required'
}
]);
if (!response.config || !response.diff || !response.output)
return;
await applyDiff({
apply: true,
left: response.config,
right: response.diff,
output: response.output
});
}
async function compareLevels() {
console.log(chalk_1.default.cyan('\\nšļø Configuration Levels Comparison'));
console.log(chalk_1.default.gray('Compare global, project, and workspace configurations'));
const response = await (0, prompts_1.default)([
{
type: 'text',
name: 'workspace',
message: 'Workspace path (optional):',
initial: process.cwd()
}
]);
// Load all configuration levels
const globalConfig = await config_1.configManager.loadGlobalConfig();
const projectConfig = await config_1.configManager.loadProjectConfig();
const workspaceConfig = response.workspace
? await config_1.configManager.loadWorkspaceConfig(response.workspace)
: null;
// Compare each level
console.log(chalk_1.default.cyan('\\nš Configuration Levels Analysis:'));
if (projectConfig) {
const globalVsProject = await config_diff_1.configDiffer.diff(globalConfig, projectConfig, 'global', 'project');
console.log(chalk_1.default.blue('\\nš Global vs Project:'));
console.log(` Changes: ${globalVsProject.summary.total}`);
if (globalVsProject.summary.total > 0) {
console.log(` ⢠Added: ${globalVsProject.summary.added}`);
console.log(` ⢠Changed: ${globalVsProject.summary.changed}`);
console.log(` ⢠Removed: ${globalVsProject.summary.removed}`);
}
}
if (workspaceConfig) {
const projectVsWorkspace = await config_diff_1.configDiffer.diff(projectConfig || globalConfig, workspaceConfig, projectConfig ? 'project' : 'global', 'workspace');
console.log(chalk_1.default.green('\\nš¢ Project vs Workspace:'));
console.log(` Changes: ${projectVsWorkspace.summary.total}`);
if (projectVsWorkspace.summary.total > 0) {
console.log(` ⢠Added: ${projectVsWorkspace.summary.added}`);
console.log(` ⢠Changed: ${projectVsWorkspace.summary.changed}`);
console.log(` ⢠Removed: ${projectVsWorkspace.summary.removed}`);
}
}
// Show inheritance chain
const merged = await config_1.configManager.getMergedConfig();
console.log(chalk_1.default.magenta('\\nš Final Configuration Source:'));
console.log(` Package Manager: ${merged.merged.packageManager} (from ${getConfigSource('packageManager', globalConfig, projectConfig, workspaceConfig)})`);
console.log(` Framework: ${merged.merged.framework} (from ${getConfigSource('framework', globalConfig, projectConfig, workspaceConfig)})`);
console.log(` Template: ${merged.merged.template} (from ${getConfigSource('template', globalConfig, projectConfig, workspaceConfig)})`);
}
async function createPatch() {
const response = await (0, prompts_1.default)([
{
type: 'text',
name: 'original',
message: 'Original configuration file:',
validate: (value) => value.trim() ? true : 'Original file is required'
},
{
type: 'text',
name: 'modified',
message: 'Modified configuration file:',
validate: (value) => value.trim() ? true : 'Modified file is required'
},
{
type: 'text',
name: 'output',
message: 'Patch file output path:',
initial: 'config.patch.json',
validate: (value) => value.trim() ? true : 'Output path is required'
}
]);
if (!response.original || !response.modified || !response.output)
return;
// Create diff (which serves as our patch)
const diff = await config_diff_1.configDiffer.diffFiles(response.original, response.modified);
// Save patch
await fs.writeFile(response.output, JSON.stringify(diff, null, 2));
console.log(chalk_1.default.green(`\\nā
Patch created: ${response.output}`));
console.log(`Changes: ${diff.summary.total}`);
console.log(chalk_1.default.gray('Apply with: re-shell config-diff apply --left <original> --right <patch> --output <result>'));
}
async function showConfigStatus(options, spinner) {
if (spinner)
spinner.setText('Analyzing configuration status...');
// Load current configurations
const globalConfig = await config_1.configManager.loadGlobalConfig();
const projectConfig = await config_1.configManager.loadProjectConfig();
const merged = await config_1.configManager.getMergedConfig();
if (spinner)
spinner.stop();
console.log(chalk_1.default.cyan('\\nš Configuration Status'));
console.log(chalk_1.default.gray('ā'.repeat(40)));
console.log(chalk_1.default.blue('\\nConfiguration Levels:'));
console.log(` š Global: ${chalk_1.default.green('ā')} (${Object.keys(globalConfig).length} properties)`);
console.log(` š Project: ${projectConfig ? chalk_1.default.green('ā') : chalk_1.default.red('ā')} ${projectConfig ? `(${Object.keys(projectConfig).length} properties)` : ''}`);
if (projectConfig) {
// Calculate inheritance
const diff = await config_diff_1.configDiffer.diff(globalConfig, projectConfig);
console.log(`\\nš Inheritance Analysis:`);
console.log(` Overrides: ${diff.summary.changed} properties`);
console.log(` Additions: ${diff.summary.added} properties`);
console.log(` Total customization: ${diff.summary.total} changes`);
}
console.log(chalk_1.default.cyan('\\nš ļø Available Operations:'));
console.log(' ⢠re-shell config-diff --diff --left <file1> --right <file2>');
console.log(' ⢠re-shell config-diff --merge --left <base> --right <incoming> --output <result>');
console.log(' ⢠re-shell config-diff --interactive');
}
// Helper functions
async function loadConfigSources(leftSource, rightSource) {
const leftConfig = await loadConfigFromSource(leftSource);
const rightConfig = await loadConfigFromSource(rightSource);
return {
leftConfig,
rightConfig,
leftSource,
rightSource
};
}
async function loadConfigFromSource(source) {
// Handle special sources
if (source === 'global') {
return config_1.configManager.loadGlobalConfig();
}
if (source === 'project') {
const config = await config_1.configManager.loadProjectConfig();
if (!config)
throw new error_handler_1.ValidationError('No project configuration found');
return config;
}
if (source.startsWith('workspace:')) {
const workspacePath = source.substring(10);
const config = await config_1.configManager.loadWorkspaceConfig(workspacePath);
if (!config)
throw new error_handler_1.ValidationError(`No workspace configuration found at ${workspacePath}`);
return config;
}
// Load from file
if (await fs.pathExists(source)) {
const content = await fs.readFile(source, 'utf8');
if (source.endsWith('.json')) {
return JSON.parse(content);
}
else if (source.endsWith('.yaml') || source.endsWith('.yml')) {
const yaml = require('yaml');
return yaml.parse(content);
}
else {
throw new error_handler_1.ValidationError(`Unsupported file format: ${source}`);
}
}
throw new error_handler_1.ValidationError(`Configuration source not found: ${source}`);
}
function getMergeStrategy(strategyName) {
switch (strategyName) {
case 'left':
return config_diff_1.MergeStrategies.leftWins();
case 'right':
return config_diff_1.MergeStrategies.rightWins();
case 'smart':
return config_diff_1.MergeStrategies.smartMerge();
case 'conservative':
return config_diff_1.MergeStrategies.conservative();
case 'interactive':
return config_diff_1.MergeStrategies.interactive();
default:
throw new error_handler_1.ValidationError(`Unknown merge strategy: ${strategyName}`);
}
}
async function getInteractiveMergeStrategy() {
const response = await (0, prompts_1.default)([
{
type: 'select',
name: 'conflictResolution',
message: 'How to resolve conflicts?',
choices: [
{ title: 'Prefer base configuration', value: 'left' },
{ title: 'Prefer incoming configuration', value: 'right' },
{ title: 'Ask for each conflict', value: 'interactive' }
]
},
{
type: 'select',
name: 'arrayMerge',
message: 'How to merge arrays?',
choices: [
{ title: 'Union (combine unique items)', value: 'union' },
{ title: 'Concatenate (append all)', value: 'concat' },
{ title: 'Replace with incoming', value: 'replace' }
]
},
{
type: 'toggle',
name: 'preserveOrder',
message: 'Preserve object key order?',
initial: true,
active: 'yes',
inactive: 'no'
}
]);
return {
arrayMerge: response.arrayMerge,
conflictResolution: response.conflictResolution,
preserveComments: true,
preserveOrder: response.preserveOrder
};
}
async function resolveConflictsInteractively(result) {
console.log(chalk_1.default.yellow(`\\nā ļø Found ${result.conflicts.length} conflicts to resolve:`));
for (const conflict of result.conflicts) {
if (conflict.resolution !== 'unresolved')
continue;
console.log(chalk_1.default.cyan(`\\nš„ Conflict at: ${conflict.path}`));
console.log(chalk_1.default.gray(conflict.reason));
const response = await (0, prompts_1.default)([
{
type: 'select',
name: 'resolution',
message: 'How to resolve this conflict?',
choices: [
{ title: `Use base value: ${formatConflictValue(conflict.leftValue)}`, value: 'left' },
{ title: `Use incoming value: ${formatConflictValue(conflict.rightValue)}`, value: 'right' },
{ title: 'Enter custom value', value: 'custom' }
]
}
]);
if (response.resolution === 'custom') {
const customResponse = await (0, prompts_1.default)([
{
type: 'text',
name: 'value',
message: 'Enter custom value:',
initial: String(conflict.rightValue)
}
]);
try {
conflict.resolvedValue = JSON.parse(customResponse.value);
}
catch {
conflict.resolvedValue = customResponse.value;
}
conflict.resolution = 'custom';
}
else if (response.resolution === 'left') {
conflict.resolvedValue = conflict.leftValue;
conflict.resolution = 'left';
}
else if (response.resolution === 'right') {
conflict.resolvedValue = conflict.rightValue;
conflict.resolution = 'right';
}
// Apply resolution to merged result
setValueAtPath(result.merged, conflict.path, conflict.resolvedValue);
}
}
function formatConflictValue(value) {
if (typeof value === 'string')
return `"${value}"`;
if (value === null)
return 'null';
if (value === undefined)
return 'undefined';
if (typeof value === 'object')
return JSON.stringify(value);
return String(value);
}
function 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;
}
function getConfigSource(property, global, project, workspace) {
if (workspace && workspace[property] !== undefined)
return 'workspace';
if (project && project[property] !== undefined)
return 'project';
return 'global';
}