Files
scarf/scripts/catalog.sh
Alan Wizemann 11732baa3c feat(catalog): stdlib-only Python validator + regenerator for templates/
Adds the catalog pipeline without introducing any external dependencies.
tools/build-catalog.py walks templates/<author>/<name>/, validates every
shipped .scarftemplate against its manifest (same invariants Swift's
ProjectTemplateService.verifyClaims enforces at install time), and emits
templates/catalog.json for the frontend to read.

Validator invariants:
- Required bundle files: template.json, README.md, AGENTS.md, dashboard.json
- contents claim cross-checked against actual zip entries (instructions,
  skills, cron count, memory appendix)
- dashboard.json widget types restricted to the vocabulary the Swift
  renderer knows
- Manifest id author component must match the template directory
- 5 MB bundle-size cap on submissions (installer's own cap is 50 MB)
- High-confidence secret patterns (private keys, GitHub PATs, Slack tokens,
  AWS access keys, OpenAI/Anthropic keys) block the bundle
- staging/ source tree must match the built bundle byte-for-byte — catches
  the common failure mode of editing staging/ but forgetting to rebuild

scripts/catalog.sh wraps the Python script with check/build/preview/serve/
publish subcommands, mirroring the scripts/wiki.sh shape. publish adds a
second-pass hard-pattern secret scan on the rendered gh-pages output so
template prose can't leak credentials even if the Python scan missed them.

tools/test_build_catalog.py has 14 unit tests covering the main validator
paths (minimal-valid, missing-AGENTS, content-claim mismatch, author
mismatch, oversized bundle, unknown widget type, secret detection,
staging-drift detection, missing bundle, catalog.json shape, and a real-
bundle end-to-end check against templates/awizemann/site-status-checker).
Python 3.9 compatible (Xcode's bundled python3), so no runtime needs
installing.

templates/catalog.json committed as the first generated aggregate index;
maintainers regenerate on merge by running `./scripts/catalog.sh build`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:35:46 +02:00

136 lines
4.7 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Scarf templates catalog helper — runs the Python validator, renders the
# static site into .gh-pages-worktree/templates/, and (on `publish`)
# commits + pushes that subdir on the gh-pages branch.
#
# Usage:
# ./scripts/catalog.sh check # validate every template; no output
# ./scripts/catalog.sh build # validate + write templates/catalog.json + .gh-pages-worktree/templates/
# ./scripts/catalog.sh preview [DIR] # render self-contained preview; DIR defaults to /tmp/scarf-catalog-preview
# ./scripts/catalog.sh publish # secret-scan + commit + push gh-pages (templates subdir only)
# ./scripts/catalog.sh serve [PORT] # serve .gh-pages-worktree/ on localhost:PORT (default 8000)
# ./scripts/catalog.sh --help # this help
#
# The secret-scan runs BEFORE publish and inspects the generated
# .gh-pages-worktree/templates/ tree — same hard-pattern regex as
# scripts/wiki.sh so template README/AGENTS content that accidentally
# leaks credentials gets blocked before it reaches the public site.
#
# Bootstrap (one-time): requires a .gh-pages-worktree/ clone of the
# gh-pages branch. The release script (scripts/release.sh) creates it on
# first use. If it's missing:
# git worktree add .gh-pages-worktree gh-pages
#
# Recovery: if .gh-pages-worktree/ is deleted, re-run the command above.
set -euo pipefail
# ---------- config ----------
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GHPAGES_DIR="$REPO_ROOT/.gh-pages-worktree"
CATALOG_SUBDIR="templates"
PY="${PYTHON:-python3}"
BUILDER="$REPO_ROOT/tools/build-catalog.py"
# ---------- helpers (same shape as scripts/wiki.sh so a reader doesn't
# have to learn two conventions) ----------
log() { printf '\033[1;34m==> %s\033[0m\n' "$*"; }
warn() { printf '\033[1;33m[WARN] %s\033[0m\n' "$*" >&2; }
die() { printf '\033[1;31m[ERR] %s\033[0m\n' "$*" >&2; exit 1; }
need_builder() {
[[ -f "$BUILDER" ]] || die "missing $BUILDER"
command -v "$PY" >/dev/null 2>&1 || die "python3 not found (set \$PYTHON if needed)"
}
need_ghpages() {
[[ -d "$GHPAGES_DIR/.git" ]] || die "no gh-pages worktree at $GHPAGES_DIR
Run: git worktree add .gh-pages-worktree gh-pages"
}
# ---------- secret-scan (mirrors scripts/wiki.sh hard-pattern set) ----------
hard_regex='(sk-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{30,}|ghs_[A-Za-z0-9]{30,}|ghu_[A-Za-z0-9]{30,}|gho_[A-Za-z0-9]{30,}|ghr_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|-----BEGIN [A-Z ]*PRIVATE KEY-----|BEGIN OPENSSH PRIVATE KEY)'
scan_hard_ghpages() {
# Scan the generated output, NOT the repo source — the validator
# already scans bundle contents. This pass catches anything that leaked
# through template.json fields or README prose.
local hits
hits="$(grep -rInE --exclude-dir=.git "$hard_regex" "$GHPAGES_DIR/$CATALOG_SUBDIR" 2>/dev/null || true)"
if [[ -n "$hits" ]]; then
printf '%s\n' "$hits" >&2
die "hard-pattern secret match in rendered site — refusing to publish."
fi
}
# ---------- commands ----------
cmd_check() {
need_builder
"$PY" "$BUILDER" --check --repo "$REPO_ROOT"
}
cmd_build() {
need_builder
"$PY" "$BUILDER" --build --repo "$REPO_ROOT"
}
cmd_preview() {
need_builder
local dir="${1:-/tmp/scarf-catalog-preview}"
rm -rf "$dir"
mkdir -p "$dir"
"$PY" "$BUILDER" --preview "$dir" --repo "$REPO_ROOT"
log "Preview rendered to $dir"
log "Serve with: (cd $dir && python3 -m http.server 8000) then open http://localhost:8000/"
}
cmd_serve() {
need_ghpages
local port="${1:-8000}"
log "Serving $GHPAGES_DIR on http://localhost:$port/"
(cd "$GHPAGES_DIR" && "$PY" -m http.server "$port")
}
cmd_publish() {
need_builder
need_ghpages
log "Validating"
"$PY" "$BUILDER" --check --repo "$REPO_ROOT"
log "Building"
"$PY" "$BUILDER" --build --repo "$REPO_ROOT"
log "Secret-scanning rendered site"
scan_hard_ghpages
log "Staging + committing gh-pages"
(cd "$GHPAGES_DIR" && git add "$CATALOG_SUBDIR")
if (cd "$GHPAGES_DIR" && git diff --cached --quiet); then
log "No changes to publish."
return 0
fi
local msg
msg="catalog: rebuild at $(date -u +%Y-%m-%dT%H:%M:%SZ)"
(cd "$GHPAGES_DIR" && git commit -m "$msg")
log "Pushing gh-pages"
(cd "$GHPAGES_DIR" && git push origin gh-pages)
log "Published."
}
cmd_help() {
sed -n '1,30p' "$0" | sed -n '/^# Usage/,/^#$/p'
}
# ---------- dispatch ----------
sub="${1:-help}"
shift || true
case "$sub" in
check) cmd_check "$@" ;;
build) cmd_build "$@" ;;
preview) cmd_preview "$@" ;;
serve) cmd_serve "$@" ;;
publish) cmd_publish "$@" ;;
help|--help|-h) cmd_help ;;
*) die "unknown command: $sub (try --help)" ;;
esac