feat: single dev run (UI+backend via concurrently) + preview_urls; UI on base port

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Abhishek Kumar 2026-06-29 13:09:27 +05:30
parent c099dad03d
commit de88d4f21e
5 changed files with 129 additions and 44 deletions

View file

@ -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 |

View file

@ -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

59
.conductor/run-dev.sh Executable file
View file

@ -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"

View file

@ -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

View file

@ -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]