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
639 lines (549 loc) • 17 kB
text/typescript
/**
* V3 Plugin Loader
* Domain-Driven Design - Plugin-Based Architecture (ADR-004)
*
* Handles plugin loading, dependency resolution, and lifecycle management
*/
import type {
ClaudeFlowPlugin,
PluginContext,
PluginInfo,
PluginLifecycleState,
PluginError as PluginErrorType,
PluginErrorCode,
} from './plugin-interface.js';
import { PluginError } from './plugin-interface.js';
import type { PluginRegistry } from './plugin-registry.js';
/**
* Plugin loader configuration
*/
export interface PluginLoaderConfig {
/**
* Maximum time to wait for plugin initialization (ms)
*/
initializationTimeout?: number;
/**
* Maximum time to wait for plugin shutdown (ms)
*/
shutdownTimeout?: number;
/**
* Enable parallel plugin initialization
*/
parallelInitialization?: boolean;
/**
* Enable strict dependency checking
*/
strictDependencies?: boolean;
/**
* Enable health checks
*/
enableHealthChecks?: boolean;
/**
* Health check interval (ms)
*/
healthCheckInterval?: number;
}
/**
* Default plugin loader configuration
*/
const DEFAULT_CONFIG: Required<PluginLoaderConfig> = {
initializationTimeout: 30000, // 30 seconds
shutdownTimeout: 10000, // 10 seconds
parallelInitialization: false, // Sequential by default for safety
strictDependencies: true,
enableHealthChecks: false,
healthCheckInterval: 60000, // 1 minute
};
/**
* Plugin dependency graph node
*/
interface DependencyNode {
plugin: ClaudeFlowPlugin;
dependencies: Set<string>;
dependents: Set<string>;
depth: number;
}
/**
* Plugin loader for managing plugin lifecycle
*/
export class PluginLoader {
private config: Required<PluginLoaderConfig>;
private registry: PluginRegistry;
private initializationOrder: string[] = [];
private healthCheckIntervalId?: NodeJS.Timeout;
constructor(registry: PluginRegistry, config?: PluginLoaderConfig) {
this.registry = registry;
this.config = { ...DEFAULT_CONFIG, ...config };
}
/**
* Load a single plugin
*/
async loadPlugin(plugin: ClaudeFlowPlugin, context: PluginContext): Promise<void> {
// Validate plugin
this.validatePlugin(plugin);
// Check for duplicates
if (this.registry.hasPlugin(plugin.name)) {
throw new PluginError(
`Plugin '${plugin.name}' is already loaded`,
plugin.name,
'DUPLICATE_PLUGIN'
);
}
// Register plugin in uninitialized state
this.registry.registerPlugin(plugin, 'uninitialized', context);
// Resolve dependencies
if (this.config.strictDependencies) {
this.validateDependencies(plugin);
}
// Initialize plugin
await this.initializePlugin(plugin, context);
// Update initialization order
this.initializationOrder.push(plugin.name);
}
/**
* Load multiple plugins with dependency resolution
*/
async loadPlugins(
plugins: ClaudeFlowPlugin[],
context: PluginContext
): Promise<LoadPluginsResult> {
const results: LoadPluginsResult = {
successful: [],
failed: [],
totalDuration: 0,
};
const startTime = Date.now();
try {
// Validate all plugins first
for (const plugin of plugins) {
this.validatePlugin(plugin);
}
// Build dependency graph
const dependencyGraph = this.buildDependencyGraph(plugins);
// Detect circular dependencies
this.detectCircularDependencies(dependencyGraph);
// Sort plugins by dependency order (topological sort)
const sortedPlugins = this.topologicalSort(dependencyGraph);
// Initialize plugins in order
if (this.config.parallelInitialization) {
await this.initializePluginsParallel(sortedPlugins, context, results);
} else {
await this.initializePluginsSequential(sortedPlugins, context, results);
}
} catch (error) {
// If error during setup, mark all as failed
for (const plugin of plugins) {
if (!results.successful.includes(plugin.name) && !results.failed.some((f) => f.name === plugin.name)) {
results.failed.push({
name: plugin.name,
error: error instanceof Error ? error : new Error(String(error)),
});
}
}
} finally {
results.totalDuration = Date.now() - startTime;
}
// Start health checks if enabled
if (this.config.enableHealthChecks) {
this.startHealthChecks();
}
return results;
}
/**
* Unload a single plugin
*/
async unloadPlugin(pluginName: string): Promise<void> {
const pluginInfo = this.registry.getPlugin(pluginName);
if (!pluginInfo) {
throw new PluginError(
`Plugin '${pluginName}' not found`,
pluginName,
'INVALID_PLUGIN'
);
}
// Check for dependents
const dependents = this.findDependents(pluginName);
if (dependents.length > 0) {
throw new PluginError(
`Cannot unload plugin '${pluginName}': depended on by ${dependents.join(', ')}`,
pluginName,
'DEPENDENCY_NOT_FOUND'
);
}
// Shutdown plugin
await this.shutdownPlugin(pluginInfo.plugin);
// Unregister plugin
this.registry.unregisterPlugin(pluginName);
// Remove from initialization order
const index = this.initializationOrder.indexOf(pluginName);
if (index !== -1) {
this.initializationOrder.splice(index, 1);
}
}
/**
* Unload all plugins in reverse initialization order
*/
async unloadAll(): Promise<void> {
// Stop health checks
if (this.healthCheckIntervalId) {
clearInterval(this.healthCheckIntervalId);
this.healthCheckIntervalId = undefined;
}
// Shutdown in reverse order
const pluginsToShutdown = [...this.initializationOrder].reverse();
for (const pluginName of pluginsToShutdown) {
try {
await this.unloadPlugin(pluginName);
} catch (error) {
// Log error but continue shutting down other plugins
console.error(`Error unloading plugin '${pluginName}':`, error);
}
}
this.initializationOrder = [];
}
/**
* Reload a plugin
*/
async reloadPlugin(pluginName: string, newPlugin: ClaudeFlowPlugin, context: PluginContext): Promise<void> {
await this.unloadPlugin(pluginName);
await this.loadPlugin(newPlugin, context);
}
/**
* Get plugin initialization order
*/
getInitializationOrder(): string[] {
return [...this.initializationOrder];
}
/**
* Validate plugin interface
*/
private validatePlugin(plugin: ClaudeFlowPlugin): void {
if (!plugin.name) {
throw new PluginError(
'Plugin must have a name',
'<unknown>',
'INVALID_PLUGIN'
);
}
if (!plugin.version) {
throw new PluginError(
`Plugin '${plugin.name}' must have a version`,
plugin.name,
'INVALID_PLUGIN'
);
}
if (typeof plugin.initialize !== 'function') {
throw new PluginError(
`Plugin '${plugin.name}' must implement initialize()`,
plugin.name,
'INVALID_PLUGIN'
);
}
if (typeof plugin.shutdown !== 'function') {
throw new PluginError(
`Plugin '${plugin.name}' must implement shutdown()`,
plugin.name,
'INVALID_PLUGIN'
);
}
}
/**
* Validate plugin dependencies
*/
private validateDependencies(plugin: ClaudeFlowPlugin): void {
if (!plugin.dependencies || plugin.dependencies.length === 0) {
return;
}
for (const dep of plugin.dependencies) {
if (!this.registry.hasPlugin(dep)) {
throw new PluginError(
`Plugin '${plugin.name}' depends on '${dep}' which is not loaded`,
plugin.name,
'DEPENDENCY_NOT_FOUND'
);
}
// Check dependency is initialized
const depInfo = this.registry.getPlugin(dep);
if (depInfo && depInfo.state !== 'initialized') {
throw new PluginError(
`Plugin '${plugin.name}' depends on '${dep}' which is not initialized (state: ${depInfo.state})`,
plugin.name,
'DEPENDENCY_NOT_FOUND'
);
}
}
}
/**
* Initialize a single plugin
*/
private async initializePlugin(plugin: ClaudeFlowPlugin, context: PluginContext): Promise<void> {
this.registry.updatePluginState(plugin.name, 'initializing');
try {
// Run initialization with timeout
await this.withTimeout(
plugin.initialize(context),
this.config.initializationTimeout,
`Plugin '${plugin.name}' initialization timed out`
);
this.registry.updatePluginState(plugin.name, 'initialized');
this.registry.collectPluginMetrics(plugin.name);
} catch (error) {
this.registry.updatePluginState(plugin.name, 'error', error instanceof Error ? error : new Error(String(error)));
throw new PluginError(
`Failed to initialize plugin '${plugin.name}': ${error}`,
plugin.name,
'INITIALIZATION_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Shutdown a single plugin
*/
private async shutdownPlugin(plugin: ClaudeFlowPlugin): Promise<void> {
this.registry.updatePluginState(plugin.name, 'shutting-down');
try {
await this.withTimeout(
plugin.shutdown(),
this.config.shutdownTimeout,
`Plugin '${plugin.name}' shutdown timed out`
);
this.registry.updatePluginState(plugin.name, 'shutdown');
} catch (error) {
this.registry.updatePluginState(plugin.name, 'error', error instanceof Error ? error : new Error(String(error)));
throw new PluginError(
`Failed to shutdown plugin '${plugin.name}': ${error}`,
plugin.name,
'SHUTDOWN_FAILED',
error instanceof Error ? error : undefined
);
}
}
/**
* Initialize plugins sequentially
*/
private async initializePluginsSequential(
plugins: ClaudeFlowPlugin[],
context: PluginContext,
results: LoadPluginsResult
): Promise<void> {
for (const plugin of plugins) {
try {
// Register and initialize
this.registry.registerPlugin(plugin, 'uninitialized', context);
await this.initializePlugin(plugin, context);
results.successful.push(plugin.name);
this.initializationOrder.push(plugin.name);
} catch (error) {
results.failed.push({
name: plugin.name,
error: error instanceof Error ? error : new Error(String(error)),
});
// Stop on first failure in sequential mode if strict
if (this.config.strictDependencies) {
break;
}
}
}
}
/**
* Initialize plugins in parallel (by dependency level)
*/
private async initializePluginsParallel(
plugins: ClaudeFlowPlugin[],
context: PluginContext,
results: LoadPluginsResult
): Promise<void> {
// Group plugins by dependency depth
const dependencyGraph = this.buildDependencyGraph(plugins);
const levels = this.groupByDepth(dependencyGraph);
// Initialize each level in parallel
for (const level of levels) {
const promises = level.map(async (plugin) => {
try {
this.registry.registerPlugin(plugin, 'uninitialized', context);
await this.initializePlugin(plugin, context);
results.successful.push(plugin.name);
this.initializationOrder.push(plugin.name);
} catch (error) {
results.failed.push({
name: plugin.name,
error: error instanceof Error ? error : new Error(String(error)),
});
}
});
await Promise.all(promises);
// Stop on failures in level if strict
if (this.config.strictDependencies && results.failed.length > 0) {
break;
}
}
}
/**
* Build dependency graph
*/
private buildDependencyGraph(plugins: ClaudeFlowPlugin[]): Map<string, DependencyNode> {
const graph = new Map<string, DependencyNode>();
// Create nodes
for (const plugin of plugins) {
graph.set(plugin.name, {
plugin,
dependencies: new Set(plugin.dependencies || []),
dependents: new Set(),
depth: 0,
});
}
// Build dependency links
for (const [name, node] of Array.from(graph.entries())) {
for (const dep of Array.from(node.dependencies)) {
const depNode = graph.get(dep);
if (depNode) {
depNode.dependents.add(name);
}
}
}
// Calculate depths
this.calculateDepths(graph);
return graph;
}
/**
* Calculate depth of each node (for topological sorting)
*/
private calculateDepths(graph: Map<string, DependencyNode>): void {
const visited = new Set<string>();
const visit = (name: string): number => {
if (visited.has(name)) {
const node = graph.get(name);
return node ? node.depth : 0;
}
visited.add(name);
const node = graph.get(name);
if (!node) return 0;
let maxDepth = 0;
for (const dep of Array.from(node.dependencies)) {
maxDepth = Math.max(maxDepth, visit(dep) + 1);
}
node.depth = maxDepth;
return maxDepth;
};
for (const name of Array.from(graph.keys())) {
visit(name);
}
}
/**
* Topological sort (dependency order)
*/
private topologicalSort(graph: Map<string, DependencyNode>): ClaudeFlowPlugin[] {
const sorted: ClaudeFlowPlugin[] = [];
const nodes = Array.from(graph.values());
// Sort by depth (dependencies first)
nodes.sort((a, b) => a.depth - b.depth);
for (const node of nodes) {
sorted.push(node.plugin);
}
return sorted;
}
/**
* Group plugins by dependency depth (for parallel initialization)
*/
private groupByDepth(graph: Map<string, DependencyNode>): ClaudeFlowPlugin[][] {
const levels: ClaudeFlowPlugin[][] = [];
const maxDepth = Math.max(...Array.from(graph.values()).map((n) => n.depth));
for (let depth = 0; depth <= maxDepth; depth++) {
const level: ClaudeFlowPlugin[] = [];
for (const node of Array.from(graph.values())) {
if (node.depth === depth) {
level.push(node.plugin);
}
}
if (level.length > 0) {
levels.push(level);
}
}
return levels;
}
/**
* Detect circular dependencies
*/
private detectCircularDependencies(graph: Map<string, DependencyNode>): void {
const visited = new Set<string>();
const stack = new Set<string>();
const visit = (name: string, path: string[]): void => {
if (stack.has(name)) {
const cycle = [...path, name];
throw new PluginError(
`Circular dependency detected: ${cycle.join(' -> ')}`,
name,
'CIRCULAR_DEPENDENCY'
);
}
if (visited.has(name)) {
return;
}
visited.add(name);
stack.add(name);
const node = graph.get(name);
if (node) {
for (const dep of Array.from(node.dependencies)) {
visit(dep, [...path, name]);
}
}
stack.delete(name);
};
for (const name of Array.from(graph.keys())) {
visit(name, []);
}
}
/**
* Find plugins that depend on a given plugin
*/
private findDependents(pluginName: string): string[] {
const dependents: string[] = [];
for (const [name, info] of Array.from(this.registry.getAllPlugins().entries())) {
if (info.plugin.dependencies?.includes(pluginName)) {
dependents.push(name);
}
}
return dependents;
}
/**
* Start periodic health checks
*/
private startHealthChecks(): void {
this.healthCheckIntervalId = setInterval(async () => {
for (const [name, info] of Array.from(this.registry.getAllPlugins().entries())) {
if (info.state === 'initialized' && info.plugin.healthCheck) {
try {
const healthy = await info.plugin.healthCheck();
if (!healthy) {
console.warn(`Plugin '${name}' health check failed`);
this.registry.updatePluginState(name, 'error', new Error('Health check failed'));
}
} catch (error) {
console.error(`Plugin '${name}' health check error:`, error);
this.registry.updatePluginState(name, 'error', error instanceof Error ? error : new Error(String(error)));
}
}
}
}, this.config.healthCheckInterval);
}
/**
* Utility: Run promise with timeout
*/
private async withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage: string): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error(errorMessage)), timeoutMs)
),
]);
}
}
/**
* Load plugins result
*/
export interface LoadPluginsResult {
successful: string[];
failed: Array<{ name: string; error: Error }>;
totalDuration: number;
}