verstak/internal/core/plugins/manager.go

219 lines
5.2 KiB
Go

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
}