#!/usr/bin/env bash set -euo pipefail APP_PUBLIC_PORT="${APP_PUBLIC_PORT:-8080}" APP_FRONTEND_PORT="${APP_FRONTEND_PORT:-3000}" APP_BACKEND_PORT="${APP_BACKEND_PORT:-18080}" UVICORN_LOOP="${UVICORN_LOOP:-asyncio}" UVICORN_HTTP="${UVICORN_HTTP:-h11}" UVICORN_WS="${UVICORN_WS:-websockets}" BEAVER_HOME="${BEAVER_HOME:-/root/.beaver}" BEAVER_CONFIG_PATH="${BEAVER_CONFIG_PATH:-$BEAVER_HOME/config.json}" BEAVER_WORKSPACE="${BEAVER_WORKSPACE:-$BEAVER_HOME/workspace}" BEAVER_AUTH_FILE="${BEAVER_AUTH_FILE:-$BEAVER_HOME/web_auth_users.json}" BEAVER_RUNTIME_ENV_FILE="${BEAVER_RUNTIME_ENV_FILE:-$BEAVER_HOME/runtime.env}" BEAVER_INITIAL_SKILLS_DIR="${BEAVER_INITIAL_SKILLS_DIR:-/opt/app/initial-skills}" BEAVER_INITIAL_SKILLS_EXCLUDE="${BEAVER_INITIAL_SKILLS_EXCLUDE:-officebench-mcp}" log() { printf '[app-instance] %s\n' "$*" } require_file() { local path="$1" local message="$2" if [[ ! -f "$path" ]]; then printf '[app-instance] %s: %s\n' "$message" "$path" >&2 exit 1 fi } seed_initial_skills() { local initial_skills_dir="$1" local target_dir="$2" if [[ ! -d "$initial_skills_dir" ]]; then return fi if [[ ! -f "$initial_skills_dir/_index/published.json" ]]; then log "initial skills source has no published index, skipping: ${initial_skills_dir}" return fi mkdir -p "$target_dir" INITIAL_SKILLS_DIR="$initial_skills_dir" TARGET_DIR="$target_dir" INITIAL_SKILLS_EXCLUDE="$BEAVER_INITIAL_SKILLS_EXCLUDE" python - <<'PY' import json import os import shutil from pathlib import Path initial = Path(os.environ["INITIAL_SKILLS_DIR"]).resolve() target = Path(os.environ["TARGET_DIR"]).resolve() excluded = {item.strip() for item in os.environ.get("INITIAL_SKILLS_EXCLUDE", "").split(",") if item.strip()} for child in sorted(initial.iterdir()): if child.name.startswith(".") or child.name in excluded: continue destination = target / child.name if destination.exists(): continue if child.is_dir(): shutil.copytree(child, destination) elif child.is_file(): shutil.copy2(child, destination) for index_name in ("published", "disabled"): initial_index = initial / "_index" / f"{index_name}.json" target_index = target / "_index" / f"{index_name}.json" if not initial_index.exists(): continue try: initial_items = json.loads(initial_index.read_text(encoding="utf-8")).get("items", []) except json.JSONDecodeError: initial_items = [] if target_index.exists(): try: target_items = json.loads(target_index.read_text(encoding="utf-8")).get("items", []) except json.JSONDecodeError: target_items = [] else: target_items = [] merged = [] for item in [*target_items, *initial_items]: text = str(item).strip() if text in excluded: continue if text and text not in merged: merged.append(text) target_index.parent.mkdir(parents=True, exist_ok=True) target_index.write_text(json.dumps({"items": merged}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") PY } cleanup() { local status=$? if [[ -n "${NGINX_PID:-}" ]]; then kill "${NGINX_PID}" 2>/dev/null || true fi if [[ -n "${FRONTEND_PID:-}" ]]; then kill "${FRONTEND_PID}" 2>/dev/null || true fi if [[ -n "${BACKEND_PID:-}" ]]; then kill "${BACKEND_PID}" 2>/dev/null || true fi wait 2>/dev/null || true exit "$status" } trap cleanup EXIT INT TERM mkdir -p "$BEAVER_HOME" "$BEAVER_WORKSPACE" if [[ -f "$BEAVER_RUNTIME_ENV_FILE" ]]; then set -a . "$BEAVER_RUNTIME_ENV_FILE" set +a fi require_file "$BEAVER_CONFIG_PATH" "Missing Beaver config" seed_initial_skills "$BEAVER_INITIAL_SKILLS_DIR" "$BEAVER_WORKSPACE/skills" export BEAVER_AUTH_FILE export BEAVER_RUNTIME_ENV_FILE export BEAVER_HOME export BEAVER_CONFIG_PATH export BEAVER_WORKSPACE export BEAVER_INITIAL_SKILLS_DIR export BEAVER_INITIAL_SKILLS_EXCLUDE export PORT="$APP_FRONTEND_PORT" export HOSTNAME="127.0.0.1" export PYTHONFAULTHANDLER="${PYTHONFAULTHANDLER:-1}" log "starting Beaver backend on 127.0.0.1:${APP_BACKEND_PORT} (loop=${UVICORN_LOOP}, http=${UVICORN_HTTP}, ws=${UVICORN_WS})" ( cd /opt/app/backend python -m uvicorn "beaver.interfaces.web.app:create_app" --factory --host 127.0.0.1 --port "$APP_BACKEND_PORT" --loop "$UVICORN_LOOP" --http "$UVICORN_HTTP" --ws "$UVICORN_WS" ) & BACKEND_PID=$! log "starting frontend on 127.0.0.1:${APP_FRONTEND_PORT}" ( cd /opt/app/frontend node server.js ) & FRONTEND_PID=$! log "starting nginx on 0.0.0.0:${APP_PUBLIC_PORT}" nginx -c /opt/app/nginx.conf -g 'daemon off;' & NGINX_PID=$! wait -n "$BACKEND_PID" "$FRONTEND_PID" "$NGINX_PID"