scripts/dev.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
#
# Start the full Maestro dev stack on macOS/Linux: infra (Docker), backend
# (FastAPI/uvicorn) and frontend (Next.js). Backend and frontend run together;
# Ctrl-C stops both.
#
# Usage:
# ./scripts/dev.sh
# ./scripts/dev.sh --skip-infra
# ./scripts/dev.sh --skip-seed
# BACKEND_PORT=8001 FRONTEND_PORT=3001 ./scripts/dev.sh
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
BACKEND="$REPO_ROOT/backend"
FRONTEND="$REPO_ROOT/frontend"
SKIP_INFRA=0
SKIP_SEED=0
for arg in "$@"; do
case "$arg" in
--skip-infra) SKIP_INFRA=1 ;;
--skip-seed) SKIP_SEED=1 ;;
esac
done
BACKEND_PORT="${BACKEND_PORT:-8000}"
FRONTEND_PORT="${FRONTEND_PORT:-3000}"
step() { printf '\n==> %s\n' "$1"; }
port_holders() { lsof -ti "tcp:$1" -sTCP:LISTEN 2>/dev/null || true; }
# Poll until nothing listens on $1, for up to $2 tenths of a second. Neither
# `kill` nor `kill -9` is synchronous: the signal is delivered, but the listening
# socket is only released once the process actually dies, and a uvicorn reloader
# is a parent plus a child sharing that socket. The old fixed `sleep 0.5` was
# therefore a race, and losing it surfaced as an opaque `[Errno 48] Address
# already in use` from uvicorn with no WARN line above it.
wait_port_free() {
local port="$1" tries="$2"
while [ "$tries" -gt 0 ]; do
[ -z "$(port_holders "$port")" ] && return 0
sleep 0.1
tries=$((tries - 1))
done
[ -z "$(port_holders "$port")" ]
}
free_port() {
local port="$1" pids
pids="$(port_holders "$port")"
if [ -n "$pids" ]; then
echo "WARN: port $port is in use (PID(s): $pids); killing."
# shellcheck disable=SC2086
kill $pids 2>/dev/null || true
if ! wait_port_free "$port" 20; then
pids="$(port_holders "$port")"
# shellcheck disable=SC2086
[ -n "$pids" ] && kill -9 $pids 2>/dev/null || true
fi
fi
# Runs even when no holder was found: a process that is mid-bind or mid-exit
# shows up in neither `lsof -sTCP:LISTEN` nor a successful bind.
wait_port_free "$port" 30 && return 0
echo "WARN: port $port is still held by PID(s) $(port_holders "$port") after 5s;"
echo " the service below will fail to bind. Free it, or set BACKEND_PORT/FRONTEND_PORT."
}
# --- 1. Infra (Docker) ----------------------------------------------------
if [ "$SKIP_INFRA" -eq 0 ]; then
if command -v docker >/dev/null 2>&1; then
step "Starting infra (Postgres, MongoDB, Qdrant) via docker-compose"
(cd "$REPO_ROOT" && docker compose up -d) || echo "WARN: docker compose failed"
else
echo "WARN: Docker not found; skipping infra (pass --skip-infra to silence)."
fi
fi
# --- 2. Backend -----------------------------------------------------------
step "Preparing backend"
VENV="$BACKEND/.venv"
# Backend requires Python >=3.11 (see CLAUDE.md). Pick the newest available
# interpreter; a venv built on an older python silently fails to resolve pins
# such as alembic>=1.17, which need >=3.10.
PYTHON_BIN=""
for candidate in python3.13 python3.12 python3.11; do
if command -v "$candidate" >/dev/null 2>&1; then
PYTHON_BIN="$candidate"
break
fi
done
if [ -z "$PYTHON_BIN" ]; then
# Fall back to python3 only if it is >=3.11.
if command -v python3 >/dev/null 2>&1 && \
python3 -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)'; then
PYTHON_BIN="python3"
else
echo "ERROR: Python >=3.11 is required but was not found." >&2
echo " Install it (e.g. 'brew install python@3.11') and re-run." >&2
exit 1
fi
fi
# Recreate the venv if it is missing or was built on an interpreter <3.11.
if [ -x "$VENV/bin/python" ] && \
! "$VENV/bin/python" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)'; then
echo "Existing virtualenv uses an unsupported Python; recreating with $PYTHON_BIN..."
rm -rf "$VENV"
fi
if [ ! -x "$VENV/bin/python" ]; then
echo "Creating virtualenv with $PYTHON_BIN ($($PYTHON_BIN --version 2>&1))..."
"$PYTHON_BIN" -m venv "$VENV"
fi
VENV_PY="$VENV/bin/python"
if [ ! -f "$BACKEND/.env" ]; then
cp "$REPO_ROOT/.env.example" "$BACKEND/.env"
echo "WARN: created backend/.env from .env.example — set real secrets before production."
fi
echo "Installing backend dependencies..."
"$VENV_PY" -m pip install --disable-pip-version-check -q -r "$BACKEND/requirements.txt"
echo "Running database migrations (alembic upgrade head)..."
( cd "$BACKEND" && "$VENV_PY" -m alembic upgrade head ) \
|| echo "WARN: alembic migration failed (is Postgres up?)"
if [ "$SKIP_SEED" -eq 0 ]; then
echo "Seeding featured marketplace teams (idempotent)..."
# Seeding is cosmetic: a missing Mongo must not stop the dev stack coming up.
( cd "$BACKEND" && "$VENV_PY" -m app.scripts.seed_marketplace ) \
|| echo "WARN: marketplace seed failed (is MongoDB up?)"
fi
# --- 3. Frontend ----------------------------------------------------------
step "Preparing frontend"
if [ ! -d "$FRONTEND/node_modules" ]; then
echo "Installing frontend dependencies..."
(cd "$FRONTEND" && npm install)
fi
if [ ! -f "$FRONTEND/.env.local" ]; then
cp "$FRONTEND/.env.local.example" "$FRONTEND/.env.local"
fi
# --- 4. Launch both, stop both on exit ------------------------------------
PIDS=()
cleanup() {
step "Shutting down..."
for pid in "${PIDS[@]}"; do kill "$pid" 2>/dev/null || true; done
}
trap cleanup EXIT INT TERM
free_port "$BACKEND_PORT"
step "Launching backend on http://localhost:$BACKEND_PORT"
# --reload-dir app restricts the watcher to the source package. Without it
# uvicorn watches the whole CWD including .venv, and touched site-package files
# (e.g. numpy) trigger an endless reload loop that drops in-flight requests.
( cd "$BACKEND" && exec "$VENV_PY" -m uvicorn app.main:app --reload --reload-dir app --port "$BACKEND_PORT" ) &
PIDS+=("$!")
free_port "$FRONTEND_PORT"
step "Launching frontend on http://localhost:$FRONTEND_PORT"
( cd "$FRONTEND" && exec npm run dev -- --port "$FRONTEND_PORT" ) &
PIDS+=("$!")
printf '\nMaestro is running:\n'
printf ' Backend : http://localhost:%s (docs: /docs)\n' "$BACKEND_PORT"
printf ' Frontend: http://localhost:%s\n' "$FRONTEND_PORT"
printf 'Press Ctrl-C to stop both.\n'
wait