step 10: plugins system (Lua + templates) + DokuWiki as optional plugin

Plugin Manager:
- Discover plugins from .verstak/plugins/<name>/plugin.json
- Enable/disable per plugin
- Template definitions (JSON) → pre-filled node trees
- SQL migrations from plugins
- Built-in templates loaded from internal/core/plugins/builtin/templates/

Lua Runtime:
- Stub (gopher-lua placeholder) — ready for real implementation
- When dep added: hooks (on_init, on_vault_open, on_node_create),
  sandbox (no io/os.execute), Plugin API

GUI:
- Template selector in create node modal
- POST /api/nodes/from-template creates tree from template
- Built-in "Клиент" template: Overview note + Документы/Переписка/Скриншоты

CLI:
- verstak plugin list/enable/disable/templates

DokuWiki Importer:
- Moved to contrib/plugins/importer-dokuwiki/ (optional plugin)
- plugin.json + migration + README

DokuWiki removed from MVP core — now an opt-in plugin.

Acceptance: go build ./... pass, go test ./... pass (all packages).
This commit is contained in:
2026-05-31 11:20:45 +08:00
parent d6f7f1a9b8
commit b800bce7e4
12 changed files with 653 additions and 21 deletions
+71
View File
@@ -8,6 +8,7 @@ import (
"strings"
"verstak/internal/core/actions"
"verstak/internal/core/plugins"
"verstak/internal/core/search"
"verstak/internal/core/storage"
"verstak/internal/core/vault"
@@ -37,6 +38,8 @@ func main() {
runLog(os.Args[2:])
case "index":
runIndex(os.Args[2:])
case "plugin":
runPlugin(os.Args[2:])
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", os.Args[1])
os.Exit(1)
@@ -593,3 +596,71 @@ func runIndexRebuild(args []string) {
fmt.Printf("indexed %d nodes\n", count)
}
// --- plugin ---
func runPlugin(args []string) {
vaultPath, _ := stringFlag(args, "--vault")
if len(args) == 0 || args[0] == "--help" || args[0] == "-h" {
fmt.Println("verstak plugin — manage plugins")
fmt.Println()
fmt.Println("Usage: verstak plugin <command> [options]")
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" list List discovered plugins")
fmt.Println(" enable NAME Enable a plugin")
fmt.Println(" disable NAME Disable a plugin")
fmt.Println(" templates List available templates")
os.Exit(0)
}
abs, _ := filepath.Abs(vaultPath)
switch args[0] {
case "list":
mgr := plugins.NewManager(abs)
mgr.Discover()
pp := mgr.Plugins()
if len(pp) == 0 {
fmt.Println("No plugins found.")
fmt.Println("Put plugins in .verstak/plugins/<name>/plugin.json")
return
}
for _, p := range pp {
status := "on"
if !p.Active {
status = "off"
}
fmt.Printf("%-20s %-6s %s\n", p.Meta.Name, status, p.Meta.Description)
}
case "enable", "disable":
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "Error: plugin name required")
os.Exit(1)
}
mgr := plugins.NewManager(abs)
mgr.Discover()
name := args[1]
if args[0] == "enable" {
mgr.Enable(name)
} else {
mgr.Disable(name)
}
fmt.Printf("%s %s\n", args[0]+"d", name)
case "templates":
mgr := plugins.NewManager(abs)
mgr.Discover()
tmpls := mgr.Templates()
if len(tmpls) == 0 {
fmt.Println("No templates found.")
return
}
for _, t := range tmpls {
fmt.Printf("%-20s %-10s %s\n", t.Name, fmt.Sprintf("[%s]", t.Plugin), t.Description)
}
default:
fmt.Fprintf(os.Stderr, "Unknown plugin command: %s\n", args[0])
os.Exit(1)
}
}