package plugins import ( "encoding/json" "os" "path/filepath" "strings" ) // Meta is the plugin.json descriptor. type Meta struct { Name string `json:"name"` Version string `json:"version"` Description string `json:"description"` Author string `json:"author"` Hooks map[string]string `json:"hooks,omitempty"` NodeTypes []string `json:"node_types,omitempty"` Templates []string `json:"templates,omitempty"` Migrations []string `json:"migrations,omitempty"` } // Plugin represents a loaded plugin. type Plugin struct { Meta Meta Dir string // absolute path to plugin directory Active bool } // Manager discovers and loads plugins from .verstak/plugins/. type Manager struct { vaultRoot string plugins []Plugin } // NewManager creates a plugin manager for a vault. func NewManager(vaultRoot string) *Manager { return &Manager{vaultRoot: vaultRoot} } // Discover scans .verstak/plugins/* for plugin.json files. func (m *Manager) Discover() { pluginsDir := filepath.Join(m.vaultRoot, ".verstak", "plugins") entries, err := os.ReadDir(pluginsDir) if err != nil { return // no plugins dir — OK } for _, e := range entries { if !e.IsDir() { continue } metaPath := filepath.Join(pluginsDir, e.Name(), "plugin.json") data, err := os.ReadFile(metaPath) if err != nil { continue // no plugin.json — skip } var meta Meta if err := json.Unmarshal(data, &meta); err != nil { continue } if meta.Name == "" { meta.Name = e.Name() } m.plugins = append(m.plugins, Plugin{ Meta: meta, Dir: filepath.Join(pluginsDir, e.Name()), Active: true, }) } } // Plugins returns all discovered plugins. func (m *Manager) Plugins() []Plugin { return m.plugins } // Active returns only active plugins. func (m *Manager) Active() []Plugin { var out []Plugin for _, p := range m.plugins { if p.Active { out = append(out, p) } } return out } // Templates returns all templates from all active plugins + builtins. func (m *Manager) Templates() []TemplateDefinition { var out []TemplateDefinition // Built-in templates. for _, t := range builtinTemplates { out = append(out, t) } // Plugin templates. for _, p := range m.Active() { for _, tmplName := range p.Meta.Templates { tmplPath := filepath.Join(p.Dir, "templates", tmplName+".json") data, err := os.ReadFile(tmplPath) if err != nil { continue } var tmpl TemplateDefinition if err := json.Unmarshal(data, &tmpl); err != nil { continue } if tmpl.Name == "" { tmpl.Name = tmplName } tmpl.Plugin = p.Meta.Name out = append(out, tmpl) } } return out } // TemplateDefinition describes a predefined tree of nodes. type TemplateDefinition struct { Name string `json:"name"` Description string `json:"description,omitempty"` Icon string `json:"icon,omitempty"` Plugin string // source plugin name RootType string `json:"root_type"` Tree []TreeNode `json:"tree"` Meta []NodeMeta `json:"meta,omitempty"` } // TreeNode is a single item in a template tree. type TreeNode struct { Type string `json:"type"` Title string `json:"title"` Slug string `json:"slug,omitempty"` Children []TreeNode `json:"children,omitempty"` } // NodeMeta is key-value metadata for the root node. type NodeMeta struct { Key string `json:"key"` Label string `json:"label"` Type string `json:"type"` // text, url, etc. } // Enable activates a plugin by name. func (m *Manager) Enable(name string) { for i := range m.plugins { if m.plugins[i].Meta.Name == name { m.plugins[i].Active = true return } } } // Disable deactivates a plugin by name. func (m *Manager) Disable(name string) { for i := range m.plugins { if m.plugins[i].Meta.Name == name { m.plugins[i].Active = false return } } } // ActiveNames returns names of active plugins. func (m *Manager) ActiveNames() []string { var out []string for _, p := range m.Active() { out = append(out, p.Meta.Name) } return out } // MigrationFiles returns paths to SQL migration files from active plugins. func (m *Manager) MigrationFiles() []string { var out []string for _, p := range m.Active() { for _, mig := range p.Meta.Migrations { path := filepath.Join(p.Dir, "migrations", mig) if _, err := os.Stat(path); err == nil { out = append(out, path) } } } return out } // Silence unused strings import. var _ = strings.ToLower // builtinTemplates are shipped with the application. var builtinTemplates = loadBuiltinTemplates() func loadBuiltinTemplates() []TemplateDefinition { var out []TemplateDefinition dir := "internal/core/plugins/builtin/templates" entries, err := os.ReadDir(dir) if err != nil { return out } for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { continue } data, err := os.ReadFile(filepath.Join(dir, e.Name())) if err != nil { continue } var tmpl TemplateDefinition if err := json.Unmarshal(data, &tmpl); err != nil { continue } if tmpl.Name == "" { tmpl.Name = strings.TrimSuffix(e.Name(), ".json") } tmpl.Plugin = "builtin" out = append(out, tmpl) } return out }