#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" PID_FILE="$PROJECT_DIR/.server.pid" LOG_FILE="$PROJECT_DIR/logs/server.log" # Optional project-root .env (gitignored; setup.sh writes credentials here, # operators can add e.g. HOST=0.0.0.0 for reverse-proxy deployments so a bare # `server.sh restart` keeps the bind address). Lines are `KEY=value` or # `export KEY='value'`. Precedence: explicit environment > .env > defaults, # so `HOST=127.0.0.1 scripts/server.sh restart` still overrides the file. if [[ -f "$PROJECT_DIR/.env" ]]; then # Values are parsed LITERALLY (no eval/source): a value is either bare, or # single-quoted in setup.sh's format ('\'' encodes a literal quote), or # double-quoted (quotes stripped, contents kept literal — no $ expansion). # One KEY=value per line; full-line comments only. # # Keys this loader itself set from .env are tracked space-delimited (keys are # validated identifiers so this is unambiguous — no Bash-4 associative # arrays, macOS ships bash 3.2). Only EXTERNAL pre-set environment is # protected; duplicate keys within .env keep normal source semantics (last # line wins) because earlier lines are recorded here and may be overridden. _envfile_set=" " _q=\' _esc=$'\x01' while IFS= read -r _line || [[ -n "$_line" ]]; do _line="${_line#"${_line%%[![:space:]]*}"}" # ltrim [[ -z "$_line" || "$_line" == \#* ]] && continue _kv="${_line#export }" _key="${_kv%%=*}" [[ "$_key" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] || continue [[ "$_kv" == *=* ]] || continue _val="${_kv#*=}" case "$_val" in "$_q"*) _val="${_val//${_q}\\${_q}${_q}/${_esc}}" # '\'' -> literal-quote marker _val="${_val//${_q}/}" # drop quoting quotes _val="${_val//${_esc}/${_q}}" # restore literal quotes ;; \"*\") _val="${_val#\"}" _val="${_val%\"}" ;; esac if [[ "$_envfile_set" == *" $_key "* || -z "${!_key+x}" ]]; then export "$_key=$_val" _envfile_set="${_envfile_set}${_key} " fi done < "$PROJECT_DIR/.env" unset _line _kv _key _val _envfile_set _q _esc fi PORT="${PORT:-9876}" cd "$PROJECT_DIR" usage() { echo "Usage: $0 {start|stop|restart|status|logs}" exit 1 } is_running() { if [[ -f "$PID_FILE" ]]; then local pid pid=$(cat "$PID_FILE") if kill -0 "$pid" 2>/dev/null; then return 0 fi rm -f "$PID_FILE" fi return 1 } do_start() { if is_running; then echo "Server already running (PID $(cat "$PID_FILE"))" return 0 fi # Surface stray maestro processes the pidfile doesn't track (e.g. an orphaned # run on another port/mode that a previous restart never stopped). The # app-level worker lock (src/instance-lock.ts) will refuse to double-start # worker mode against the same DB, but warn here so the operator notices. local strays strays=$(pgrep -f "node dist/main.js" 2>/dev/null | grep -vx "$$" || true) if [[ -n "$strays" ]]; then echo "WARNING: other 'node dist/main.js' process(es) detected (PID: $(echo "$strays" | tr '\n' ' '))." echo " If a previous run was not stopped cleanly, stop it first to avoid port/DB conflicts." fi mkdir -p "$(dirname "$LOG_FILE")" echo "Checking runtime dependencies..." "$SCRIPT_DIR/prepare.sh" echo "Building..." npm run build --silent 2>&1 | tail -1 echo "Starting server on port $PORT..." # AAO is a single binary with two modes (dist/main.js dispatches): # AAO_MODE=worker (default) — full orchestrator (DB + bridge API + workers) # AAO_MODE=gateway — OpenAI-compatible LLM gateway only # To launch as a gateway: `AAO_MODE=gateway scripts/server.sh start` and # set `gateway.listen_port` in config.yaml (default 4000). dist/index.js # is preserved as a worker-mode shim for legacy paths. PORT="$PORT" AAO_MODE="${AAO_MODE:-worker}" nohup node dist/main.js >> "$LOG_FILE" 2>&1 & local pid=$! echo "$pid" > "$PID_FILE" # Wait briefly and verify it started sleep 2 if kill -0 "$pid" 2>/dev/null; then echo "Server started (PID $pid, log: $LOG_FILE)" else rm -f "$PID_FILE" echo "Server failed to start. Check $LOG_FILE" tail -5 "$LOG_FILE" return 1 fi } do_stop() { if ! is_running; then echo "Server not running" # Also kill any stray process on the port local stray stray=$(lsof -ti:"$PORT" 2>/dev/null || true) if [[ -n "$stray" ]]; then echo "Found stray process on port $PORT (PID $stray), killing..." kill "$stray" 2>/dev/null || true fi return 0 fi local pid pid=$(cat "$PID_FILE") echo "Stopping server (PID $pid)..." kill "$pid" 2>/dev/null || true # Wait for graceful shutdown for i in {1..10}; do if ! kill -0 "$pid" 2>/dev/null; then rm -f "$PID_FILE" echo "Server stopped" return 0 fi sleep 0.5 done # Force kill echo "Force killing..." kill -9 "$pid" 2>/dev/null || true rm -f "$PID_FILE" echo "Server stopped (forced)" } do_status() { if is_running; then local pid pid=$(cat "$PID_FILE") echo "Server running (PID $pid, port $PORT)" else echo "Server not running" fi } do_logs() { if [[ -f "$LOG_FILE" ]]; then tail -f "$LOG_FILE" else echo "No log file found at $LOG_FILE" fi } case "${1:-}" in start) do_start ;; stop) do_stop ;; restart) do_stop; do_start ;; status) do_status ;; logs) do_logs ;; *) usage ;; esac