Feat v0.2.9 builtin retirement (#13)
Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #13.
This commit is contained in:
23
CHANGELOG.md
23
CHANGELOG.md
@@ -2,6 +2,29 @@
|
|||||||
|
|
||||||
All notable changes to Switchboard Core are documented here.
|
All notable changes to Switchboard Core are documented here.
|
||||||
|
|
||||||
|
## v0.2.9 — Builtin Extension Retirement
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **6 builtin extensions → standard packages**: csv-table, diff-viewer,
|
||||||
|
js-sandbox, katex-renderer, mermaid-renderer, and regex-tester moved from
|
||||||
|
`extensions/builtin/` to `packages/` with standard `js/` layout. Each
|
||||||
|
manifest now declares `"requires": ["chat"]` and `"type": "extension"`.
|
||||||
|
Built via `build.sh` like all other packages — no auto-install.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **`SeedBuiltinPackages` seeder**: Deleted `seed_packages.go` (function,
|
||||||
|
`builtinManifest` struct, `buildFullManifest` helper) and the startup call
|
||||||
|
in `main.go`. Extensions are no longer auto-seeded into the DB.
|
||||||
|
- **`extensions/builtin/` directory**: Removed from repo and Dockerfile COPY.
|
||||||
|
- **Seed tests**: Removed 8 `TestSeed_*` functions and `makeSeedDir` helper
|
||||||
|
from `extension_test.go` (~220 lines).
|
||||||
|
- **Stale comments**: Removed `SeedBuiltinPackages` references from
|
||||||
|
`package_iface.go` and `trigger_sync.go`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.2.8 — Team Admin Settings Audit (Pass 1)
|
## v0.2.8 — Team Admin Settings Audit (Pass 1)
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|||||||
@@ -85,9 +85,6 @@ COPY --from=vendor /vendor/katex/katex.min.css /usr/share/nginx/html/vendor/kate
|
|||||||
COPY --from=vendor /vendor/katex/fonts/ /usr/share/nginx/html/vendor/katex/fonts/
|
COPY --from=vendor /vendor/katex/fonts/ /usr/share/nginx/html/vendor/katex/fonts/
|
||||||
COPY --from=cm6-build /build/dist/ /usr/share/nginx/html/vendor/codemirror/
|
COPY --from=cm6-build /build/dist/ /usr/share/nginx/html/vendor/codemirror/
|
||||||
|
|
||||||
# Builtin extensions (seeded into DB on startup by backend)
|
|
||||||
COPY extensions/builtin/ /app/extensions/builtin/
|
|
||||||
|
|
||||||
# nginx config (template — envsubst injects BASE_PATH at runtime)
|
# nginx config (template — envsubst injects BASE_PATH at runtime)
|
||||||
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
|
COPY nginx.conf.template /etc/nginx/templates/default.conf.template
|
||||||
# Only substitute BASE_PATH — preserve nginx variables ($host, $uri, etc.)
|
# Only substitute BASE_PATH — preserve nginx variables ($host, $uri, etc.)
|
||||||
|
|||||||
@@ -114,8 +114,8 @@ SDK stabilization, and the first rebuilt extension (tasks).
|
|||||||
|
|
||||||
| Step | Status | Description |
|
| Step | Status | Description |
|
||||||
|------|--------|-------------|
|
|------|--------|-------------|
|
||||||
| Retire builtin seeder | ⬚ | Stop auto-seeding chat-centric extensions (csv-table, diff-viewer, js-sandbox, katex-renderer, mermaid-renderer, regex-tester). Remove `SeedBuiltinPackages`, Dockerfile COPY, and `extensions/builtin/` directory. These extensions are not OBE — they're dormant until a chat surface exists to consume them. |
|
| Retire builtin seeder | ✅ | Removed `SeedBuiltinPackages`, `seed_packages.go`, Dockerfile COPY, and `extensions/builtin/` directory. These extensions are dormant until a chat surface exists to consume them. |
|
||||||
| Convert to regular packages | ⬚ | Repackage the 6 extensions as standard `.pkg` archives in `packages/`. Add chat dependency metadata to manifests so they can be installed when the chat extension ships post-MVP. No auto-install — explicit install only, matching the distribution model. |
|
| Convert to regular packages | ✅ | Repackaged 6 extensions as standard directories in `packages/` with `js/` layout and `"requires": ["chat"]` manifest metadata. Built via `build.sh` like all other packages. No auto-install — explicit install only. |
|
||||||
|
|
||||||
## v0.3.x — Workflow Architecture
|
## v0.3.x — Workflow Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
"id": "csv-table",
|
"id": "csv-table",
|
||||||
"name": "CSV Table Viewer",
|
"name": "CSV Table Viewer",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "switchboard",
|
||||||
"description": "Renders ```csv code blocks as sortable, interactive HTML tables",
|
"description": "Renders ```csv code blocks as sortable, interactive HTML tables",
|
||||||
|
"requires": ["chat"],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"tools": [],
|
"tools": [],
|
||||||
"surfaces": [],
|
"surfaces": [],
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
"id": "diff-viewer",
|
"id": "diff-viewer",
|
||||||
"name": "Diff Viewer",
|
"name": "Diff Viewer",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "switchboard",
|
||||||
"description": "Renders ```diff code blocks with syntax-highlighted additions, deletions, and context lines",
|
"description": "Renders ```diff code blocks with syntax-highlighted additions, deletions, and context lines",
|
||||||
|
"requires": ["chat"],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"tools": [],
|
"tools": [],
|
||||||
"surfaces": [],
|
"surfaces": [],
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
"id": "js-sandbox",
|
"id": "js-sandbox",
|
||||||
"name": "JavaScript Sandbox",
|
"name": "JavaScript Sandbox",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "switchboard",
|
||||||
"description": "Executes JavaScript in a sandboxed iframe. Allows the LLM to run code, verify calculations, and transform data — no server compute required.",
|
"description": "Executes JavaScript in a sandboxed iframe. Allows the LLM to run code, verify calculations, and transform data — no server compute required.",
|
||||||
|
"requires": ["chat"],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"tools": [
|
"tools": [
|
||||||
{
|
{
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
"id": "katex-renderer",
|
"id": "katex-renderer",
|
||||||
"name": "KaTeX Math",
|
"name": "KaTeX Math",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "switchboard",
|
||||||
"description": "Renders LaTeX math expressions: ```latex blocks and inline $...$ / $$...$$ syntax",
|
"description": "Renders LaTeX math expressions: ```latex blocks and inline $...$ / $$...$$ syntax",
|
||||||
|
"requires": ["chat"],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"tools": [],
|
"tools": [],
|
||||||
"surfaces": [],
|
"surfaces": [],
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
"id": "mermaid-renderer",
|
"id": "mermaid-renderer",
|
||||||
"name": "Mermaid Diagrams",
|
"name": "Mermaid Diagrams",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "switchboard",
|
||||||
"description": "Renders ```mermaid code blocks as interactive SVG diagrams with zoom/pan, SVG/PNG export, and source copy",
|
"description": "Renders ```mermaid code blocks as interactive SVG diagrams with zoom/pan, SVG/PNG export, and source copy",
|
||||||
|
"requires": ["chat"],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"tools": [],
|
"tools": [],
|
||||||
"surfaces": [],
|
"surfaces": [],
|
||||||
@@ -2,9 +2,11 @@
|
|||||||
"id": "regex-tester",
|
"id": "regex-tester",
|
||||||
"name": "Regex Tester",
|
"name": "Regex Tester",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "switchboard",
|
||||||
"description": "Tests regular expressions against input strings. Allows the LLM to verify regex patterns and see matches, groups, and indices.",
|
"description": "Tests regular expressions against input strings. Allows the LLM to verify regex patterns and see matches, groups, and indices.",
|
||||||
|
"requires": ["chat"],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"tools": [
|
"tools": [
|
||||||
{
|
{
|
||||||
@@ -5,9 +5,7 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"testing"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
@@ -806,219 +804,3 @@ func TestExtension_StoreDeleteUserSettings(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════
|
|
||||||
// Seed Logic (F26/F27)
|
|
||||||
// ═══════════════════════════════════════════════
|
|
||||||
|
|
||||||
// makeSeedDir creates a temp directory with extension subdirectories.
|
|
||||||
// Each entry in exts maps a dir name to {manifest, script}.
|
|
||||||
func makeSeedDir(t *testing.T, exts map[string]struct{ manifest, script string }) string {
|
|
||||||
t.Helper()
|
|
||||||
dir := t.TempDir()
|
|
||||||
for name, files := range exts {
|
|
||||||
extDir := filepath.Join(dir, name)
|
|
||||||
if err := os.MkdirAll(extDir, 0755); err != nil {
|
|
||||||
t.Fatalf("mkdir %s: %v", extDir, err)
|
|
||||||
}
|
|
||||||
if files.manifest != "" {
|
|
||||||
if err := os.WriteFile(filepath.Join(extDir, "manifest.json"), []byte(files.manifest), 0644); err != nil {
|
|
||||||
t.Fatalf("write manifest: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if files.script != "" {
|
|
||||||
if err := os.WriteFile(filepath.Join(extDir, "script.js"), []byte(files.script), 0644); err != nil {
|
|
||||||
t.Fatalf("write script: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeed_NewExtension(t *testing.T) {
|
|
||||||
h := setupExtensionHarness(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
dir := makeSeedDir(t, map[string]struct{ manifest, script string }{
|
|
||||||
"test-seeded": {
|
|
||||||
manifest: `{"id":"test-seeded","name":"Seeded Extension","version":"1.0.0","tier":"browser","author":"test"}`,
|
|
||||||
script: `console.log("seeded");`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
SeedBuiltinPackages(h.stores, dir)
|
|
||||||
|
|
||||||
// Should be created as system extension
|
|
||||||
pkg, err := h.stores.Packages.Get(ctx, "test-seeded")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("seeded extension not found: %v", err)
|
|
||||||
}
|
|
||||||
if pkg.Title != "Seeded Extension" {
|
|
||||||
t.Fatalf("name: got %q", pkg.Title)
|
|
||||||
}
|
|
||||||
if pkg.Version != "1.0.0" {
|
|
||||||
t.Fatalf("version: got %q", pkg.Version)
|
|
||||||
}
|
|
||||||
if !pkg.IsSystem {
|
|
||||||
t.Fatal("should be system extension")
|
|
||||||
}
|
|
||||||
if !pkg.Enabled {
|
|
||||||
t.Fatal("should be enabled")
|
|
||||||
}
|
|
||||||
// Verify _script is inlined in manifest
|
|
||||||
if _, ok := pkg.Manifest["_script"]; !ok {
|
|
||||||
t.Fatal("manifest should contain _script")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeed_SameVersionSkips(t *testing.T) {
|
|
||||||
h := setupExtensionHarness(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
dir := makeSeedDir(t, map[string]struct{ manifest, script string }{
|
|
||||||
"skip-ext": {
|
|
||||||
manifest: `{"id":"skip-ext","name":"Skip Test","version":"1.0.0"}`,
|
|
||||||
script: `console.log("v1");`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Seed twice
|
|
||||||
SeedBuiltinPackages(h.stores, dir)
|
|
||||||
SeedBuiltinPackages(h.stores, dir)
|
|
||||||
|
|
||||||
// Should still have exactly one
|
|
||||||
pkgs, err := h.stores.Packages.List(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("list: %v", err)
|
|
||||||
}
|
|
||||||
count := 0
|
|
||||||
for _, p := range pkgs {
|
|
||||||
if p.ID == "skip-ext" {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if count != 1 {
|
|
||||||
t.Fatalf("expected 1 skip-ext, got %d", count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeed_VersionChangeUpdates(t *testing.T) {
|
|
||||||
h := setupExtensionHarness(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Seed v1
|
|
||||||
dir1 := makeSeedDir(t, map[string]struct{ manifest, script string }{
|
|
||||||
"versioned-ext": {
|
|
||||||
manifest: `{"id":"versioned-ext","name":"V1 Name","version":"1.0.0","author":"original"}`,
|
|
||||||
script: `console.log("v1");`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
SeedBuiltinPackages(h.stores, dir1)
|
|
||||||
|
|
||||||
pkg, _ := h.stores.Packages.Get(ctx, "versioned-ext")
|
|
||||||
if pkg.Version != "1.0.0" {
|
|
||||||
t.Fatalf("v1 version: got %q", pkg.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed v2 (different version triggers update)
|
|
||||||
dir2 := makeSeedDir(t, map[string]struct{ manifest, script string }{
|
|
||||||
"versioned-ext": {
|
|
||||||
manifest: `{"id":"versioned-ext","name":"V2 Name","version":"2.0.0","author":"updated"}`,
|
|
||||||
script: `console.log("v2");`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
SeedBuiltinPackages(h.stores, dir2)
|
|
||||||
|
|
||||||
pkg, _ = h.stores.Packages.Get(ctx, "versioned-ext")
|
|
||||||
if pkg.Version != "2.0.0" {
|
|
||||||
t.Fatalf("v2 version: got %q", pkg.Version)
|
|
||||||
}
|
|
||||||
if pkg.Title != "V2 Name" {
|
|
||||||
t.Fatalf("v2 name: got %q", pkg.Title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeed_MissingManifestSkips(t *testing.T) {
|
|
||||||
h := setupExtensionHarness(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Directory with no manifest.json
|
|
||||||
dir := makeSeedDir(t, map[string]struct{ manifest, script string }{
|
|
||||||
"no-manifest": {script: `console.log("orphan");`},
|
|
||||||
})
|
|
||||||
|
|
||||||
SeedBuiltinPackages(h.stores, dir)
|
|
||||||
|
|
||||||
pkgs, err := h.stores.Packages.List(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("list: %v", err)
|
|
||||||
}
|
|
||||||
if len(pkgs) != 0 {
|
|
||||||
t.Fatalf("should skip dir without manifest, got %d extensions", len(pkgs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeed_InvalidManifestSkips(t *testing.T) {
|
|
||||||
h := setupExtensionHarness(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
dir := makeSeedDir(t, map[string]struct{ manifest, script string }{
|
|
||||||
"bad-manifest": {manifest: `{not valid json`, script: `console.log("bad");`},
|
|
||||||
})
|
|
||||||
|
|
||||||
SeedBuiltinPackages(h.stores, dir)
|
|
||||||
|
|
||||||
pkgs, _ := h.stores.Packages.List(ctx)
|
|
||||||
if len(pkgs) != 0 {
|
|
||||||
t.Fatalf("should skip invalid manifest, got %d extensions", len(pkgs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeed_MissingIDSkips(t *testing.T) {
|
|
||||||
h := setupExtensionHarness(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Valid JSON but no "id" field
|
|
||||||
dir := makeSeedDir(t, map[string]struct{ manifest, script string }{
|
|
||||||
"no-id": {manifest: `{"name":"No ID Extension","version":"1.0.0"}`, script: `console.log("no id");`},
|
|
||||||
})
|
|
||||||
|
|
||||||
SeedBuiltinPackages(h.stores, dir)
|
|
||||||
|
|
||||||
pkgs, _ := h.stores.Packages.List(ctx)
|
|
||||||
if len(pkgs) != 0 {
|
|
||||||
t.Fatalf("should skip manifest without id, got %d extensions", len(pkgs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeed_NonexistentDirectorySkips(t *testing.T) {
|
|
||||||
h := setupExtensionHarness(t)
|
|
||||||
|
|
||||||
// Should not panic on missing directory
|
|
||||||
SeedBuiltinPackages(h.stores, "/nonexistent/path/extensions")
|
|
||||||
|
|
||||||
pkgs, _ := h.stores.Packages.List(context.Background())
|
|
||||||
if len(pkgs) != 0 {
|
|
||||||
t.Fatalf("should seed nothing from missing dir, got %d", len(pkgs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSeed_ScriptOptional(t *testing.T) {
|
|
||||||
h := setupExtensionHarness(t)
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Manifest-only extension (no script.js)
|
|
||||||
dir := makeSeedDir(t, map[string]struct{ manifest, script string }{
|
|
||||||
"manifest-only": {manifest: `{"id":"manifest-only","name":"Manifest Only","version":"1.0.0"}`},
|
|
||||||
})
|
|
||||||
|
|
||||||
SeedBuiltinPackages(h.stores, dir)
|
|
||||||
|
|
||||||
pkg, err := h.stores.Packages.Get(ctx, "manifest-only")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("should create manifest-only ext: %v", err)
|
|
||||||
}
|
|
||||||
// Manifest should NOT contain _script
|
|
||||||
if _, ok := pkg.Manifest["_script"]; ok {
|
|
||||||
t.Fatal("manifest should not contain _script when no script.js exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"switchboard-core/store"
|
|
||||||
)
|
|
||||||
|
|
||||||
// builtinManifest is the on-disk manifest.json shape.
|
|
||||||
type builtinManifest struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Tier string `json:"tier"`
|
|
||||||
Author string `json:"author"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Permissions json.RawMessage `json:"permissions"`
|
|
||||||
Tools json.RawMessage `json:"tools"`
|
|
||||||
Surfaces json.RawMessage `json:"surfaces"`
|
|
||||||
Settings json.RawMessage `json:"settings"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SeedBuiltinPackages scans a directory for builtin extension bundles
|
|
||||||
// (each subdirectory contains manifest.json + script.js) and upserts them
|
|
||||||
// into the packages table as system packages.
|
|
||||||
//
|
|
||||||
// Layout:
|
|
||||||
//
|
|
||||||
// extensions/builtin/
|
|
||||||
// mermaid-renderer/
|
|
||||||
// manifest.json
|
|
||||||
// script.js
|
|
||||||
//
|
|
||||||
// Idempotent: skips packages whose version matches what's already installed,
|
|
||||||
// updates those with a newer version, creates new ones.
|
|
||||||
func SeedBuiltinPackages(stores store.Stores, extensionsDir string) {
|
|
||||||
if stores.Packages == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
entries, err := os.ReadDir(extensionsDir)
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
log.Printf(" 📦 No builtin extensions directory at %s (skipping)", extensionsDir)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Printf("⚠ Failed to read extensions directory %s: %v", extensionsDir, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
seeded, updated, skipped := 0, 0, 0
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !entry.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
extDir := filepath.Join(extensionsDir, entry.Name())
|
|
||||||
manifestPath := filepath.Join(extDir, "manifest.json")
|
|
||||||
scriptPath := filepath.Join(extDir, "script.js")
|
|
||||||
|
|
||||||
// Read manifest
|
|
||||||
manifestData, err := os.ReadFile(manifestPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("⚠ Skipping %s: no manifest.json: %v", entry.Name(), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var manifest builtinManifest
|
|
||||||
if err := json.Unmarshal(manifestData, &manifest); err != nil {
|
|
||||||
log.Printf("⚠ Skipping %s: invalid manifest.json: %v", entry.Name(), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if manifest.ID == "" {
|
|
||||||
log.Printf("⚠ Skipping %s: manifest missing 'id'", entry.Name())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read script (optional — some extensions might be manifest-only)
|
|
||||||
var script string
|
|
||||||
if scriptData, err := os.ReadFile(scriptPath); err == nil {
|
|
||||||
script = string(scriptData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the full manifest with _script inlined
|
|
||||||
fullManifest := buildFullManifest(manifestData, script)
|
|
||||||
|
|
||||||
// Parse into map for Seed/Update
|
|
||||||
var manifestMap map[string]any
|
|
||||||
json.Unmarshal(fullManifest, &manifestMap)
|
|
||||||
|
|
||||||
// Check if already installed
|
|
||||||
existing, err := stores.Packages.Get(ctx, manifest.ID)
|
|
||||||
if err == nil && existing != nil {
|
|
||||||
// Resolve tier for fixup + update paths
|
|
||||||
tier := manifest.Tier
|
|
||||||
if tier == "" {
|
|
||||||
tier = "browser"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same version — skip, but fixup type/tier if wrong
|
|
||||||
// (v0.28.7 initial seed created builtins with type='surface'
|
|
||||||
// because Seed() uses schema DEFAULT and Update() didn't set type)
|
|
||||||
if existing.Version == manifest.Version {
|
|
||||||
if existing.Type != "extension" || existing.Tier != tier {
|
|
||||||
pkg := &store.PackageRegistration{
|
|
||||||
Title: existing.Title, Type: "extension", Version: existing.Version,
|
|
||||||
Description: existing.Description, Author: existing.Author,
|
|
||||||
Tier: tier, Manifest: existing.Manifest,
|
|
||||||
IsSystem: true, Enabled: existing.Enabled,
|
|
||||||
}
|
|
||||||
if err := stores.Packages.Update(ctx, manifest.ID, pkg); err != nil {
|
|
||||||
log.Printf("⚠ Failed to fixup builtin package %s: %v", manifest.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
skipped++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Version changed — update
|
|
||||||
pkg := &store.PackageRegistration{
|
|
||||||
Title: manifest.Name,
|
|
||||||
Type: "extension",
|
|
||||||
Version: manifest.Version,
|
|
||||||
Description: manifest.Description,
|
|
||||||
Author: manifest.Author,
|
|
||||||
Tier: tier,
|
|
||||||
Manifest: manifestMap,
|
|
||||||
IsSystem: true,
|
|
||||||
Enabled: true,
|
|
||||||
}
|
|
||||||
if err := stores.Packages.Update(ctx, manifest.ID, pkg); err != nil {
|
|
||||||
log.Printf("⚠ Failed to update builtin package %s: %v", manifest.ID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Sync triggers on version update (v0.2.2)
|
|
||||||
SyncManifestTriggers(ctx, stores, nil, manifest.ID, manifestMap)
|
|
||||||
updated++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// New package — seed with full metadata
|
|
||||||
if err := stores.Packages.Seed(ctx, manifest.ID, manifest.Name, "builtin", manifestMap); err != nil {
|
|
||||||
log.Printf("⚠ Failed to seed builtin package %s: %v", manifest.ID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set extension-specific fields that Seed doesn't cover
|
|
||||||
tier := manifest.Tier
|
|
||||||
if tier == "" {
|
|
||||||
tier = "browser"
|
|
||||||
}
|
|
||||||
pkg := &store.PackageRegistration{
|
|
||||||
Title: manifest.Name,
|
|
||||||
Type: "extension",
|
|
||||||
Version: manifest.Version,
|
|
||||||
Description: manifest.Description,
|
|
||||||
Author: manifest.Author,
|
|
||||||
Tier: tier,
|
|
||||||
Manifest: manifestMap,
|
|
||||||
IsSystem: true,
|
|
||||||
Enabled: true,
|
|
||||||
}
|
|
||||||
if err := stores.Packages.Update(ctx, manifest.ID, pkg); err != nil {
|
|
||||||
log.Printf("⚠ Failed to update builtin package metadata %s: %v", manifest.ID, err)
|
|
||||||
}
|
|
||||||
// Sync triggers from manifest (v0.2.2)
|
|
||||||
SyncManifestTriggers(ctx, stores, nil, manifest.ID, manifestMap)
|
|
||||||
|
|
||||||
seeded++
|
|
||||||
}
|
|
||||||
|
|
||||||
if seeded+updated > 0 {
|
|
||||||
log.Printf(" 📦 Builtin packages: %d seeded, %d updated, %d unchanged", seeded, updated, skipped)
|
|
||||||
} else if skipped > 0 {
|
|
||||||
log.Printf(" 📦 Builtin packages: %d up to date", skipped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildFullManifest merges the raw manifest JSON with an inline _script field.
|
|
||||||
func buildFullManifest(rawManifest []byte, script string) json.RawMessage {
|
|
||||||
if script == "" {
|
|
||||||
return json.RawMessage(rawManifest)
|
|
||||||
}
|
|
||||||
|
|
||||||
var m map[string]json.RawMessage
|
|
||||||
if err := json.Unmarshal(rawManifest, &m); err != nil {
|
|
||||||
return json.RawMessage(rawManifest)
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptJSON, err := json.Marshal(script)
|
|
||||||
if err != nil {
|
|
||||||
return json.RawMessage(rawManifest)
|
|
||||||
}
|
|
||||||
|
|
||||||
m["_script"] = json.RawMessage(scriptJSON)
|
|
||||||
result, err := json.Marshal(m)
|
|
||||||
if err != nil {
|
|
||||||
return json.RawMessage(rawManifest)
|
|
||||||
}
|
|
||||||
|
|
||||||
return json.RawMessage(result)
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,7 @@ type manifestTrigger struct {
|
|||||||
// manifest (declarative sync). For webhooks, generates an HMAC secret on
|
// manifest (declarative sync). For webhooks, generates an HMAC secret on
|
||||||
// first create.
|
// first create.
|
||||||
//
|
//
|
||||||
// Called from SeedBuiltinPackages, admin install, and package install.
|
// Called from admin install and package install.
|
||||||
func SyncManifestTriggers(ctx context.Context, stores store.Stores, engine *triggers.Engine, pkgID string, manifest map[string]any) {
|
func SyncManifestTriggers(ctx context.Context, stores store.Stores, engine *triggers.Engine, pkgID string, manifest map[string]any) {
|
||||||
if stores.Triggers == nil {
|
if stores.Triggers == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -116,8 +116,6 @@ func main() {
|
|||||||
// Seed additional users from env (dev/test only, skipped in production)
|
// Seed additional users from env (dev/test only, skipped in production)
|
||||||
handlers.SeedUsers(cfg, stores, uekCache)
|
handlers.SeedUsers(cfg, stores, uekCache)
|
||||||
|
|
||||||
// Seed builtin extensions from disk (idempotent, version-aware)
|
|
||||||
handlers.SeedBuiltinPackages(stores, "extensions/builtin")
|
|
||||||
|
|
||||||
}
|
}
|
||||||
defer database.Close()
|
defer database.Close()
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ type PackageStore interface {
|
|||||||
|
|
||||||
// Seed upserts a package at startup. Does NOT overwrite the enabled
|
// Seed upserts a package at startup. Does NOT overwrite the enabled
|
||||||
// flag if the row already exists (admin toggle survives restarts).
|
// flag if the row already exists (admin toggle survives restarts).
|
||||||
// Called by the page engine for core surfaces and by
|
// Called by the page engine for core surfaces.
|
||||||
// SeedBuiltinPackages() for builtin extensions.
|
|
||||||
Seed(ctx context.Context, id, title, source string, manifest map[string]any) error
|
Seed(ctx context.Context, id, title, source string, manifest map[string]any) error
|
||||||
|
|
||||||
// List returns all registered packages, ordered by source then title.
|
// List returns all registered packages, ordered by source then title.
|
||||||
@@ -39,7 +38,7 @@ type PackageStore interface {
|
|||||||
// ── Extended lifecycle ───────────────────────────────
|
// ── Extended lifecycle ───────────────────────────────
|
||||||
|
|
||||||
// Create inserts a new package with all fields. Used by the install
|
// Create inserts a new package with all fields. Used by the install
|
||||||
// handler and by SeedBuiltinPackages() for first-time seeding.
|
// handler for .pkg uploads.
|
||||||
Create(ctx context.Context, pkg *PackageRegistration) error
|
Create(ctx context.Context, pkg *PackageRegistration) error
|
||||||
|
|
||||||
// Update modifies mutable fields on an existing package.
|
// Update modifies mutable fields on an existing package.
|
||||||
|
|||||||
Reference in New Issue
Block a user