This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/handlers/seed_packages.go
Jeffrey Smith 11fd8c1e57 step 1: rename module chat-switchboard → switchboard-core
- go.mod module name
- All 714 import references across 289 Go files
- VERSION: 0.1.0
- CI DB names: switchboard_core_{ci,dev,test}
- Docker image: gobha/switchboard-core
- Test fixtures: JWT issuer, repo names
- .env.example, docker-compose container name
- Compiles clean (go build exit 0)
2026-03-25 19:48:04 -04:00

204 lines
5.8 KiB
Go

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
}
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)
}
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)
}