219 lines
5.2 KiB
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
|
|
}
|