package plugins import ( "encoding/json" "fmt" "os" "path/filepath" "sync" "time" ) // PluginLoader loads and manages plugin lifecycle type PluginLoader struct { mu sync.RWMutex registry *PluginRegistry pluginPath string loadedPlugins map[string]*LoadedPlugin loadTime map[string]time.Time failedLoads map[string]error } // LoadedPlugin represents a loaded plugin with additional metadata type LoadedPlugin struct { Plugin SkillPlugin LoadedAt time.Time ReloadCount int LastError error } // PluginConfig represents plugin configuration from file type PluginConfig struct { Name string `json:"name"` Version string `json:"version"` Author string `json:"author"` Description string `json:"description"` Path string `json:"path"` Config map[string]interface{} `json:"config,omitempty"` Enabled bool `json:"enabled"` } // NewPluginLoader creates a new plugin loader func NewPluginLoader(registry *PluginRegistry, pluginPath string) *PluginLoader { return &PluginLoader{ registry: registry, pluginPath: pluginPath, loadedPlugins: make(map[string]*LoadedPlugin), loadTime: make(map[string]time.Time), failedLoads: make(map[string]error), } } // LoadPlugin loads a plugin from URL func (pl *PluginLoader) LoadPlugin(url string) error { pl.mu.Lock() defer pl.mu.Unlock() if IsPluginURL(url) { // Already loaded - no need to load from file return nil } // Try to load from file return pl.loadFromFileLocked(url) } // loadFromFileLocked loads a plugin from a file (must be called with lock held) func (pl *PluginLoader) loadFromFileLocked(path string) error { // Read config file data, err := os.ReadFile(path) if err != nil { pl.failedLoads[path] = err return fmt.Errorf("failed to read plugin config: %w", err) } var config PluginConfig if err := json.Unmarshal(data, &config); err != nil { pl.failedLoads[path] = err return fmt.Errorf("failed to parse plugin config: %w", err) } // For now, return a placeholder plugin load // In a real implementation, this would use reflection or plugin packages // to dynamically load compiled plugins pl.loadTime[config.Name] = time.Now() return nil } // LoadPluginDirectory loads all plugins from a directory func (pl *PluginLoader) LoadPluginDirectory(directory string) error { entries, err := os.ReadDir(directory) if err != nil { return fmt.Errorf("failed to read plugin directory: %w", err) } for _, entry := range entries { if entry.IsDir() { continue } if filepath.Ext(entry.Name()) == ".json" { pluginPath := filepath.Join(directory, entry.Name()) if err := pl.LoadPlugin(pluginPath); err != nil { // Log error but continue loading other plugins pl.mu.Lock() pl.failedLoads[pluginPath] = err pl.mu.Unlock() } } } return nil } // RegisterLoadedPlugin registers a loaded plugin with the registry func (pl *PluginLoader) RegisterLoadedPlugin(plugin SkillPlugin, author string, config map[string]interface{}) error { pl.mu.Lock() defer pl.mu.Unlock() if err := pl.registry.Register(plugin, author, config); err != nil { pl.failedLoads[plugin.Name()] = err return err } now := time.Now() pl.loadedPlugins[plugin.Name()] = &LoadedPlugin{ Plugin: plugin, LoadedAt: now, LastError: nil, } pl.loadTime[plugin.Name()] = now return nil } // UnloadPlugin unloads a plugin func (pl *PluginLoader) UnloadPlugin(name string) error { pl.mu.Lock() defer pl.mu.Unlock() if _, exists := pl.loadedPlugins[name]; !exists { return fmt.Errorf("plugin not loaded: %s", name) } err := pl.registry.Unregister(name) if err == nil { delete(pl.loadedPlugins, name) delete(pl.loadTime, name) } return err } // ReloadPlugin reloads a plugin func (pl *PluginLoader) ReloadPlugin(name string) error { pl.mu.Lock() defer pl.mu.Unlock() loadedPlugin, exists := pl.loadedPlugins[name] if !exists { return fmt.Errorf("plugin not loaded: %s", name) } // Re-validate plugin if err := loadedPlugin.Plugin.Validate(); err != nil { pl.failedLoads[name] = err loadedPlugin.LastError = err return fmt.Errorf("plugin validation failed: %w", err) } loadedPlugin.ReloadCount++ loadedPlugin.LastError = nil pl.loadTime[name] = time.Now() return nil } // GetLoadedPlugins returns all loaded plugins func (pl *PluginLoader) GetLoadedPlugins() map[string]*LoadedPlugin { pl.mu.RLock() defer pl.mu.RUnlock() result := make(map[string]*LoadedPlugin) for name, plugin := range pl.loadedPlugins { result[name] = plugin } return result } // GetFailedLoads returns all failed plugin loads func (pl *PluginLoader) GetFailedLoads() map[string]error { pl.mu.RLock() defer pl.mu.RUnlock() result := make(map[string]error) for path, err := range pl.failedLoads { result[path] = err } return result } // GetLoadTime returns when a plugin was loaded func (pl *PluginLoader) GetLoadTime(name string) (time.Time, bool) { pl.mu.RLock() defer pl.mu.RUnlock() t, exists := pl.loadTime[name] return t, exists } // IsPluginLoaded checks if a plugin is loaded func (pl *PluginLoader) IsPluginLoaded(name string) bool { pl.mu.RLock() defer pl.mu.RUnlock() _, exists := pl.loadedPlugins[name] return exists } // ExecutePlugin executes a loaded plugin func (pl *PluginLoader) ExecutePlugin(name string, input map[string]interface{}) (map[string]interface{}, error) { pl.mu.RLock() if _, exists := pl.loadedPlugins[name]; !exists { pl.mu.RUnlock() return nil, fmt.Errorf("plugin not loaded: %s", name) } pl.mu.RUnlock() return pl.registry.Execute(name, input) } // GetPluginStats returns stats for a loaded plugin func (pl *PluginLoader) GetPluginStats(name string) (*PluginStats, error) { pl.mu.RLock() defer pl.mu.RUnlock() if _, exists := pl.loadedPlugins[name]; !exists { return nil, fmt.Errorf("plugin not loaded: %s", name) } return pl.registry.GetStats(), nil } // Close closes the plugin loader and unloads all plugins func (pl *PluginLoader) Close() error { pl.mu.Lock() defer pl.mu.Unlock() var lastErr error for name := range pl.loadedPlugins { if err := pl.registry.Unregister(name); err != nil { lastErr = err } } pl.loadedPlugins = make(map[string]*LoadedPlugin) pl.loadTime = make(map[string]time.Time) return lastErr } // GetPluginRegistry returns the underlying registry func (pl *PluginLoader) GetPluginRegistry() *PluginRegistry { return pl.registry }