#!/bin/bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" BACKEND_DIR="$SCRIPT_DIR/backend" FRONTEND_DIR="$SCRIPT_DIR/frontend" LOG_DIR="$SCRIPT_DIR/logs" ROOT_ENV="$SCRIPT_DIR/.env" BACKEND_ENV="$BACKEND_DIR/.env" BACKEND_ENV_EXAMPLE="$BACKEND_DIR/.env.example" FRONTEND_ENV_LOCAL="$FRONTEND_DIR/.env.local" HOST_OS="$(uname -s)" USE_WINDOWS_TOOLS=false if command -v cmd.exe >/dev/null 2>&1 || command -v powershell.exe >/dev/null 2>&1; then USE_WINDOWS_TOOLS=true fi if [[ "$USE_WINDOWS_TOOLS" == true ]]; then BACKEND_PYTHON="$BACKEND_DIR/.venv/Scripts/python.exe" FRONTEND_NPM="npm.cmd" else BACKEND_PYTHON="$BACKEND_DIR/.venv/bin/python" FRONTEND_NPM="npm" fi RUN_ID="$(date +%Y%m%d-%H%M%S)" BACKEND_LOG="$LOG_DIR/backend-start-${RUN_ID}.log" BACKEND_ERR_LOG="$LOG_DIR/backend-start-${RUN_ID}.err.log" FRONTEND_LOG="$LOG_DIR/frontend-start-${RUN_ID}.log" FRONTEND_ERR_LOG="$LOG_DIR/frontend-start-${RUN_ID}.err.log" KILL_PORT=false ENABLE_RELOAD=false for arg in "$@"; do case "$arg" in --kill-port) KILL_PORT=true ;; --reload) ENABLE_RELOAD=true ;; *) echo "[ERROR] Unsupported argument: $arg" echo "[ERROR] Usage: bash start.sh [--kill-port] [--reload]" exit 1 ;; esac done mkdir -p "$LOG_DIR" port_in_use() { "$BACKEND_PYTHON" - "$1" <<'PY' import socket, sys port = int(sys.argv[1]) with socket.socket() as sock: sock.settimeout(0.2) sys.exit(0 if sock.connect_ex(("127.0.0.1", port)) == 0 else 1) PY } to_windows_path() { local path="$1" if [[ "$path" =~ ^/mnt/([a-zA-Z])/(.*)$ ]]; then printf '%s:/%s' "${BASH_REMATCH[1]^}" "${BASH_REMATCH[2]}" elif [[ "$path" =~ ^/cygdrive/([a-zA-Z])/(.*)$ ]]; then printf '%s:/%s' "${BASH_REMATCH[1]^}" "${BASH_REMATCH[2]}" elif [[ "$path" =~ ^/([a-zA-Z])/(.*)$ ]]; then printf '%s:/%s' "${BASH_REMATCH[1]^}" "${BASH_REMATCH[2]}" else printf '%s' "$path" fi } quote_ps() { local value="$1" value="$(printf '%s' "$value" | sed "s/'/''/g")" printf '%s' "$value" } sync_backend_env() { if [[ -f "$BACKEND_ENV" ]]; then return 0 fi if [[ -f "$ROOT_ENV" ]]; then cp "$ROOT_ENV" "$BACKEND_ENV" echo "[INFO] Created backend/.env from root .env" return 0 fi if [[ -f "$BACKEND_ENV_EXAMPLE" ]]; then cp "$BACKEND_ENV_EXAMPLE" "$BACKEND_ENV" echo "[INFO] Created backend/.env from backend/.env.example" echo "[INFO] Review $BACKEND_ENV before first run." return 0 fi echo "[ERROR] backend/.env was not found and could not be initialized." echo "[ERROR] Run: bash setup.sh" exit 1 } read_env_value() { local key="$1" local file="$2" grep "^${key}=" "$file" | cut -d'=' -f2- | tr -d '\r' || true } write_frontend_env() { local backend_host="$1" local backend_port="$2" printf 'VITE_API_URL=http://%s:%s\n' "$backend_host" "$backend_port" > "$FRONTEND_ENV_LOCAL" } kill_port_process() { local port="$1" if [[ "$USE_WINDOWS_TOOLS" == true ]]; then local pids pids="$(powershell.exe -NoProfile -Command "Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique" | tr -d '\r' || true)" if [[ -z "$pids" ]]; then return 0 fi while IFS= read -r pid; do [[ -z "$pid" ]] && continue cmd.exe /c "taskkill /PID $pid /T /F" >/dev/null 2>&1 || true powershell.exe -NoProfile -Command "Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue" >/dev/null 2>&1 || true done <<< "$pids" return 0 fi local pids if command -v ss >/dev/null 2>&1; then pids="$(ss -ltnp "sport = :$port" 2>/dev/null | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' | sort -u || true)" elif command -v lsof >/dev/null 2>&1; then pids="$(lsof -ti tcp:"$port" 2>/dev/null | sort -u || true)" else return 0 fi if [[ -z "$pids" ]]; then return 0 fi while IFS= read -r pid; do [[ -z "$pid" ]] && continue kill "$pid" >/dev/null 2>&1 || true done <<< "$pids" } kill_process_tree() { local pid="$1" [[ -z "$pid" ]] && return 0 kill "$pid" >/dev/null 2>&1 || true } cleanup() { local exit_code=$? trap - INT TERM EXIT if [[ -n "${FRONTEND_PID:-}" ]]; then kill_process_tree "$FRONTEND_PID" fi if [[ -n "${BACKEND_PID:-}" ]]; then kill_process_tree "$BACKEND_PID" fi exit "$exit_code" } trap cleanup INT TERM EXIT echo "==========================================" echo " Jarvis - Start" echo "==========================================" echo sync_backend_env if [[ ! -f "$BACKEND_ENV" ]]; then echo "[ERROR] Backend env file was not found: $BACKEND_ENV" echo "[ERROR] Run: bash setup.sh" exit 1 fi if [[ "$USE_WINDOWS_TOOLS" == true ]]; then if [[ ! -f "$BACKEND_PYTHON" ]]; then echo "[ERROR] Backend virtual environment Python was not found: $BACKEND_PYTHON" echo "[ERROR] Run: bash setup.sh" exit 1 fi if ! command -v "$FRONTEND_NPM" >/dev/null 2>&1; then echo "[ERROR] npm.cmd was not found." echo "[ERROR] Install Node.js and ensure npm is on PATH." exit 1 fi else if [[ ! -x "$BACKEND_PYTHON" ]]; then echo "[ERROR] Backend virtual environment Python was not found: $BACKEND_PYTHON" echo "[ERROR] Run: bash setup.sh" exit 1 fi if ! command -v "$FRONTEND_NPM" >/dev/null 2>&1; then echo "[ERROR] npm was not found." echo "[ERROR] Install Node.js and ensure npm is on PATH." exit 1 fi fi if [[ "$USE_WINDOWS_TOOLS" == true ]] && ! command -v powershell.exe >/dev/null 2>&1; then echo "[ERROR] powershell.exe was not found." exit 1 fi BACKEND_HOST="$(read_env_value HOST "$BACKEND_ENV")" BACKEND_PORT="$(read_env_value PORT "$BACKEND_ENV")" [[ -z "$BACKEND_HOST" ]] && BACKEND_HOST="127.0.0.1" if [[ -z "$BACKEND_PORT" ]]; then echo "[ERROR] PORT was not found in $BACKEND_ENV" echo "[ERROR] Set PORT and run start.sh again." exit 1 fi if ! [[ "$BACKEND_PORT" =~ ^[0-9]+$ ]]; then echo "[ERROR] PORT must be numeric in $BACKEND_ENV: $BACKEND_PORT" exit 1 fi if (( BACKEND_PORT < 1 || BACKEND_PORT > 65535 )); then echo "[ERROR] PORT must be between 1 and 65535 in $BACKEND_ENV: $BACKEND_PORT" exit 1 fi write_frontend_env "$BACKEND_HOST" "$BACKEND_PORT" BACKEND_ARGS=(-m uvicorn app.main:app --host "$BACKEND_HOST" --port "$BACKEND_PORT") if [[ "$ENABLE_RELOAD" == true ]]; then BACKEND_ARGS+=(--reload) fi echo "[1/3] Backend environment ready." echo "[2/3] Frontend environment ready." echo echo "[3/3] Start services..." if port_in_use "$BACKEND_PORT"; then if [[ "$KILL_PORT" == true ]]; then echo "[INFO] Port ${BACKEND_PORT} is in use. Killing existing process..." kill_port_process "$BACKEND_PORT" sleep 2 fi fi if port_in_use "$BACKEND_PORT"; then echo "[ERROR] Port ${BACKEND_PORT} is already in use." echo "[ERROR] Stop the existing service first, or run: bash start.sh --kill-port" exit 1 fi ( cd "$BACKEND_DIR" exec "$BACKEND_PYTHON" "${BACKEND_ARGS[@]}" ) >"$BACKEND_LOG" 2>"$BACKEND_ERR_LOG" & BACKEND_PID="$!" echo "Waiting for backend..." backend_ready=false for _ in $(seq 1 30); do if port_in_use "$BACKEND_PORT"; then backend_ready=true break fi sleep 1 done if [[ "$backend_ready" != true ]]; then echo "[ERROR] Backend did not start successfully." echo "[ERROR] Check logs: $BACKEND_LOG $BACKEND_ERR_LOG" exit 1 fi FRONTEND_HOST="0.0.0.0" FRONTEND_BROWSER_HOST="localhost" FRONTEND_PORT="5173" echo "Starting frontend on port ${FRONTEND_PORT}..." if [[ "$USE_WINDOWS_TOOLS" == true ]]; then ( cd "$FRONTEND_DIR" exec cmd.exe /c npm.cmd run dev -- --host "$FRONTEND_HOST" ) >"$FRONTEND_LOG" 2>"$FRONTEND_ERR_LOG" & FRONTEND_PID="$!" else ( cd "$FRONTEND_DIR" exec "$FRONTEND_NPM" run dev -- --host "$FRONTEND_HOST" ) >"$FRONTEND_LOG" 2>"$FRONTEND_ERR_LOG" & FRONTEND_PID="$!" fi echo echo "==========================================" echo " Started" echo echo " Backend: http://${BACKEND_HOST}:${BACKEND_PORT}" echo " Frontend: http://${FRONTEND_BROWSER_HOST}:${FRONTEND_PORT}" echo " API docs: http://${BACKEND_HOST}:${BACKEND_PORT}/docs" echo echo " Logs:" echo " - $BACKEND_LOG" echo " - $BACKEND_ERR_LOG" echo " - $FRONTEND_LOG" echo " - $FRONTEND_ERR_LOG" echo "==========================================" echo echo "Press Ctrl+C to stop both services." echo while true; do if ! kill -0 "$BACKEND_PID" >/dev/null 2>&1; then echo "[ERROR] Backend process exited." echo "[ERROR] Check logs: $BACKEND_LOG $BACKEND_ERR_LOG" exit 1 fi if ! kill -0 "$FRONTEND_PID" >/dev/null 2>&1; then echo "[ERROR] Frontend process exited." echo "[ERROR] Check logs: $FRONTEND_LOG $FRONTEND_ERR_LOG" exit 1 fi sleep 2 done