#!/usr/bin/env bash # Native dev loop with no external watcher deps. # # Postgres runs in Docker; the server and web app compile on the host and # rebuild on change. Everything is served from the Gleam server on :8080 so # the OAuth origin stays consistent. # # Browse http://127.0.0.1:8080 (NOT localhost: the atproto localhost client # redirects to the loopback IP, and the session cookie is host-specific). # # Web edits -> bundle rebuilt, just refresh the browser (no restart). # Server/shared -> server recompiled and restarted (sessions survive: they # live in Postgres and SECRET_KEY_BASE is stable). # Lexicon edits -> codegen reruns, then web + server rebuild. set -uo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." # Local secrets (Discogs app credential etc.); gitignored, same file compose reads. if [ -f .env ]; then set -a . ./.env set +a fi log() { printf '\033[36m[dev]\033[0m %s\n' "$*"; } # Stable secret so sessions survive the frequent restarts this loop does. export SECRET_KEY_BASE="${SECRET_KEY_BASE:-dev-only-change-me-to-a-long-random-string}" # At-rest encryption key for stored tokens (required; base64, 32 bytes). export STORE_KEY="${STORE_KEY:-ZGV2LW9ubHktMzItYnl0ZS1zdG9yZS1rZXktISEhISE=}" # BASE_URL left unset => http://localhost:8080 => OAuth redirect to 127.0.0.1:8080. free_8080() { # gleam run spawns a BEAM child, so kill by port rather than by pid to be sure. local pids pids="$(lsof -ti tcp:8080 -sTCP:LISTEN 2>/dev/null || true)" [ -n "$pids" ] && kill $pids 2>/dev/null || true for _ in $(seq 1 25); do lsof -ti tcp:8080 -sTCP:LISTEN >/dev/null 2>&1 || break sleep 0.2 done } build_web() { log "building web bundle" ( cd web && gleam run -m lustre/dev build at_record_web ) || { log "web build failed" return 1 } mkdir -p server/priv/static cp -r web/priv/static/. server/priv/static/ cp web/dist/at_record_web.js server/priv/static/at_record_web.js make css } start_server() { log "starting server -> http://127.0.0.1:8080" ( cd server && exec gleam run ) & } restart_server() { free_8080 start_server } cleanup() { log "shutting down" free_8080 exit 0 } trap cleanup INT TERM # --- boot ------------------------------------------------------------------- log "starting postgres (docker); stopping any dockerized server on :8080" docker compose up -d db docker compose stop server >/dev/null 2>&1 || true free_8080 # Reach Postgres by the db container's IP (works on OrbStack, where the host can # route to container IPs). A published host port is avoided: it's flaky here and # clashes with any local Postgres on 5432. Override DATABASE_URL to bypass this. if [ -z "${DATABASE_URL:-}" ]; then db_ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$(docker compose ps -q db)" 2>/dev/null)" if [ -n "$db_ip" ]; then export DATABASE_URL="postgres://atrecord:atrecord@${db_ip}:5432/atrecord" log "postgres at ${db_ip}:5432" else log "could not resolve db container IP; server will use in-memory sessions" fi fi make gen make vendor build_web start_server # --- poll watcher ----------------------------------------------------------- SENT="$(mktemp -d)" touch "$SENT/web" "$SENT/server" "$SENT/lex" changed() { # local sentinel="$1" shift if find "$@" -type f -newer "$sentinel" -print -quit 2>/dev/null | grep -q .; then touch "$sentinel" return 0 fi return 1 } log "watching lexicons/, server/src, shared/src, web/src, web/css, web/priv/static (Ctrl-C to stop)" while true; do if changed "$SENT/lex" lexicons; then log "lexicons changed -> codegen + full rebuild" make gen && build_web && restart_server elif changed "$SENT/server" server/src shared/src; then log "server/shared changed -> rebuild + restart" build_web && restart_server elif changed "$SENT/web" web/src web/css web/priv/static; then log "web changed -> rebuild bundle (refresh the browser)" build_web fi sleep 1 done