#!/usr/bin/env bash # # local-dev.sh — tmux orchestrator for fully offline Tangled development. # # Launches a tmux session with 4 horizontal panes: # ┌────────────────────────────────────────────┐ # │ VM │ # │ nix run --impure .#vm │ # │ │ # ├────────────────────────────────────────────┤ # │ Appview (waits for DID, starts appview) │ # │ │ # ├────────────────────────────────────────────┤ # │ Tailwind — nix run .#watch-tailwind │ # ├────────────────────────────────────────────┤ # │ Redis — redis-server │ # └────────────────────────────────────────────┘ # # Must be run from within `nix develop` with TANGLED_VM_LOCAL_DEV=1. set -euo pipefail SESSION="tangled-dev" TMUX_SOCKET="tangled-dev" tmux_cmd="tmux -L $TMUX_SOCKET" DID_FILE="nix/vm-data/atproto/owner-did" # ── Sub-commands (invoked by tmux panes) ───────────────────────────── run_appview() { echo "── Appview ──" echo "appview: waiting for VM bootstrap (DID file)..." echo "appview: this may take several minutes on first build" local timeout=600 local elapsed=0 while [[ ! -s "$DID_FILE" ]]; do sleep 2 elapsed=$((elapsed + 2)) if ((elapsed % 30 == 0)); then echo "appview: still waiting... (${elapsed}s)" fi if [[ $elapsed -ge $timeout ]]; then echo "appview: ERROR: timed out waiting for DID file after ${timeout}s" echo "appview: check the VM pane for errors" return 1 fi done local did did=$(cat "$DID_FILE") echo "appview: owner DID = $did" # Build TANGLED_LABEL_DEFAULTS from the owner DID local labels=() for rkey in wontfix good-first-issue duplicate documentation assignee; do labels+=("at://$did/sh.tangled.label.definition/$rkey") done local IFS=',' export TANGLED_LABEL_DEFAULTS="${labels[*]}" export TANGLED_LABEL_GFI="at://$did/sh.tangled.label.definition/good-first-issue" echo "appview: TANGLED_LABEL_DEFAULTS set" echo "" nix run .#watch-appview } # ── Sub-command / flag dispatch ─────────────────────────────────────── # When invoked as a sub-command from a tmux pane, dispatch immediately if [[ "${1:-}" == "--appview" ]]; then run_appview exit $? fi # --reset: wipe all persistent state and start fresh if [[ "${1:-}" == "--reset" ]]; then root_dir=$(jj --ignore-working-copy root 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null) || { echo "error: can't find repo root" exit 1 } cd "$root_dir" # Kill running session first if $tmux_cmd has-session -t "$SESSION" 2>/dev/null; then echo "Stopping running local-dev session..." for pid in $($tmux_cmd list-panes -s -t "$SESSION" -F '#{pane_pid}' 2>/dev/null); do pkill -TERM -P "$pid" 2>/dev/null || true done sleep 0.5 $tmux_cmd kill-session -t "$SESSION" 2>/dev/null || true fi echo "This will delete ALL local-dev state:" echo " - nix/vm-data/knot/*" echo " - nix/vm-data/spindle/*" echo " - nix/vm-data/atproto/*" echo " - nixos.qcow2 (VM disk image)" echo " - appview.db* (appview database)" echo "" read -rp "Are you sure? [y/N] " confirm if [[ "$confirm" != [yY] ]]; then echo "Aborted." exit 0 fi rm -rf nix/vm-data/knot/* rm -rf nix/vm-data/spindle/* rm -rf nix/vm-data/atproto/* rm -f nixos.qcow2 rm -f appview.db appview.db-shm appview.db-wal echo "Done. All state has been reset." echo "Run 'nix run .#local-dev' to start fresh." exit 0 fi # ── Set up local-dev environment ────────────────────────────────────── # This is the offline dev orchestrator — always enable local-dev mode export TANGLED_VM_LOCAL_DEV=1 export TANGLED_DEV=true # AT Protocol endpoints (same as the devshell hook) export TANGLED_PLC_URL="http://localhost:2582" export TANGLED_PDS_HOST="http://localhost:2583" export TANGLED_PDS_ADMIN_SECRET="tangled-local-dev" export TANGLED_JETSTREAM_ENDPOINT="ws://localhost:6008/subscribe" # Check we're in the devshell (need go, air, etc. on PATH) if [[ -z "${TANGLED_OAUTH_CLIENT_KID:-}" ]]; then echo "error: must be run from within the nix devshell" echo "" echo " nix develop --impure" echo " nix run .#local-dev" exit 1 fi # Check tmux is available if ! command -v tmux &>/dev/null; then echo "error: tmux is not installed" exit 1 fi # Find repo root root_dir=$(jj --ignore-working-copy root 2>/dev/null || git rev-parse --show-toplevel 2>/dev/null) || { echo "error: can't find repo root" exit 1 } cd "$root_dir" # Ensure atproto data dir exists mkdir -p nix/vm-data/atproto # Path to this script (for tmux pane sub-commands) self="$(realpath "$0")" # ── Kill existing session and its processes ─────────────────────────── if $tmux_cmd has-session -t "$SESSION" 2>/dev/null; then # List all pane PIDs in the session and kill their process trees for pid in $($tmux_cmd list-panes -s -t "$SESSION" -F '#{pane_pid}' 2>/dev/null); do pkill -TERM -P "$pid" 2>/dev/null || true done sleep 0.5 $tmux_cmd kill-session -t "$SESSION" 2>/dev/null || true fi # ── Create tmux session ────────────────────────────────────────────── # # Layout (all horizontal splits): # ┌────────────────────────┐ # │ VM (%0) │ ← largest # ├────────────────────────┤ # │ Appview (%1) │ ← medium # ├────────────────────────┤ # │ Tailwind (%2) │ ← few lines # ├────────────────────────┤ # │ Redis (%3) │ ← few lines # └────────────────────────┘ # # We use a dedicated tmux socket (-L tangled-dev) so the server is # always started fresh from the nix devshell. All panes inherit the # correct environment (TANGLED_*, PATH, etc.) without any workarounds. # # We use the unique pane IDs (%N) returned by tmux to target splits # reliably, avoiding issues with session/window/pane index lookups. vm_pane=$($tmux_cmd new-session -d -s "$SESSION" -c "$root_dir" -P -F '#{pane_id}' \ "echo '── VM ──'; nix run --impure .#vm; exec \$SHELL") appview_pane=$($tmux_cmd split-window -t "$vm_pane" -v -P -F '#{pane_id}' \ "bash \"$self\" --appview; exec \$SHELL") tailwind_pane=$($tmux_cmd split-window -t "$appview_pane" -v -P -F '#{pane_id}' \ "echo '── Tailwind ──'; nix run .#watch-tailwind; exec \$SHELL") redis_pane=$($tmux_cmd split-window -t "$tailwind_pane" -v -P -F '#{pane_id}' \ "echo '── Redis ──'; redis-server; exec \$SHELL") # Give VM the most space; tailwind and redis just a few lines each $tmux_cmd resize-pane -t "$redis_pane" -y 5 $tmux_cmd resize-pane -t "$tailwind_pane" -y 5 $tmux_cmd select-pane -t "$appview_pane" echo "Tangled local-dev session starting..." echo " tmux session: $SESSION" echo " appview: http://localhost:3000" echo " PDS: http://localhost:2583" echo "" # Attach to the session (always attach since we use a dedicated socket) exec $tmux_cmd attach-session -t "$SESSION"