Files
alvaro abd03ed07d refactor: rename API and frontend projects to Vocero
Rename the project directories, Python distribution and import package, and npm package. Update launcher, Docker, tooling, tests, lockfiles, and documentation while preserving existing API contracts.

Make AGENTS.md self-contained and remove external skill and workflow references.
2026-10-06 19:26:55 -03:00

156 lines
5.2 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
API_DIR="$ROOT_DIR/vocero-api"
VENV_DIR="$API_DIR/.venv"
FRONTEND_DIR="$ROOT_DIR/vocero-frontend"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
source "$ROOT_DIR/.env"
set +a
fi
UI_PORT="${UI_PORT:-8420}"
UI_HOST="${UI_HOST:-0.0.0.0}"
S2S_BACKEND_PORT="${S2S_BACKEND_PORT:-8421}"
S2S_PROXY_TARGET="${S2S_PROXY_TARGET:-ws://ai-server:12345/v1/realtime}"
if (( BASH_VERSINFO[0] < 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 4) )); then
echo "Error: start_ui.sh requires Bash 4.4 or newer."
exit 1
fi
for required_command in uv node npm setsid curl; do
if ! command -v "$required_command" >/dev/null 2>&1; then
echo "Error: $required_command was not found."
exit 1
fi
done
if ! node -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit((major === 22 && minor >= 18) || major >= 24 ? 0 : 1)'; then
echo "Error: vocero-frontend requires Node.js 22.18+ on the 22.x line, or Node.js 24+."
exit 1
fi
for port_name in UI_PORT S2S_BACKEND_PORT; do
port_value="${!port_name}"
if [[ ! "$port_value" =~ ^[0-9]{1,5}$ ]] || (( 10#$port_value < 1 || 10#$port_value > 65535 )); then
echo "Error: $port_name must be a port between 1 and 65535."
exit 1
fi
# Normalize leading zeroes before passing ports to Node and Uvicorn.
printf -v "$port_name" '%d' "$((10#$port_value))"
done
if [ "$UI_PORT" = "$S2S_BACKEND_PORT" ]; then
echo "Error: UI_PORT and S2S_BACKEND_PORT must be different."
exit 1
fi
if [ ! -f "$API_DIR/pyproject.toml" ] || [ ! -f "$API_DIR/uv.lock" ]; then
echo "Error: vocero-api/pyproject.toml and vocero-api/uv.lock are required."
exit 1
fi
# uv manages only the API environment; an existing root speech venv is separate.
UV_PROJECT_ENVIRONMENT="$VENV_DIR" uv sync --project "$API_DIR" --locked
# Keep restarts offline when the installed frontend manifests have not changed.
if [ ! -f "$FRONTEND_DIR/node_modules/vite/bin/vite.js" ] || \
! cmp -s "$FRONTEND_DIR/package-lock.json" "$FRONTEND_DIR/node_modules/.s2s-package-lock.json" || \
! cmp -s "$FRONTEND_DIR/package.json" "$FRONTEND_DIR/node_modules/.s2s-package.json"; then
echo "Installing frontend dependencies from vocero-frontend/package-lock.json..."
npm --prefix "$FRONTEND_DIR" ci --include=dev
cp "$FRONTEND_DIR/package-lock.json" "$FRONTEND_DIR/node_modules/.s2s-package-lock.json"
cp "$FRONTEND_DIR/package.json" "$FRONTEND_DIR/node_modules/.s2s-package.json"
fi
# The combined launcher always proxies to the backend it starts. An alternate
# S2S_BACKEND_URL is only used when running Vite independently.
export S2S_BACKEND_URL="http://127.0.0.1:$S2S_BACKEND_PORT"
export S2S_PROXY_TARGET
child_pids=()
cleanup() {
local exit_status=$?
trap - EXIT
trap '' INT TERM HUP
for child_pid in "${child_pids[@]}"; do
kill -TERM -- "-$child_pid" 2>/dev/null || true
done
# Stop whole process groups, including Uvicorn's reload worker. Bound the
# graceful shutdown so a stuck connection cannot leave ports occupied.
for ((attempt = 0; attempt < 20; attempt++)); do
local alive=false
for child_pid in "${child_pids[@]}"; do
if kill -0 -- "-$child_pid" 2>/dev/null; then
alive=true
fi
done
if [ "$alive" = false ]; then
break
fi
sleep 0.05
done
for child_pid in "${child_pids[@]}"; do
kill -KILL -- "-$child_pid" 2>/dev/null || true
wait "$child_pid" 2>/dev/null || true
done
exit "$exit_status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
trap 'exit 129' HUP
echo "💻 Starting the React UI at http://$UI_HOST:$UI_PORT (Vite hot reload)..."
echo "🔧 FastAPI backend: $S2S_BACKEND_URL (Python reload enabled)"
echo "🔌 Realtime proxy target: $S2S_PROXY_TARGET"
echo " Press Ctrl+C in this terminal to stop both servers."
echo "----------------------------------------------------"
cd "$ROOT_DIR"
setsid "$VENV_DIR/bin/python" -m uvicorn vocero_api.main:app \
--app-dir "$API_DIR" \
--host 127.0.0.1 --port "$S2S_BACKEND_PORT" \
--reload --reload-dir "$API_DIR" &
backend_pid=$!
child_pids+=("$backend_pid")
backend_ready=false
startup_deadline=$((SECONDS + 30))
while (( SECONDS < startup_deadline )); do
if ! kill -0 "$backend_pid" 2>/dev/null; then
echo "Error: FastAPI exited before it was ready."
exit 1
fi
if curl --noproxy '*' --silent --fail --max-time 1 \
--output /dev/null "$S2S_BACKEND_URL/api/config"; then
backend_ready=true
break
fi
sleep 0.2
done
if [ "$backend_ready" = false ]; then
echo "Error: FastAPI did not become ready within 30 seconds."
exit 1
fi
(
cd "$FRONTEND_DIR"
# Keep React Fast Refresh enabled even if PM2 exports NODE_ENV=production.
export NODE_ENV=development
exec setsid node node_modules/vite/bin/vite.js --host "$UI_HOST" --port "$UI_PORT" --strictPort
) &
frontend_pid=$!
child_pids+=("$frontend_pid")
# PM2 supervises this script. If either server exits, stop its sibling and fail
# the launcher so PM2 can restart the complete pair instead of a partial UI.
exit_status=0
wait -n "$backend_pid" "$frontend_pid" || exit_status=$?
echo "Error: a UI server exited; stopping both servers."
if [ "$exit_status" -eq 0 ]; then
exit_status=1
fi
exit "$exit_status"