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

684 lines (683 loc) • 27.3 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.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'; }