Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ site/node_modules/
site/dist/
.DS_Store
.claude/launch.json

# Per-worktree dev port slot (scripts/dev-port.sh) — never commit a lane's slot.
.dev-port
8 changes: 8 additions & 0 deletions orca.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,11 @@ defaultTabs:
command: |-
g="$(git rev-parse --git-dir 2>/dev/null)"; [ "$g" = "$(git rev-parse --git-common-dir 2>/dev/null)" ] || { i=0; while [ ! -e "$g/orca-setup-done" ] && [ "$i" -lt 150 ]; do sleep 1; i=$((i+1)); done; [ -e "$g/orca-setup-done" ] || echo "orca-tab: gave up waiting ${i}s for setup - it may have failed or been skipped"; }; bun run test
color: "#059669"
# On-demand: resolves this lane's port and prints how to serve + look at the
# roadmap view. Boots nothing. `nextup serve` defaults to 4177, which the mac
# holds permanently via the dev.hansenexus.nextup-serve LaunchAgent, so a lane
# must not use the default.
- title: preview
command: |-
g="$(git rev-parse --git-dir 2>/dev/null)"; [ "$g" = "$(git rev-parse --git-common-dir 2>/dev/null)" ] || { i=0; while [ ! -e "$g/orca-setup-done" ] && [ "$i" -lt 150 ]; do sleep 1; i=$((i+1)); done; [ -e "$g/orca-setup-done" ] || echo "orca-tab: gave up waiting ${i}s for setup - it may have failed or been skipped"; }; echo "this lane: $(bash scripts/dev-port.sh --url)"; echo; echo 'serve it: bun cli.ts serve --port $(bash scripts/dev-port.sh)'; echo 'open it: orca tab create --worktree current --url "$(bash scripts/dev-port.sh --url)"'; echo 'look: orca snapshot | orca screenshot --format png'; echo; exec "${SHELL:-bash}" -i
color: "#0ea5e9"
75 changes: 75 additions & 0 deletions scripts/dev-port.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# dev-port.sh — this worktree's dev port.
#
# The dev server binds a fixed port (4177). Run two Orca lanes at once and the
# second one either dies on EADDRINUSE or, worse, does not: the first lane
# already owns the port, so the browser reaches THAT server and you review one
# lane's code while looking at another's output. It fails green.
#
# Each worktree gets a slot (0..15) from a stable hash of its absolute path;
# the port is `base + slot * 100`. The main checkout is always slot 0, so its
# port stays exactly 4177 and nothing changes for anyone not using worktrees.
#
# A worktree's own prior claim (recorded in its `.dev-port`) wins over a fresh
# hash, so removing an unrelated sibling cannot silently move a surviving
# worktree to a different port between two runs.
#
# Here the collision is not just lane-vs-lane: the mac runs `nextup serve` on
# 4177 permanently as the LaunchAgent dev.hansenexus.nextup-serve (verified
# holding the port on 2026-09-09), so a lane using the default would fight the
# machine's own long-running server. Slot 0 still resolves 4177 because the main
# checkout is not where lanes run.
#
# Scope: isolates lanes of THIS repo from each other, not across repos.
#
# scripts/dev-port.sh # bare port, for `--port $(…)`
# scripts/dev-port.sh --url # http://localhost:<port>
set -euo pipefail

BASE=4177
MAX_SLOTS=16
STRIDE=100

root="$(git rev-parse --show-toplevel)"

read_slot() { # $1 = worktree toplevel
local v
v="$(cat "$1/.dev-port" 2>/dev/null || true)"
case "$v" in
''|*[!0-9]*) return 1 ;;
*) [ "$v" -lt "$MAX_SLOTS" ] && printf '%s' "$v" || return 1 ;;
esac
}

if [ "$(git rev-parse --git-dir)" = "$(git rev-parse --git-common-dir)" ]; then
slot=0 # main checkout
else
claimed=" 0 "
while IFS= read -r line; do
case "$line" in worktree\ *) ;; *) continue ;; esac
top="${line#worktree }"
[ "$top" = "$root" ] && continue
s="$(read_slot "$top" || true)"
[ -n "$s" ] && claimed="$claimed$s "
done < <(git worktree list --porcelain)

own="$(read_slot "$root" || true)"
if [ -n "$own" ] && [ "$own" -gt 0 ] && [[ "$claimed" != *" $own "* ]]; then
slot="$own"
else
start="$(printf '%s' "$root" | cksum | awk -v m="$MAX_SLOTS" '{print $1 % m}')"
slot=""
for i in $(seq 0 $((MAX_SLOTS - 1))); do
cand=$(((start + i) % MAX_SLOTS))
if [[ "$claimed" != *" $cand "* ]]; then slot="$cand"; break; fi
done
if [ -z "$slot" ]; then
echo "dev-port: all $MAX_SLOTS worktree slots are taken — remove an unused worktree" >&2
exit 1
fi
fi
printf '%s\n' "$slot" > "$root/.dev-port"
fi

port=$((BASE + slot * STRIDE))
if [ "${1:-}" = "--url" ]; then printf 'http://localhost:%s\n' "$port"; else printf '%s\n' "$port"; fi
Loading