#!/usr/bin/env bash # # dev-local — Start the full local Roomy stack for development. # # Boots two layers, all on localhost, no tunnel or production services needed: # # 1. PLC directory + PLC DB (docker compose, port 3001) # 2. Appserver (host Bun process, port 8080) # 3. app-lite (SvelteKit dev server) (host Vite process, port 5180) # # The appserver owns its own event store (local SQLite); no Leaf server is # involved. app-lite talks to the local appserver via VITE_APPSERVER_WS_ORIGIN # (which drives both the sync WebSocket and the XRPC HTTP client — see # commit 47d283e4). # # Nothing needs to be publicly exposed: app-lite calls the appserver directly, # and dev OAuth uses the standard atproto loopback client id whose only # redirect is http://127.0.0.1:/ — no extra env needed. # # Usage: # ./scripts/dev-local # start everything (foreground, Ctrl-C stops all) # ./scripts/dev-local --port 8080 # custom appserver port # ./scripts/dev-local --app-port 5180 # custom app-lite port # # Run from the repo root. # # Requirements: # - Docker with the compose plugin # - bun and pnpm on PATH (or install via .local) # - Dependencies installed: `pnpm install` at repo root # # Notes: # - The first `docker compose up` pull is slow (amd64 images via QEMU on # arm64 hosts). Subsequent starts reuse cached images. # - The appserver SQLite DB lives at packages/appserver/data/ and persists # across restarts. Wipe it (rm packages/appserver/data/*.sqlite) for a # clean slate. set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" # ── Defaults ────────────────────────────────────────────────────────────── APPSERVER_PORT=8080 APP_LITE_PORT=5180 while [[ $# -gt 0 ]]; do case "$1" in --port) APPSERVER_PORT="$2"; shift 2 ;; --app-port) APP_LITE_PORT="$2"; shift 2 ;; -h|--help) sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done # ── Preflight ──────────────────────────────────────────────────────────── if ! command -v docker >/dev/null 2>&1; then echo "✗ docker not found on PATH." exit 1 fi if ! command -v bun >/dev/null 2>&1; then echo "✗ bun not found on PATH." exit 1 fi if ! command -v pnpm >/dev/null 2>&1; then echo "✗ pnpm not found on PATH." exit 1 fi if [[ ! -d packages/appserver ]] || [[ ! -d packages/app-lite ]]; then echo "✗ Run from the repo root — packages/appserver and packages/app-lite not found." exit 1 fi if [[ ! -d node_modules ]]; then echo "✗ node_modules missing. Run 'pnpm install' first." exit 1 fi # ── Track child PIDs for cleanup ──────────────────────────────────────── PIDS=() cleanup() { echo "" echo "Stopping dev-local …" for pid in "${PIDS[@]}"; do if kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true fi done # Give them a moment, then force-kill sleep 1 for pid in "${PIDS[@]}"; do kill -9 "$pid" 2>/dev/null || true done } trap cleanup EXIT INT TERM # ── 1. PLC directory (docker compose) ──────────────────────────────────── echo "▸ Starting local PLC directory (docker compose) …" # The compose file declares the network as external — create it if missing. if ! docker network inspect roomy-dev >/dev/null 2>&1; then echo " creating network roomy-dev …" docker network create roomy-dev >/dev/null fi docker compose up plc-directory plc-db -d # ── 2. Appserver (host Bun process) ────────────────────────────────────── echo "▸ Starting appserver on port $APPSERVER_PORT …" # Use the REAL PLC directory for DID resolution — the local PLC only has # test DIDs, but JWT verification needs to resolve the PDS's signing key # (e.g. bsky.social's DID) which lives in the real PLC directory. APPSERVER_ENV=( "PORT=$APPSERVER_PORT" "APPSERVER_DID=did:web:localhost" "APPSERVER_ORIGIN=http://localhost:$APPSERVER_PORT" "CORS_ORIGIN=*" "PLC_DIRECTORY_URL=https://plc.directory" ) # Export and run env "${APPSERVER_ENV[@]}" \ bun run --watch packages/appserver/src/index.ts & APPSERVER_PID=$! PIDS+=("$APPSERVER_PID") echo " waiting for appserver to be ready …" for i in $(seq 1 15); do if curl -sf -o /dev/null "http://127.0.0.1:$APPSERVER_PORT/xrpc/space.roomy.space.getSpaces" 2>/dev/null; then echo " ✔ appserver ready (port $APPSERVER_PORT)" break fi sleep 1 [[ $i -eq 15 ]] && { echo "✗ appserver did not start in 15s"; exit 1; } done # Pass through PUBLIC_TEST_* vars from packages/app-lite/.env if present, # enabling headless E2E testing via app-password auto-login. TEST_ENV=() if [[ -f packages/app-lite/.env ]]; then while IFS= read -r line; do case "$line" in PUBLIC_TEST_IDENTIFIER=*) TEST_ENV+=("$line") ;; PUBLIC_TEST_APP_PASSWORD=*) TEST_ENV+=("$line") ;; esac done < packages/app-lite/.env fi env "${TEST_ENV[@]}" \ VITE_APPSERVER_DID=did:web:localhost \ VITE_APPSERVER_WS_ORIGIN=ws://127.0.0.1:$APPSERVER_PORT \ PUBLIC_WEB_ORIGIN=http://127.0.0.1:$APP_LITE_PORT \ VITE_PUBLIC_WEB_ORIGIN=http://127.0.0.1:$APP_LITE_PORT \ VITE_PORT=$APP_LITE_PORT \ pnpm --filter app-lite exec vite dev --host 127.0.0.1 --port "$APP_LITE_PORT" & APP_LITE_PID=$! PIDS+=("$APP_LITE_PID") echo " waiting for app-lite to be ready …" for i in $(seq 1 20); do if curl -sf -o /dev/null "http://127.0.0.1:$APP_LITE_PORT/" 2>/dev/null; then echo " ✔ app-lite ready (port $APP_LITE_PORT)" break fi sleep 1 [[ $i -eq 20 ]] && { echo "✗ app-lite did not start in 20s"; exit 1; } done # ── Done ───────────────────────────────────────────────────────────────── echo "" echo "════════════════════════════════════════════════════════════════" echo " Roomy local dev stack is running." echo "" echo " app-lite: http://127.0.0.1:$APP_LITE_PORT/" echo " appserver: http://127.0.0.1:$APPSERVER_PORT/" echo " plc-dir: http://127.0.0.1:3001/ (local)" echo "" echo " Press Ctrl-C to stop everything." echo "════════════════════════════════════════════════════════════════" # Wait for either process to exit (or Ctrl-C) wait