#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PIDS=() usage() { cat <<'EOF' Usage: ./scripts/dev.sh [start|stop|restart|status|--reset] Starts MySQL, Redis, MinIO, Qdrant, backend, admin frontend and legacy mobile fallback. start start local demo services stop stop backend and frontend dev processes restart stop backend and frontend, then start again status show local demo process status --reset rebuild local demo database before starting services EOF } cleanup() { trap - INT TERM EXIT for pid in "${PIDS[@]}"; do kill "$pid" >/dev/null 2>&1 || true done } ensure_docker() { if docker info >/dev/null 2>&1; then return fi if [[ "$(uname -s)" == "Darwin" ]] && [[ -d /Applications/Docker.app ]]; then echo "Docker daemon is not running. Opening Docker Desktop..." open -a Docker for _ in {1..90}; do if docker info >/dev/null 2>&1; then echo "Docker daemon is ready." return fi sleep 2 done fi echo "Docker daemon is not running. Start Docker Desktop, then run ./scripts/dev.sh again." >&2 exit 1 } ensure_port_free() { local port="$1" if lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then echo "Port $port is already in use:" >&2 lsof -nP -iTCP:"$port" -sTCP:LISTEN >&2 echo "Stop the existing process, then run ./scripts/dev.sh again." >&2 exit 1 fi } stop_port() { local port="$1" local pids pids="$(lsof -tiTCP:"$port" -sTCP:LISTEN || true)" if [[ -z "$pids" ]]; then echo "Port $port is not in use." return fi echo "Stopping port $port: $pids" kill $pids >/dev/null 2>&1 || true for _ in {1..20}; do if ! lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then return fi sleep 0.2 done kill -9 $pids >/dev/null 2>&1 || true } stop_portless_route() { local name="$1" local pids if ! command -v portless >/dev/null 2>&1; then return fi pids="$(portless list 2>/dev/null | sed -nE "s#.*https://${name}[.]localhost.*[(]pid ([0-9]+)[)].*#\\1#p" || true)" if [[ -z "$pids" ]]; then if portless list 2>/dev/null | grep -q "https://${name}[.]localhost"; then echo "Removing portless alias $name" portless alias --remove "$name" >/dev/null 2>&1 || true return fi echo "Portless route $name is not running." return fi echo "Stopping portless route $name: $pids" kill $pids >/dev/null 2>&1 || true for _ in {1..20}; do if ! portless list 2>/dev/null | grep -q "https://${name}[.]localhost"; then return fi sleep 0.2 done kill -9 $pids >/dev/null 2>&1 || true } stop_launchctl_job() { local label="$1" local domain="gui/$(id -u)/$label" if ! command -v launchctl >/dev/null 2>&1; then return fi if ! launchctl print "$domain" >/dev/null 2>&1; then return fi echo "Stopping launchctl job $label" launchctl bootout "$domain" >/dev/null 2>&1 || true } using_portless() { [[ "${PORTLESS:-1}" != "0" ]] && command -v portless >/dev/null 2>&1 } using_portless_backend() { using_portless } using_portless_admin() { using_portless } backend_url() { if using_portless_backend; then echo "https://wygj-api.localhost/" else echo "http://127.0.0.1:8080/" fi } admin_url() { if using_portless_admin; then echo "https://wygj-admin.localhost/" else echo "http://127.0.0.1:5173/" fi } stop_dev() { stop_launchctl_job local.wygj.backend.dev stop_launchctl_job local.wygj.admin.dev stop_portless_route wygj-api stop_port 8080 stop_portless_route wygj-admin stop_port 5173 stop_port 5174 } status() { docker compose -f docker-compose.dev.yml ps lsof -nP -iTCP:8080 -sTCP:LISTEN || true lsof -nP -iTCP:5173 -sTCP:LISTEN || true lsof -nP -iTCP:5174 -sTCP:LISTEN || true if command -v portless >/dev/null 2>&1; then portless list || true fi } if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then usage exit 0 fi COMMAND="${1:-start}" RESET=false case "$COMMAND" in start) ;; --reset) COMMAND="start" RESET=true ;; stop) cd "$ROOT_DIR" stop_dev exit 0 ;; restart) cd "$ROOT_DIR" stop_dev COMMAND="start" ;; status) cd "$ROOT_DIR" status exit 0 ;; *) usage exit 1 ;; esac cd "$ROOT_DIR" ensure_docker if ! using_portless_backend; then ensure_port_free 8080 fi if ! using_portless_admin; then ensure_port_free 5173 fi ensure_port_free 5174 if [[ "$RESET" == true ]]; then ./scripts/reset-dev-db.sh else docker compose -f docker-compose.dev.yml up -d mysql redis qdrant minio minio-init fi trap cleanup INT TERM EXIT if using_portless_backend; then portless alias --remove wygj-api >/dev/null 2>&1 || true portless run --name wygj-api ./scripts/dev-backend.sh & else ./scripts/dev-backend.sh & fi PIDS+=("$!") ./scripts/dev-frontend.sh & PIDS+=("$!") ./scripts/dev-mobile.sh & PIDS+=("$!") echo "Backend: $(backend_url)" echo "Admin: $(admin_url)" echo "Legacy mobile fallback: http://127.0.0.1:5174/h5/user" echo "Current user side: https://wygj-mobile-uni.localhost/h5/ (run: cd mobile-uni && portless run --name wygj-mobile-uni npm run dev)" echo "Qdrant: http://127.0.0.1:6333/" echo "Press Ctrl+C to stop backend, admin frontend and legacy mobile frontend." while :; do for pid in "${PIDS[@]}"; do if ! kill -0 "$pid" >/dev/null 2>&1; then wait "$pid" exit $? fi done sleep 2 done