claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
383 lines • 14.4 kB
JavaScript
/**
* Plugin Manager
* Handles actual plugin installation, persistence, and lifecycle
* Bridges discovery service with file system persistence
*/
import * as fs from 'fs';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
// ============================================================================
// Plugin Manager
// ============================================================================
/**
* Manages plugin installation, persistence, and lifecycle.
*
* Unlike the simulated version, this actually:
* - Persists plugins to disk
* - Downloads from npm
* - Tracks enabled/disabled state
* - Loads plugin modules
*/
export class PluginManager {
config;
manifest = null;
constructor(baseDir = process.cwd()) {
const pluginsDir = path.join(baseDir, '.claude-flow', 'plugins');
this.config = {
pluginsDir,
manifestPath: path.join(pluginsDir, 'installed.json'),
};
}
// =========================================================================
// Initialization
// =========================================================================
/**
* Initialize the plugin manager, creating directories and loading manifest
*/
async initialize() {
// Ensure plugins directory exists
await this.ensureDirectory(this.config.pluginsDir);
// Load or create manifest
this.manifest = await this.loadManifest();
}
async ensureDirectory(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
async loadManifest() {
try {
if (fs.existsSync(this.config.manifestPath)) {
const content = fs.readFileSync(this.config.manifestPath, 'utf-8');
return JSON.parse(content);
}
}
catch (error) {
console.warn('[PluginManager] Failed to load manifest, creating new one');
}
return {
version: '1.0.0',
lastUpdated: new Date().toISOString(),
plugins: {},
};
}
async saveManifest() {
if (!this.manifest)
return;
this.manifest.lastUpdated = new Date().toISOString();
await this.ensureDirectory(path.dirname(this.config.manifestPath));
fs.writeFileSync(this.config.manifestPath, JSON.stringify(this.manifest, null, 2), 'utf-8');
}
// =========================================================================
// Installation
// =========================================================================
/**
* Install a plugin from npm
*/
async installFromNpm(packageName, version) {
if (!this.manifest) {
await this.initialize();
}
const versionSpec = version ? `${packageName}@${version}` : packageName;
try {
// Check if already installed
if (this.manifest.plugins[packageName]) {
return {
success: false,
error: `Plugin ${packageName} is already installed. Use upgrade to update.`,
};
}
// Install to local plugins directory
const installDir = path.join(this.config.pluginsDir, 'node_modules');
await this.ensureDirectory(installDir);
// Use npm to install
console.log(`[PluginManager] Installing ${versionSpec}...`);
const { stdout, stderr } = await execAsync(`npm install --prefix "${this.config.pluginsDir}" ${versionSpec}`, { timeout: 120000 });
// Get installed version
const packageJsonPath = path.join(installDir, packageName, 'package.json');
let installedVersion = version || 'latest';
let commands = [];
let hooks = [];
if (fs.existsSync(packageJsonPath)) {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
installedVersion = pkg.version;
// Check for claude-flow plugin metadata
if (pkg['claude-flow']) {
commands = pkg['claude-flow'].commands || [];
hooks = pkg['claude-flow'].hooks || [];
}
}
// Create plugin entry
const plugin = {
name: packageName,
version: installedVersion,
installedAt: new Date().toISOString(),
enabled: true,
source: 'npm',
path: path.join(installDir, packageName),
commands,
hooks,
};
// Save to manifest
this.manifest.plugins[packageName] = plugin;
await this.saveManifest();
console.log(`[PluginManager] Installed ${packageName}@${installedVersion}`);
return { success: true, plugin };
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error(`[PluginManager] Failed to install ${packageName}:`, errorMsg);
return { success: false, error: errorMsg };
}
}
/**
* Install a plugin from a local path
*/
async installFromLocal(sourcePath) {
if (!this.manifest) {
await this.initialize();
}
try {
const absolutePath = path.resolve(sourcePath);
if (!fs.existsSync(absolutePath)) {
return { success: false, error: `Path does not exist: ${absolutePath}` };
}
// Read package.json
const packageJsonPath = path.join(absolutePath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return { success: false, error: 'No package.json found at path' };
}
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
const packageName = pkg.name;
// Check if already installed
if (this.manifest.plugins[packageName]) {
return {
success: false,
error: `Plugin ${packageName} is already installed`,
};
}
// Create plugin entry (link to local path, don't copy)
const plugin = {
name: packageName,
version: pkg.version,
installedAt: new Date().toISOString(),
enabled: true,
source: 'local',
path: absolutePath,
commands: pkg['claude-flow']?.commands || [],
hooks: pkg['claude-flow']?.hooks || [],
};
// Save to manifest
this.manifest.plugins[packageName] = plugin;
await this.saveManifest();
console.log(`[PluginManager] Installed local plugin ${packageName}@${pkg.version}`);
return { success: true, plugin };
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error(`[PluginManager] Failed to install from local:`, errorMsg);
return { success: false, error: errorMsg };
}
}
// =========================================================================
// Uninstallation
// =========================================================================
/**
* Uninstall a plugin
*/
async uninstall(packageName) {
if (!this.manifest) {
await this.initialize();
}
const plugin = this.manifest.plugins[packageName];
if (!plugin) {
return { success: false, error: `Plugin ${packageName} is not installed` };
}
try {
// For npm-installed plugins, remove from node_modules
if (plugin.source === 'npm') {
await execAsync(`npm uninstall --prefix "${this.config.pluginsDir}" ${packageName}`, { timeout: 60000 });
}
// Remove from manifest
delete this.manifest.plugins[packageName];
await this.saveManifest();
console.log(`[PluginManager] Uninstalled ${packageName}`);
return { success: true };
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
console.error(`[PluginManager] Failed to uninstall ${packageName}:`, errorMsg);
return { success: false, error: errorMsg };
}
}
// =========================================================================
// Enable/Disable
// =========================================================================
/**
* Enable a plugin
*/
async enable(packageName) {
if (!this.manifest) {
await this.initialize();
}
const plugin = this.manifest.plugins[packageName];
if (!plugin) {
return { success: false, error: `Plugin ${packageName} is not installed` };
}
plugin.enabled = true;
await this.saveManifest();
return { success: true };
}
/**
* Disable a plugin
*/
async disable(packageName) {
if (!this.manifest) {
await this.initialize();
}
const plugin = this.manifest.plugins[packageName];
if (!plugin) {
return { success: false, error: `Plugin ${packageName} is not installed` };
}
plugin.enabled = false;
await this.saveManifest();
return { success: true };
}
/**
* Toggle a plugin's enabled state
*/
async toggle(packageName) {
if (!this.manifest) {
await this.initialize();
}
const plugin = this.manifest.plugins[packageName];
if (!plugin) {
return { success: false, error: `Plugin ${packageName} is not installed` };
}
plugin.enabled = !plugin.enabled;
await this.saveManifest();
return { success: true, enabled: plugin.enabled };
}
// =========================================================================
// Query
// =========================================================================
/**
* Get all installed plugins
*/
async getInstalled() {
if (!this.manifest) {
await this.initialize();
}
return Object.values(this.manifest.plugins);
}
/**
* Get enabled plugins
*/
async getEnabled() {
const all = await this.getInstalled();
return all.filter(p => p.enabled);
}
/**
* Check if a plugin is installed
*/
async isInstalled(packageName) {
if (!this.manifest) {
await this.initialize();
}
return packageName in this.manifest.plugins;
}
/**
* Get a specific installed plugin
*/
async getPlugin(packageName) {
if (!this.manifest) {
await this.initialize();
}
return this.manifest.plugins[packageName];
}
// =========================================================================
// Upgrade
// =========================================================================
/**
* Upgrade a plugin to a new version
*/
async upgrade(packageName, version) {
if (!this.manifest) {
await this.initialize();
}
const existing = this.manifest.plugins[packageName];
if (!existing) {
return { success: false, error: `Plugin ${packageName} is not installed` };
}
if (existing.source !== 'npm') {
return { success: false, error: 'Can only upgrade npm-installed plugins' };
}
try {
const versionSpec = version ? `${packageName}@${version}` : `${packageName}`;
// Reinstall with new version
await execAsync(`npm install --prefix "${this.config.pluginsDir}" ${versionSpec}`, { timeout: 120000 });
// Update manifest
const installDir = path.join(this.config.pluginsDir, 'node_modules');
const packageJsonPath = path.join(installDir, packageName, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
existing.version = pkg.version;
existing.commands = pkg['claude-flow']?.commands || existing.commands;
existing.hooks = pkg['claude-flow']?.hooks || existing.hooks;
}
await this.saveManifest();
console.log(`[PluginManager] Upgraded ${packageName} to ${existing.version}`);
return { success: true, plugin: existing };
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
return { success: false, error: errorMsg };
}
}
// =========================================================================
// Config
// =========================================================================
/**
* Update plugin config
*/
async setConfig(packageName, config) {
if (!this.manifest) {
await this.initialize();
}
const plugin = this.manifest.plugins[packageName];
if (!plugin) {
return { success: false, error: `Plugin ${packageName} is not installed` };
}
plugin.config = { ...plugin.config, ...config };
await this.saveManifest();
return { success: true };
}
/**
* Get plugins directory path
*/
getPluginsDir() {
return this.config.pluginsDir;
}
/**
* Get manifest path
*/
getManifestPath() {
return this.config.manifestPath;
}
}
// ============================================================================
// Singleton Instance
// ============================================================================
let defaultManager = null;
export function getPluginManager(baseDir) {
if (!defaultManager) {
defaultManager = new PluginManager(baseDir);
}
return defaultManager;
}
export function resetPluginManager() {
defaultManager = null;
}
//# sourceMappingURL=manager.js.map