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