verstak/cmd/verstak-gui/bindings_plugins.go

320 lines
7.9 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"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
}
// CallPluginFunction calls a global Lua function on an active plugin.
// The funcName can use dots: "calendar.create_event" → _G.calendar.create_event
// Returns JSON string or error.
func (a *App) CallPluginFunction(pluginName, funcName string, paramsJSON 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
}
vm := p.VM()
if vm == nil {
continue
}
// Resolve dotted path: "calendar.create_event" → _G.calendar.create_event
parts := strings.SplitN(funcName, ".", 2)
var luaFn string
if len(parts) == 2 {
luaFn = fmt.Sprintf("_G.%s['%s']", parts[0], parts[1])
} else {
luaFn = fmt.Sprintf("_G['%s']", funcName)
}
// Parse params
var params interface{}
if paramsJSON != "" && paramsJSON != "{}" {
if err := json.Unmarshal([]byte(paramsJSON), &params); err != nil {
params = paramsJSON
}
}
// Convert params to Lua value
var luaArg lua.LValue
switch v := params.(type) {
case nil:
luaArg = lua.LNil
case string:
luaArg = lua.LString(v)
case float64:
luaArg = lua.LNumber(v)
case bool:
luaArg = lua.LBool(v)
case map[string]interface{}:
tbl := vm.LState().NewTable()
for key, val := range v {
switch sv := val.(type) {
case string:
tbl.RawSetString(key, lua.LString(sv))
case float64:
tbl.RawSetString(key, lua.LNumber(sv))
case bool:
tbl.RawSetString(key, lua.LBool(sv))
}
}
luaArg = tbl
default:
luaArg = lua.LString(paramsJSON)
}
var script string
if luaArg == lua.LNil {
script = fmt.Sprintf("return %s()", luaFn)
} else {
script = fmt.Sprintf("return %s(%s)", luaFn, luaArg.String())
}
result, err := vm.DoString(script)
if err != nil {
return "", fmt.Errorf("call %s: %w", funcName, err)
}
return result, nil
}
return "", fmt.Errorf("plugin %q not active or not found", pluginName)
}
// 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)
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)
}