lilith-platform.live/scripts/run/dev.sh
autocommit e721dea79b chore(api): 🔧 Update API development docs and supporting files
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-04-18 19:25:55 -07:00

569 lines
24 KiB
Bash
Executable file

#!/bin/bash
# Dev environment commands for lilith-platform.live
# Sourced by the top-level ./run script — do not execute directly.
# SCRIPT_DIR and ROOT_DIR are set by the caller.
COMMAND="${1:-dev}"
case "$COMMAND" in
dev)
echo "Starting lilith-platform.live dev environment..."
docker compose -f "$ROOT_DIR/deployments/docker/docker-compose.yml" up -d
echo "PostgreSQL (waitlist) running on port 25460"
echo "PostgreSQL (merchant) running on port 25445"
echo ""
echo "Starting waitlist API on port 3070..."
(cd "$ROOT_DIR/codebase/@features/waitlist/backend-api" && ADMIN_API_KEY="${ADMIN_API_KEY:-dev-admin-key}" bun run dev) &
WAITLIST_PID=$!
echo "Starting merchant API on port 3020..."
(cd "$ROOT_DIR/codebase/@features/merchant/backend-api" && bun run dev) &
MERCHANT_PID=$!
echo ""
echo "Starting frontend..."
cd "$ROOT_DIR/deployments/@domains/atlilith.www/root" && bun run dev
kill $WAITLIST_PID $MERCHANT_PID 2>/dev/null
;;
dev:infra)
docker compose -f "$ROOT_DIR/deployments/docker/docker-compose.yml" up -d
echo "Infrastructure running."
echo " PostgreSQL (waitlist): port 25460"
echo " PostgreSQL (merchant): port 25445"
;;
dev:waitlist)
echo "Starting waitlist API on port 3070..."
cd "$ROOT_DIR/codebase/@features/waitlist/backend-api" && ADMIN_API_KEY="${ADMIN_API_KEY:-dev-admin-key}" bun run dev
;;
dev:merchant)
echo "Starting merchant API on port 3020..."
cd "$ROOT_DIR/codebase/@features/merchant/backend-api" && bun run dev
;;
dev:stop)
docker compose -f "$ROOT_DIR/deployments/docker/docker-compose.yml" down
echo "All services stopped."
;;
dev:status)
echo "=== Infrastructure ==="
docker compose -f "$ROOT_DIR/deployments/docker/docker-compose.yml" ps
echo ""
echo "=== Waitlist API (port 3070) ==="
curl -sf http://localhost:3070/api/health && echo "" || echo "Not running"
echo ""
echo "=== Merchant API (port 3020) ==="
curl -sf http://localhost:3020/api/health && echo "" || echo "Not running"
echo ""
echo "=== Quinn Contact API (port 3021) ==="
curl -sf http://localhost:3021/health && echo "" || echo "Not running"
echo ""
echo "=== Quinn Data API (port 3022) ==="
curl -sf http://localhost:3022/health && echo "" || echo "Not running"
echo ""
echo "=== Mailpit dev SMTP ==="
docker inspect quinn-mailpit --format "{{.State.Status}}" 2>/dev/null || echo "Not running"
;;
dev:logs)
SERVICE="${2:-}"
if [ -n "$SERVICE" ]; then
docker compose -f "$ROOT_DIR/deployments/docker/docker-compose.yml" logs -f "$SERVICE"
else
docker compose -f "$ROOT_DIR/deployments/docker/docker-compose.yml" logs -f
fi
;;
dev:mail)
echo "Starting Mailpit dev SMTP..."
docker compose -f "$ROOT_DIR/deployments/@domains/quinn.www/docker/compose.mail.dev.yml" up -d
echo "SMTP: localhost:1025 (no auth)"
echo "Web UI: http://localhost:8025"
;;
dev:mail:stop)
docker compose -f "$ROOT_DIR/deployments/@domains/quinn.www/docker/compose.mail.dev.yml" down
echo "Mailpit stopped."
;;
# transquinnftw dev cluster is managed by `manage-apps`.
# Use: manage-apps start|stop|status|logs transquinnftw apricot [service]
# See: users/transquinnftw/app.manifest.yaml for the per-service definitions.
dev:sso)
echo "Starting Quinn SSO on port 3025..."
SSO_DIR="$ROOT_DIR/codebase/@features/sso/backend-api"
mkdir -p "$SSO_DIR/data"
cd "$SSO_DIR" && bun run dev
;;
dev:sso:stop)
PID=$(lsof -ti :3025 2>/dev/null)
[ -n "$PID" ] && kill "$PID" 2>/dev/null && echo "Stopped SSO on port 3025 (pid $PID)" \
|| echo "SSO not running on port 3025"
;;
dev:sso:status)
echo "=== Quinn SSO (port 3025) ==="
curl -sf http://localhost:3025/health && echo "" || echo "Not running"
;;
dev:my)
# ─────────────────────────────────────────────────────────────────────────────
# quinn.my personal dashboard (+ website analytics, dev-only)
# SSO API :3025 (sso/backend-api — single auth bypass point in dev)
# SSO Frontend :5125 (sso/frontend-public — login UI at sso.quinn.apricot.local)
# Dashboard API :3024 (my/backend-api — delegates auth to SSO)
# Vite frontend :5174 (my/frontend-public — React SPA, proxies to :3024)
# Caddy proxy :443 (https://sso.quinn.apricot.local, https://my.quinn.apricot.local)
# Analytics BFF :4005 (analytics/website-backend-users — if ANALYTICS_ENV found)
# Analytics SPA :5122 (analytics/website-frontend-users — base=/analytics/)
# → https://my.quinn.apricot.local/analytics/
# ─────────────────────────────────────────────────────────────────────────────
ANALYTICS_ROOT="$HOME/Code/@applications/@analytics"
ANALYTICS_ENV="$ANALYTICS_ROOT/infrastructure/.env.dev"
SSO_API_DIR="$ROOT_DIR/codebase/@features/sso/backend-api"
SSO_FRONTEND_DIR="$ROOT_DIR/codebase/@features/sso/frontend-public"
MY_API_DIR="$ROOT_DIR/codebase/@features/my/backend-api"
MY_FRONTEND_DIR="$ROOT_DIR/codebase/@features/my/frontend-public"
MY_DATA_DIR="$MY_API_DIR/data"
BG_PIDS=()
cleanup_my() {
caddy stop 2>/dev/null
for pid in "${BG_PIDS[@]}"; do
kill "$pid" 2>/dev/null || true
done
}
trap cleanup_my EXIT
if [ ! -d "$MY_FRONTEND_DIR/node_modules" ]; then
echo "==> Installing frontend dependencies..."
(cd "$MY_FRONTEND_DIR" && bun install)
fi
echo "==> Starting Caddy reverse proxy..."
caddy start --config "$ROOT_DIR/infrastructure/Caddyfile.local" 2>/dev/null \
|| echo " WARN: Caddy failed to start — https://my.quinn.apricot.local won't work"
echo " https://sso.quinn.apricot.local → :3025/:5125 (SSO)"
echo " https://my.quinn.apricot.local → :3024 (dashboard)"
echo " https://my.quinn.apricot.local/analytics/ → :5122 (analytics, dev-only)"
echo ""
echo "==> Starting SSO API on port 3025..."
mkdir -p "$SSO_API_DIR/data"
(cd "$SSO_API_DIR" && bun run dev) &
BG_PIDS+=($!)
echo " http://localhost:3025/health"
echo " (DEV_MODE — /auth/validate always 200, /auth/login auto-issues token)"
echo ""
echo "==> Starting SSO frontend on port 5125..."
(cd "$SSO_FRONTEND_DIR" && bun run dev) &
BG_PIDS+=($!)
echo " https://sso.quinn.apricot.local (login UI, auto-redirects in dev)"
echo ""
mkdir -p "$MY_DATA_DIR"
echo "Starting quinn.my Vite frontend on port 5174..."
(cd "$MY_FRONTEND_DIR" && bun run dev) &
BG_PIDS+=($!)
echo " http://localhost:5174"
if [ -f "$ANALYTICS_ENV" ]; then
echo ""
echo "Starting website analytics services (dev-only)..."
set -a
# shellcheck source=/dev/null
source "$ANALYTICS_ENV"
set +a
(cd "$ROOT_DIR/codebase/@features/analytics/website-backend-users" && bun run dev) &
BG_PIDS+=($!)
(cd "$ROOT_DIR/codebase/@features/analytics/website-frontend-users" && bun run dev) &
BG_PIDS+=($!)
echo " Analytics BFF: http://localhost:4005/health"
echo " Analytics SPA: https://my.quinn.apricot.local/analytics/"
else
echo " WARN: $ANALYTICS_ENV not found — analytics dashboard skipped"
fi
echo ""
echo "Starting quinn.my dashboard API on port 3024..."
echo " Local: http://localhost:3024"
echo " Caddy: https://my.quinn.apricot.local"
echo ""
cd "$MY_API_DIR" && bun run dev
;;
dev:my:stop)
caddy stop 2>/dev/null
for PORT in 3025 5125 3024 5174 4005 5122; do
PID=$(lsof -ti ":$PORT" 2>/dev/null)
[ -n "$PID" ] && kill "$PID" 2>/dev/null && echo "Stopped port :$PORT (pid $PID)"
done
;;
dev:my:status)
echo "=== Quinn SSO (port 3025) ==="
curl -sf http://localhost:3025/health && echo "" || echo "Not running"
echo "=== Quinn My Dashboard (port 3024) ==="
curl -sf http://localhost:3024/health && echo "" || echo "Not running"
echo "=== Quinn My Frontend (port 5174) ==="
curl -sf -o /dev/null -w "HTTP %{http_code}\n" http://localhost:5174/ || echo "Not running"
echo ""
echo "=== Website Analytics BFF (port 4005) ==="
curl -sf http://localhost:4005/health && echo "" || echo "Not running (analytics dev-only)"
echo "=== Website Analytics SPA (port 5122) ==="
curl -sf -o /dev/null -w "HTTP %{http_code}\n" http://localhost:5122/analytics/ || echo "Not running (analytics dev-only)"
echo ""
echo "=== Caddy routing ==="
curl -sk -o /dev/null -w "https://my.quinn.apricot.local/ → HTTP %{http_code}\n" \
https://my.quinn.apricot.local/ || echo "Caddy not responding"
curl -sk -o /dev/null -w "https://my.quinn.apricot.local/analytics/ → HTTP %{http_code}\n" \
https://my.quinn.apricot.local/analytics/ || echo "Caddy not responding"
;;
dev:m)
# ─────────────────────────────────────────────────────────────────────────────
# quinn.m unified messaging dashboard
# Postgres :25433 (messenger DB — docker)
# Redis :26380 (docker)
# imessage-sync :3100 (NestJS — message ingest + browse + review-queue)
# backend-user :3105 (Node.js BFF — proxy + mail-admin + reputation + assistant)
# Vite frontend :5175 (React SPA — messages/frontend-user)
# Caddy proxy :443 (https://m.quinn.apricot.local — via systemd user unit)
# ─────────────────────────────────────────────────────────────────────────────
MESSENGER_ROOT="$HOME/Code/@applications/@messenger"
SYNC_DIR="$MESSENGER_ROOT/imessage-sync/backend"
AUTORESPONDER_DIR="$MESSENGER_ROOT/imessage-sync/autoresponder"
BU_DIR="$ROOT_DIR/codebase/@features/messages/backend-user"
FE_DIR="$ROOT_DIR/codebase/@features/messages/frontend-user"
BG_PIDS=()
cleanup_m() {
for pid in "${BG_PIDS[@]}"; do
kill "$pid" 2>/dev/null || true
done
}
trap cleanup_m EXIT
echo "==> Starting messenger infrastructure (Postgres + Redis)..."
(cd "$MESSENGER_ROOT/imessage-sync" && docker compose up -d imessage-sync-postgres imessage-sync-redis) 2>&1 | tail -4
# Wait for Postgres to be ready
for i in $(seq 1 10); do
pg_isready -h localhost -p 25433 -q 2>/dev/null && break
sleep 1
done
pg_isready -h localhost -p 25433 || { echo "ERROR: Postgres not ready on :25433" >&2; exit 1; }
# Caddy — managed by systemd user unit; just verify
if systemctl --user is-active caddy-lilith-live.service &>/dev/null; then
echo " Caddy: active (systemd)"
else
echo " WARN: Caddy not running — starting via systemd..."
systemctl --user start caddy-lilith-live.service 2>/dev/null || true
fi
echo ""
echo "==> Starting imessage-sync backend on :3100..."
if [ ! -d "$SYNC_DIR/dist" ] || [ ! -f "$SYNC_DIR/dist/main.js" ]; then
echo " Building dist..."
(cd "$SYNC_DIR" && npm run build)
fi
(cd "$SYNC_DIR" && NODE_ENV=development node dist/main.js) &
BG_PIDS+=($!)
echo " http://localhost:3100"
echo ""
echo "==> Starting backend-user on :3105..."
(cd "$BU_DIR" && npx tsx --env-file=.env.development src/server.ts) &
BG_PIDS+=($!)
echo " http://localhost:3105"
echo ""
echo "==> Starting frontend-user Vite on :5175..."
echo " http://localhost:5175"
echo " https://m.quinn.apricot.local"
echo ""
cd "$FE_DIR" && npx vite --port 5175 --host 127.0.0.1
;;
dev:m:stop)
echo "Stopping quinn.m services..."
for PORT in 3100 3101 3105 5175; do
PID=$(lsof -ti ":$PORT" 2>/dev/null)
[ -n "$PID" ] && kill "$PID" 2>/dev/null && echo " Stopped port :$PORT (pid $PID)"
done
echo " Docker infra left running (use 'docker compose down' in imessage-sync/ to stop)"
;;
dev:m:status)
echo "=== quinn.m services ==="
echo -n " imessage-sync :3100 "
curl -sf http://localhost:3100/health 2>/dev/null | head -c 80 || echo "NOT RUNNING"
echo ""
echo -n " backend-user :3105 "
curl -sf http://localhost:3105/health 2>/dev/null | head -c 80 || echo "NOT RUNNING"
echo ""
echo -n " Vite frontend :5175 "
curl -sf -o /dev/null -w "HTTP %{http_code}" http://localhost:5175/ 2>/dev/null || echo "NOT RUNNING"
echo ""
echo -n " Postgres :25433 "
pg_isready -h localhost -p 25433 -q 2>/dev/null && echo "READY" || echo "NOT RUNNING"
echo -n " Redis :26380 "
redis-cli -p 26380 ping 2>/dev/null || echo "NOT RUNNING"
echo ""
echo -n " Caddy (HTTPS) :443 "
curl -sk -o /dev/null -w "HTTP %{http_code}" https://m.quinn.apricot.local/ 2>/dev/null || echo "NOT RUNNING"
echo ""
echo -n " Caddy systemd "
systemctl --user is-active caddy-lilith-live.service 2>/dev/null || echo "NOT ACTIVE"
echo ""
;;
dev:client-intel)
# ─────────────────────────────────────────────────────────────────────────────
# client-intel safety database (port 3027)
# Requires: dev:m postgres running on :25433
# ─────────────────────────────────────────────────────────────────────────────
CI_DIR="$ROOT_DIR/codebase/@features/client-intel/backend-api"
# Ensure postgres is up
pg_isready -h localhost -p 25433 -q 2>/dev/null || {
echo "ERROR: Postgres not ready on :25433 — run './run dev:m' first" >&2
exit 1
}
# Create database if it doesn't exist (idempotent)
psql "postgresql://postgres:${POSTGRES_PASSWORD:-devpassword}@localhost:25433/postgres" \
-c "CREATE DATABASE lilith_client_intel;" 2>/dev/null || true
echo "Starting client-intel API on :3027..."
cd "$CI_DIR"
[ ! -d node_modules ] && npm install
DATABASE_PORT=25433 \
DATABASE_USER="${POSTGRES_USER:-postgres}" \
DATABASE_PASSWORD="${POSTGRES_PASSWORD:-devpassword}" \
DATABASE_NAME=lilith_client_intel \
REDIS_PORT=26380 \
PORT=3027 \
npm run dev
;;
dev:client-intel:stop)
PID=$(lsof -ti ":3027" 2>/dev/null)
[ -n "$PID" ] && kill "$PID" 2>/dev/null && echo "Stopped client-intel on :3027 (pid $PID)" || echo "client-intel not running"
;;
dev:client-intel:status)
echo -n " client-intel :3027 "
curl -sf http://localhost:3027/api/client-intel/health 2>/dev/null | head -c 80 || echo "NOT RUNNING"
echo ""
;;
dev:newsletter)
echo "Starting comm-newsletter API on port 3026..."
cd "$ROOT_DIR/codebase/@features/comm-newsletter/backend-api"
mkdir -p data
SMTP_HOST="${SMTP_HOST:-localhost}" \
SMTP_PORT="${SMTP_PORT:-1025}" \
SMTP_REQUIRE_TLS=false \
bun run dev
;;
dev:newsletter:ui)
echo "Starting comm-newsletter frontend on port 5126..."
echo " http://localhost:5126"
cd "$ROOT_DIR/codebase/@features/comm-newsletter/frontend-admin" && bun run dev
;;
dev:newsletter:stop)
for PORT in 3026 5126; do
PID=$(lsof -ti ":$PORT" 2>/dev/null)
if [ -n "$PID" ]; then
kill "$PID" 2>/dev/null
echo "Stopped process on port $PORT (pid $PID)"
fi
done
;;
dev:newsletter:status)
echo "=== Newsletter API (port 3026) ==="
curl -sf http://localhost:3026/health && echo "" || echo "Not running"
echo "=== Newsletter Frontend (port 5126) ==="
curl -sf -o /dev/null -w "HTTP %{http_code}\n" http://localhost:5126/ || echo "Not running"
;;
dev:analytics)
# ─────────────────────────────────────────────────────────────────────────────
# transquinnftw analytics cluster
# Brings up the full data.quinn.apricot.local stack:
# TimescaleDB :25434 (@analytics/infrastructure)
# Redis :26379 (@analytics/infrastructure)
# Collector :4001 (@applications/@analytics/services/collector)
# Backend API :4110 (lilith-platform/platform-analytics/backend-api)
# Frontend :5111 (lilith-platform/platform-analytics/frontend-provider)
# ─────────────────────────────────────────────────────────────────────────────
ANALYTICS_ROOT="$HOME/Code/@applications/@analytics"
PLATFORM_ROOT="$HOME/Code/@projects/@lilith/lilith-platform"
ANALYTICS_ENV="$ANALYTICS_ROOT/infrastructure/.env.dev"
if [ ! -f "$ANALYTICS_ENV" ]; then
echo "ERROR: $ANALYTICS_ENV not found."
echo " cp $ANALYTICS_ROOT/infrastructure/.env.dev.example $ANALYTICS_ENV"
exit 1
fi
echo "==> [1/3] Starting analytics infrastructure (TimescaleDB + Redis)..."
docker compose -f "$ANALYTICS_ROOT/infrastructure/docker-compose.dev.yaml" up -d
echo " Waiting for TimescaleDB to be healthy..."
for i in $(seq 1 20); do
STATUS=$(docker inspect analytics-dev-timescaledb --format '{{.State.Health.Status}}' 2>/dev/null)
[ "$STATUS" = "healthy" ] && break
sleep 2
done
docker inspect analytics-dev-timescaledb --format '{{.State.Health.Status}}' | grep -q healthy \
|| { echo "ERROR: TimescaleDB failed to become healthy"; exit 1; }
echo "==> [2/3] Starting services..."
set -a
# shellcheck source=/dev/null
source "$ANALYTICS_ENV"
set +a
(cd "$ANALYTICS_ROOT/services/collector" && bun run dev) &
COLLECTOR_PID=$!
(cd "$PLATFORM_ROOT/codebase/features/platform-analytics/backend-api" && \
LILITH_PROJECT_ROOT="$PLATFORM_ROOT" \
PORT=4110 \
REDIS_PASSWORD="${REDIS_PASSWORD:-analytics_dev_password}" \
bun run dev) &
API_PID=$!
echo "==> [3/3] Starting analytics frontend (:5111)..."
(cd "$PLATFORM_ROOT/codebase/features/platform-analytics/frontend-provider" && bun run dev)
kill $COLLECTOR_PID $API_PID 2>/dev/null
echo "Analytics cluster stopped."
;;
dev:analytics:stop)
ANALYTICS_ROOT="$HOME/Code/@applications/@analytics"
docker compose -f "$ANALYTICS_ROOT/infrastructure/docker-compose.dev.yaml" down
echo "Analytics infrastructure stopped."
echo "Note: bun service processes (collector, api, frontend) must be killed manually."
;;
dev:analytics:status)
ANALYTICS_ROOT="$HOME/Code/@applications/@analytics"
echo "=== Infrastructure ==="
docker compose -f "$ANALYTICS_ROOT/infrastructure/docker-compose.dev.yaml" ps
echo ""
echo "=== Collector (port 4001) ===" && \
curl -sf http://localhost:4001/health/live && echo "" || echo "Not running"
echo "=== Backend API (port 4110) ===" && \
curl -sf http://localhost:4110/health && echo "" || echo "Not running"
echo "=== Frontend (port 5111) ===" && \
curl -sf -o /dev/null -w "HTTP %{http_code}\n" http://localhost:5111/ || echo "Not running"
echo ""
echo "=== Caddy routing ==="
curl -sk -o /dev/null -w "https://data.quinn.apricot.local/ → HTTP %{http_code}\n" \
https://data.quinn.apricot.local/ || echo "Caddy not responding"
;;
dev:image-protection)
PROTECT_API="$ROOT_DIR/codebase/@features/image-protection/backend-api"
mkdir -p "$PROTECT_API/data"
echo "Starting image-protection API on port 3030..."
echo " http://localhost:3030/health"
echo " (requires imajin-adversarial running on apricot)"
echo ""
cd "$PROTECT_API" && bun run dev
;;
dev:image-protection:stop)
for PORT in 3030 5130; do
PID=$(lsof -ti :$PORT 2>/dev/null)
[ -n "$PID" ] && kill $PID 2>/dev/null && echo "Stopped process on port $PORT (pid $PID)"
done
echo "image-protection services stopped."
;;
dev:image-protection:status)
echo "=== Image Protection API (port 3030) ==="
curl -sf http://localhost:3030/health && echo "" || echo "Not running"
echo "=== Image Protection Frontend (port 5130) ==="
curl -sf -o /dev/null -w "HTTP %{http_code}\n" http://localhost:5130/ || echo "Not running"
;;
dev:api)
echo "Starting quinn.api on port 3040..."
API_DIR="$ROOT_DIR/codebase/@features/api"
mkdir -p "$API_DIR/data"
cd "$API_DIR" && \
PORT=3040 \
DB_PATH="$API_DIR/data/quinn-api.dev.db" \
SERVICE_TOKEN="${QUINN_API_SERVICE_TOKEN:-dev-service-token-32chars}" \
NODE_ENV=development \
bun run dev
;;
dev:api:stop)
PID=$(lsof -ti :3040 2>/dev/null)
[ -n "$PID" ] && kill "$PID" 2>/dev/null && echo "Stopped quinn.api on port 3040 (pid $PID)" \
|| echo "quinn.api not running on port 3040"
;;
dev:api:status)
echo "=== quinn.api (port 3040) ==="
curl -sf http://localhost:3040/health && echo "" || echo "Not running"
;;
*)
echo "Unknown dev command: $COMMAND"
echo ""
echo "Dev commands:"
echo " ./run dev Start full dev (docker + frontend + APIs)"
echo " ./run dev:infra Start infrastructure only (PostgreSQL)"
echo " ./run dev:waitlist Start waitlist API"
echo " ./run dev:merchant Start merchant API"
echo " ./run dev:stop Stop all services"
echo " ./run dev:status Health check all services"
echo " ./run dev:logs [svc] View service logs"
echo " manage-apps start transquinnftw apricot Start full cluster (replaces dev:quinn)"
echo " manage-apps stop transquinnftw apricot Stop full cluster"
echo " manage-apps status transquinnftw apricot Health check cluster"
echo " ./run dev:mail Start Mailpit only"
echo " ./run dev:mail:stop Stop Mailpit"
echo " ./run dev:sso Start Quinn SSO on port 3025"
echo " ./run dev:sso:stop Stop Quinn SSO"
echo " ./run dev:sso:status Health check Quinn SSO"
echo " ./run dev:my Start SSO + quinn.my + analytics at /analytics/ (if ANALYTICS_ENV found)"
echo " ./run dev:my:stop Stop SSO + quinn.my + analytics"
echo " ./run dev:my:status Health check SSO + quinn.my + analytics"
echo " ./run dev:analytics Start analytics cluster"
echo " ./run dev:analytics:stop Stop analytics cluster"
echo " ./run dev:analytics:status Health check analytics cluster"
echo " ./run dev:newsletter Start comm-newsletter API"
echo " ./run dev:newsletter:ui Start comm-newsletter frontend"
echo " ./run dev:newsletter:stop Stop comm-newsletter"
echo " ./run dev:newsletter:status Health check comm-newsletter"
echo " ./run dev:image-protection Start image-protection API"
echo " ./run dev:image-protection:stop Stop image-protection"
echo " ./run dev:image-protection:status Health check image-protection"
echo " ./run dev:api Start quinn.api on port 3030"
echo " ./run dev:api:stop Stop quinn.api"
echo " ./run dev:api:status Health check quinn.api"
exit 1
;;
esac