UNPKG

@semantest/chrome-extension

Version:

Browser extension for ChatGPT-buddy - AI automation extension built on Web-Buddy framework

689 lines (579 loc) 20.4 kB
/** * @fileoverview Plugin Registry implementation for Web-Buddy plugin system * @description Manages plugin lifecycle, discovery, and registration */ import { WebBuddyPlugin, PluginManifest, PluginState, PluginLifecycleEvent, PluginEventBus, PluginEvent, PluginContext, PluginMetadata, PluginError, PluginLoadError, PluginInitializationError, PluginDependencyError, PluginEvents } from './plugin-interface'; /** * Plugin registration information */ interface PluginRegistration { plugin: WebBuddyPlugin; manifest: PluginManifest; context: PluginContext; state: PluginState; loadedAt: Date; activatedAt?: Date; deactivatedAt?: Date; dependencies: string[]; dependents: string[]; errorCount: number; lastError?: Error; } /** * Plugin discovery options */ interface PluginDiscoveryOptions { source?: 'manifest' | 'url' | 'registry' | 'filesystem'; domain?: string; includeInactive?: boolean; includeSystem?: boolean; } /** * Plugin loading options */ interface PluginLoadOptions { skipDependencies?: boolean; skipValidation?: boolean; allowReload?: boolean; timeout?: number; } /** * Core plugin registry implementation */ export class PluginRegistry { private plugins: Map<string, PluginRegistration> = new Map(); private eventBus: PluginEventBus; private loadOrder: string[] = []; private isShuttingDown = false; constructor(eventBus: PluginEventBus) { this.eventBus = eventBus; this.setupEventHandlers(); } /** * Register a plugin with the registry */ async register(plugin: WebBuddyPlugin, manifest: PluginManifest, context: PluginContext): Promise<void> { const pluginId = plugin.id; try { // Validate plugin this.validatePlugin(plugin, manifest); // Check for existing registration if (this.plugins.has(pluginId)) { throw new PluginLoadError(`Plugin ${pluginId} is already registered`, pluginId); } // Check dependencies await this.checkDependencies(manifest.dependencies || []); // Create registration const registration: PluginRegistration = { plugin, manifest, context, state: 'uninitialized', loadedAt: new Date(), dependencies: manifest.dependencies || [], dependents: [], errorCount: 0 }; // Register the plugin this.plugins.set(pluginId, registration); this.updateLoadOrder(pluginId); this.updateDependents(pluginId, manifest.dependencies || []); // Emit plugin loaded event await this.emitPluginEvent(PluginEvents.PLUGIN_LOADED, pluginId, { metadata: plugin.metadata, manifest: manifest }); console.log(`✅ Plugin registered: ${pluginId} v${plugin.version}`); } catch (error) { const pluginError = error instanceof PluginError ? error : new PluginLoadError(`Failed to register plugin ${pluginId}: ${(error as Error).message}`, pluginId, error); await this.emitPluginEvent(PluginEvents.PLUGIN_ERROR, pluginId, { error: pluginError.message, details: pluginError.details }); throw pluginError; } } /** * Unregister a plugin from the registry */ async unregister(pluginId: string): Promise<void> { const registration = this.plugins.get(pluginId); if (!registration) { throw new PluginError(`Plugin ${pluginId} is not registered`, pluginId, 'PLUGIN_NOT_FOUND'); } try { // Check if other plugins depend on this one if (registration.dependents.length > 0) { throw new PluginDependencyError( `Cannot unregister plugin ${pluginId}: ${registration.dependents.length} plugins depend on it`, pluginId, { dependents: registration.dependents } ); } // Deactivate if active if (registration.state === 'active') { await this.deactivate(pluginId); } // Destroy the plugin if (registration.state !== 'destroyed') { await this.destroyPlugin(registration); } // Remove from registry this.plugins.delete(pluginId); this.removeFromLoadOrder(pluginId); this.removeDependents(pluginId, registration.dependencies); await this.emitPluginEvent(PluginEvents.PLUGIN_DESTROYED, pluginId); console.log(`🗑️ Plugin unregistered: ${pluginId}`); } catch (error) { const pluginError = error instanceof PluginError ? error : new PluginError(`Failed to unregister plugin ${pluginId}: ${(error as Error).message}`, pluginId, 'UNREGISTER_ERROR'); await this.emitPluginEvent(PluginEvents.PLUGIN_ERROR, pluginId, { error: pluginError.message }); throw pluginError; } } /** * Get a registered plugin */ getPlugin(pluginId: string): WebBuddyPlugin | null { const registration = this.plugins.get(pluginId); return registration ? registration.plugin : null; } /** * Get plugin registration information */ getPluginRegistration(pluginId: string): PluginRegistration | null { return this.plugins.get(pluginId) || null; } /** * Get all registered plugins */ getAllPlugins(): WebBuddyPlugin[] { return Array.from(this.plugins.values()).map(reg => reg.plugin); } /** * Get plugins by domain */ getPluginsByDomain(domain: string): WebBuddyPlugin[] { return Array.from(this.plugins.values()) .filter(reg => reg.plugin.capabilities.supportedDomains.some(d => d === domain || domain.match(new RegExp(d.replace(/\*/g, '.*'))) )) .map(reg => reg.plugin); } /** * Get plugins by state */ getPluginsByState(state: PluginState): WebBuddyPlugin[] { return Array.from(this.plugins.values()) .filter(reg => reg.state === state) .map(reg => reg.plugin); } /** * Initialize a plugin */ async initialize(pluginId: string): Promise<void> { const registration = this.plugins.get(pluginId); if (!registration) { throw new PluginError(`Plugin ${pluginId} is not registered`, pluginId, 'PLUGIN_NOT_FOUND'); } if (registration.state !== 'uninitialized') { throw new PluginError(`Plugin ${pluginId} is not in uninitialized state`, pluginId, 'INVALID_STATE'); } try { // Initialize dependencies first for (const depId of registration.dependencies) { const depRegistration = this.plugins.get(depId); if (depRegistration && depRegistration.state === 'uninitialized') { await this.initialize(depId); } } // Initialize the plugin registration.state = 'initialized'; await registration.plugin.initialize(registration.context); await this.emitPluginEvent(PluginEvents.PLUGIN_INITIALIZED, pluginId); console.log(`🔧 Plugin initialized: ${pluginId}`); } catch (error) { registration.state = 'error'; registration.errorCount++; registration.lastError = error as Error; const pluginError = new PluginInitializationError( `Failed to initialize plugin ${pluginId}: ${(error as Error).message}`, pluginId, error ); await this.emitPluginEvent(PluginEvents.PLUGIN_ERROR, pluginId, { error: pluginError.message, phase: 'initialization' }); throw pluginError; } } /** * Activate a plugin */ async activate(pluginId: string): Promise<void> { const registration = this.plugins.get(pluginId); if (!registration) { throw new PluginError(`Plugin ${pluginId} is not registered`, pluginId, 'PLUGIN_NOT_FOUND'); } if (registration.state === 'active') { return; // Already active } if (registration.state !== 'initialized' && registration.state !== 'inactive') { throw new PluginError(`Plugin ${pluginId} cannot be activated from ${registration.state} state`, pluginId, 'INVALID_STATE'); } try { // Activate dependencies first for (const depId of registration.dependencies) { const depRegistration = this.plugins.get(depId); if (depRegistration && depRegistration.state !== 'active') { if (depRegistration.state === 'uninitialized') { await this.initialize(depId); } if (depRegistration.state === 'initialized' || depRegistration.state === 'inactive') { await this.activate(depId); } } } // Activate the plugin await registration.plugin.activate(); registration.state = 'active'; registration.activatedAt = new Date(); await this.emitPluginEvent(PluginEvents.PLUGIN_ACTIVATED, pluginId); console.log(`🟢 Plugin activated: ${pluginId}`); } catch (error) { registration.state = 'error'; registration.errorCount++; registration.lastError = error as Error; const pluginError = new PluginError( `Failed to activate plugin ${pluginId}: ${(error as Error).message}`, pluginId, 'ACTIVATION_ERROR', error ); await this.emitPluginEvent(PluginEvents.PLUGIN_ERROR, pluginId, { error: pluginError.message, phase: 'activation' }); throw pluginError; } } /** * Deactivate a plugin */ async deactivate(pluginId: string): Promise<void> { const registration = this.plugins.get(pluginId); if (!registration) { throw new PluginError(`Plugin ${pluginId} is not registered`, pluginId, 'PLUGIN_NOT_FOUND'); } if (registration.state !== 'active') { return; // Already inactive } try { // Deactivate dependents first for (const dependentId of registration.dependents) { const dependentRegistration = this.plugins.get(dependentId); if (dependentRegistration && dependentRegistration.state === 'active') { await this.deactivate(dependentId); } } // Deactivate the plugin await registration.plugin.deactivate(); registration.state = 'inactive'; registration.deactivatedAt = new Date(); await this.emitPluginEvent(PluginEvents.PLUGIN_DEACTIVATED, pluginId); console.log(`🔴 Plugin deactivated: ${pluginId}`); } catch (error) { registration.state = 'error'; registration.errorCount++; registration.lastError = error as Error; const pluginError = new PluginError( `Failed to deactivate plugin ${pluginId}: ${(error as Error).message}`, pluginId, 'DEACTIVATION_ERROR', error ); await this.emitPluginEvent(PluginEvents.PLUGIN_ERROR, pluginId, { error: pluginError.message, phase: 'deactivation' }); throw pluginError; } } /** * Discover plugins from various sources */ async discoverPlugins(options: PluginDiscoveryOptions = {}): Promise<PluginManifest[]> { console.log('🔍 Discovering plugins...', options); const manifests: PluginManifest[] = []; try { // Discover from manifest (built-in plugins) if (!options.source || options.source === 'manifest') { const manifestPlugins = await this.discoverFromManifest(); manifests.push(...manifestPlugins); } // Discover from URLs (remote plugins) if (!options.source || options.source === 'url') { const urlPlugins = await this.discoverFromURL(); manifests.push(...urlPlugins); } // Discover from registry (marketplace plugins) if (!options.source || options.source === 'registry') { const registryPlugins = await this.discoverFromRegistry(); manifests.push(...registryPlugins); } // Filter by domain if specified if (options.domain) { return manifests.filter(manifest => manifest.capabilities.supportedDomains.some(d => d === options.domain || options.domain!.match(new RegExp(d.replace(/\*/g, '.*'))) ) ); } return manifests; } catch (error) { console.error('❌ Plugin discovery failed:', error); throw new PluginError( `Plugin discovery failed: ${(error as Error).message}`, 'SYSTEM', 'DISCOVERY_ERROR', error ); } } /** * Send message to a specific plugin */ async sendMessageToPlugin(pluginId: string, message: any): Promise<any> { const registration = this.plugins.get(pluginId); if (!registration) { throw new PluginError(`Plugin ${pluginId} is not registered`, pluginId, 'PLUGIN_NOT_FOUND'); } if (registration.state !== 'active') { throw new PluginError(`Plugin ${pluginId} is not active`, pluginId, 'PLUGIN_NOT_ACTIVE'); } try { const event: PluginEvent = { type: 'message', source: 'system', target: pluginId, data: message, timestamp: new Date().toISOString() }; await registration.plugin.onEvent(event); return { success: true }; } catch (error) { throw new PluginError( `Failed to send message to plugin ${pluginId}: ${(error as Error).message}`, pluginId, 'MESSAGE_ERROR', error ); } } /** * Broadcast event to all active plugins */ async broadcastEvent(event: PluginEvent): Promise<void> { const activePlugins = this.getPluginsByState('active'); const promises = activePlugins.map(async (plugin) => { try { await plugin.onEvent(event); } catch (error) { console.error(`Error broadcasting event to plugin ${plugin.id}:`, error); // Don't throw here to avoid stopping other plugins } }); await Promise.allSettled(promises); } /** * Shutdown all plugins */ async shutdown(): Promise<void> { if (this.isShuttingDown) { return; } this.isShuttingDown = true; console.log('🔄 Shutting down plugin registry...'); try { // Deactivate all active plugins in reverse load order const reverseLoadOrder = [...this.loadOrder].reverse(); for (const pluginId of reverseLoadOrder) { const registration = this.plugins.get(pluginId); if (registration && registration.state === 'active') { try { await this.deactivate(pluginId); } catch (error) { console.error(`Error deactivating plugin ${pluginId} during shutdown:`, error); } } } // Destroy all plugins for (const pluginId of reverseLoadOrder) { const registration = this.plugins.get(pluginId); if (registration && registration.state !== 'destroyed') { try { await this.destroyPlugin(registration); } catch (error) { console.error(`Error destroying plugin ${pluginId} during shutdown:`, error); } } } // Clear all registrations this.plugins.clear(); this.loadOrder = []; await this.emitPluginEvent(PluginEvents.SYSTEM_SHUTDOWN, 'system'); console.log('✅ Plugin registry shutdown complete'); } catch (error) { console.error('❌ Error during plugin registry shutdown:', error); throw error; } finally { this.isShuttingDown = false; } } /** * Get plugin registry statistics */ getStatistics() { const stats = { total: this.plugins.size, byState: {} as Record<PluginState, number>, loadOrder: this.loadOrder, errors: 0, activeContracts: 0 }; // Count plugins by state for (const state of ['uninitialized', 'initialized', 'active', 'inactive', 'error', 'destroyed'] as PluginState[]) { stats.byState[state] = 0; } for (const registration of this.plugins.values()) { stats.byState[registration.state]++; stats.errors += registration.errorCount; if (registration.state === 'active') { stats.activeContracts += registration.plugin.getContracts().length; } } return stats; } // Private helper methods private async destroyPlugin(registration: PluginRegistration): Promise<void> { try { await registration.plugin.destroy(); registration.state = 'destroyed'; } catch (error) { console.error(`Error destroying plugin ${registration.plugin.id}:`, error); registration.state = 'error'; registration.errorCount++; registration.lastError = error as Error; } } private validatePlugin(plugin: WebBuddyPlugin, manifest: PluginManifest): void { if (!plugin.id || typeof plugin.id !== 'string') { throw new PluginLoadError('Plugin must have a valid string ID', plugin.id || 'unknown'); } if (!plugin.name || typeof plugin.name !== 'string') { throw new PluginLoadError('Plugin must have a valid string name', plugin.id); } if (!plugin.version || typeof plugin.version !== 'string') { throw new PluginLoadError('Plugin must have a valid string version', plugin.id); } if (!manifest.metadata || manifest.metadata.id !== plugin.id) { throw new PluginLoadError('Plugin ID must match manifest metadata ID', plugin.id); } // Validate required methods const requiredMethods = ['initialize', 'activate', 'deactivate', 'destroy', 'getContracts', 'executeCapability', 'getUIComponents', 'getMenuItems', 'onEvent']; for (const method of requiredMethods) { if (typeof (plugin as any)[method] !== 'function') { throw new PluginLoadError(`Plugin must implement ${method} method`, plugin.id); } } } private async checkDependencies(dependencies: string[]): Promise<void> { for (const depId of dependencies) { const depRegistration = this.plugins.get(depId); if (!depRegistration) { throw new PluginDependencyError(`Required dependency ${depId} is not available`, depId); } } } private updateLoadOrder(pluginId: string): void { if (!this.loadOrder.includes(pluginId)) { this.loadOrder.push(pluginId); } } private removeFromLoadOrder(pluginId: string): void { const index = this.loadOrder.indexOf(pluginId); if (index !== -1) { this.loadOrder.splice(index, 1); } } private updateDependents(pluginId: string, dependencies: string[]): void { for (const depId of dependencies) { const depRegistration = this.plugins.get(depId); if (depRegistration && !depRegistration.dependents.includes(pluginId)) { depRegistration.dependents.push(pluginId); } } } private removeDependents(pluginId: string, dependencies: string[]): void { for (const depId of dependencies) { const depRegistration = this.plugins.get(depId); if (depRegistration) { const index = depRegistration.dependents.indexOf(pluginId); if (index !== -1) { depRegistration.dependents.splice(index, 1); } } } } private async emitPluginEvent(type: string, pluginId: string, data?: any): Promise<void> { const event: PluginEvent = { type, source: 'plugin-registry', target: pluginId, data: data || {}, timestamp: new Date().toISOString() }; try { await this.eventBus.emit(event); } catch (error) { console.error('Error emitting plugin event:', error); } } private setupEventHandlers(): void { // Listen for system events this.eventBus.on(PluginEvents.SYSTEM_SHUTDOWN, async () => { await this.shutdown(); }); } // Discovery method implementations (placeholder) private async discoverFromManifest(): Promise<PluginManifest[]> { // TODO: Implement discovery from extension manifest return []; } private async discoverFromURL(): Promise<PluginManifest[]> { // TODO: Implement discovery from URLs return []; } private async discoverFromRegistry(): Promise<PluginManifest[]> { // TODO: Implement discovery from plugin registry return []; } }