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