Changeset 0.28.7 (#193)
This commit is contained in:
203
server/handlers/seed_packages.go
Normal file
203
server/handlers/seed_packages.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.gobha.me/xcaliber/chat-switchboard/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)
|
||||
}
|
||||
Reference in New Issue
Block a user