#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" source "$ROOT_DIR/scripts/lib/process-tree.sh" PIDS=() usage() { cat <<'EOF' Usage: ./scripts/dev.sh [start|stop|restart|status|--reset] Starts MySQL, Redis, MinIO, Qdrant, backend, admin frontend and current mobile-uni frontend. 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 stop_process_tree "$pid" 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" for pid in $pids; do stop_process_tree "$pid" done for _ in {1..20}; do if ! portless list 2>/dev/null | grep -q "https://${name}[.]localhost"; then return fi sleep 0.2 done } 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 } using_portless_mobile() { 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 } mobile_url() { if using_portless_mobile; then echo "https://wygj-mobile-uni.localhost/h5/" else echo "http://127.0.0.1:5174/h5/user" 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_portless_route wygj-mobile-uni 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 if ! using_portless_mobile; then ensure_port_free 5174 fi 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 --force ./scripts/dev-backend.sh & else ./scripts/dev-backend.sh & fi PIDS+=("$!") ./scripts/dev-frontend.sh & PIDS+=("$!") if using_portless_mobile; then portless alias --remove wygj-mobile-uni >/dev/null 2>&1 || true VITE_PROXY_TARGET="${VITE_PROXY_TARGET:-https://wygj-api.localhost}" portless run --name wygj-mobile-uni --force npm --prefix mobile-uni run dev & else ./scripts/dev-mobile.sh & fi PIDS+=("$!") echo "Backend: $(backend_url)" echo "Admin: $(admin_url)" echo "Mobile: $(mobile_url)" if using_portless_mobile; then echo "Legacy mobile fallback: PORTLESS=0 ./scripts/dev.sh" fi echo "Qdrant: http://127.0.0.1:6333/" echo "Press Ctrl+C to stop backend, admin frontend and 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