@ace-sdk/cli
Version:
ACE CLI - Command-line tool for intelligent pattern learning and playbook management
282 lines • 8.91 kB
JavaScript
/**
* Plugin loader and management service
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
/**
* Plugin loader - discovers, loads, and manages ACE CLI plugins
*
* Plugins are stored in ~/.ace/plugins/
* Plugin registry is stored in ~/.ace/plugins.json
*/
export class PluginLoader {
pluginsDir;
registryFile;
registry;
loadedPlugins;
logger;
constructor(logger) {
this.logger = logger;
this.pluginsDir = join(homedir(), '.ace', 'plugins');
this.registryFile = join(homedir(), '.ace', 'plugins.json');
this.loadedPlugins = new Map();
// Ensure plugins directory exists
if (!existsSync(this.pluginsDir)) {
mkdirSync(this.pluginsDir, { recursive: true });
}
// Load registry
this.registry = this.loadRegistry();
}
/**
* Load plugin registry from disk
*/
loadRegistry() {
if (!existsSync(this.registryFile)) {
return {
trusted: [],
enabled: [],
disabled: []
};
}
try {
const data = readFileSync(this.registryFile, 'utf8');
return JSON.parse(data);
}
catch {
return {
trusted: [],
enabled: [],
disabled: []
};
}
}
/**
* Save plugin registry to disk
*/
saveRegistry() {
writeFileSync(this.registryFile, JSON.stringify(this.registry, null, 2));
}
/**
* Get plugin registry
*/
getRegistry() {
return { ...this.registry };
}
/**
* Discover all plugins in plugins directory
*/
discoverPlugins() {
if (!existsSync(this.pluginsDir)) {
return [];
}
const plugins = [];
const entries = readdirSync(this.pluginsDir);
for (const entry of entries) {
const pluginPath = join(this.pluginsDir, entry);
const stat = statSync(pluginPath);
if (stat.isDirectory()) {
try {
const manifest = this.loadManifest(pluginPath);
plugins.push(manifest);
}
catch (error) {
this.logger.debug(`Failed to load plugin manifest: ${entry}`, error);
}
}
}
return plugins;
}
/**
* Load plugin manifest from directory
*/
loadManifest(pluginPath) {
const manifestFile = join(pluginPath, 'plugin.json');
if (!existsSync(manifestFile)) {
throw new Error('Plugin manifest (plugin.json) not found');
}
const data = readFileSync(manifestFile, 'utf8');
const manifest = JSON.parse(data);
// Validate required fields
if (!manifest.name || !manifest.version || !manifest.main) {
throw new Error('Invalid plugin manifest: missing required fields (name, version, main)');
}
return manifest;
}
/**
* Load and activate a plugin
*/
async loadPlugin(name, context) {
// Check if already loaded
if (this.loadedPlugins.has(name)) {
return this.loadedPlugins.get(name);
}
// Find plugin directory
const pluginPath = join(this.pluginsDir, name);
if (!existsSync(pluginPath)) {
throw new Error(`Plugin not found: ${name}`);
}
// Load manifest
const manifest = this.loadManifest(pluginPath);
// Check if plugin is trusted
const trusted = this.isTrusted(manifest);
if (!trusted) {
throw new Error(`Plugin "${name}" is not trusted. Run "ce-ace plugin trust ${name}" to trust this plugin.`);
}
// Check if plugin is enabled
const enabled = this.isEnabled(manifest.name);
// Load plugin module
const mainFile = join(pluginPath, manifest.main);
if (!existsSync(mainFile)) {
throw new Error(`Plugin entry point not found: ${manifest.main}`);
}
let module;
try {
module = await import(mainFile);
}
catch (error) {
throw new Error(`Failed to load plugin module: ${error instanceof Error ? error.message : String(error)}`);
}
// Create plugin instance
const plugin = {
metadata: manifest,
path: pluginPath,
module,
enabled,
trusted
};
// Initialize plugin if it has an init function
if (module.init && typeof module.init === 'function') {
try {
await module.init(context);
}
catch (error) {
throw new Error(`Plugin initialization failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Store loaded plugin
this.loadedPlugins.set(name, plugin);
return plugin;
}
/**
* Check if plugin is trusted
*/
isTrusted(manifest) {
return this.registry.trusted.some(trust => trust.name === manifest.name && trust.version === manifest.version);
}
/**
* Check if plugin is enabled
*/
isEnabled(name) {
// Enabled by default if not explicitly disabled
return !this.registry.disabled.includes(name);
}
/**
* Trust a plugin
*/
trustPlugin(name, version) {
// Remove existing trust for this plugin
this.registry.trusted = this.registry.trusted.filter(t => t.name !== name);
// Add new trust
const trust = {
name,
version,
trustedAt: new Date().toISOString(),
trustedBy: 'user'
};
this.registry.trusted.push(trust);
this.saveRegistry();
}
/**
* Untrust a plugin
*/
untrustPlugin(name) {
this.registry.trusted = this.registry.trusted.filter(t => t.name !== name);
this.saveRegistry();
// Unload plugin if loaded
this.loadedPlugins.delete(name);
}
/**
* Enable a plugin
*/
enablePlugin(name) {
// Remove from disabled list
this.registry.disabled = this.registry.disabled.filter(p => p !== name);
// Add to enabled list if not already there
if (!this.registry.enabled.includes(name)) {
this.registry.enabled.push(name);
}
this.saveRegistry();
}
/**
* Disable a plugin
*/
disablePlugin(name) {
// Remove from enabled list
this.registry.enabled = this.registry.enabled.filter(p => p !== name);
// Add to disabled list if not already there
if (!this.registry.disabled.includes(name)) {
this.registry.disabled.push(name);
}
this.saveRegistry();
// Unload plugin if loaded
this.loadedPlugins.delete(name);
}
/**
* Get loaded plugin
*/
getPlugin(name) {
return this.loadedPlugins.get(name);
}
/**
* List all loaded plugins
*/
listLoadedPlugins() {
return Array.from(this.loadedPlugins.values());
}
/**
* Execute a plugin command
*/
async executePluginCommand(pluginName, commandName, args, context) {
// Load plugin if not loaded
let plugin = this.loadedPlugins.get(pluginName);
if (!plugin) {
plugin = await this.loadPlugin(pluginName, context);
}
// Check if plugin is enabled
if (!plugin.enabled) {
throw new Error(`Plugin "${pluginName}" is disabled. Run "ce-ace plugin enable ${pluginName}" to enable it.`);
}
// Find command in manifest
const command = plugin.metadata.commands?.find(cmd => cmd.name === commandName);
if (!command) {
throw new Error(`Command "${commandName}" not found in plugin "${pluginName}"`);
}
// Get handler function from module
const handler = plugin.module[command.handler];
if (!handler || typeof handler !== 'function') {
throw new Error(`Handler function "${command.handler}" not found in plugin module`);
}
// Execute handler
try {
return await handler(args, context);
}
catch (error) {
throw new Error(`Plugin command failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
/**
* Singleton plugin loader instance
*/
let loaderInstance = null;
/**
* Get the global plugin loader instance
*/
export function getPluginLoader(logger) {
if (!loaderInstance) {
loaderInstance = new PluginLoader(logger);
}
return loaderInstance;
}
//# sourceMappingURL=plugin-loader.js.map