From de88d4f21e751c16642d5158f47166a4257de35b Mon Sep 17 00:00:00 2001 From: Abhishek Kumar Date: Mon, 29 Jun 2026 13:09:27 +0530 Subject: [PATCH] feat: single dev run (UI+backend via concurrently) + preview_urls; UI on base port Co-Authored-By: Claude Opus 4.8 (1M context) --- .conductor/README.md | 43 +++++++++++++++++----------- .conductor/run-backend.sh | 18 ++++++------ .conductor/run-dev.sh | 59 +++++++++++++++++++++++++++++++++++++++ .conductor/run-ui.sh | 15 +++++----- .conductor/settings.toml | 38 +++++++++++++++++-------- 5 files changed, 129 insertions(+), 44 deletions(-) create mode 100755 .conductor/run-dev.sh diff --git a/.conductor/README.md b/.conductor/README.md index 7cf2de26..5c8df4f6 100644 --- a/.conductor/README.md +++ b/.conductor/README.md @@ -12,13 +12,16 @@ Conductor gives every workspace a block of **10 ports** starting at | Port | Service | Script | | --------------- | ------------------ | ----------------------- | -| `CONDUCTOR_PORT` | Backend (uvicorn) | `run-backend.sh` | -| `CONDUCTOR_PORT + 1` | UI (Next.js) | `run-ui.sh` | +| `CONDUCTOR_PORT` | UI (Next.js) | `run-ui.sh` | +| `CONDUCTOR_PORT + 1` | Backend (uvicorn) | `run-backend.sh` | | `+2 .. +9` | reserved | — | -The UI is wired to its own workspace's backend (`NEXT_PUBLIC_BACKEND_URL`, -`BACKEND_URL`) and the backend's CORS / `UI_APP_URL` point back at its own UI — -all derived from `$CONDUCTOR_PORT`, so no two workspaces interfere. +The UI sits on the base port so Conductor's **Open** button (`preview_urls`) +lands on it directly — preview templates only substitute `$CONDUCTOR_PORT`, with +no `+1` arithmetic. The UI is wired to its own workspace's backend +(`NEXT_PUBLIC_BACKEND_URL`, `BACKEND_URL`) and the backend's CORS / `UI_APP_URL` +point back at its own UI — all derived from `$CONDUCTOR_PORT`, so no two +workspaces interfere. ### Shared, NOT per-workspace @@ -32,21 +35,28 @@ for everyone. ## The Run menu -Conductor shows three buttons (`run_mode = "concurrent"`, so they coexist): +> **Important:** a Conductor workspace runs **one run script at a time**. +> `run_mode = "concurrent"` lets *different workspaces* run simultaneously — it +> does **not** let you click two run buttons in the *same* workspace. That's why +> **dev** starts the UI and backend together instead of relying on two buttons. -- **backend** (default) — FastAPI/uvicorn with `--reload` on `$CONDUCTOR_PORT` -- **ui** — Next.js dev server on `$CONDUCTOR_PORT + 1` -- **worker** — Arq worker; start in just one workspace +The Run dropdown offers: -Start **backend** and **ui** in each workspace you want to use. Start **worker** -once (e.g. in your primary workspace) when you need background jobs — the same -way you used to run a single arq worker in VS Code. +- **dev** (default) — UI (`$CONDUCTOR_PORT`) **and** backend (`$CONDUCTOR_PORT+1`) + together, via `concurrently`. **Use this day to day.** +- **ui** — Next.js only, for debugging the frontend alone. +- **backend** — uvicorn only, for debugging the API alone. +- **worker** — Arq worker; start in just one workspace. + +Hit **dev** and both servers come up; click Conductor's **Open** button to launch +the UI. Start **worker** once (e.g. in your primary workspace) when you need +background jobs — the same way you used to run a single arq worker in VS Code. ## Proving you're in the right worktree `run-ui.sh` exports `NEXT_PUBLIC_WORKSPACE_NAME=$CONDUCTOR_WORKSPACE_NAME`, which `ui/src/components/WorkspaceBadge.tsx` renders as a small color-coded pill in the -bottom-left corner (e.g. `⬡ pattaya :8201`). The color is derived from the +bottom-left corner (e.g. `⬡ pattaya :8200`). The color is derived from the workspace name, so two worktrees are instantly distinguishable. The badge is invisible in production and in a plain `npm run dev` (no env var set). @@ -69,10 +79,11 @@ The first setup is slow (deps); afterwards the Run buttons are instant. | File | Purpose | | --------------------- | -------------------------------------------------- | -| `settings.toml` | Conductor config: setup + run scripts, run_mode | +| `settings.toml` | Conductor config: setup + run scripts, preview_urls | | `setup.sh` | One-time workspace bootstrap | -| `run-backend.sh` | Foreground uvicorn on `$CONDUCTOR_PORT` | -| `run-ui.sh` | Foreground Next.js on `$CONDUCTOR_PORT + 1` | +| `run-dev.sh` | Default run: UI + backend together (`concurrently`) | +| `run-ui.sh` | Foreground Next.js on `$CONDUCTOR_PORT` | +| `run-backend.sh` | Foreground uvicorn on `$CONDUCTOR_PORT + 1` | | `run-worker.sh` | Foreground Arq worker (run in one workspace only) | | `../.worktreeinclude` | Gitignored env files Conductor copies per workspace | diff --git a/.conductor/run-backend.sh b/.conductor/run-backend.sh index 160ed88a..54cb86b5 100755 --- a/.conductor/run-backend.sh +++ b/.conductor/run-backend.sh @@ -1,21 +1,21 @@ #!/usr/bin/env bash # Conductor run script — Backend (FastAPI/uvicorn) for THIS workspace. # -# Binds $CONDUCTOR_PORT and points CORS / UI_APP_URL at this workspace's UI -# ($CONDUCTOR_PORT + 1). Runs in the FOREGROUND with exec (no &) so Conductor's -# SIGHUP cleanly tears it down. +# Binds $CONDUCTOR_PORT + 1 and points CORS / UI_APP_URL at this workspace's UI +# (which runs on $CONDUCTOR_PORT). Runs in the FOREGROUND with exec (no &) so +# Conductor's SIGHUP cleanly tears it down. set -euo pipefail cd "${CONDUCTOR_WORKSPACE_PATH:-$PWD}" -PORT="${CONDUCTOR_PORT:-8000}" -UI_PORT="$((PORT + 1))" +UI_PORT="${CONDUCTOR_PORT:-8000}" +BACKEND_PORT="$((UI_PORT + 1))" # Load the workspace's api/.env, then override the port-specific bits. The # backend reads config via os.getenv, and these exports happen after the source, -# so they win over the values copied from the main checkout (which assume 8000). +# so they win over the values copied from the main checkout (which assume 8000/3000). if [[ -f api/.env ]]; then set -a; # shellcheck disable=SC1091 source api/.env; set +a; fi -export FASTAPI_PORT="$PORT" +export FASTAPI_PORT="$BACKEND_PORT" export UI_APP_URL="http://localhost:${UI_PORT}" export CORS_ALLOWED_ORIGINS="http://localhost:${UI_PORT},http://127.0.0.1:${UI_PORT}" @@ -26,5 +26,5 @@ fi # shellcheck disable=SC1091 source venv/bin/activate -echo "[backend] workspace=${CONDUCTOR_WORKSPACE_NAME:-?} port=${PORT} ui=${UI_PORT}" -exec uvicorn api.app:app --host 0.0.0.0 --port "$PORT" --reload --reload-dir api +echo "[backend] workspace=${CONDUCTOR_WORKSPACE_NAME:-?} port=${BACKEND_PORT} ui=${UI_PORT}" +exec uvicorn api.app:app --host 0.0.0.0 --port "$BACKEND_PORT" --reload --reload-dir api diff --git a/.conductor/run-dev.sh b/.conductor/run-dev.sh new file mode 100755 index 00000000..d9d7fe99 --- /dev/null +++ b/.conductor/run-dev.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Conductor run script — full dev stack (UI + backend) for THIS workspace. +# +# A Conductor workspace runs ONE run script at a time (run_mode governs +# concurrency ACROSS workspaces, not multiple run buttons within one). So to get +# the UI AND the backend up together, we launch both with `concurrently`. +# +# Conductor stops a run with SIGHUP (then SIGKILL after 200ms). Neither npx nor +# concurrently reliably tears down their child tree on SIGHUP, so we supervise: +# trap the signal and recursively kill the whole tree ourselves. We do NOT exec, +# so this shell stays alive to handle the trap. +# +# Ports: UI on $CONDUCTOR_PORT, backend on $CONDUCTOR_PORT + 1. +set -uo pipefail +cd "${CONDUCTOR_WORKSPACE_PATH:-$PWD}" + +UI_PORT="${CONDUCTOR_PORT:-8000}" +BACKEND_PORT="$((UI_PORT + 1))" + +# Ensure node/npx is on PATH (load nvm + honor .nvmrc if needed). +if ! command -v npx >/dev/null 2>&1; then + export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + # shellcheck disable=SC1091 + [[ -s "$NVM_DIR/nvm.sh" ]] && . "$NVM_DIR/nvm.sh" + command -v nvm >/dev/null 2>&1 && nvm use >/dev/null 2>&1 || true +fi + +# Prefer a locally-installed concurrently (fast); fall back to npx, which fetches +# it once then caches — so this also works in workspaces created before it existed. +if [[ -x ui/node_modules/.bin/concurrently ]]; then + RUNNER=(ui/node_modules/.bin/concurrently) +else + RUNNER=(npx --yes concurrently) +fi + +# Recursively SIGTERM a process and every descendant (children first). npm/npx/ +# next don't reliably forward signals, so we signal each PID in the tree directly. +kill_tree() { + local pid=$1 child + for child in $(pgrep -P "$pid" 2>/dev/null); do kill_tree "$child"; done + kill -TERM "$pid" 2>/dev/null || true +} + +shutdown() { + trap - HUP INT TERM EXIT + [[ -n "${CHILD:-}" ]] && kill_tree "$CHILD" + exit 0 +} +trap shutdown HUP INT TERM EXIT + +echo "[dev] workspace=${CONDUCTOR_WORKSPACE_NAME:-?} ui=:${UI_PORT} backend=:${BACKEND_PORT}" +"${RUNNER[@]}" \ + --names "ui,backend" \ + --prefix-colors "magenta,cyan" \ + --kill-others \ + "bash .conductor/run-ui.sh" \ + "bash .conductor/run-backend.sh" & +CHILD=$! +wait "$CHILD" diff --git a/.conductor/run-ui.sh b/.conductor/run-ui.sh index 1fe3ca3d..949325ab 100755 --- a/.conductor/run-ui.sh +++ b/.conductor/run-ui.sh @@ -1,16 +1,17 @@ #!/usr/bin/env bash # Conductor run script — UI (Next.js) for THIS workspace. # -# Binds $CONDUCTOR_PORT + 1, talks to this workspace's backend on $CONDUCTOR_PORT, -# and tags the build with the workspace identity (NEXT_PUBLIC_WORKSPACE_NAME) so -# the in-app WorkspaceBadge shows which worktree you're looking at. -# Runs in the FOREGROUND with exec (no &) so Conductor can stop it cleanly. +# Binds $CONDUCTOR_PORT (so Conductor's Open button / preview_urls land here), +# talks to this workspace's backend on $CONDUCTOR_PORT + 1, and tags the build +# with the workspace identity (NEXT_PUBLIC_WORKSPACE_NAME) so the in-app +# WorkspaceBadge shows which worktree you're looking at. Foreground exec (no &) +# so Conductor can stop it cleanly. set -euo pipefail cd "${CONDUCTOR_WORKSPACE_PATH:-$PWD}" -PORT="${CONDUCTOR_PORT:-8000}" -UI_PORT="$((PORT + 1))" -BACKEND="http://localhost:${PORT}" +UI_PORT="${CONDUCTOR_PORT:-8000}" +BACKEND_PORT="$((UI_PORT + 1))" +BACKEND="http://localhost:${BACKEND_PORT}" # Ensure node is on PATH (load nvm + honor .nvmrc if needed). if ! command -v node >/dev/null 2>&1; then diff --git a/.conductor/settings.toml b/.conductor/settings.toml index 31afe775..5691aab5 100644 --- a/.conductor/settings.toml +++ b/.conductor/settings.toml @@ -1,38 +1,52 @@ # Conductor project config — shared, committed so every contributor (and every # new workspace) gets the same per-worktree dev workflow. # -# Model: each Conductor workspace is its own git worktree and gets its own -# 10-port range starting at $CONDUCTOR_PORT. We use: -# $CONDUCTOR_PORT -> backend (FastAPI/uvicorn) -# $CONDUCTOR_PORT + 1 -> ui (Next.js) +# Each Conductor workspace is its own git worktree and gets its own 10-port range +# starting at $CONDUCTOR_PORT. We use: +# $CONDUCTOR_PORT -> ui (Next.js) <- Conductor's Open button lands here +# $CONDUCTOR_PORT + 1 -> backend (FastAPI/uvicorn) # +2 .. +9 -> reserved for future per-workspace services # # Postgres/Redis/MinIO are a single SHARED Docker stack (project name "dograh"), # not per-workspace — see .conductor/setup.sh and .conductor/README.md. "$schema" = "https://conductor.build/schemas/settings.repo.schema.json" +# Conductor's Open button. The UI runs on $CONDUCTOR_PORT, so this opens it. +# (Preview URL templates only substitute $CONDUCTOR_PORT — no "+1" arithmetic — +# which is why the UI, the thing you actually open, sits on the base port.) +[[preview_urls]] +name = "UI" +url = "http://localhost:$CONDUCTOR_PORT" + [scripts] # Runs once when a workspace is created: copies gitignored env files, inits the # pipecat submodule, builds the venv + node_modules, ensures the shared Docker # stack, and runs migrations. setup = "bash .conductor/setup.sh" -# concurrent => multiple workspaces can run their dev servers at the same time -# (each on its own CONDUCTOR_PORT range), and within a workspace the backend + -# ui + worker run buttons don't kill each other. +# concurrent => multiple WORKSPACES can run their dev servers at the same time +# (each on its own CONDUCTOR_PORT range). NOTE: within a single workspace, +# Conductor runs ONE run script at a time — that's why the default "dev" script +# below starts the UI and backend together rather than relying on two buttons. run_mode = "concurrent" -# Backend (FastAPI/uvicorn) on $CONDUCTOR_PORT. Default Run button. -[scripts.run.backend] -command = "bash .conductor/run-backend.sh" -icon = "server" +# Full dev stack: UI ($CONDUCTOR_PORT) + backend ($CONDUCTOR_PORT+1) together. +# Default Run button — this is the one to use day to day. +[scripts.run.dev] +command = "bash .conductor/run-dev.sh" +icon = "play" default = true -# UI (Next.js) on $CONDUCTOR_PORT + 1, wired to this workspace's backend. +# UI only (Next.js) on $CONDUCTOR_PORT — handy for debugging the frontend alone. [scripts.run.ui] command = "bash .conductor/run-ui.sh" icon = "play" +# Backend only (FastAPI/uvicorn) on $CONDUCTOR_PORT + 1 — debugging the API alone. +[scripts.run.backend] +command = "bash .conductor/run-backend.sh" +icon = "server" + # Arq background worker. Start in ONE workspace only — all workspaces share the # same Redis/Postgres, so a single worker drains the queue for everyone. [scripts.run.worker]