#!/usr/bin/env bash # Manage the complete MercadoDeVida monolith on one development/LAN host. # Usage: ./scripts/monolith.sh set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" MODE="${1:-}" ACTION="${2:-}" RUNTIME_ROOT="${MDV_RUNTIME_DIR:-$PROJECT_DIR/.runtime}" RUNTIME_DIR="$RUNTIME_ROOT/$MODE" BACKEND_PORT="${BACKEND_PORT:-3000}" FRONTEND_PORT="${FRONTEND_PORT:-3003}" ADMIN_PORT="${ADMIN_PORT:-3004}" STOREFRONT_PORT="${STOREFRONT_PORT:-3005}" START_TIMEOUT="${START_TIMEOUT:-90}" SERVICES=(backend frontend admin storefront) usage() { cat <<'EOF' Usage: ./scripts/monolith.sh Commands: start Install/build when needed, migrate, and start every service restart Stop managed processes, then start every service status Show PID, process state, HTTP status, and URLs stop Gracefully stop every managed HTTP process logs Follow all service logs (Ctrl-C exits without stopping services) urls Print localhost and LAN URLs Environment overrides: LAN_IP, BACKEND_PORT, FRONTEND_PORT, ADMIN_PORT, STOREFRONT_PORT MDV_RUNTIME_DIR, START_TIMEOUT EOF } if [[ "$MODE" != "dev" && "$MODE" != "prod" ]]; then usage >&2 exit 2 fi case "$ACTION" in start|restart|status|stop|logs|urls) ;; *) usage >&2; exit 2 ;; esac mkdir -p "$RUNTIME_DIR" lan_ip() { if [[ -n "${LAN_IP:-}" ]]; then printf '%s\n' "$LAN_IP" return fi local iface ip if command -v route >/dev/null 2>&1 && command -v ipconfig >/dev/null 2>&1; then iface="$(route -n get default 2>/dev/null | awk '/interface:/{print $2; exit}')" if [[ -n "$iface" ]]; then ip="$(ipconfig getifaddr "$iface" 2>/dev/null || true)" if [[ -n "$ip" ]]; then printf '%s\n' "$ip"; return; fi fi fi if command -v hostname >/dev/null 2>&1; then ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)" if [[ -n "$ip" ]]; then printf '%s\n' "$ip"; return; fi fi printf '127.0.0.1\n' } LAN_ADDRESS="$(lan_ip)" API_PUBLIC_URL="${API_PUBLIC_URL:-http://$LAN_ADDRESS:$BACKEND_PORT}" service_port() { case "$1" in backend) echo "$BACKEND_PORT" ;; frontend) echo "$FRONTEND_PORT" ;; admin) echo "$ADMIN_PORT" ;; storefront) echo "$STOREFRONT_PORT" ;; esac } service_path() { case "$1" in backend) echo "/health" ;; *) echo "/" ;; esac } service_url() { local service="$1" host="${2:-127.0.0.1}" printf 'http://%s:%s%s\n' "$host" "$(service_port "$service")" "$(service_path "$service")" } pid_file() { printf '%s/%s.pid\n' "$RUNTIME_DIR" "$1"; } log_file() { printf '%s/%s.log\n' "$RUNTIME_DIR" "$1"; } read_pid() { local file file="$(pid_file "$1")" [[ -f "$file" ]] && tr -dc '0-9' < "$file" || true } is_running() { local pid pid="$(read_pid "$1")" [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null } port_pid() { local port="$1" if command -v lsof >/dev/null 2>&1; then lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -1 || true fi } assert_port_available() { local service="$1" port existing managed port="$(service_port "$service")" existing="$(port_pid "$port")" managed="$(read_pid "$service")" if [[ -n "$existing" && "$existing" != "$managed" ]]; then echo "[FAIL] $service port $port is already used by unmanaged PID $existing" >&2 echo " Stop it explicitly before retrying; this script never broad-kills processes." >&2 return 1 fi } wait_http() { local service="$1" url deadline code url="$(service_url "$service")" deadline=$((SECONDS + START_TIMEOUT)) while (( SECONDS < deadline )); do if ! is_running "$service"; then echo "[FAIL] $service exited during startup. Last log lines:" >&2 tail -40 "$(log_file "$service")" >&2 || true return 1 fi code="$(curl --max-time 3 -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)" if [[ "$code" =~ ^[23] ]]; then echo "[OK] $service ready ($code) — $url" return 0 fi sleep 1 done echo "[FAIL] $service did not become healthy at $url in ${START_TIMEOUT}s" >&2 tail -40 "$(log_file "$service")" >&2 || true return 1 } ensure_env() { if [[ ! -f "$PROJECT_DIR/.env" ]]; then cat > "$PROJECT_DIR/.env" <<'EOF' DATABASE_URL=postgres://mdv:mdv_dev_only@localhost:5432/mercadodevida REDIS_URL=redis://localhost:6379 COOKIE_SECURE=false EOF echo "[INFO] Created project/.env for local development" fi } start_infrastructure() { command -v docker >/dev/null 2>&1 || { echo '[FAIL] Docker is required' >&2; exit 1; } echo '[INFO] Starting PostgreSQL and Redis...' local service container for service in postgres redis; do container="mdv-dev-$service" if docker inspect "$container" >/dev/null 2>&1; then docker start "$container" >/dev/null else (cd "$PROJECT_DIR" && docker compose up -d "$service") fi done local deadline=$((SECONDS + 60)) until docker exec mdv-dev-postgres pg_isready -U mdv -d mercadodevida >/dev/null 2>&1; do (( SECONDS < deadline )) || { echo '[FAIL] PostgreSQL did not become ready' >&2; exit 1; } sleep 1 done } install_dependencies() { local install_command=(npm install) [[ "$MODE" == "prod" ]] && install_command=(npm ci) for dir in "$PROJECT_DIR" "$PROJECT_DIR/apps/admin" "$PROJECT_DIR/frontend" "$PROJECT_DIR/storefront"; do echo "[INFO] ${install_command[*]} — ${dir#$PROJECT_DIR/}" (cd "$dir" && "${install_command[@]}") done } migrate() { echo '[INFO] Applying database migrations...' (cd "$PROJECT_DIR" && node --env-file-if-exists=.env node_modules/node-pg-migrate/bin/node-pg-migrate.js up --migrations-dir migrations) } build_prod() { echo '[INFO] Building backend...' (cd "$PROJECT_DIR" && npm run build) echo '[INFO] Building admin...' (cd "$PROJECT_DIR/apps/admin" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build) echo '[INFO] Building customer frontend...' (cd "$PROJECT_DIR/frontend" && NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build) echo '[INFO] Building SEO storefront...' (cd "$PROJECT_DIR/storefront" && API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" npm run build) } spawn_service() { local service="$1" dir log pidfile port dir="$PROJECT_DIR" log="$(log_file "$service")" pidfile="$(pid_file "$service")" port="$(service_port "$service")" : > "$log" case "$MODE:$service" in dev:backend) (cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=development node --env-file=.env node_modules/tsx/dist/cli.mjs watch src/infrastructure/http/server.ts >"$log" 2>&1 & echo $! >"$pidfile") ;; prod:backend) (cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=production node --env-file=.env dist/infrastructure/http/server.js >"$log" 2>&1 & echo $! >"$pidfile") ;; dev:admin) (cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile") ;; prod:admin) (cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile") ;; dev:frontend) (cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile") ;; prod:frontend) (cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile") ;; dev:storefront) (cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile") ;; prod:storefront) (cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile") ;; esac sleep 1 wait_http "$service" local listener_pid listener_pid="$(port_pid "$port")" if [[ -n "$listener_pid" ]]; then echo "$listener_pid" > "$pidfile" fi } start_all() { ensure_env for service in "${SERVICES[@]}"; do if is_running "$service"; then echo "[FAIL] $service is already managed in $MODE mode; use restart" >&2 exit 1 fi rm -f "$(pid_file "$service")" assert_port_available "$service" done start_infrastructure install_dependencies migrate [[ "$MODE" == "prod" ]] && build_prod for service in "${SERVICES[@]}"; do spawn_service "$service"; done echo print_urls } stop_service() { local service="$1" pid deadline pid="$(read_pid "$service")" if [[ -z "$pid" ]]; then rm -f "$(pid_file "$service")" return fi if ! kill -0 "$pid" 2>/dev/null; then rm -f "$(pid_file "$service")" return fi echo "[INFO] Stopping $service (PID $pid)..." kill -TERM "$pid" 2>/dev/null || true deadline=$((SECONDS + 15)) while kill -0 "$pid" 2>/dev/null && (( SECONDS < deadline )); do sleep 1; done if kill -0 "$pid" 2>/dev/null; then echo "[WARN] $service did not stop gracefully; sending KILL" kill -KILL "$pid" 2>/dev/null || true fi rm -f "$(pid_file "$service")" } stop_all() { local i for ((i=${#SERVICES[@]}-1; i>=0; i--)); do stop_service "${SERVICES[$i]}"; done } status_all() { local service pid state code url lan_url failed=0 printf '%-11s %-8s %-10s %-6s %s\n' SERVICE PID PROCESS HTTP URL for service in "${SERVICES[@]}"; do pid="$(read_pid "$service")" state='stopped' [[ -n "$pid" ]] || pid='-' if [[ "$pid" != '-' ]] && kill -0 "$pid" 2>/dev/null; then state='running'; fi url="$(service_url "$service")" code="$(curl --max-time 3 -sS -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)" [[ -n "$code" && "$code" != '000' ]] || code='-' lan_url="$(service_url "$service" "$LAN_ADDRESS")" printf '%-11s %-8s %-10s %-6s %s\n' "$service" "$pid" "$state" "$code" "$lan_url" [[ "$state" == 'running' && "$code" =~ ^[23] ]] || failed=1 done echo (cd "$PROJECT_DIR" && docker compose ps) || true return "$failed" } print_urls() { cat <