package main import ( "fmt" "log" "os" "path/filepath" "verstak/internal/core/config" lua "github.com/yuin/gopher-lua" ) // PluginDTO represents a discovered plugin with its current state. type PluginDTO struct { Name string `json:"name"` Version string `json:"version"` Author string `json:"author,omitempty"` Description string `json:"description,omitempty"` Active bool `json:"active"` Installed bool `json:"installed"` HasInstall bool `json:"hasInstall"` HasPanel bool `json:"hasPanel"` HasSettings bool `json:"hasSettings"` UIContribs UIContribDTO `json:"uiContribs"` } // UIContribDTO describes what a plugin adds to the UI. type UIContribDTO struct { SidebarItems []SidebarItemDTO `json:"sidebarItems"` NodeTabs []NodeTabDTO `json:"nodeTabs"` } type SidebarItemDTO struct { ID string `json:"id"` Label string `json:"label"` Icon string `json:"icon,omitempty"` } type NodeTabDTO struct { ID string `json:"id"` Label string `json:"label"` Page string `json:"page"` } // ListPlugins returns all discovered plugins with their current enabled/disabled state. func (a *App) ListPlugins() []PluginDTO { if a.plugins == nil { return nil } all := a.plugins.Plugins() out := make([]PluginDTO, 0, len(all)) appCfg, _ := config.LoadAppConfig() enabledSet := make(map[string]bool) if appCfg != nil { for _, name := range appCfg.EnabledPlugins { enabledSet[name] = true } } for _, p := range all { active := enabledSet[p.Meta.Name] || p.Active contribs := UIContribDTO{} for _, item := range p.Meta.UI.SidebarItems { contribs.SidebarItems = append(contribs.SidebarItems, SidebarItemDTO{ ID: item.ID, Label: item.Label, Icon: item.Icon, }) } for _, tab := range p.Meta.UI.NodeTabs { contribs.NodeTabs = append(contribs.NodeTabs, NodeTabDTO{ ID: tab.ID, Label: tab.Label, Page: tab.Page, }) } hasPanel := false if p.Meta.Panel != "" { panelPath := filepath.Join(p.Dir, p.Meta.Panel) if _, err := os.Stat(panelPath); err == nil { hasPanel = true } } out = append(out, PluginDTO{ Name: p.Meta.Name, Version: p.Meta.Version, Author: p.Meta.Author, Description: p.Meta.Description, Active: active, Installed: p.Installed, HasInstall: p.HasInstall, HasPanel: hasPanel, UIContribs: contribs, }) } return out } // SetPluginEnabled persists the enabled/disabled state and applies it to the runtime. // Returns error if the plugin is not installed but has install lifecycle. func (a *App) SetPluginEnabled(name string, enabled bool) error { if a.plugins == nil { return fmt.Errorf("plugin manager not ready") } if enabled { if err := a.plugins.Enable(name); err != nil { return err } } else { if err := a.plugins.Disable(name); err != nil { return err } } appCfg, _ := config.LoadAppConfig() if appCfg == nil { appCfg = config.DefaultAppConfig() } existing := make(map[string]bool) for _, n := range appCfg.EnabledPlugins { existing[n] = true } if enabled { existing[name] = true } else { delete(existing, name) } appCfg.EnabledPlugins = make([]string, 0, len(existing)) for n := range existing { appCfg.EnabledPlugins = append(appCfg.EnabledPlugins, n) } if err := config.SaveAppConfig(appCfg); err != nil { return fmt.Errorf("save config: %w", err) } if enabled { a.plugins.ActivatePlugin(name) } else { a.plugins.DeactivatePlugin(name) } return nil } // GetPluginPanelHTML returns the HTML panel content for a plugin. func (a *App) GetPluginPanelHTML(pluginName string) (string, error) { if a.plugins == nil { return "", fmt.Errorf("plugin manager not ready") } for _, p := range a.plugins.Plugins() { if p.Meta.Name != pluginName || !p.Active { continue } if p.Meta.Panel == "" { return "", nil } panelPath := filepath.Join(p.Dir, p.Meta.Panel) data, err := os.ReadFile(panelPath) if err != nil { return "", fmt.Errorf("read panel %s: %w", p.Meta.Panel, err) } return string(data), nil } return "", nil } // ListSystemViewsWithPlugins returns system views + plugin sidebar items. func (a *App) ListSystemViewsWithPlugins() []SystemViewDTO { base := a.ListSystemViews() if a.plugins == nil { return base } appCfg, _ := config.LoadAppConfig() enabledSet := make(map[string]bool) if appCfg != nil { for _, name := range appCfg.EnabledPlugins { enabledSet[name] = true } } for _, p := range a.plugins.Plugins() { active := enabledSet[p.Meta.Name] || p.Active if !active { continue } for _, item := range p.Meta.UI.SidebarItems { pageID := "plugin:" + p.Meta.Name + ":" + item.ID base = append(base, SystemViewDTO{ ID: pageID, Label: item.Label, Icon: item.Icon, }) } } return base } // CallPluginAction invokes a named Lua hook on a specific plugin. func (a *App) CallPluginAction(pluginName, action string, paramsJSON string) (string, error) { if a.plugins == nil { return "", fmt.Errorf("plugin manager not ready") } result, err := a.plugins.CallPluginHook(pluginName, action, lua.LString(paramsJSON)) if err != nil { return "", fmt.Errorf("plugin call %s: %w", action, err) } return result.String(), nil } // ReloadPlugins re-scans the plugins directory and re-initializes runtimes. func (a *App) ReloadPlugins() error { if a.plugins == nil { return fmt.Errorf("plugin manager not ready") } log.Print("[plugins] reload requested") a.plugins.CloseRuntimes() a.plugins.Discover() appCfg, _ := config.LoadAppConfig() a.plugins.SyncConfig(appCfg) // Apply enable/disable state from config: deactivate everything not in enabled set for _, p := range a.plugins.Plugins() { if !p.Active || !p.Installed { a.plugins.DeactivatePlugin(p.Meta.Name) } } a.plugins.InitRuntimes() a.plugins.CallInitHooks() a.plugins.StartSchedulers() log.Print("[plugins] reload complete") return nil } // InstallPlugin creates plugin's database tables and defaults via on_install hook. // Does NOT activate the plugin — use SetPluginEnabled after. func (a *App) InstallPlugin(name string) error { if a.plugins == nil { return fmt.Errorf("plugin manager not ready") } return a.plugins.Install(name) } // UninstallPlugin drops plugin's database tables and cleans data via on_uninstall hook. // Disables the plugin first if active. Does NOT delete plugin files from disk. func (a *App) UninstallPlugin(name string) error { if a.plugins == nil { return fmt.Errorf("plugin manager not ready") } return a.plugins.Uninstall(name) }