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
This commit is contained in:
parent
d72ebeb7ec
commit
1c75389535
|
|
@ -1,3 +1,4 @@
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
build/bin/verstak-desktop
|
build/bin/verstak-desktop
|
||||||
|
plugins/
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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/<plugin-id>/`
|
||||||
|
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/<plugin-id>/`
|
||||||
|
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/<plugin-id>/` 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
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -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"
|
||||||
Loading…
Reference in New Issue