#!/usr/bin/env sh set -eu if (set -o pipefail) >/dev/null 2>&1; then set -o pipefail fi export MSYS_NO_PATHCONV=1 SCRIPT_PATH="$0" case "$SCRIPT_PATH" in /*) ;; *) SCRIPT_PATH="$(pwd)/$SCRIPT_PATH" ;; esac SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)" cd "$SCRIPT_DIR" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" ROOT_ENV_FILE="$ROOT_DIR/.env" ROOT_ENV_EXAMPLE_FILE="$ROOT_DIR/.env.example" MODE="${1:-start}" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' info() { printf '%b\n' "${GREEN}[INFO]${NC} $*"; } warn() { printf '%b\n' "${YELLOW}[WARN]${NC} $*"; } error() { printf '%b\n' "${RED}[ERROR]${NC} $*"; exit 1; } is_container() { [ -f "/.dockerenv" ] && return 0 if [ -r /proc/1/cgroup ] && grep -Eq "(docker|containerd|kubepods)" /proc/1/cgroup 2>/dev/null; then return 0 fi return 1 } is_wsl() { grep -qi microsoft /proc/version 2>/dev/null } is_msys() { case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) return 0 ;; *) return 1 ;; esac } to_windows_path() { target_path="$1" if is_wsl && command -v wslpath >/dev/null 2>&1; then wslpath -w "$target_path" return 0 fi if is_msys && command -v cygpath >/dev/null 2>&1; then cygpath -aw "$target_path" return 0 fi printf '%s\n' "$target_path" } DEFAULT_VENV_DIR="$SCRIPT_DIR/.venv" DEFAULT_VENV_DISPLAY_PATH="./server/.venv" if [ -n "${SERVER_VENV_DIR:-}" ]; then VENV_DIR="$SERVER_VENV_DIR" VENV_DISPLAY_PATH="$SERVER_VENV_DIR" elif is_container; then VENV_DIR="/tmp/x-financial-server-venv" VENV_DISPLAY_PATH="$VENV_DIR" else VENV_DIR="$DEFAULT_VENV_DIR" VENV_DISPLAY_PATH="$DEFAULT_VENV_DISPLAY_PATH" fi if [ ! -f "$ROOT_ENV_FILE" ]; then if [ -f "$ROOT_ENV_EXAMPLE_FILE" ]; then warn "Root .env not found. Creating it from .env.example" cp "$ROOT_ENV_EXAMPLE_FILE" "$ROOT_ENV_FILE" else error "Root .env and .env.example are both missing." fi fi ENV_OVERRIDE_SERVER_HOST_SET=false ENV_OVERRIDE_SERVER_PORT_SET=false ENV_OVERRIDE_POSTGRES_HOST_SET=false ENV_OVERRIDE_DATABASE_URL_SET=false ENV_OVERRIDE_STARTUP_BOOTSTRAP_ENABLED_SET=false ENV_OVERRIDE_BACKGROUND_SCHEDULERS_ENABLED_SET=false ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=false ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=false ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=false ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=false PREFER_ENV_FILE_FOR_ONLYOFFICE=false case "${X_FINANCIAL_PREFER_ENV_FILE:-false}" in 1|true|TRUE|yes|YES|on|ON) PREFER_ENV_FILE_FOR_ONLYOFFICE=true ;; esac if [ "${SERVER_HOST+x}" = x ]; then ENV_OVERRIDE_SERVER_HOST_SET=true ENV_OVERRIDE_SERVER_HOST="$SERVER_HOST" fi if [ "${SERVER_PORT+x}" = x ]; then ENV_OVERRIDE_SERVER_PORT_SET=true ENV_OVERRIDE_SERVER_PORT="$SERVER_PORT" fi if [ "${POSTGRES_HOST+x}" = x ]; then ENV_OVERRIDE_POSTGRES_HOST_SET=true ENV_OVERRIDE_POSTGRES_HOST="$POSTGRES_HOST" fi if [ "${DATABASE_URL+x}" = x ]; then ENV_OVERRIDE_DATABASE_URL_SET=true ENV_OVERRIDE_DATABASE_URL="$DATABASE_URL" fi if [ "${STARTUP_BOOTSTRAP_ENABLED+x}" = x ]; then ENV_OVERRIDE_STARTUP_BOOTSTRAP_ENABLED_SET=true ENV_OVERRIDE_STARTUP_BOOTSTRAP_ENABLED="$STARTUP_BOOTSTRAP_ENABLED" fi if [ "${BACKGROUND_SCHEDULERS_ENABLED+x}" = x ]; then ENV_OVERRIDE_BACKGROUND_SCHEDULERS_ENABLED_SET=true ENV_OVERRIDE_BACKGROUND_SCHEDULERS_ENABLED="$BACKGROUND_SCHEDULERS_ENABLED" fi if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_ENABLED+x}" = x ]; then ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET=true ENV_OVERRIDE_ONLYOFFICE_ENABLED="$ONLYOFFICE_ENABLED" fi if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_PUBLIC_URL+x}" = x ]; then ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET=true ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL="$ONLYOFFICE_PUBLIC_URL" fi if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_BACKEND_URL+x}" = x ]; then ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET=true ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL="$ONLYOFFICE_BACKEND_URL" fi if [ "$PREFER_ENV_FILE_FOR_ONLYOFFICE" != true ] && [ "${ONLYOFFICE_JWT_SECRET+x}" = x ]; then ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET=true ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET="$ONLYOFFICE_JWT_SECRET" fi set -a . "$ROOT_ENV_FILE" set +a if [ "$ENV_OVERRIDE_SERVER_HOST_SET" = true ]; then SERVER_HOST="$ENV_OVERRIDE_SERVER_HOST" fi if [ "$ENV_OVERRIDE_SERVER_PORT_SET" = true ]; then SERVER_PORT="$ENV_OVERRIDE_SERVER_PORT" fi if [ "$ENV_OVERRIDE_POSTGRES_HOST_SET" = true ]; then POSTGRES_HOST="$ENV_OVERRIDE_POSTGRES_HOST" fi if [ "$ENV_OVERRIDE_DATABASE_URL_SET" = true ]; then DATABASE_URL="$ENV_OVERRIDE_DATABASE_URL" fi if [ "$ENV_OVERRIDE_STARTUP_BOOTSTRAP_ENABLED_SET" = true ]; then STARTUP_BOOTSTRAP_ENABLED="$ENV_OVERRIDE_STARTUP_BOOTSTRAP_ENABLED" fi if [ "$ENV_OVERRIDE_BACKGROUND_SCHEDULERS_ENABLED_SET" = true ]; then BACKGROUND_SCHEDULERS_ENABLED="$ENV_OVERRIDE_BACKGROUND_SCHEDULERS_ENABLED" fi if [ "$ENV_OVERRIDE_ONLYOFFICE_ENABLED_SET" = true ]; then ONLYOFFICE_ENABLED="$ENV_OVERRIDE_ONLYOFFICE_ENABLED" fi if [ "$ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL_SET" = true ]; then ONLYOFFICE_PUBLIC_URL="$ENV_OVERRIDE_ONLYOFFICE_PUBLIC_URL" fi if [ "$ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL_SET" = true ]; then ONLYOFFICE_BACKEND_URL="$ENV_OVERRIDE_ONLYOFFICE_BACKEND_URL" fi if [ "$ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET_SET" = true ]; then ONLYOFFICE_JWT_SECRET="$ENV_OVERRIDE_ONLYOFFICE_JWT_SECRET" fi SERVER_HOST="${SERVER_HOST:-0.0.0.0}" SERVER_PORT="${SERVER_PORT:-8000}" DEFAULT_SERVER_RELOAD="false" case "${APP_ENV:-local}" in local|dev|development) DEFAULT_SERVER_RELOAD="true" ;; esac if [ "${APP_DEBUG:-true}" = "true" ]; then DEFAULT_SERVER_RELOAD="true" fi if is_container; then DEFAULT_SERVER_RELOAD="false" fi SERVER_RELOAD="${SERVER_RELOAD:-$DEFAULT_SERVER_RELOAD}" SERVER_WORKERS="${SERVER_WORKERS:-${WEB_CONCURRENCY:-1}}" export SERVER_WORKERS needs_windows_python() { is_msys || is_wsl } find_unix_python() { if command -v python3 >/dev/null 2>&1; then echo "python3" return 0 fi if command -v python >/dev/null 2>&1; then echo "python" return 0 fi return 1 } find_windows_python() { if command -v py.exe >/dev/null 2>&1; then echo "py.exe -3" return 0 fi if command -v python.exe >/dev/null 2>&1; then echo "python.exe" return 0 fi return 1 } venv_python_path() { if [ -x "$VENV_DIR/Scripts/python.exe" ]; then echo "$VENV_DIR/Scripts/python.exe" return 0 fi if [ -x "$VENV_DIR/bin/python" ]; then echo "$VENV_DIR/bin/python" return 0 fi return 1 } run_bootstrap_python() { $PYTHON_BOOTSTRAP "$@" } dependencies_ready() { "$PYTHON_BIN" -c "import alembic, dotenv, email_validator, fastapi, jieba, jwt, lightrag, multipart, openpyxl, psycopg, pydantic_settings, qdrant_client, sqlalchemy, uvicorn" >/dev/null 2>&1 } pip_ready() { "$PYTHON_BIN" -m pip --version >/dev/null 2>&1 } create_venv() { info "Creating virtual environment at $VENV_DISPLAY_PATH" if [ -d "$VENV_DIR" ]; then rm -rf "$VENV_DIR" fi mkdir -p "$(dirname "$VENV_DIR")" venv_target="$VENV_DIR" if [ "$PYTHON_BOOTSTRAP_IS_WINDOWS" = "true" ]; then venv_target="$(to_windows_path "$VENV_DIR")" fi run_bootstrap_python -m venv "$venv_target" if ! PYTHON_BIN="$(venv_python_path)"; then error "Virtual environment was not created successfully." fi } ensure_pip() { if pip_ready; then return 0 fi warn "pip is missing in .venv, attempting repair" if "$PYTHON_BIN" -m ensurepip --upgrade >/dev/null 2>&1 && pip_ready; then info "pip restored successfully" return 0 fi warn "Recreating .venv because pip repair failed" rm -rf "$VENV_DIR" create_venv if "$PYTHON_BIN" -m ensurepip --upgrade >/dev/null 2>&1 && pip_ready; then info "pip restored after recreating .venv" return 0 fi error "pip could not be created inside .venv. Install Python with venv and pip support, then rerun the script." } ensure_python_bootstrap() { PYTHON_BOOTSTRAP_IS_WINDOWS="false" if needs_windows_python; then if find_windows_python >/dev/null 2>&1; then PYTHON_BOOTSTRAP="$(find_windows_python)" PYTHON_BOOTSTRAP_IS_WINDOWS="true" info "Detected Windows bash environment, using Windows Python" return 0 fi if find_unix_python >/dev/null 2>&1; then PYTHON_BOOTSTRAP="$(find_unix_python)" warn "Windows Python not found, falling back to system Python" return 0 fi error "Python is not available in PATH." fi if ! PYTHON_BOOTSTRAP="$(find_unix_python)"; then error "Python is not installed or not available in PATH. Install Python 3.11+ first so the script can create server/.venv automatically." fi } ensure_dependencies() { ensure_python_bootstrap if is_container && [ "$VENV_DIR" != "$DEFAULT_VENV_DIR" ]; then info "Docker runtime detected, using isolated Python environment at $VENV_DISPLAY_PATH" fi if ! PYTHON_BIN="$(venv_python_path)"; then warn "Python virtual environment not found" create_venv fi ensure_pip if dependencies_ready; then info "Server dependencies are ready." return 0 fi warn "Server dependencies are missing or incomplete" info "Running .venv Python dependency installation" "$PYTHON_BIN" -m pip install --upgrade pip "$PYTHON_BIN" -m pip install -e ".[dev]" if ! dependencies_ready; then error "Server dependencies are still incomplete after installation." fi info "Server dependencies are ready." } start_server() { info "Starting FastAPI server..." info "Access: http://$SERVER_HOST:$SERVER_PORT" echo "" if [ "$SERVER_RELOAD" = "true" ]; then exec "$PYTHON_BIN" -m uvicorn app.main:app --reload --app-dir src --host "$SERVER_HOST" --port "$SERVER_PORT" fi if [ "$SERVER_WORKERS" -gt 1 ] 2>/dev/null; then BACKGROUND_SCHEDULERS_ENABLED="${BACKGROUND_SCHEDULERS_ENABLED:-false}" export BACKGROUND_SCHEDULERS_ENABLED exec "$PYTHON_BIN" -m uvicorn app.main:app --app-dir src --host "$SERVER_HOST" --port "$SERVER_PORT" --workers "$SERVER_WORKERS" fi exec "$PYTHON_BIN" -m uvicorn app.main:app --app-dir src --host "$SERVER_HOST" --port "$SERVER_PORT" } case "$MODE" in deps) ensure_dependencies ;; start) ensure_dependencies start_server ;; *) error "Unknown mode: $MODE. Use one of: deps, start" ;; esac