diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..22ed738 --- /dev/null +++ b/setup.sh @@ -0,0 +1,204 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BACKEND_DIR="$SCRIPT_DIR/backend" +FRONTEND_DIR="$SCRIPT_DIR/frontend" +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 + +print_header() { + echo "==========================================" + echo " Jarvis - Setup" + echo "==========================================" + echo +} + +require_command() { + local command_name="$1" + local hint="$2" + if ! command -v "$command_name" >/dev/null 2>&1; then + echo "[ERROR] $command_name was not found." + echo "[ERROR] $hint" + exit 1 + fi +} + +ensure_backend_env() { + if [[ -f "$BACKEND_ENV" ]]; then + echo "[OK] Backend env file exists: $BACKEND_ENV" + 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] Edit $BACKEND_ENV before running start.sh if needed." + return 0 + fi + + echo "[ERROR] Could not create backend/.env" + exit 1 +} + +sync_frontend_env() { + local backend_host + local backend_port + + backend_host="$(grep '^HOST=' "$BACKEND_ENV" | cut -d'=' -f2- | tr -d '\r' || true)" + backend_port="$(grep '^PORT=' "$BACKEND_ENV" | cut -d'=' -f2- | tr -d '\r' || true)" + + [[ -z "$backend_host" ]] && backend_host="127.0.0.1" + [[ -z "$backend_port" ]] && backend_port="3337" + + printf 'VITE_API_URL=http://%s:%s\n' "$backend_host" "$backend_port" > "$FRONTEND_ENV_LOCAL" + echo "[OK] Synced frontend/.env.local -> http://$backend_host:$backend_port" +} + +print_header + +require_command npm "Install Node.js and npm first." + +if [[ "$USE_WINDOWS_TOOLS" == true ]]; then + if command -v py.exe >/dev/null 2>&1; then + PYTHON_BOOTSTRAP=("py.exe" "-3") + elif command -v python.exe >/dev/null 2>&1; then + PYTHON_BOOTSTRAP=("python.exe") + elif command -v python >/dev/null 2>&1; then + PYTHON_BOOTSTRAP=("python") + else + echo "[ERROR] Python was not found." + echo "[ERROR] Install Python 3.12+ for Windows first." + exit 1 + fi +else + if command -v python3 >/dev/null 2>&1; then + PYTHON_BOOTSTRAP=("python3") + elif command -v python >/dev/null 2>&1; then + PYTHON_BOOTSTRAP=("python") + else + echo "[ERROR] python3/python was not found." + echo "[ERROR] Install Python 3.12+ first." + exit 1 + fi +fi + +install_backend_dependencies() { + if command -v uv >/dev/null 2>&1; then + echo "[INFO] Using uv to install backend dependencies..." + ( + cd "$BACKEND_DIR" + uv sync + ) + return 0 + fi + + echo "[INFO] uv was not found. Falling back to venv + pip..." + ( + cd "$BACKEND_DIR" + if ! "${PYTHON_BOOTSTRAP[@]}" -m venv .venv; then + echo "[WARN] python -m venv failed. Trying virtualenv fallback..." + "${PYTHON_BOOTSTRAP[@]}" -m pip install --user virtualenv + "${PYTHON_BOOTSTRAP[@]}" -m virtualenv .venv + fi + if [[ "$USE_WINDOWS_TOOLS" == true ]]; then + .venv/Scripts/python.exe -m pip install --upgrade pip + .venv/Scripts/python.exe -m pip install -e . + else + .venv/bin/python -m pip install --upgrade pip + .venv/bin/python -m pip install -e . + fi + ) +} + +install_frontend_dependencies() { + if [[ "$USE_WINDOWS_TOOLS" == true ]]; then + ( + cd "$FRONTEND_DIR" + if [[ -f "package-lock.json" ]]; then + cmd.exe /c npm ci + else + cmd.exe /c npm install + fi + ) + else + ( + cd "$FRONTEND_DIR" + if [[ -f "package-lock.json" ]]; then + npm ci + else + npm install + fi + ) + fi + + if [[ -f "$FRONTEND_DIR/node_modules/.bin/vite" || -f "$FRONTEND_DIR/node_modules/.bin/vite.cmd" ]]; then + return 0 + fi + + echo "[ERROR] Frontend dependencies were installed, but vite was not found." + echo "[ERROR] Check frontend/node_modules and npm output." + exit 1 +} + +echo "[1/4] Prepare backend environment file..." +ensure_backend_env + +echo +echo "[2/4] Install backend dependencies..." +install_backend_dependencies + +if [[ "$USE_WINDOWS_TOOLS" == true ]]; then + if [[ ! -f "$BACKEND_PYTHON" ]]; then + echo "[ERROR] Backend virtual environment Python was not found after setup: $BACKEND_PYTHON" + exit 1 + fi +else + if [[ ! -x "$BACKEND_PYTHON" ]]; then + echo "[ERROR] Backend virtual environment Python was not found after setup: $BACKEND_PYTHON" + exit 1 + fi +fi + +echo "[OK] Backend dependencies installed." + +echo +echo "[3/4] Install frontend dependencies..." +install_frontend_dependencies +echo "[OK] Frontend dependencies installed." + +echo +echo "[4/4] Sync frontend API configuration..." +sync_frontend_env + +echo +echo "==========================================" +echo " Setup complete" +echo +echo " Backend env: $BACKEND_ENV" +echo " Frontend env: $FRONTEND_ENV_LOCAL" +echo +echo " Next: bash start.sh" +echo "==========================================" diff --git a/start.sh b/start.sh index b132cc6..51ac5f8 100644 --- a/start.sh +++ b/start.sh @@ -5,24 +5,48 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" BACKEND_DIR="$SCRIPT_DIR/backend" FRONTEND_DIR="$SCRIPT_DIR/frontend" LOG_DIR="$SCRIPT_DIR/logs" -PROJECT_ENV="$SCRIPT_DIR/.env" +ROOT_ENV="$SCRIPT_DIR/.env" +BACKEND_ENV="$BACKEND_DIR/.env" +BACKEND_ENV_EXAMPLE="$BACKEND_DIR/.env.example" FRONTEND_ENV_LOCAL="$FRONTEND_DIR/.env.local" -BACKEND_PYTHON="$BACKEND_DIR/.venv/Scripts/python.exe" -FRONTEND_VITE="$FRONTEND_DIR/node_modules/.bin/vite.cmd" +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 -if [[ "${1:-}" == "--kill-port" ]]; then - KILL_PORT=true -elif [[ $# -gt 0 ]]; then - echo "[ERROR] Unsupported argument: $1" - echo "[ERROR] Usage: bash start.sh [--kill-port]" - exit 1 -fi +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" @@ -40,6 +64,8 @@ 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 @@ -47,13 +73,74 @@ to_windows_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 - pids="$(powershell.exe -NoProfile -Command '& { - Get-NetTCPConnection -LocalPort '"$1"' -State Listen -ErrorAction SilentlyContinue | - Select-Object -ExpandProperty OwningProcess -Unique - }' | tr -d '\r' || true)" + 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 @@ -61,15 +148,14 @@ kill_port_process() { while IFS= read -r pid; do [[ -z "$pid" ]] && continue - powershell.exe -NoProfile -Command "Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue" >/dev/null 2>&1 || true - cmd.exe /c "taskkill /PID $pid /F" >/dev/null 2>&1 || true + kill "$pid" >/dev/null 2>&1 || true done <<< "$pids" } kill_process_tree() { local pid="$1" [[ -z "$pid" ]] && return 0 - cmd.exe /c "taskkill /PID $pid /T /F" >/dev/null 2>&1 || true + kill "$pid" >/dev/null 2>&1 || true } cleanup() { @@ -81,72 +167,84 @@ cleanup() { if [[ -n "${BACKEND_PID:-}" ]]; then kill_process_tree "$BACKEND_PID" fi - if [[ -n "${BACKEND_PORT:-}" ]]; then - kill_port_process "$BACKEND_PORT" - fi exit "$exit_code" } trap cleanup INT TERM EXIT echo "==========================================" -echo " Jarvis - Quick Start" +echo " Jarvis - Start" echo "==========================================" echo -if [[ ! -x "$BACKEND_PYTHON" ]]; then - echo "[ERROR] backend/.venv/Scripts/python.exe was not found." - echo "[ERROR] Create the backend virtual environment first." +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 [[ ! -f "$FRONTEND_VITE" ]]; then - echo "[ERROR] frontend/node_modules/.bin/vite.cmd was not found." - echo "[ERROR] Install frontend dependencies first." - exit 1 +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 ! command -v powershell.exe >/dev/null 2>&1; then +if [[ "$USE_WINDOWS_TOOLS" == true ]] && ! command -v powershell.exe >/dev/null 2>&1; then echo "[ERROR] powershell.exe was not found." exit 1 fi -if [[ ! -f "$PROJECT_ENV" ]]; then - echo "[INFO] .env was not found in the project root." - echo "[INFO] Create it before first run." - echo -fi +BACKEND_HOST="$(read_env_value HOST "$BACKEND_ENV")" +BACKEND_PORT="$(read_env_value PORT "$BACKEND_ENV")" -echo "[1/3] Check backend environment..." -echo "[OK] Backend virtual environment is available." - -echo -echo "[2/3] Check frontend dependencies..." -if [[ ! -x "$FRONTEND_DIR/node_modules/.bin/vite" && ! -x "$FRONTEND_DIR/node_modules/.bin/vite.cmd" ]]; then - echo "[ERROR] frontend dependencies are missing." - echo "[ERROR] Run: cd frontend && npm install" - exit 1 -fi -echo "[OK] Frontend dependencies are available." - -BACKEND_HOST="127.0.0.1" -BACKEND_PORT="" -if [[ -f "$PROJECT_ENV" ]]; then - ENV_HOST="$(grep '^HOST=' "$PROJECT_ENV" | cut -d'=' -f2- | tr -d '\r' || true)" - ENV_PORT="$(grep '^PORT=' "$PROJECT_ENV" | cut -d'=' -f2- | tr -d '\r' || true)" - if [[ -n "$ENV_HOST" ]]; then BACKEND_HOST="$ENV_HOST"; fi - if [[ -n "$ENV_PORT" ]]; then BACKEND_PORT="$ENV_PORT"; fi -fi +[[ -z "$BACKEND_HOST" ]] && BACKEND_HOST="127.0.0.1" if [[ -z "$BACKEND_PORT" ]]; then - echo "[ERROR] PORT was not found in .env." - echo "[ERROR] Set PORT in the project root .env and run start.sh again." + echo "[ERROR] PORT was not found in $BACKEND_ENV" + echo "[ERROR] Set PORT and run start.sh again." exit 1 fi -FRONTEND_API_URL="http://${BACKEND_HOST}:${BACKEND_PORT}" -printf 'VITE_API_URL=%s\n' "$FRONTEND_API_URL" > "$FRONTEND_ENV_LOCAL" +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 @@ -158,67 +256,72 @@ fi if port_in_use "$BACKEND_PORT"; then echo "[ERROR] Port ${BACKEND_PORT} is already in use." - echo "[ERROR] Stop the existing service first, then run start.sh again." + echo "[ERROR] Stop the existing service first, or run: bash start.sh --kill-port" exit 1 fi -BACKEND_WIN_DIR="$(to_windows_path "$BACKEND_DIR")" -BACKEND_WIN_LOG="$(to_windows_path "$BACKEND_LOG")" -BACKEND_WIN_ERR_LOG="$(to_windows_path "$BACKEND_ERR_LOG")" -BACKEND_WIN_PYTHON="$(to_windows_path "$BACKEND_PYTHON")" -BACKEND_PID="$(powershell.exe -NoProfile -Command "& { - \$process = Start-Process -FilePath '$BACKEND_WIN_PYTHON' -ArgumentList '-m','uvicorn','app.main:app','--reload','--host','$BACKEND_HOST','--port','$BACKEND_PORT' -WorkingDirectory '$BACKEND_WIN_DIR' -RedirectStandardOutput '$BACKEND_WIN_LOG' -RedirectStandardError '$BACKEND_WIN_ERR_LOG' -PassThru - \$process.Id -}" | tr -d '\r')" +( + cd "$BACKEND_DIR" + exec "$BACKEND_PYTHON" "${BACKEND_ARGS[@]}" +) >"$BACKEND_LOG" 2>"$BACKEND_ERR_LOG" & +BACKEND_PID="$!" echo "Waiting for backend..." sleep 5 if ! port_in_use "$BACKEND_PORT"; then echo "[ERROR] Backend did not start successfully." - echo "[ERROR] Check log: $BACKEND_LOG" + echo "[ERROR] Check logs: $BACKEND_LOG $BACKEND_ERR_LOG" exit 1 fi -echo "Starting frontend on port 5173..." -FRONTEND_WIN_DIR="$(to_windows_path "$FRONTEND_DIR")" -FRONTEND_WIN_LOG="$(to_windows_path "$FRONTEND_LOG")" -FRONTEND_WIN_ERR_LOG="$(to_windows_path "$FRONTEND_ERR_LOG")" -FRONTEND_WIN_VITE="$(to_windows_path "$FRONTEND_VITE")" -FRONTEND_PID="$(powershell.exe -NoProfile -Command "& { - \$process = Start-Process -FilePath '$FRONTEND_WIN_VITE' -ArgumentList '--host','0.0.0.0' -WorkingDirectory '$FRONTEND_WIN_DIR' -RedirectStandardOutput '$FRONTEND_WIN_LOG' -RedirectStandardError '$FRONTEND_WIN_ERR_LOG' -PassThru - \$process.Id -}" | tr -d '\r')" +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 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://localhost:5173" -echo " API docs: http://${BACKEND_HOST}:${BACKEND_PORT}/docs" +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 - backend_alive="$(powershell.exe -NoProfile -Command "Get-Process -Id $BACKEND_PID -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id" | tr -d '\r')" - frontend_alive="$(powershell.exe -NoProfile -Command "Get-Process -Id $FRONTEND_PID -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id" | tr -d '\r')" - - if [[ -z "$backend_alive" ]]; then + if ! kill -0 "$BACKEND_PID" >/dev/null 2>&1; then echo "[ERROR] Backend process exited." - echo "[ERROR] Check log: $BACKEND_LOG" + echo "[ERROR] Check logs: $BACKEND_LOG $BACKEND_ERR_LOG" exit 1 fi - if [[ -z "$frontend_alive" ]]; then + if ! kill -0 "$FRONTEND_PID" >/dev/null 2>&1; then echo "[ERROR] Frontend process exited." - echo "[ERROR] Check log: $FRONTEND_LOG" + echo "[ERROR] Check logs: $FRONTEND_LOG $FRONTEND_ERR_LOG" exit 1 fi