From 40ffb08b00cc41e99096fe66e2d28dac4bdf55e3 Mon Sep 17 00:00:00 2001 From: hugocasa Date: Mon, 17 Aug 2026 16:34:04 +0200 Subject: [PATCH] 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) --- scripts/post-create.sh | 12 +------- scripts/worktree-common.sh | 59 ++++++++++++++++++++++++++++++++++++++ scripts/worktree-env | 52 ++------------------------------- 3 files changed, 62 insertions(+), 61 deletions(-) diff --git a/scripts/post-create.sh b/scripts/post-create.sh index 431e944d74..7ca17551aa 100755 --- a/scripts/post-create.sh +++ b/scripts/post-create.sh @@ -11,15 +11,5 @@ if [[ -z "$backend_port" || -z "$frontend_port" ]]; then exit 1 fi -cat > .env.local <> .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)" diff --git a/scripts/worktree-common.sh b/scripts/worktree-common.sh index 672b9f9f65..fd741f6312 100755 --- a/scripts/worktree-common.sh +++ b/scripts/worktree-common.sh @@ -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 " " 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" <> "$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 diff --git a/scripts/worktree-env b/scripts/worktree-env index e45daba75b..390a12fd66 100755 --- a/scripts/worktree-env +++ b/scripts/worktree-env @@ -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 <> .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)"