Monorepo for Tangled
at local-dev 210 lines 8.1 kB view raw
1#!/usr/bin/env bash 2# 3# local-dev.sh — tmux orchestrator for fully offline Tangled development. 4# 5# Launches a tmux session with 4 horizontal panes: 6# ┌────────────────────────────────────────────┐ 7# │ VM │ 8# │ nix run --impure .#vm │ 9# │ │ 10# ├────────────────────────────────────────────┤ 11# │ Appview (waits for DID, starts appview) │ 12# │ │ 13# ├────────────────────────────────────────────┤ 14# │ Tailwind — nix run .#watch-tailwind │ 15# ├────────────────────────────────────────────┤ 16# │ Redis — redis-server │ 17# └────────────────────────────────────────────┘ 18# 19# Must be run from within `nix develop` with TANGLED_VM_LOCAL_DEV=1. 20set -euo pipefail 21 22SESSION="tangled-dev" 23TMUX_SOCKET="tangled-dev" 24tmux_cmd="tmux -L $TMUX_SOCKET" 25DID_FILE="nix/vm-data/atproto/owner-did" 26 27# ── Sub-commands (invoked by tmux panes) ───────────────────────────── 28 29run_appview() { 30 echo "── Appview ──" 31 echo "appview: waiting for VM bootstrap (DID file)..." 32 echo "appview: this may take several minutes on first build" 33 local timeout=600 34 local elapsed=0 35 while [[ ! -s "$DID_FILE" ]]; do 36 sleep 2 37 elapsed=$((elapsed + 2)) 38 if ((elapsed % 30 == 0)); then 39 echo "appview: still waiting... (${elapsed}s)" 40 fi 41 if [[ $elapsed -ge $timeout ]]; then 42 echo "appview: ERROR: timed out waiting for DID file after ${timeout}s" 43 echo "appview: check the VM pane for errors" 44 return 1 45 fi 46 done 47 48 local did 49 did=$(cat "$DID_FILE") 50 echo "appview: owner DID = $did" 51 52 # Build TANGLED_LABEL_DEFAULTS from the owner DID 53 local labels=() 54 for rkey in wontfix good-first-issue duplicate documentation assignee; do 55 labels+=("at://$did/sh.tangled.label.definition/$rkey") 56 done 57 local IFS=',' 58 export TANGLED_LABEL_DEFAULTS="${labels[*]}" 59 export TANGLED_LABEL_GFI="at://$did/sh.tangled.label.definition/good-first-issue" 60 echo "appview: TANGLED_LABEL_DEFAULTS set" 61 echo "" 62 63 nix run .#watch-appview 64} 65 66# ── Sub-command / flag dispatch ─────────────────────────────────────── 67 68# When invoked as a sub-command from a tmux pane, dispatch immediately 69if [[ "${1:-}" == "--appview" ]]; then 70 run_appview 71 exit $? 72fi 73 74# --reset: wipe all persistent state and start fresh 75if [[ "${1:-}" == "--reset" ]]; then 76 root_dir=$(jj --ignore-working-copy root 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null) || { 77 echo "error: can't find repo root" 78 exit 1 79 } 80 cd "$root_dir" 81 82 # Kill running session first 83 if $tmux_cmd has-session -t "$SESSION" 2>/dev/null; then 84 echo "Stopping running local-dev session..." 85 for pid in $($tmux_cmd list-panes -s -t "$SESSION" -F '#{pane_pid}' 2>/dev/null); do 86 pkill -TERM -P "$pid" 2>/dev/null || true 87 done 88 sleep 0.5 89 $tmux_cmd kill-session -t "$SESSION" 2>/dev/null || true 90 fi 91 92 echo "This will delete ALL local-dev state:" 93 echo " - nix/vm-data/knot/*" 94 echo " - nix/vm-data/spindle/*" 95 echo " - nix/vm-data/atproto/*" 96 echo " - nixos.qcow2 (VM disk image)" 97 echo " - appview.db* (appview database)" 98 echo "" 99 read -rp "Are you sure? [y/N] " confirm 100 if [[ "$confirm" != [yY] ]]; then 101 echo "Aborted." 102 exit 0 103 fi 104 105 rm -rf nix/vm-data/knot/* 106 rm -rf nix/vm-data/spindle/* 107 rm -rf nix/vm-data/atproto/* 108 rm -f nixos.qcow2 109 rm -f appview.db appview.db-shm appview.db-wal 110 echo "Done. All state has been reset." 111 echo "Run 'nix run .#local-dev' to start fresh." 112 exit 0 113fi 114 115# ── Set up local-dev environment ────────────────────────────────────── 116 117# This is the offline dev orchestrator — always enable local-dev mode 118export TANGLED_VM_LOCAL_DEV=1 119export TANGLED_DEV=true 120 121# AT Protocol endpoints (same as the devshell hook) 122export TANGLED_PLC_URL="http://localhost:2582" 123export TANGLED_PDS_HOST="http://localhost:2583" 124export TANGLED_PDS_ADMIN_SECRET="tangled-local-dev" 125export TANGLED_JETSTREAM_ENDPOINT="ws://localhost:6008/subscribe" 126 127# Check we're in the devshell (need go, air, etc. on PATH) 128if [[ -z "${TANGLED_OAUTH_CLIENT_KID:-}" ]]; then 129 echo "error: must be run from within the nix devshell" 130 echo "" 131 echo " nix develop --impure" 132 echo " nix run .#local-dev" 133 exit 1 134fi 135 136# Check tmux is available 137if ! command -v tmux &>/dev/null; then 138 echo "error: tmux is not installed" 139 exit 1 140fi 141 142# Find repo root 143root_dir=$(jj --ignore-working-copy root 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null) || { 144 echo "error: can't find repo root" 145 exit 1 146} 147cd "$root_dir" 148 149# Ensure atproto data dir exists 150mkdir -p nix/vm-data/atproto 151 152# Path to this script (for tmux pane sub-commands) 153self="$(realpath "$0")" 154 155# ── Kill existing session and its processes ─────────────────────────── 156if $tmux_cmd has-session -t "$SESSION" 2>/dev/null; then 157 # List all pane PIDs in the session and kill their process trees 158 for pid in $($tmux_cmd list-panes -s -t "$SESSION" -F '#{pane_pid}' 2>/dev/null); do 159 pkill -TERM -P "$pid" 2>/dev/null || true 160 done 161 sleep 0.5 162 $tmux_cmd kill-session -t "$SESSION" 2>/dev/null || true 163fi 164 165# ── Create tmux session ────────────────────────────────────────────── 166# 167# Layout (all horizontal splits): 168# ┌────────────────────────┐ 169# │ VM (%0) │ ← largest 170# ├────────────────────────┤ 171# │ Appview (%1) │ ← medium 172# ├────────────────────────┤ 173# │ Tailwind (%2) │ ← few lines 174# ├────────────────────────┤ 175# │ Redis (%3) │ ← few lines 176# └────────────────────────┘ 177# 178# We use a dedicated tmux socket (-L tangled-dev) so the server is 179# always started fresh from the nix devshell. All panes inherit the 180# correct environment (TANGLED_*, PATH, etc.) without any workarounds. 181# 182# We use the unique pane IDs (%N) returned by tmux to target splits 183# reliably, avoiding issues with session/window/pane index lookups. 184 185vm_pane=$($tmux_cmd new-session -d -s "$SESSION" -c "$root_dir" -P -F '#{pane_id}' \ 186 "echo '── VM ──'; nix run --impure .#vm; exec \$SHELL") 187 188appview_pane=$($tmux_cmd split-window -t "$vm_pane" -v -P -F '#{pane_id}' \ 189 "bash \"$self\" --appview; exec \$SHELL") 190 191tailwind_pane=$($tmux_cmd split-window -t "$appview_pane" -v -P -F '#{pane_id}' \ 192 "echo '── Tailwind ──'; nix run .#watch-tailwind; exec \$SHELL") 193 194redis_pane=$($tmux_cmd split-window -t "$tailwind_pane" -v -P -F '#{pane_id}' \ 195 "echo '── Redis ──'; redis-server; exec \$SHELL") 196 197# Give VM the most space; tailwind and redis just a few lines each 198$tmux_cmd resize-pane -t "$redis_pane" -y 5 199$tmux_cmd resize-pane -t "$tailwind_pane" -y 5 200 201$tmux_cmd select-pane -t "$appview_pane" 202 203echo "Tangled local-dev session starting..." 204echo " tmux session: $SESSION" 205echo " appview: http://localhost:3000" 206echo " PDS: http://localhost:2583" 207echo "" 208 209# Attach to the session (always attach since we use a dedicated socket) 210exec $tmux_cmd attach-session -t "$SESSION"