309 lines
13 KiB
Bash
Executable File
309 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Start the Den server stack inside a Daytona sandbox.
|
|
# Services: MySQL, Den API, and Den Web.
|
|
|
|
if [ -n "${OPENWORK_WORKSPACE_DIR:-}" ]; then
|
|
REPO_DIR="$OPENWORK_WORKSPACE_DIR"
|
|
elif [ -f /workspace/package.json ]; then
|
|
REPO_DIR="/workspace"
|
|
else
|
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
fi
|
|
|
|
cd "$REPO_DIR"
|
|
|
|
DEN_API_PORT="${DEN_API_PORT:-8788}"
|
|
DEN_WEB_PORT="${DEN_WEB_PORT:-3005}"
|
|
PNPM_STORE="${PNPM_STORE:-$REPO_DIR/.openwork-daytona/pnpm-store}"
|
|
|
|
DEN_API_PUBLIC_URL="${DEN_API_PUBLIC_URL:-http://localhost:$DEN_API_PORT}"
|
|
DEN_WEB_PUBLIC_URL="${DEN_WEB_PUBLIC_URL:-http://localhost:$DEN_WEB_PORT}"
|
|
DEN_WEB_PUBLIC_HOST="${DEN_WEB_PUBLIC_URL#http://}"
|
|
DEN_WEB_PUBLIC_HOST="${DEN_WEB_PUBLIC_HOST#https://}"
|
|
DEN_WEB_PUBLIC_HOST="${DEN_WEB_PUBLIC_HOST%%/*}"
|
|
|
|
export OPENWORK_DEV_MODE="${OPENWORK_DEV_MODE:-1}"
|
|
export DEN_ORG_MODE="${DEN_ORG_MODE:-multi_org}"
|
|
# Eval sign-ups must not depend on the HIBP API.
|
|
export DEN_PASSWORD_BREACH_SCREENING_ENABLED="${DEN_PASSWORD_BREACH_SCREENING_ENABLED:-false}"
|
|
export DEN_GENERATED_ARTIFACT_VIEWS_ENABLED="${DEN_GENERATED_ARTIFACT_VIEWS_ENABLED:-false}"
|
|
export DATABASE_URL="${DATABASE_URL:-mysql://root:password@127.0.0.1:3306/openwork_den}"
|
|
export DEN_DB_ENCRYPTION_KEY="${DEN_DB_ENCRYPTION_KEY:-daytona-den-db-encryption-key-please-change-1234567890}"
|
|
export BETTER_AUTH_SECRET="${BETTER_AUTH_SECRET:-daytona-den-auth-secret-please-change-1234567890}"
|
|
export BETTER_AUTH_URL="${BETTER_AUTH_URL:-$DEN_WEB_PUBLIC_URL}"
|
|
export DEN_BETTER_AUTH_URL="$BETTER_AUTH_URL"
|
|
export DEN_API_PUBLIC_URL
|
|
export DEN_MCP_RESOURCE_URL="${DEN_MCP_RESOURCE_URL:-$DEN_API_PUBLIC_URL/mcp}"
|
|
# DEN_API_BASE is the externally reachable Den API origin (see
|
|
# packages/docs/start-here/private-network-deployment.mdx): Den Web publishes
|
|
# it through /api/runtime-config as the desktop's denApiUrl. Desktops run in
|
|
# other sandboxes and can only reach the public preview URL, never this
|
|
# sandbox's loopback.
|
|
export DEN_API_BASE="${DEN_API_BASE:-${DEN_API_PUBLIC_URL:-http://127.0.0.1:$DEN_API_PORT}}"
|
|
export DEN_AUTH_ORIGIN="${DEN_AUTH_ORIGIN:-$DEN_WEB_PUBLIC_URL}"
|
|
export DEN_AUTH_FALLBACK_BASE="${DEN_AUTH_FALLBACK_BASE:-http://127.0.0.1:$DEN_API_PORT}"
|
|
export NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL="${NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL:-$DEN_WEB_PUBLIC_URL}"
|
|
export DEN_PROVISIONER_MODE="${DEN_PROVISIONER_MODE:-stub}"
|
|
export DEN_WORKER_URL_TEMPLATE="${DEN_WORKER_URL_TEMPLATE:-https://workers.local/{workerId}}"
|
|
export DEN_WEB_ALLOWED_DEV_ORIGINS="${DEN_WEB_ALLOWED_DEV_ORIGINS:-$DEN_WEB_PUBLIC_HOST}"
|
|
|
|
DEFAULT_ORIGINS="$DEN_WEB_PUBLIC_URL,$DEN_API_PUBLIC_URL,http://localhost:$DEN_WEB_PORT,http://127.0.0.1:$DEN_WEB_PORT,http://localhost:$DEN_API_PORT,http://127.0.0.1:$DEN_API_PORT"
|
|
export CORS_ORIGINS="${CORS_ORIGINS:-$DEFAULT_ORIGINS}"
|
|
|
|
# Daytona mints a fresh preview hostname on every preview-url call, so any
|
|
# origin baked at boot goes stale immediately. Trust the preview proxy domain
|
|
# by wildcard (better-auth supports wildcard trusted origins) so rotated
|
|
# preview URLs keep working. Local hosts produce no wildcard.
|
|
PREVIEW_PROXY_HOST="${DEN_WEB_PUBLIC_URL#http://}"
|
|
PREVIEW_PROXY_HOST="${PREVIEW_PROXY_HOST#https://}"
|
|
PREVIEW_PROXY_HOST="${PREVIEW_PROXY_HOST%%/*}"
|
|
PREVIEW_PROXY_WILDCARD=""
|
|
case "$PREVIEW_PROXY_HOST" in
|
|
localhost*|127.*|0.0.0.0*|\[*) ;;
|
|
*.*.*)
|
|
PREVIEW_PROXY_WILDCARD="https://*.${PREVIEW_PROXY_HOST#*.}"
|
|
;;
|
|
esac
|
|
# Preview hosts are not app.*: tell den-api the preview domain serves den-web
|
|
# so desktop handoff links point at the den-web /api/den proxy.
|
|
export DEN_WEB_APP_HOSTS="${DEN_WEB_APP_HOSTS:-${PREVIEW_PROXY_WILDCARD:+.${PREVIEW_PROXY_HOST#*.}}}"
|
|
export DEN_BETTER_AUTH_TRUSTED_ORIGINS="${DEN_BETTER_AUTH_TRUSTED_ORIGINS:-$CORS_ORIGINS${PREVIEW_PROXY_WILDCARD:+,$PREVIEW_PROXY_WILDCARD}}"
|
|
|
|
run_root() {
|
|
if [ "$(id -u)" -eq 0 ]; then
|
|
"$@"
|
|
elif command -v sudo >/dev/null 2>&1; then
|
|
sudo "$@"
|
|
else
|
|
echo "ERROR: root privileges are required to start MySQL." >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
MYSQL_ROOT_CMD=(run_root mysql -uroot)
|
|
|
|
wait_for_http() {
|
|
local url="$1"
|
|
local label="$2"
|
|
local max_wait="${3:-180}"
|
|
local elapsed=0
|
|
|
|
while [ "$elapsed" -lt "$max_wait" ]; do
|
|
if curl -sf "$url" >/dev/null 2>&1; then
|
|
echo "==> $label ready after ${elapsed}s"
|
|
return 0
|
|
fi
|
|
sleep 5
|
|
elapsed=$((elapsed + 5))
|
|
done
|
|
|
|
echo "ERROR: $label did not become ready at $url" >&2
|
|
return 1
|
|
}
|
|
|
|
echo "==> Starting MySQL..."
|
|
run_root service mysql start >/tmp/openwork-mysql-service.log 2>&1 || run_root service mariadb start >/tmp/openwork-mysql-service.log 2>&1
|
|
|
|
for _ in $(seq 1 60); do
|
|
if mysql -uroot -ppassword -e "SELECT 1" >/dev/null 2>&1; then
|
|
MYSQL_ROOT_CMD=(mysql -uroot -ppassword)
|
|
break
|
|
fi
|
|
if run_root mysql -uroot -e "SELECT 1" >/dev/null 2>&1; then
|
|
MYSQL_ROOT_CMD=(run_root mysql -uroot)
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
|
|
"${MYSQL_ROOT_CMD[@]}" <<'SQL'
|
|
CREATE DATABASE IF NOT EXISTS openwork_den;
|
|
ALTER USER 'root'@'localhost' IDENTIFIED BY 'password';
|
|
CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'password';
|
|
GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION;
|
|
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
|
|
FLUSH PRIVILEGES;
|
|
SQL
|
|
|
|
echo "==> Installing dependencies if needed..."
|
|
mkdir -p "$PNPM_STORE" .openwork-daytona
|
|
baseline=.openwork-daytona/pnpm-lock.sha256
|
|
current="$(sha256sum pnpm-lock.yaml | cut -d " " -f 1)"
|
|
# The server stack never runs Electron or browser automation; skip their
|
|
# binary downloads on reinstalls too.
|
|
INSTALL_ENV=(CI=1 ELECTRON_SKIP_BINARY_DOWNLOAD=1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 PUPPETEER_SKIP_DOWNLOAD=1 CYPRESS_INSTALL_BINARY=0)
|
|
if [ ! -d node_modules ] || [ ! -f "$baseline" ] || [ "$(cat "$baseline")" != "$current" ]; then
|
|
env "${INSTALL_ENV[@]}" pnpm install --store-dir "$PNPM_STORE" --frozen-lockfile \
|
|
|| env "${INSTALL_ENV[@]}" pnpm install --store-dir "$PNPM_STORE"
|
|
printf "%s" "$current" > "$baseline"
|
|
else
|
|
echo "==> Skipping pnpm install (node_modules present and lockfile unchanged)."
|
|
fi
|
|
|
|
# Content keys for the sandbox-invariant builds baked into the snapshot.
|
|
# A build is skipped only when the git trees that feed it and the lockfile
|
|
# both match what the snapshot (or a previous boot) built. Snapshots without
|
|
# markers, refs that changed those trees, or refs where a tree is missing
|
|
# rebuild exactly as before. An empty key never matches and is never stored.
|
|
build_key() {
|
|
local trees
|
|
trees="$(git rev-parse "$@" 2>/dev/null)" || return 0
|
|
# Byte-identical to the snapshot bake: one tree hash per line, then the
|
|
# lockfile sha, all newline-terminated.
|
|
printf '%s\n%s\n' "$trees" "$current" | sha256sum | cut -d ' ' -f 1
|
|
}
|
|
|
|
echo "==> Pushing Den DB schema..."
|
|
pnpm --filter @openwork-ee/den-db db:push > /tmp/den-db-push.log 2>&1
|
|
|
|
den_api_assets_marker=.openwork-daytona/den-api-assets.tree
|
|
den_api_assets_key="$(build_key HEAD:packages/mcp-apps)"
|
|
if [ -n "$den_api_assets_key" ] && [ -d packages/mcp-apps/dist ] && [ -f "$den_api_assets_marker" ] \
|
|
&& [ "$(cat "$den_api_assets_marker")" = "$den_api_assets_key" ]; then
|
|
echo "==> Skipping Den API runtime asset build (baked assets match this ref)."
|
|
else
|
|
echo "==> Building Den API runtime assets..."
|
|
pnpm --filter @openwork-ee/den-api run build:mcp-apps
|
|
if [ -n "$den_api_assets_key" ]; then
|
|
printf "%s" "$den_api_assets_key" > "$den_api_assets_marker"
|
|
else
|
|
rm -f "$den_api_assets_marker"
|
|
fi
|
|
fi
|
|
|
|
echo "==> Starting Den API on :$DEN_API_PORT..."
|
|
# The den-api process cmdline is "tsx watch src/main.ts" (cwd-relative), so a
|
|
# pattern anchored on the repo path never matches and restarts silently keep
|
|
# the old server alive on the port. main.ts is unique to den-api here.
|
|
pkill -f "tsx watch src/main.ts" >/dev/null 2>&1 || true
|
|
nohup env \
|
|
PORT="$DEN_API_PORT" \
|
|
DATABASE_URL="$DATABASE_URL" \
|
|
DEN_DB_ENCRYPTION_KEY="$DEN_DB_ENCRYPTION_KEY" \
|
|
BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
|
|
BETTER_AUTH_URL="$BETTER_AUTH_URL" \
|
|
DEN_API_PUBLIC_URL="$DEN_API_PUBLIC_URL" \
|
|
DEN_WEB_APP_HOSTS="$DEN_WEB_APP_HOSTS" \
|
|
DEN_MCP_RESOURCE_URL="$DEN_MCP_RESOURCE_URL" \
|
|
DEN_BETTER_AUTH_TRUSTED_ORIGINS="$DEN_BETTER_AUTH_TRUSTED_ORIGINS" \
|
|
CORS_ORIGINS="$CORS_ORIGINS" \
|
|
DEN_BOOTSTRAP_ADMIN_EMAILS="${DEN_BOOTSTRAP_ADMIN_EMAILS:-}" \
|
|
PROVISIONER_MODE="$DEN_PROVISIONER_MODE" \
|
|
WORKER_URL_TEMPLATE="$DEN_WORKER_URL_TEMPLATE" \
|
|
DEN_ORG_MODE="$DEN_ORG_MODE" \
|
|
DEN_PASSWORD_BREACH_SCREENING_ENABLED="$DEN_PASSWORD_BREACH_SCREENING_ENABLED" \
|
|
DEN_GENERATED_ARTIFACT_VIEWS_ENABLED="$DEN_GENERATED_ARTIFACT_VIEWS_ENABLED" \
|
|
OPENWORK_DEV_MODE="$OPENWORK_DEV_MODE" \
|
|
NODE_OPTIONS="--conditions=development" \
|
|
pnpm --filter @openwork-ee/den-api exec tsx watch src/main.ts > /tmp/den-api.log 2>&1 &
|
|
|
|
wait_for_http "http://127.0.0.1:$DEN_API_PORT/health" "Den API" 180
|
|
|
|
if [ "${RUN_SEED:-0}" = "1" ]; then
|
|
demo_email="${DEN_DEMO_OWNER_EMAIL:-alex@acme.test}"
|
|
demo_password="${DEN_DEMO_OWNER_PASSWORD:-OpenWorkDemo123!}"
|
|
signin_ok() {
|
|
curl -sf -o /dev/null -X POST "http://127.0.0.1:$DEN_API_PORT/api/auth/sign-in/email" \
|
|
-H 'content-type: application/json' \
|
|
-d "{\"email\":\"$demo_email\",\"password\":\"$demo_password\"}"
|
|
}
|
|
if signin_ok; then
|
|
echo "==> Demo org already present ($demo_email)"
|
|
else
|
|
echo "==> Seeding demo org..."
|
|
(cd ee/apps/den-api && env \
|
|
DATABASE_URL="$DATABASE_URL" \
|
|
DEN_DB_ENCRYPTION_KEY="$DEN_DB_ENCRYPTION_KEY" \
|
|
BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
|
|
BETTER_AUTH_URL="$BETTER_AUTH_URL" \
|
|
DEN_API_PUBLIC_URL="$DEN_API_PUBLIC_URL" \
|
|
DEN_ORG_MODE="$DEN_ORG_MODE" \
|
|
OPENWORK_DEV_MODE="$OPENWORK_DEV_MODE" \
|
|
DEN_DEMO_SEED_FETCH_GITHUB="${DEN_DEMO_SEED_FETCH_GITHUB:-0}" \
|
|
node --conditions=development --import tsx scripts/seed-demo-org.ts) > /tmp/den-seed.log 2>&1
|
|
if signin_ok; then
|
|
echo "==> Demo org seeded ($demo_email)"
|
|
else
|
|
echo "ERROR: seed completed but $demo_email cannot sign in. See /tmp/den-seed.log" >&2
|
|
tail -40 /tmp/den-seed.log >&2 || true
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo "DEMO_OWNER_READY=$demo_email"
|
|
fi
|
|
|
|
den_web_marker=.openwork-daytona/den-web-build.tree
|
|
den_web_key="$(build_key HEAD:packages/ui HEAD:ee/packages/utils HEAD:ee/apps/den-web)"
|
|
if [ -n "$den_web_key" ] && [ -d ee/apps/den-web/.next ] && [ -f "$den_web_marker" ] \
|
|
&& [ "$(cat "$den_web_marker")" = "$den_web_key" ]; then
|
|
echo "==> Skipping Den Web build (baked build matches this ref)."
|
|
else
|
|
# Production build instead of next dev: dev-mode HMR websockets 502 through
|
|
# the preview proxy and block hydration. The build bakes no per-sandbox
|
|
# values (all DEN_* env is consumed at runtime by next start), so a build
|
|
# from the snapshot's base commit is reusable across sandboxes.
|
|
echo "==> Building Den Web..."
|
|
if ! env \
|
|
DEN_WEB_PORT="$DEN_WEB_PORT" \
|
|
DEN_API_BASE="$DEN_API_BASE" \
|
|
DEN_AUTH_ORIGIN="$DEN_AUTH_ORIGIN" \
|
|
DEN_AUTH_FALLBACK_BASE="$DEN_AUTH_FALLBACK_BASE" \
|
|
NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL="$NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL" \
|
|
NEXT_PUBLIC_POSTHOG_KEY= \
|
|
NEXT_PUBLIC_POSTHOG_API_KEY= \
|
|
DEN_ORG_MODE="$DEN_ORG_MODE" \
|
|
OPENWORK_DEV_MODE="$OPENWORK_DEV_MODE" \
|
|
DEN_WEB_ALLOWED_DEV_ORIGINS="$DEN_WEB_ALLOWED_DEV_ORIGINS" \
|
|
bash -c 'pnpm --filter @openwork/ui build && pnpm --filter @openwork-ee/utils build && pnpm --filter @openwork-ee/den-web build' > /tmp/den-web-build.log 2>&1; then
|
|
echo "ERROR: Den Web build failed. Last 80 lines:" >&2
|
|
tail -n 80 /tmp/den-web-build.log >&2
|
|
exit 1
|
|
fi
|
|
if [ -n "$den_web_key" ]; then
|
|
printf "%s" "$den_web_key" > "$den_web_marker"
|
|
else
|
|
rm -f "$den_web_marker"
|
|
fi
|
|
fi
|
|
|
|
echo "==> Starting Den Web on :$DEN_WEB_PORT..."
|
|
pkill -f "next dev --hostname 0.0.0.0 --port $DEN_WEB_PORT" >/dev/null 2>&1 || true
|
|
pkill -f "next start --hostname 0.0.0.0 --port $DEN_WEB_PORT" >/dev/null 2>&1 || true
|
|
nohup env \
|
|
DEN_WEB_PORT="$DEN_WEB_PORT" \
|
|
DEN_API_BASE="$DEN_API_BASE" \
|
|
DEN_AUTH_ORIGIN="$DEN_AUTH_ORIGIN" \
|
|
DEN_AUTH_FALLBACK_BASE="$DEN_AUTH_FALLBACK_BASE" \
|
|
NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL="$NEXT_PUBLIC_OPENWORK_AUTH_CALLBACK_URL" \
|
|
NEXT_PUBLIC_POSTHOG_KEY= \
|
|
NEXT_PUBLIC_POSTHOG_API_KEY= \
|
|
DEN_ORG_MODE="$DEN_ORG_MODE" \
|
|
OPENWORK_DEV_MODE="$OPENWORK_DEV_MODE" \
|
|
DEN_WEB_ALLOWED_DEV_ORIGINS="$DEN_WEB_ALLOWED_DEV_ORIGINS" \
|
|
pnpm --filter @openwork-ee/den-web exec next start --hostname 0.0.0.0 --port "$DEN_WEB_PORT" > /tmp/den-web.log 2>&1 &
|
|
|
|
wait_for_http "http://127.0.0.1:$DEN_WEB_PORT/api/den/health" "Den Web" 180
|
|
|
|
cat > .openwork-daytona/server-env <<EOF
|
|
DEN_API_URL=$DEN_API_PUBLIC_URL
|
|
DEN_WEB_URL=$DEN_WEB_PUBLIC_URL
|
|
BETTER_AUTH_URL=$BETTER_AUTH_URL
|
|
DEN_MCP_RESOURCE_URL=$DEN_MCP_RESOURCE_URL
|
|
DEN_PROVISIONER_MODE=$DEN_PROVISIONER_MODE
|
|
EOF
|
|
|
|
echo ""
|
|
echo "============================================"
|
|
echo " OpenWork Daytona server stack ready"
|
|
echo ""
|
|
echo " Den Web: $DEN_WEB_PUBLIC_URL"
|
|
echo " Den API: $DEN_API_PUBLIC_URL"
|
|
echo ""
|
|
echo " Logs:"
|
|
echo " /tmp/den-api.log"
|
|
echo " /tmp/den-web.log"
|
|
echo " /tmp/den-db-push.log"
|
|
echo "============================================"
|