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

697 lines (696 loc) 27.5 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.PluginRegistry = void 0; exports.createPluginRegistry = createPluginRegistry; exports.discoverPlugins = discoverPlugins; exports.validatePluginManifest = validatePluginManifest; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const events_1 = require("events"); const chalk_1 = __importDefault(require("chalk")); const error_handler_1 = require("./error-handler"); const plugin_lifecycle_1 = require("./plugin-lifecycle"); const plugin_hooks_1 = require("./plugin-hooks"); const plugin_dependency_1 = require("./plugin-dependency"); // Plugin Registry and Discovery System class PluginRegistry extends events_1.EventEmitter { constructor(rootPath = process.cwd()) { super(); this.plugins = new Map(); this.discoveryCache = new Map(); this.isInitialized = false; this.rootPath = rootPath; this.pluginPaths = this.getDefaultPluginPaths(); this.lifecycleManager = (0, plugin_lifecycle_1.createPluginLifecycleManager)({ timeout: 30000, validateSecurity: true, enableHotReload: process.env.NODE_ENV === 'development' }); this.hookSystem = (0, plugin_hooks_1.createHookSystem)({ debugMode: process.env.NODE_ENV === 'development' }); this.dependencyResolver = (0, plugin_dependency_1.createDependencyResolver)({ strategy: 'strict', allowPrerelease: false, preferStable: true, maxDepth: 10 }); // Forward lifecycle events this.lifecycleManager.on('state-changed', (event) => { this.emit('plugin-state-changed', event); // Emit hook events for plugin lifecycle changes this.hookSystem.execute(plugin_hooks_1.HookType.PLUGIN_LOAD, { plugin: event.pluginName, state: event.newState }); }); // Forward hook system events this.hookSystem.on('hook-registered', (event) => { this.emit('hook-registered', event); }); } // Get default plugin discovery paths getDefaultPluginPaths() { const paths = []; // Local project plugins paths.push(path.join(this.rootPath, '.re-shell', 'plugins')); paths.push(path.join(this.rootPath, 'plugins')); // Global CLI plugins const globalPaths = this.getGlobalPluginPaths(); paths.push(...globalPaths); // Built-in plugins paths.push(path.join(__dirname, '..', 'plugins')); return paths.filter(p => fs.existsSync(p)); } // Get global plugin paths based on installation method getGlobalPluginPaths() { const paths = []; try { // npm global modules const { execSync } = require('child_process'); const npmGlobal = execSync('npm root -g', { encoding: 'utf8' }).trim(); paths.push(path.join(npmGlobal, '@re-shell')); // User's home directory const homeDir = require('os').homedir(); paths.push(path.join(homeDir, '.re-shell', 'plugins')); // System-wide plugins (Unix-like systems) if (process.platform !== 'win32') { paths.push('/usr/local/share/re-shell/plugins'); paths.push('/usr/share/re-shell/plugins'); } } catch (error) { // Ignore errors in path detection } return paths; } // Initialize the plugin registry async initialize() { if (this.isInitialized) return; try { // Initialize lifecycle manager await this.lifecycleManager.initialize(); // Ensure plugin directories exist await this.ensurePluginDirectories(); // Discover plugins const discoveryResult = await this.discoverPlugins(); // Register discovered plugins with both registry and lifecycle manager for (const plugin of discoveryResult.found) { this.plugins.set(plugin.manifest.name, plugin); await this.lifecycleManager.registerPlugin(plugin); } // Report discovery results this.emit('initialized', { totalPlugins: this.plugins.size, errors: discoveryResult.errors.length, skipped: discoveryResult.skipped.length }); this.isInitialized = true; } catch (error) { this.emit('error', new error_handler_1.ValidationError(`Failed to initialize plugin registry: ${error instanceof Error ? error.message : String(error)}`)); throw error; } } // Ensure plugin directories exist async ensurePluginDirectories() { const localPluginPath = path.join(this.rootPath, '.re-shell', 'plugins'); await fs.ensureDir(localPluginPath); // Create default plugin structure const pluginConfigPath = path.join(this.rootPath, '.re-shell', 'plugins.json'); if (!await fs.pathExists(pluginConfigPath)) { await fs.writeJSON(pluginConfigPath, { version: '1.0.0', plugins: {}, disabled: [], settings: { autoUpdate: false, security: { allowUnverified: false, trustedSources: ['npm', 'builtin'] } } }, { spaces: 2 }); } } // Discover plugins from all configured sources async discoverPlugins(options = {}) { const { sources = ['local', 'npm', 'builtin'], includeDisabled = false, includeDev = true, maxDepth = 3, timeout = 10000, useCache = true, cacheMaxAge = 300000 // 5 minutes } = options; const cacheKey = JSON.stringify({ sources, includeDisabled, includeDev }); // Check cache if enabled if (useCache && this.discoveryCache.has(cacheKey)) { const cached = this.discoveryCache.get(cacheKey); if (Date.now() - cached.timestamp < cacheMaxAge) { return cached; } } const result = { found: [], errors: [], skipped: [] }; // Discover from each source for (const source of sources) { try { const sourceResult = await this.discoverFromSource(source, { includeDisabled, includeDev, maxDepth, timeout }); result.found.push(...sourceResult.found); result.errors.push(...sourceResult.errors); result.skipped.push(...sourceResult.skipped); } catch (error) { result.errors.push({ path: source, error: error instanceof Error ? error : new Error(String(error)) }); } } // Remove duplicates (prefer local over global) result.found = this.deduplicatePlugins(result.found); // Cache result if (useCache) { result.timestamp = Date.now(); this.discoveryCache.set(cacheKey, result); } this.emit('discovery-completed', result); return result; } // Discover plugins from a specific source async discoverFromSource(source, options) { switch (source) { case 'local': return this.discoverLocalPlugins(options); case 'npm': return this.discoverNpmPlugins(options); case 'builtin': return this.discoverBuiltinPlugins(options); default: throw new Error(`Unknown plugin source: ${source}`); } } // Discover local plugins async discoverLocalPlugins(options) { const result = { found: [], errors: [], skipped: [] }; const localPaths = [ path.join(this.rootPath, '.re-shell', 'plugins'), path.join(this.rootPath, 'plugins') ]; for (const basePath of localPaths) { if (!await fs.pathExists(basePath)) continue; try { const entries = await fs.readdir(basePath, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const pluginPath = path.join(basePath, entry.name); const manifestPath = path.join(pluginPath, 'package.json'); if (!await fs.pathExists(manifestPath)) { result.skipped.push({ path: pluginPath, reason: 'No package.json found' }); continue; } try { const manifestData = await fs.readJSON(manifestPath); const manifest = this.validateManifest(manifestData); result.found.push({ manifest, pluginPath, isLoaded: false, isActive: false, usageCount: 0 }); } catch (error) { result.errors.push({ path: pluginPath, error: error instanceof Error ? error : new Error(String(error)) }); } } } catch (error) { result.errors.push({ path: basePath, error: error instanceof Error ? error : new Error(String(error)) }); } } return result; } // Discover npm plugins async discoverNpmPlugins(options) { const result = { found: [], errors: [], skipped: [] }; try { // Search for packages with 'reshell-plugin' keyword const { execSync } = require('child_process'); // Check local node_modules first const localNodeModules = path.join(this.rootPath, 'node_modules'); if (await fs.pathExists(localNodeModules)) { const localResult = await this.scanNodeModules(localNodeModules); result.found.push(...localResult.found); result.errors.push(...localResult.errors); result.skipped.push(...localResult.skipped); } // Check global node_modules try { const globalNodeModules = execSync('npm root -g', { encoding: 'utf8' }).trim(); if (await fs.pathExists(globalNodeModules)) { const globalResult = await this.scanNodeModules(globalNodeModules); result.found.push(...globalResult.found); result.errors.push(...globalResult.errors); result.skipped.push(...globalResult.skipped); } } catch (error) { // Ignore global npm errors } } catch (error) { result.errors.push({ path: 'npm', error: error instanceof Error ? error : new Error(String(error)) }); } return result; } // Scan node_modules for plugins async scanNodeModules(nodeModulesPath) { const result = { found: [], errors: [], skipped: [] }; try { const entries = await fs.readdir(nodeModulesPath, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const packagePath = path.join(nodeModulesPath, entry.name); // Handle scoped packages if (entry.name.startsWith('@')) { const scopedEntries = await fs.readdir(packagePath, { withFileTypes: true }); for (const scopedEntry of scopedEntries) { if (!scopedEntry.isDirectory()) continue; const scopedPackagePath = path.join(packagePath, scopedEntry.name); await this.checkPackageForPlugin(scopedPackagePath, result); } } else { await this.checkPackageForPlugin(packagePath, result); } } } catch (error) { result.errors.push({ path: nodeModulesPath, error: error instanceof Error ? error : new Error(String(error)) }); } return result; } // Check if a package is a Re-Shell plugin async checkPackageForPlugin(packagePath, result) { const manifestPath = path.join(packagePath, 'package.json'); if (!await fs.pathExists(manifestPath)) { return; } try { const manifestData = await fs.readJSON(manifestPath); // Check if it's a Re-Shell plugin const isPlugin = manifestData.keywords?.includes('reshell-plugin') || manifestData.name?.startsWith('reshell-plugin-') || manifestData.reshell || manifestData.name?.startsWith('@re-shell/'); if (!isPlugin) { return; } const manifest = this.validateManifest(manifestData); result.found.push({ manifest, pluginPath: packagePath, isLoaded: false, isActive: false, usageCount: 0 }); } catch (error) { result.errors.push({ path: packagePath, error: error instanceof Error ? error : new Error(String(error)) }); } } // Discover built-in plugins async discoverBuiltinPlugins(options) { const result = { found: [], errors: [], skipped: [] }; const builtinPath = path.join(__dirname, '..', 'plugins'); if (!await fs.pathExists(builtinPath)) { return result; } try { const entries = await fs.readdir(builtinPath, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const pluginPath = path.join(builtinPath, entry.name); const manifestPath = path.join(pluginPath, 'plugin.json'); if (!await fs.pathExists(manifestPath)) { result.skipped.push({ path: pluginPath, reason: 'No plugin.json found' }); continue; } try { const manifestData = await fs.readJSON(manifestPath); const manifest = this.validateManifest(manifestData); result.found.push({ manifest, pluginPath, isLoaded: false, isActive: false, usageCount: 0 }); } catch (error) { result.errors.push({ path: pluginPath, error: error instanceof Error ? error : new Error(String(error)) }); } } } catch (error) { result.errors.push({ path: builtinPath, error: error instanceof Error ? error : new Error(String(error)) }); } return result; } // Remove duplicate plugins (prefer local over global) deduplicatePlugins(plugins) { const seen = new Map(); // Sort by preference: local, npm, builtin const sorted = plugins.sort((a, b) => { const aIsLocal = a.pluginPath.includes('.re-shell') || a.pluginPath.includes('/plugins'); const bIsLocal = b.pluginPath.includes('.re-shell') || b.pluginPath.includes('/plugins'); if (aIsLocal && !bIsLocal) return -1; if (!aIsLocal && bIsLocal) return 1; return 0; }); for (const plugin of sorted) { const key = plugin.manifest.name; if (!seen.has(key)) { seen.set(key, plugin); } } return Array.from(seen.values()); } // Validate plugin manifest validateManifest(data) { if (!data.name || typeof data.name !== 'string') { throw new error_handler_1.ValidationError('Plugin manifest must have a valid name'); } if (!data.version || typeof data.version !== 'string') { throw new error_handler_1.ValidationError('Plugin manifest must have a valid version'); } if (!data.description || typeof data.description !== 'string') { throw new error_handler_1.ValidationError('Plugin manifest must have a description'); } if (!data.main || typeof data.main !== 'string') { throw new error_handler_1.ValidationError('Plugin manifest must specify a main entry point'); } return { name: data.name, version: data.version, description: data.description, author: data.author, license: data.license, homepage: data.homepage, keywords: data.keywords || [], main: data.main, bin: data.bin, engines: data.engines, dependencies: data.dependencies, peerDependencies: data.peerDependencies, reshell: data.reshell || {} }; } // Register a plugin manually async registerPlugin(pluginPath, manifest) { try { let pluginManifest = manifest; if (!pluginManifest) { const manifestPath = path.join(pluginPath, 'package.json'); if (!await fs.pathExists(manifestPath)) { throw new error_handler_1.ValidationError(`No package.json found at ${manifestPath}`); } const manifestData = await fs.readJSON(manifestPath); pluginManifest = this.validateManifest(manifestData); } const registration = { manifest: pluginManifest, pluginPath, isLoaded: false, isActive: false, usageCount: 0 }; this.plugins.set(pluginManifest.name, registration); this.emit('plugin-registered', registration); } catch (error) { this.emit('error', new error_handler_1.ValidationError(`Failed to register plugin at ${pluginPath}: ${error instanceof Error ? error.message : String(error)}`)); throw error; } } // Unregister a plugin async unregisterPlugin(name) { const registration = this.plugins.get(name); if (!registration) { return false; } // Deactivate if active if (registration.isActive && registration.instance?.deactivate) { try { await registration.instance.deactivate(this.createPluginContext(registration)); } catch (error) { this.emit('error', error); } } this.plugins.delete(name); this.emit('plugin-unregistered', { name, registration }); return true; } // Get all registered plugins getPlugins() { return Array.from(this.plugins.values()); } // Get a specific plugin getPlugin(name) { return this.plugins.get(name); } // Check if a plugin is registered hasPlugin(name) { return this.plugins.has(name); } // Get plugin count getPluginCount() { return this.plugins.size; } // Get active plugins getActivePlugins() { return Array.from(this.plugins.values()).filter(p => p.isActive); } // Clear discovery cache clearCache() { this.discoveryCache.clear(); this.emit('cache-cleared'); } // Plugin lifecycle management methods async loadPlugin(pluginName) { return await this.lifecycleManager.loadPlugin(pluginName); } async initializePlugin(pluginName) { return await this.lifecycleManager.initializePlugin(pluginName); } async activatePlugin(pluginName) { return await this.lifecycleManager.activatePlugin(pluginName); } async deactivatePlugin(pluginName) { return await this.lifecycleManager.deactivatePlugin(pluginName); } async unloadPlugin(pluginName) { return await this.lifecycleManager.unloadPlugin(pluginName); } async reloadPlugin(pluginName) { return await this.lifecycleManager.reloadPlugin(pluginName); } // Get managed plugin (with lifecycle state) getManagedPlugin(name) { return this.lifecycleManager.getPlugin(name); } // Get all managed plugins getManagedPlugins() { return this.lifecycleManager.getPlugins(); } // Get plugins by state getPluginsByState(state) { return this.lifecycleManager.getPluginsByState(state); } // Get lifecycle statistics getLifecycleStats() { return this.lifecycleManager.getLifecycleStats(); } // Get lifecycle manager getLifecycleManager() { return this.lifecycleManager; } // Hook system methods getHookSystem() { return this.hookSystem; } // Create plugin hook API for a specific plugin createPluginHookAPI(pluginName) { return this.hookSystem.createPluginScope(pluginName); } // Execute hooks async executeHooks(hookType, data) { return await this.hookSystem.execute(hookType, data); } // Get hook statistics getHookStats() { return this.hookSystem.getStats(); } // Dependency resolver methods getDependencyResolver() { return this.dependencyResolver; } // Resolve dependencies for a plugin async resolveDependencies(pluginName) { const plugin = this.plugins.get(pluginName); if (!plugin) { throw new error_handler_1.ValidationError(`Plugin '${pluginName}' not found`); } // Register all plugins with dependency resolver this.plugins.forEach(p => this.dependencyResolver.registerPlugin(p)); return await this.dependencyResolver.resolveDependencies(plugin.manifest); } // Get dependency statistics getDependencyStats() { return this.dependencyResolver.getStats(); } // Create plugin context for activation createPluginContext(registration) { return { cli: { version: '0.7.0', // This should come from package.json rootPath: this.rootPath, configPath: path.join(this.rootPath, '.re-shell'), workspaces: {} // This should be loaded from workspace manager }, plugin: { name: registration.manifest.name, version: registration.manifest.version, config: {}, // Plugin-specific configuration dataPath: path.join(this.rootPath, '.re-shell', 'data', registration.manifest.name), cachePath: path.join(this.rootPath, '.re-shell', 'cache', registration.manifest.name) }, logger: this.createPluginLogger(registration.manifest.name), hooks: this.createPluginHookAPI(registration.manifest.name), utils: this.createPluginUtils() }; } // Create plugin logger createPluginLogger(pluginName) { const prefix = `[${pluginName}]`; return { debug: (message, ...args) => { console.debug(chalk_1.default.gray(`${prefix} ${message}`), ...args); }, info: (message, ...args) => { console.info(chalk_1.default.blue(`${prefix} ${message}`), ...args); }, warn: (message, ...args) => { console.warn(chalk_1.default.yellow(`${prefix} ${message}`), ...args); }, error: (message, ...args) => { console.error(chalk_1.default.red(`${prefix} ${message}`), ...args); } }; } // Create plugin utilities createPluginUtils() { const { exec, spawn } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); return { path, fs, chalk: chalk_1.default, exec: execAsync, spawn: (command, args, options) => { return new Promise((resolve, reject) => { const child = spawn(command, args, options); child.on('exit', (code) => resolve(code || 0)); child.on('error', reject); }); } }; } } exports.PluginRegistry = PluginRegistry; // Utility functions function createPluginRegistry(rootPath) { return new PluginRegistry(rootPath); } async function discoverPlugins(rootPath, options) { const registry = new PluginRegistry(rootPath); return await registry.discoverPlugins(options); } function validatePluginManifest(data) { const registry = new PluginRegistry(); return registry.validateManifest(data); }