From 09bd45adad698be6627b06875b21e3b5d4fcfdc3 Mon Sep 17 00:00:00 2001 From: Jeffrey Smith Date: Tue, 31 Mar 2026 12:14:12 +0000 Subject: [PATCH] feat(registry): add generate-registry.sh script and docs Shell script reads .pkg ZIPs, extracts manifests, and emits a registry.json for self-hosted package discovery. Docs cover the JSON format, admin config, and self-hosting setup. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/PACKAGE-REGISTRY.md | 94 ++++++++++++++++++++++++++++++++++++ scripts/generate-registry.sh | 81 +++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 docs/PACKAGE-REGISTRY.md create mode 100755 scripts/generate-registry.sh diff --git a/docs/PACKAGE-REGISTRY.md b/docs/PACKAGE-REGISTRY.md new file mode 100644 index 0000000..ade0005 --- /dev/null +++ b/docs/PACKAGE-REGISTRY.md @@ -0,0 +1,94 @@ +# Package Registry + +The package registry lets admins browse and install packages from an external +JSON index — a lightweight alternative to manually uploading `.pkg` files. + +## Registry JSON Format + +The registry is a static JSON file matching the `RegistryResponse` struct: + +```json +{ + "packages": [ + { + "id": "notes", + "title": "Notes", + "version": "0.8.0", + "description": "Markdown notes with backlinks and graph view", + "author": "switchboard", + "type": "extension", + "tier": "core", + "download_url": "https://cdn.example.com/pkg/notes.pkg", + "size": 48200, + "updated_at": "2026-03-20T00:00:00Z" + } + ] +} +``` + +### Required Fields + +| Field | Description | +|----------------|------------------------------------------------| +| `id` | Package identifier (matches `manifest.json`) | +| `title` | Display name | +| `version` | Semver version string | +| `description` | Short description | +| `download_url` | HTTPS URL to the `.pkg` file (must be HTTPS) | + +### Optional Fields + +| Field | Description | +|--------------|------------------------------------------| +| `author` | Package author | +| `type` | `extension` or `library` | +| `tier` | `core`, `official`, or `community` | +| `size` | File size in bytes | +| `updated_at` | ISO 8601 timestamp of last update | + +## Configuring the Registry URL + +### Via Admin UI + +Navigate to **Admin > Settings > Package Registry** and enter the registry URL. + +### Via API + +```bash +curl -X PUT /api/v1/admin/settings/package_registry \ + -H "Content-Type: application/json" \ + -d '{"value": {"url": "https://cdn.example.com/pkg/registry.json"}}' +``` + +## Generating a Registry + +Use `scripts/generate-registry.sh` to build a `registry.json` from a +directory of `.pkg` files: + +```bash +# Default: reads dist/, uses placeholder base URL +./scripts/generate-registry.sh + +# Custom directory and base URL +./scripts/generate-registry.sh ./my-packages https://cdn.example.com/pkg > registry.json +``` + +Requirements: `jq`, `unzip`, and `stat` (GNU or BSD). + +## Self-Hosting + +Any HTTPS-capable file server works. Upload your `.pkg` files and the +generated `registry.json` to the same directory, then point the admin +setting to the `registry.json` URL. + +Example with a static file server: + +``` +/var/www/packages/ + registry.json + notes.pkg + chat.pkg + chat-core.pkg +``` + +The registry is fetched and cached for 5 minutes on each browse request. diff --git a/scripts/generate-registry.sh b/scripts/generate-registry.sh new file mode 100755 index 0000000..0ad3072 --- /dev/null +++ b/scripts/generate-registry.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# generate-registry.sh — Build a registry.json from a directory of .pkg files. +# +# Usage: +# ./scripts/generate-registry.sh [PKG_DIR] [BASE_URL] +# +# Arguments: +# PKG_DIR Directory containing .pkg files (default: dist/) +# BASE_URL HTTPS base URL for download links (default: https://example.com/packages) +# +# Output: +# Writes registry.json to stdout. Redirect to a file: +# ./scripts/generate-registry.sh dist/ https://cdn.example.com/pkg > registry.json + +set -euo pipefail + +PKG_DIR="${1:-dist}" +BASE_URL="${2:-https://example.com/packages}" + +# Strip trailing slash +BASE_URL="${BASE_URL%/}" + +if [ ! -d "$PKG_DIR" ]; then + echo "Error: directory '$PKG_DIR' not found" >&2 + exit 1 +fi + +# Collect entries +entries=() + +for pkg in "$PKG_DIR"/*.pkg; do + [ -f "$pkg" ] || continue + + # Extract manifest.json from the ZIP archive + manifest=$(unzip -p "$pkg" manifest.json 2>/dev/null) || { + echo "Warning: skipping $pkg (no manifest.json)" >&2 + continue + } + + # Read fields from manifest + id=$(echo "$manifest" | jq -r '.id // empty') + title=$(echo "$manifest" | jq -r '.title // .id') + version=$(echo "$manifest" | jq -r '.version // "0.0.0"') + description=$(echo "$manifest" | jq -r '.description // ""') + author=$(echo "$manifest" | jq -r '.author // ""') + type=$(echo "$manifest" | jq -r '.type // "extension"') + tier=$(echo "$manifest" | jq -r '.tier // "community"') + + if [ -z "$id" ]; then + echo "Warning: skipping $pkg (no id in manifest)" >&2 + continue + fi + + filename=$(basename "$pkg") + size=$(stat -c%s "$pkg" 2>/dev/null || stat -f%z "$pkg" 2>/dev/null || echo 0) + updated_at=$(date -r "$pkg" -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ") + + entry=$(jq -n \ + --arg id "$id" \ + --arg title "$title" \ + --arg version "$version" \ + --arg description "$description" \ + --arg author "$author" \ + --arg type "$type" \ + --arg tier "$tier" \ + --arg download_url "$BASE_URL/$filename" \ + --argjson size "$size" \ + --arg updated_at "$updated_at" \ + '{id: $id, title: $title, version: $version, description: $description, + author: $author, type: $type, tier: $tier, download_url: $download_url, + size: $size, updated_at: $updated_at}') + + entries+=("$entry") +done + +# Build final JSON +if [ ${#entries[@]} -eq 0 ]; then + echo '{"packages": []}' | jq . +else + printf '%s\n' "${entries[@]}" | jq -s '{packages: .}' +fi