# shellcheck shell=bash # lib-dev-hub-datasette.sh — install Datasette (SQLite web UI) via pipx + launchd. # # Datasette serves any SQLite file as an interactive web UI + JSON API. # We install via pipx (isolated venv, no system Python pollution) and run as # a user-level launchd agent on http://127.0.0.1:8001/. # # READ-ONLY by design: every DB is opened with `--immutable` so a stray UI # action cannot mutate the user's project databases. # # Architecture: # plist (datasette.plist.tmpl) → ${KIT}/dev-hub/datasette-serve.sh wrapper # wrapper discovers SQLite DBs at launch time → exec datasette serve # This way newly-added DBs are picked up on next launchctl kickstart, with # no plist rewrite needed. # # Sources: lib-log.sh (say/warn/err), lib-launchd.sh (install_service/unload_plist). # Globals read: $KIT_DIR, $HOME_DIR. # ---------- helpers (private) ---------- # Verify Python 3.11+. Returns 0 on OK, 1 on missing/old. _datasette_check_python() { if ! command -v python3 >/dev/null 2>&1; then err "python3 not found — install via: brew install python@3.11" return 1 fi local ver ver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)" local major="${ver%.*}" local minor="${ver#*.}" if [ "$major" -lt 3 ] || { [ "$major" -eq 3 ] && [ "$minor" -lt 11 ]; }; then err "python3 $ver too old (need 3.11+) — install via: brew install python@3.11" return 1 fi say " → python3 $ver OK" } # Ensure pipx is installed; if missing, install via brew. _datasette_ensure_pipx() { if command -v pipx >/dev/null 2>&1; then say " → pipx $(pipx --version 2>/dev/null) already installed" return 0 fi if ! command -v brew >/dev/null 2>&1; then err "neither pipx nor brew found — install Homebrew first: https://brew.sh" return 1 fi say " → installing pipx via brew" brew install pipx pipx ensurepath } # Write the runtime wrapper script that discovers DBs and execs datasette. # Args: . _datasette_write_wrapper() { local wrapper="$1" mkdir -p "$(dirname "$wrapper")" cat > "$wrapper" <<'WRAPPER_EOF' #!/usr/bin/env bash # Auto-generated by lib-dev-hub-datasette.sh — do not edit by hand. # Discovers SQLite DBs at launch time and serves them via Datasette (read-only). set -eu DBS=() for f in "$HOME/.claude/agents"/*.sqlite "$HOME/Projects"/*/*.sqlite; do [ -f "$f" ] && DBS+=("$f") done META="$HOME/Library/Application Support/keisei/datasette/metadata.yaml" # Empty-array expansion is unsafe under `set -u` on bash 3.2 (macOS); branch. if [ "${#DBS[@]}" -eq 0 ]; then exec "$HOME/.local/bin/datasette" serve \ --host 127.0.0.1 --port 8001 \ --metadata "$META" else exec "$HOME/.local/bin/datasette" serve \ --host 127.0.0.1 --port 8001 \ --metadata "$META" \ --immutable "${DBS[@]}" fi WRAPPER_EOF chmod +x "$wrapper" } # Write metadata.yaml if not already present (single-user, read-only defaults). # Args: . _datasette_write_metadata() { local data_dir="$1" local meta="$data_dir/metadata.yaml" if [ -f "$meta" ]; then say " → metadata.yaml already present (preserving user edits)" return 0 fi mkdir -p "$data_dir" cat > "$meta" <<'META_EOF' title: KeiSeiKit DBs description: | Read-only browser for KeiSeiKit SQLite databases (kei-ledger, kei-memory, projects-index, etc.) and any project DB under ~/Projects/. Served by Datasette in --immutable mode; no UI action can mutate these files. allow_facet: true allow_download: false default_page_size: 100 META_EOF say " → wrote $meta" } # ---------- public API ---------- # List SQLite paths to expose to Datasette. # Walks: ~/.claude/agents/*.sqlite + ~/Projects/*/*.sqlite (depth 2). # Output: newline-separated paths on stdout. discover_databases() { local f for f in "$HOME_DIR/.claude/agents"/*.sqlite "$HOME_DIR/Projects"/*/*.sqlite; do [ -f "$f" ] && printf '%s\n' "$f" done } # Install Datasette + plugins + launchd service. Idempotent. install_dev_hub_datasette() { say "installing dev-hub-datasette" _datasette_check_python || return 1 _datasette_ensure_pipx || return 1 say " → pipx install datasette" pipx install datasette 2>&1 | grep -v "already seems to be installed" || true say " → injecting plugins (cluster-map, vega, render-markdown)" pipx inject datasette \ datasette-cluster-map \ datasette-vega \ datasette-render-markdown 2>&1 | grep -v "already" || true local data_dir="$HOME_DIR/Library/Application Support/keisei/datasette" _datasette_write_metadata "$data_dir" local wrapper="$HOME_DIR/.claude/agents/_primitives/dev-hub/datasette-serve.sh" _datasette_write_wrapper "$wrapper" say " → wrote wrapper $wrapper" # shellcheck source=./lib-launchd.sh . "$KIT_DIR/install/lib-launchd.sh" install_service datasette say "Datasette running on http://127.0.0.1:8001/. Browse SQLite databases from the cortex-ui dashboard." } # Unload + remove plist. Optional --purge also removes the pipx install. # Args: [--purge] uninstall_dev_hub_datasette() { say "uninstalling dev-hub-datasette" # shellcheck source=./lib-launchd.sh . "$KIT_DIR/install/lib-launchd.sh" unload_plist datasette rm -f "$HOME_DIR/.claude/agents/_primitives/dev-hub/datasette-serve.sh" if [ "${1:-}" = "--purge" ] && command -v pipx >/dev/null 2>&1; then say " → pipx uninstall datasette" pipx uninstall datasette || true fi } # Verify Datasette health endpoint returns 200. Returns 0 on OK, 1 on fail. verify_dev_hub_datasette() { local code code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8001/-/health 2>/dev/null || echo 000)" if [ "$code" = "200" ]; then say "datasette health OK (200)" return 0 fi err "datasette health failed (HTTP $code) — check logs at $HOME_DIR/Library/Logs/keisei/datasette/" return 1 }