From 1c753895357c75019dbec1837083b711484e5ded Mon Sep 17 00:00:00 2001 From: mirivlad Date: Tue, 16 Jun 2026 16:46:00 +0800 Subject: [PATCH] feat: dev plugin install flow + smoke-platform - .gitignore: add plugins/ (local dev install, never committed) - scripts/install-dev-plugins.sh: install dist package from ../verstak-official-plugins/dist/ into ./plugins/ - scripts/smoke-platform.sh: headless verification of plugin discovery, manifest, capabilities, contributions - cmd/smoke-platform/main.go: Go smoke command for headless plugin verification - docs/DEV_PLUGINS.md: dev plugin flow documentation --- .gitignore | 1 + cmd/smoke-platform/main.go | 172 +++++++++++++++++++++++++++++++++ docs/DEV_PLUGINS.md | 75 ++++++++++++++ scripts/install-dev-plugins.sh | 56 +++++++++++ scripts/smoke-platform.sh | 64 ++++++++++++ 5 files changed, 368 insertions(+) create mode 100644 cmd/smoke-platform/main.go create mode 100644 docs/DEV_PLUGINS.md create mode 100755 scripts/install-dev-plugins.sh create mode 100755 scripts/smoke-platform.sh diff --git a/.gitignore b/.gitignore index 9a901c0..abe373b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ frontend/node_modules/ frontend/dist/ build/bin/verstak-desktop +plugins/ diff --git a/cmd/smoke-platform/main.go b/cmd/smoke-platform/main.go new file mode 100644 index 0000000..47dfa18 --- /dev/null +++ b/cmd/smoke-platform/main.go @@ -0,0 +1,172 @@ +// Smoke-platform validates that the platform-test plugin is discovered correctly +// by the Verstak desktop runtime. This runs headless — no Wails GUI needed. +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/verstak/verstak-desktop/internal/core/capability" + "github.com/verstak/verstak-desktop/internal/core/plugin" +) + +func main() { + exitCode := 0 + defer func() { + os.Exit(exitCode) + }() + + root, _ := os.Getwd() + pluginDir := filepath.Join(root, "plugins") + + fmt.Printf("=== smoke-platform: headless plugin verification ===\n\n") + fmt.Printf(" plugin dir: %s\n", pluginDir) + + // ── 1. Discover plugins ── + fmt.Printf("\n[discovery]\n") + plugins, errs := plugin.DiscoverPlugins([]string{pluginDir}) + + if len(errs) > 0 { + for _, e := range errs { + fmt.Printf(" ⚠️ discovery warning: %v\n", e) + } + } + + if len(plugins) == 0 { + fmt.Printf(" ❌ no plugins discovered\n") + exitCode = 1 + return + } + + fmt.Printf(" ✅ discovered %d plugin(s)\n", len(plugins)) + + // ── 2. Find platform-test ── + fmt.Printf("\n[platform-test lookup]\n") + var target *plugin.Plugin + for i, p := range plugins { + if p.Manifest.ID == "verstak.platform-test" { + target = &plugins[i] + break + } + } + + if target == nil { + fmt.Printf(" ❌ platform-test (id=verstak.platform-test) not found among discovered plugins\n") + exitCode = 1 + return + } + fmt.Printf(" ✅ found: %s\n", target.Manifest.ID) + + // ── 3. Validate manifest fields ── + fmt.Printf("\n[manifest validation]\n") + allGood := true + m := &target.Manifest + + checks := []struct { + name string + value string + }{ + {"name", m.Name}, + {"version", m.Version}, + {"apiVersion", m.APIVersion}, + } + for _, c := range checks { + if c.value == "" { + fmt.Printf(" ❌ manifest.%s is empty\n", c.name) + allGood = false + } else { + fmt.Printf(" ✅ %s: %s\n", c.name, c.value) + } + } + + if m.SchemaVersion != 1 { + fmt.Printf(" ❌ schemaVersion: expected 1, got %d\n", m.SchemaVersion) + allGood = false + } else { + fmt.Printf(" ✅ schemaVersion: 1\n") + } + + // ── 4. provides ── + fmt.Printf("\n[provides]\n") + if len(m.Provides) == 0 { + fmt.Printf(" ❌ provides is empty\n") + allGood = false + } else { + for _, p := range m.Provides { + fmt.Printf(" ✅ provides: %s\n", p) + } + } + + // ── 5. requires / optionalRequires ── + fmt.Printf("\n[requires]\n") + if len(m.Requires) > 0 { + for _, r := range m.Requires { + fmt.Printf(" ✅ requires: %s\n", r) + } + } else { + fmt.Printf(" ℹ️ requires: none\n") + } + + fmt.Printf("\n[optionalRequires]\n") + if len(m.OptionalRequires) > 0 { + for _, r := range m.OptionalRequires { + fmt.Printf(" ✅ optionalRequires: %s\n", r) + } + } else { + fmt.Printf(" ℹ️ optionalRequires: none\n") + } + + // ── 6. contributions ── + fmt.Printf("\n[contributions]\n") + if m.Contributes != nil { + c := m.Contributes + if len(c.Views) > 0 { + for _, v := range c.Views { + fmt.Printf(" ✅ view: %s (%s)\n", v.ID, v.Title) + } + } + if len(c.Commands) > 0 { + for _, cmd := range c.Commands { + fmt.Printf(" ✅ command: %s (%s)\n", cmd.ID, cmd.Title) + } + } + if len(c.SidebarItems) > 0 { + for _, s := range c.SidebarItems { + fmt.Printf(" ✅ sidebarItem: %s (%s)\n", s.ID, s.Title) + } + } + if len(c.StatusBarItems) > 0 { + for _, s := range c.StatusBarItems { + fmt.Printf(" ✅ statusBarItem: %s (%s)\n", s.ID, s.Label) + } + } + if len(c.Views)+len(c.Commands)+len(c.SidebarItems)+len(c.StatusBarItems) == 0 { + fmt.Printf(" ℹ️ contributes: empty sections only\n") + } + } else { + fmt.Printf(" ℹ️ contributes: none\n") + } + + // ── 7. Capability registration ── + fmt.Printf("\n[capability registration]\n") + reg := capability.NewRegistry() + for _, p := range m.Provides { + err := reg.Register(target.Manifest.ID, []string{p}) + if err != nil { + fmt.Printf(" ❌ register capability %s: %v\n", p, err) + allGood = false + } else { + fmt.Printf(" ✅ registered capability: %s\n", p) + } + } + + // ── 8. Summary ── + fmt.Printf("\n=== summary ===\n") + if allGood { + fmt.Printf("✅ smoke-platform passed\n") + } else { + fmt.Printf("❌ smoke-platform failed\n") + exitCode = 1 + } +} diff --git a/docs/DEV_PLUGINS.md b/docs/DEV_PLUGINS.md new file mode 100644 index 0000000..5f8ec9e --- /dev/null +++ b/docs/DEV_PLUGINS.md @@ -0,0 +1,75 @@ +# Verstak Desktop — Development Plugin Flow + +## Overview + +Official plugins live in the **verstak-official-plugins** monorepo and are developed +separately from the desktop core. During development, plugins are installed into +the desktop's local `plugins/` directory as **packaged bundles** (not source code). + +``` +git/ + verstak-desktop/ ← desktop core + verstak-official-plugins/ ← official plugin source + dist output +``` + +## Plugin Source → Package Flow + +1. **Source code** lives in `verstak-official-plugins/plugins//` +2. **Running `build.sh`** in official-plugins: + - Builds frontend (if frontend/package.json exists) + - Builds backend Go binary (if backend/main.go exists) + - Packages the result into `dist//` +3. **Package structure** (`dist/platform-test/`): + ``` + plugin.json + frontend/dist/index.js + backend/platform-test (compiled binary) + ``` + +## Installing Dev Plugins in Desktop + +From the `verstak-desktop/` directory: + +```bash +./scripts/install-dev-plugins.sh +``` + +This script: +- Locates `../verstak-official-plugins/` +- Builds packages there if `dist/` is stale +- Creates `./plugins//` from the dist package +- Does **not** affect other plugins in `./plugins/` + +The `plugins/` directory is in `.gitignore` — it is never committed. + +## Smoke Test + +After installing, verify that the desktop runtime discovers the plugin: + +```bash +./scripts/smoke-platform.sh +``` + +This validates: +- Plugin directory exists +- `plugin.json` manifest is valid +- All required manifest fields are present +- `DiscoverPlugins()` finds `verstak.platform-test` +- Capabilities are registerable + +## Desktop Runtime Scanning Paths + +The desktop scans two directories for plugins: + +| Path | Purpose | +|------|---------| +| `~/.config/verstak/plugins/` | User-installed plugins | +| `./plugins/` | Bundled/dev plugins (project-local) | + +## Important Rules + +- **Never commit** `plugins/` to `verstak-desktop` — it's in `.gitignore` +- **Never copy source code** from `verstak-official-plugins/plugins/` directly — + always use the dist package from `verstak-official-plugins/dist/` +- **Run `install-dev-plugins.sh`** after any change in the plugin source +- **Run `smoke-platform.sh`** after installing to verify discovery diff --git a/scripts/install-dev-plugins.sh b/scripts/install-dev-plugins.sh new file mode 100755 index 0000000..122da89 --- /dev/null +++ b/scripts/install-dev-plugins.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +echo "=== verstak-desktop: install dev plugins ===" + +# ── locate sibling repo ── +OFFICIAL_PLUGINS="$ROOT/../verstak-official-plugins" +if [ ! -d "$OFFICIAL_PLUGINS" ]; then + echo "❌ sibling repo not found at $OFFICIAL_PLUGINS" + echo " Expected structure:" + echo " ../verstak-official-plugins/" + echo " ../verstak-desktop/ (this repo)" + exit 1 +fi + +# ── ensure dist package exists ── +DIST_PACKAGE="$OFFICIAL_PLUGINS/dist/platform-test" +if [ ! -d "$DIST_PACKAGE" ]; then + echo " ℹ️ dist package not found at $DIST_PACKAGE" + echo " → Running build.sh in verstak-official-plugins..." + (cd "$OFFICIAL_PLUGINS" && ./scripts/build.sh) + echo "" + if [ ! -d "$DIST_PACKAGE" ]; then + echo "❌ dist package still missing after build" + exit 1 + fi +fi + +# ── create ./plugins/platform-test ── +PLUGIN_DIR="$ROOT/plugins/platform-test" +echo " → installing platform-test to $PLUGIN_DIR" + +mkdir -p "$ROOT/plugins" + +# Atomic replace: install to temp then rename +TMP_DIR=$(mktemp -d "$ROOT/plugins/.platform-test-tmp.XXXXXX") +cp -r "$DIST_PACKAGE/." "$TMP_DIR/" +rm -f "$PLUGIN_DIR" 2>/dev/null # remove broken symlink if any +rm -rf "$PLUGIN_DIR" # remove old directory +mv "$TMP_DIR" "$PLUGIN_DIR" + +# ── verify ── +if [ -f "$PLUGIN_DIR/plugin.json" ]; then + PLUGIN_ID=$(python3 -c "import json; print(json.load(open('$PLUGIN_DIR/plugin.json')).get('id','unknown'))" 2>/dev/null || echo "unknown") + FILE_COUNT=$(find "$PLUGIN_DIR" -type f | wc -l) + echo " ✅ installed: $PLUGIN_DIR" + echo " plugin id: $PLUGIN_ID" + echo " files: $FILE_COUNT" +else + echo "❌ install failed: plugin.json missing in $PLUGIN_DIR" + exit 1 +fi + +echo "✅ install-dev-plugins done" diff --git a/scripts/smoke-platform.sh b/scripts/smoke-platform.sh new file mode 100755 index 0000000..4cd6f1e --- /dev/null +++ b/scripts/smoke-platform.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +echo "=== verstak-desktop: smoke-platform ===" + +# ── verify platform-test is installed ── +PLUGIN_DIR="$ROOT/plugins/platform-test" +if [ ! -d "$PLUGIN_DIR" ]; then + echo "❌ platform-test not installed at $PLUGIN_DIR" + echo " Run: ./scripts/install-dev-plugins.sh" + exit 1 +fi +echo " ✅ plugin directory: $PLUGIN_DIR" + +# ── validate plugin.json ── +if [ ! -f "$PLUGIN_DIR/plugin.json" ]; then + echo " ❌ plugin.json not found" + exit 1 +fi + +if command -v python3 &>/dev/null; then + python3 -c " +import json +with open('$PLUGIN_DIR/plugin.json') as f: + m = json.load(f) +checks = { + 'id': m.get('id') == 'verstak.platform-test', + 'name': m.get('name') == 'Platform Test', + 'version': m.get('version') == '0.1.0', + 'apiVersion': m.get('apiVersion') == '0.1.0', + 'schemaVersion': m.get('schemaVersion') == 1, + 'provides': 'verstak/platform-test/v1' in m.get('provides', []), + 'permissions': 'vault.read' in m.get('permissions', []), + 'frontend.entry': m.get('frontend', {}).get('entry') == 'frontend/dist/index.js', + 'contributes.views': len(m.get('contributes', {}).get('views', [])) > 0, + 'contributes.commands': len(m.get('contributes', {}).get('commands', [])) > 0, +} +all_ok = True +for name, ok in checks.items(): + print(f\" {'✅' if ok else '❌'} manifest.{name}\") + if not ok: + all_ok = False +if not all_ok: + exit(1) +" 2>&1 || { echo " ❌ manifest validation failed"; exit 1; } + echo " ✅ manifest validation passed" +else + echo " ℹ️ python3 not available — skipping manifest validation" +fi + +# ── run Go smoke command ── +echo "" +echo "[go smoke]" +(cd "$ROOT" && go run -mod=mod ./cmd/smoke-platform/ 2>&1) +SMOKE_EXIT=$? +if [ "$SMOKE_EXIT" -ne 0 ]; then + echo " ❌ smoke-platform: Go verification failed" + exit 1 +fi + +echo "" +echo "✅ smoke-platform done"