forked from
tangled.org/core
Monorepo for Tangled
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"