diff --git a/.vscode/launch.json b/.vscode/launch.json index d735bedb..4c82e511 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,6 +23,8 @@ "api.app:app", "--reload", "--host", "0.0.0.0" + // Port comes from UVICORN_PORT in api/.env (per-worktree); + // unset -> uvicorn's default 8000. See scripts/worktree-assign-port.sh. ], "cwd": "${workspaceFolder}", "envFile": "${workspaceFolder}/api/.env", diff --git a/.vscode/settings.json b/.vscode/settings.json index f1daa359..73f0e040 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,9 @@ { - "python.defaultInterpreterPath": "${workspaceFolder}/venv/bin/python" + "python.defaultInterpreterPath": "${workspaceFolder}/venv/bin/python", + "git.detectWorktrees": true, + "git.worktreeIncludeFiles": [ + "api/.env", + "api/.env.test", + "ui/.env.local" + ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..9bcd39ce --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,23 @@ +{ + // Auto-runs when a worktree folder is opened. The first time, VS Code asks + // to "Allow Automatic Tasks in Folder" (or run it via the command palette: + // "Tasks: Allow Automatic Tasks in Folder"). Assigns this worktree a unique + // backend port and points the UI env at it — see scripts/worktree-assign-port.sh. + "version": "2.0.0", + "tasks": [ + { + "label": "Assign worktree port", + "type": "shell", + "command": "${workspaceFolder}/scripts/worktree-assign-port.sh", + "presentation": { + "reveal": "silent", + "panel": "shared", + "close": true + }, + "runOptions": { + "runOn": "folderOpen" + }, + "problemMatcher": [] + } + ] +} diff --git a/scripts/worktree-assign-port.sh b/scripts/worktree-assign-port.sh new file mode 100755 index 00000000..8daf4eae --- /dev/null +++ b/scripts/worktree-assign-port.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# Assign a unique backend port to this git worktree and rewrite the env files +# that depend on it. Runs automatically as a VS Code "folderOpen" task (see +# .vscode/tasks.json), so it executes once per worktree when you open it. +# +# Scheme: +# - The MAIN worktree is left untouched (backend stays on uvicorn's default 8000). +# - Each linked worktree gets the next free backend port: 8001, 8002, ... +# - api/.env : UVICORN_PORT -> the assigned backend port +# - ui/.env.local : BACKEND_URL -> http://localhost: +# NEXT_PUBLIC_BACKEND_URL -> http://localhost: +# +# CORS is intentionally NOT touched: local dev runs DEPLOYMENT_MODE="oss", where +# the API forces allow_origins=["*"] and ignores CORS_ALLOWED_ORIGINS entirely. +# +# Idempotent: re-running keeps an already-assigned, non-colliding port. The UI +# dev server is left alone — `npm run dev` auto-selects a free port (3000, 3001…). +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel)" +MAIN="$(git worktree list --porcelain | sed -n '1s/^worktree //p')" +[ "$ROOT" = "$MAIN" ] && { echo "[worktree] main worktree -> backend 8000 (untouched)"; exit 0; } + +AENV="$ROOT/api/.env" +UENV="$ROOT/ui/.env.local" +[ -f "$AENV" ] || { echo "[worktree] no api/.env yet; skipping"; exit 0; } + +# Echo the UVICORN_PORT value from an env file (empty if unset/missing). +port_of() { { grep -E '^[[:space:]]*UVICORN_PORT=' "$1" 2>/dev/null | tail -1 | sed -E 's/^[^=]*=//; s/[[:space:]]//g'; } || true; } + +# Ports already in use by OTHER worktrees (main implicitly uses 8000). +used=(8000) +while IFS= read -r line; do + case "$line" in + "worktree "*) + wt="${line#worktree }" + [ "$wt" = "$ROOT" ] && continue + p="$(port_of "$wt/api/.env")" + [ -n "$p" ] && used+=("$p") + ;; + esac +done < <(git worktree list --porcelain) + +mine="$(port_of "$AENV")" + +# Keep my port if it's set and not claimed by another worktree; else take max+1. +reassign=1 +if [ -n "$mine" ]; then + reassign=0 + for u in "${used[@]}"; do [ "$u" = "$mine" ] && reassign=1; done +fi +if [ "$reassign" -eq 1 ]; then + max=0 + for u in "${used[@]}"; do [ "$u" -gt "$max" ] && max="$u"; done + B=$((max + 1)) +else + B="$mine" +fi + +# Insert or update KEY=VALUE in an env file, preserving everything else. +upsert() { + local key="$1" val="$2" file="$3" + if grep -qE "^[[:space:]]*${key}=" "$file"; then + sed -i.bak -E "s|^[[:space:]]*${key}=.*|${key}=${val}|" "$file" && rm -f "$file.bak" + else + printf '\n%s=%s\n' "$key" "$val" >> "$file" + fi +} + +upsert UVICORN_PORT "$B" "$AENV" +if [ -f "$UENV" ]; then + upsert BACKEND_URL "http://localhost:$B" "$UENV" + upsert NEXT_PUBLIC_BACKEND_URL "http://localhost:$B" "$UENV" +fi + +echo "[worktree] $(basename "$ROOT"): backend=$B (UI auto-port via 'npm run dev')"