chore: share worktree port assignment across worktree tooling

The slot scan and the .env.local writer existed twice, once inline in
scripts/worktree-env and once in scripts/post-create.sh. Both are now
wm_assign_ports and wm_write_env_local in scripts/worktree-common.sh, so a
third caller cannot drift from the other two.

wm_assign_ports takes the worktree root instead of reading the process cwd,
which lets it run from outside the worktree, and prints its slot line to stderr
so stdout carries only the port pair.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-08-17 16:34:04 +02:00
parent 9c557859c5
commit 40ffb08b00
3 changed files with 62 additions and 61 deletions
+1 -11
View File
@@ -11,15 +11,5 @@ if [[ -z "$backend_port" || -z "$frontend_port" ]]; then
exit 1
fi
cat > .env.local <<EOF
BACKEND_PORT=$backend_port
FRONTEND_PORT=$frontend_port
REMOTE=http://localhost:$backend_port
EOF
if [[ -n "${CARGO_FEATURES:-}" ]]; then
echo "CARGO_FEATURES=$CARGO_FEATURES" >> .env.local
fi
echo "Created .env.local with ports: backend=$backend_port, frontend=$frontend_port"
wm_write_env_local "$(pwd)" "$backend_port" "$frontend_port"
wm_shared_post_create "$(pwd)"
+59
View File
@@ -13,6 +13,65 @@ wm_main_repo_root() {
cd "$(git -C "$repo_root" rev-parse --git-common-dir 2>/dev/null)/.." && pwd
}
wm_port_in_use() {
lsof -nP -iTCP:"$1" -sTCP:LISTEN &>/dev/null
}
# Prints "<backend_port> <frontend_port>" for the lowest free slot. Slots are read from
# each worktree's .env.local rather than from position in `git worktree list`: removing a
# worktree in the middle would otherwise hand its slot to a new one while a live
# neighbour still listens on those ports. Slot 0 belongs to the main checkout.
wm_assign_ports() {
local repo_root=$1
local slot=${WM_SLOT:-}
local used_slots=() wt_path backend_port frontend_port bp
if [[ -z "$slot" ]]; then
while IFS= read -r wt_path; do
[[ "$wt_path" == "$repo_root" ]] && continue
[[ -f "$wt_path/.env.local" ]] || continue
bp=$(grep '^BACKEND_PORT=' "$wt_path/.env.local" | cut -d= -f2 || true)
if [[ -n "$bp" && "$bp" -gt 8000 ]]; then
used_slots+=("$(( (bp - 8000) / 10 ))")
fi
done < <(git -C "$repo_root" worktree list --porcelain | sed -n 's/^worktree //p')
slot=1
while [[ " ${used_slots[*]:-} " == *" $slot "* ]]; do
((slot++))
done
echo "Auto-assigned slot $slot (used: ${used_slots[*]:-none})" >&2
fi
backend_port=$((8000 + slot * 10))
frontend_port=$((3000 + slot * 10))
if wm_port_in_use "$backend_port" || wm_port_in_use "$frontend_port"; then
echo "WARNING: slot $slot ports ($backend_port/$frontend_port) already in use" >&2
fi
printf '%s %s\n' "$backend_port" "$frontend_port"
}
wm_write_env_local() {
local repo_root=$1
local backend_port=$2
local frontend_port=$3
local env_file="${repo_root}/.env.local"
cat > "$env_file" <<EOF
BACKEND_PORT=$backend_port
FRONTEND_PORT=$frontend_port
REMOTE=http://localhost:$backend_port
EOF
if [[ -n "${CARGO_FEATURES:-}" ]]; then
echo "CARGO_FEATURES=$CARGO_FEATURES" >> "$env_file"
fi
echo "Created .env.local with ports: backend=$backend_port, frontend=$frontend_port"
}
wm_setup_database() {
local repo_root=$1
local env_file=$2
+2 -50
View File
@@ -3,54 +3,6 @@ set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/worktree-common.sh"
port_in_use() {
lsof -nP -iTCP:"$1" -sTCP:LISTEN &>/dev/null
}
if [[ -z "${WM_SLOT:-}" ]]; then
# Scan .env.local files of existing worktrees to find which slots are claimed,
# then pick the lowest free slot. This avoids collisions when worktrees are
# removed and new ones created (position-based indexing would re-use slots
# still held by surviving worktrees).
used_slots=()
current_dir="$(pwd)"
while IFS= read -r wt_path; do
[[ "$wt_path" == "$current_dir" ]] && continue
if [[ -f "$wt_path/.env.local" ]]; then
bp=$(grep '^BACKEND_PORT=' "$wt_path/.env.local" | cut -d= -f2 || true)
if [[ -n "$bp" && "$bp" -gt 8000 ]]; then
used_slots+=("$(( (bp - 8000) / 10 ))")
fi
fi
done < <(git worktree list --porcelain | sed -n 's/^worktree //p')
# Find lowest available slot (slot 0 = 8000/3000 is reserved for main)
WM_SLOT=1
while [[ " ${used_slots[*]:-} " == *" $WM_SLOT "* ]]; do
((WM_SLOT++))
done
echo "Auto-assigned slot $WM_SLOT (used: ${used_slots[*]:-none})"
fi
# Slot-based: predictable ports for SSH forwarding
# Slot 0 = 8000/3000, slot 1 = 8010/3010, slot 2 = 8020/3020, etc.
backend_port=$((8000 + WM_SLOT * 10))
frontend_port=$((3000 + WM_SLOT * 10))
if port_in_use "$backend_port" || port_in_use "$frontend_port"; then
echo "WARNING: Slot $WM_SLOT ports ($backend_port/$frontend_port) already in use" >&2
fi
# Generate .env.local with port overrides
cat > .env.local <<EOF
BACKEND_PORT=$backend_port
FRONTEND_PORT=$frontend_port
REMOTE=http://localhost:$backend_port
EOF
if [[ -n "${CARGO_FEATURES:-}" ]]; then
echo "CARGO_FEATURES=$CARGO_FEATURES" >> .env.local
fi
echo "Created .env.local with ports: backend=$backend_port, frontend=$frontend_port"
read -r backend_port frontend_port < <(wm_assign_ports "$(pwd)")
wm_write_env_local "$(pwd)" "$backend_port" "$frontend_port"
wm_shared_post_create "$(pwd)"