Your music, beautifully tracked. All yours. (coming soon) teal.fm
teal-fm atproto
Something went wrong. Try again.
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)cd "$ROOT_DIR"
usage() { cat <<'USAGE'Usage: pnpm dev [--proxy]
Run the full Teal stack. Postgres, Garnet, and Caddy run in Docker; Expo, Aqua,Cadet, and Satellite run on the host under their watchers. The app is served atDEV_PUBLIC_ORIGIN (default http://localhost:8081) with /xrpc/* and OAuthmetadata on the same origin. Nothing is exposed publicly by default.
Options: --proxy Also start the named Cloudflare tunnel and serve the stack at the public DEV_PUBLIC_ORIGIN (default https://sigilyph.teal.fm). The ignored root .env must define CLOUDFLARED_TUNNEL_TOKEN. -h, --helpUSAGE}
PROXY=0for arg in "$@"; do case "$arg" in --proxy) PROXY=1 ;; -h|--help) usage; exit 0 ;; *) echo "Unknown option: $arg" >&2; usage >&2; exit 1 ;; esacdone
if ! command -v cargo-watch >/dev/null 2>&1 && ! cargo watch --version >/dev/null 2>&1; then echo "cargo-watch is required. Install it with: cargo install cargo-watch --locked" >&2 exit 1fi
if [[ $PROXY == 1 ]]; then export DEV_PUBLIC_ORIGIN=${DEV_PUBLIC_ORIGIN:-https://sigilyph.teal.fm} node -e 'const u = new URL(process.env.DEV_PUBLIC_ORIGIN); if (u.origin !== process.env.DEV_PUBLIC_ORIGIN || u.protocol !== "https:") throw new Error("DEV_PUBLIC_ORIGIN must be an HTTPS origin with no path or trailing slash")'else export DEV_PUBLIC_ORIGIN=${DEV_PUBLIC_ORIGIN:-http://localhost:8081} node -e 'const u = new URL(process.env.DEV_PUBLIC_ORIGIN); if (u.origin !== process.env.DEV_PUBLIC_ORIGIN || !["http:","https:"].includes(u.protocol)) throw new Error("DEV_PUBLIC_ORIGIN must be an HTTP(S) origin with no path or trailing slash")'fi
DEV_DATABASE_URL=${DEV_DATABASE_URL:-postgres://teal:teal@127.0.0.1:5432/teal}DEV_REDIS_URL=${DEV_REDIS_URL:-redis://127.0.0.1:6379}mkdir -p .codex-runDEV_CADET_CURSOR_FILE=${DEV_CADET_CURSOR_FILE:-$ROOT_DIR/.codex-run/cadet-cursor.txt}
COMPOSE=(docker compose -f compose.dev.yml -f compose.watch.yml)if [[ $PROXY == 1 ]]; then COMPOSE+=(--profile named-tunnel)fi
# Reuse the already-installed Caddy runtime when available. Its bundled static# files are unused by Caddyfile.dev; no application image build is needed.if [[ -z ${DEV_PROXY_IMAGE:-} ]] && ! docker image inspect caddy:2.8-alpine >/dev/null 2>&1 && docker image inspect teal-amethyst >/dev/null 2>&1; then export DEV_PROXY_IMAGE=teal-amethystfi
# Reject a second runner before changing the running stack. Port 8081 is owned# by the reused Caddy container, so it is intentionally not checked here.for port in 3000 3001 8082; do if lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then echo "Port $port is occupied. Stop the existing app process before pnpm dev." >&2 exit 1 fidone
"${COMPOSE[@]}" config --quiet
if [[ $PROXY == 1 ]]; then "${COMPOSE[@]}" config --format json | node -e 'let s=""; process.stdin.on("data", x => s += x); process.stdin.on("end", () => { const c=JSON.parse(s).services["cloudflared-named"].command; if (!c[c.indexOf("--token")+1]) { console.error("Set CLOUDFLARED_TUNNEL_TOKEN in the ignored .env file before pnpm dev --proxy."); process.exit(1); } });'fi
# Stop containerized app services the host watchers replace, plus any tunnel# left running from an earlier proxy session so local mode stays private.docker compose -f compose.dev.yml stop aqua-api cadet satellite >/dev/nulldocker compose -f compose.dev.yml --profile named-tunnel stop cloudflared-named >/dev/null 2>&1 || true
"${COMPOSE[@]}" up -d postgres garnetfor attempt in {1..30}; do if "${COMPOSE[@]}" exec -T postgres pg_isready -U teal -d teal >/dev/null; then break fi if [[ $attempt == 30 ]]; then echo "Postgres did not become ready." >&2; exit 1; fi sleep 1done
DATABASE_URL="$DEV_DATABASE_URL" pnpm db:migrate
export DATABASE_URL=$DEV_DATABASE_URLexport REDIS_URL=$DEV_REDIS_URLexport CURSOR_FILE=$DEV_CADET_CURSOR_FILEexport SQLX_OFFLINE=${SQLX_OFFLINE:-true}# Keep incremental builds without multi-gigabyte debug symbols by default.export CARGO_PROFILE_DEV_DEBUG=${CARGO_PROFILE_DEV_DEBUG:-0}export EXPO_PUBLIC_BASE_URL=$DEV_PUBLIC_ORIGINexport EXPO_PUBLIC_AQUA_URL=$DEV_PUBLIC_ORIGINexport EXPO_PACKAGER_PROXY_URL=$DEV_PUBLIC_ORIGINexport CADET_STREAM_MODE=jetstreamexport CADET_DEFER_MATERIALIZED_VIEW_REFRESH=1export CADET_MATERIALIZED_VIEW_REFRESH_INTERVAL_SECS=60export EXPO_PUBLIC_GIT_BRANCH=${EXPO_PUBLIC_GIT_BRANCH:-$(git branch --show-current)}export EXPO_PUBLIC_GIT_COMMIT=${EXPO_PUBLIC_GIT_COMMIT:-$(git rev-parse --short HEAD)}
"${COMPOSE[@]}" up -d --no-build --no-deps amethystif [[ $PROXY == 1 ]]; then "${COMPOSE[@]}" up -d --no-build --no-deps cloudflared-named echo "Public development preview: $DEV_PUBLIC_ORIGIN"else echo "Local development preview: $DEV_PUBLIC_ORIGIN"fi# exec keeps SIGINT/SIGTERM attached to Turbo, which owns all watch processes.exec pnpm dev:apps