334 lines
9.6 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
2025-08-21 15:24:40 +09:00
# 🔧 Development Environment Manager
# Clean, portable helper for local dev services & apps
2025-08-21 15:24:40 +09:00
set -Eeuo pipefail
IFS=$'\n\t'
2025-08-21 15:24:40 +09:00
########################################
# Config (override via env if you like)
########################################
2025-08-21 15:24:40 +09:00
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="${PROJECT_ROOT:-"$(cd "$SCRIPT_DIR/../.." && pwd)"}"
COMPOSE_FILE="${COMPOSE_FILE:-"$PROJECT_ROOT/docker/dev/docker-compose.yml"}"
ENV_FILE="${ENV_FILE:-"$PROJECT_ROOT/.env"}"
ENV_EXAMPLE_FILE="${ENV_EXAMPLE_FILE:-"$PROJECT_ROOT/.env.example"}"
PROJECT_NAME="${PROJECT_NAME:-portal-dev}"
2025-08-21 15:24:40 +09:00
DB_USER_DEFAULT="dev"
DB_NAME_DEFAULT="portal_dev"
DB_WAIT_SECS="${DB_WAIT_SECS:-30}"
2025-08-21 15:24:40 +09:00
NEXT_PORT_DEFAULT=3000
BFF_PORT_DEFAULT=4000
2025-08-21 15:24:40 +09:00
########################################
# Colors (fallback if tput missing)
########################################
if command -v tput >/dev/null 2>&1 && [ -t 1 ]; then
GREEN="$(tput setaf 2)"
YELLOW="$(tput setaf 3)"
RED="$(tput setaf 1)"
NC="$(tput sgr0)"
else
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
fi
2025-08-21 15:24:40 +09:00
log() { echo -e "${GREEN}[DEV] $*${NC}"; }
warn() { echo -e "${YELLOW}[DEV] $*${NC}"; }
fail() { echo -e "${RED}[DEV] ERROR: $*${NC}"; exit 1; }
trap 'fail "Command failed (exit $?) at line $LINENO. See logs above."' ERR
########################################
# Docker Compose wrapper (v2 & v1)
########################################
detect_compose() {
if docker compose version >/dev/null 2>&1; then
echo "docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
echo "docker-compose"
else
fail "Docker Compose not found. Install Docker Desktop or docker-compose."
fi
}
COMPOSE_BIN="$(detect_compose)"
compose() {
# shellcheck disable=SC2086
eval $COMPOSE_BIN -f "$COMPOSE_FILE" -p "$PROJECT_NAME" "$@"
}
########################################
# Preflight checks
########################################
preflight() {
command -v docker >/dev/null 2>&1 || fail "Docker is required."
[ -f "$COMPOSE_FILE" ] || fail "Compose file not found: $COMPOSE_FILE"
# Suggest Docker running if ps fails
if ! docker info >/dev/null 2>&1; then
fail "Docker daemon not reachable. Is Docker running?"
fi
# pnpm required for app tasks
if [[ "${1:-}" == "apps" || "${1:-}" == "migrate" ]]; then
command -v pnpm >/dev/null 2>&1 || fail "pnpm is required for app commands."
fi
}
########################################
# Env handling
########################################
ensure_env() {
if [ ! -f "$ENV_FILE" ]; then
warn "Environment file not found at $ENV_FILE"
if [ -f "$ENV_EXAMPLE_FILE" ]; then
log "Creating .env from example..."
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
warn "Please edit $ENV_FILE with your actual values."
else
warn "No .env.example found at $ENV_EXAMPLE_FILE. Creating empty .env..."
: > "$ENV_FILE"
fi
fi
}
load_env_exported() {
# Export so child processes see env (compose, pnpm etc.)
set +u
set -a
[ -f "$ENV_FILE" ] && . "$ENV_FILE" || true
set +a
set -u
}
########################################
# Helpers
########################################
services_running() {
compose ps | grep -q "Up"
}
wait_for_postgres() {
local user="${POSTGRES_USER:-$DB_USER_DEFAULT}"
local db="${POSTGRES_DB:-$DB_NAME_DEFAULT}"
local timeout="$DB_WAIT_SECS"
log "⏳ Waiting for database ($db) to be ready (timeout: ${timeout}s)..."
local elapsed=0
local step=2
until compose exec -T postgres pg_isready -U "$user" -d "$db" >/dev/null 2>&1; do
sleep "$step"
elapsed=$((elapsed + step))
if (( elapsed >= timeout )); then
fail "Database failed to become ready within ${timeout}s"
fi
done
log "✅ Database is ready!"
}
kill_by_port() {
local port="$1"
# Prefer lsof on macOS; fall back to fuser on Linux
if command -v lsof >/dev/null 2>&1; then
if lsof -tiTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then
log " Killing process on port $port..."
lsof -tiTCP:"$port" -sTCP:LISTEN | xargs -r kill -9 2>/dev/null || true
2025-08-21 15:24:40 +09:00
fi
elif command -v fuser >/dev/null 2>&1; then
if fuser -n tcp "$port" >/dev/null 2>&1; then
log " Killing process on port $port..."
fuser -k -n tcp "$port" 2>/dev/null || true
2025-08-21 15:24:40 +09:00
fi
else
warn "Neither lsof nor fuser found; skipping port cleanup for $port."
fi
}
########################################
# Commands
########################################
start_services() {
preflight "start"
cd "$PROJECT_ROOT"
ensure_env
load_env_exported
log "🚀 Starting development services..."
compose up -d postgres redis
wait_for_postgres
local next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"
log "✅ Development services are running!"
log "🔗 Database: postgresql://${POSTGRES_USER:-$DB_USER_DEFAULT}:${POSTGRES_PASSWORD:-${POSTGRES_PASSWORD:-dev}}@localhost:5432/${POSTGRES_DB:-$DB_NAME_DEFAULT}"
log "🔗 Redis: redis://localhost:6379"
log "🔗 BFF API (expected): http://localhost:${bff}/api"
log "🔗 Frontend (expected): http://localhost:${next}"
2025-08-21 15:24:40 +09:00
}
start_with_tools() {
preflight "tools"
cd "$PROJECT_ROOT"
ensure_env
load_env_exported
log "🛠️ Starting development services with admin tools..."
compose --profile tools up -d
wait_for_postgres
log "🔗 Database Admin: http://localhost:8080"
log "🔗 Redis Commander: http://localhost:8081"
2025-08-21 15:24:40 +09:00
}
stop_services() {
preflight "stop"
cd "$PROJECT_ROOT"
log "⏹️ Stopping development services..."
compose down --remove-orphans
log "✅ Services stopped"
2025-08-21 15:24:40 +09:00
}
show_status() {
preflight "status"
cd "$PROJECT_ROOT"
log "📊 Development Services Status:"
compose ps
2025-08-21 15:24:40 +09:00
}
show_logs() {
preflight "logs"
cd "$PROJECT_ROOT"
# Pass-through any service names after "logs"
# e.g. ./dev.sh logs postgres redis
compose logs -f --tail=100 "${@:2}"
}
cleanup_dev() {
log "🧹 Cleaning up all development processes and ports..."
# Pull ports from env if present; include common defaults
local ports=()
ports+=("${NEXT_PORT:-$NEXT_PORT_DEFAULT}")
ports+=("${BFF_PORT:-$BFF_PORT_DEFAULT}")
ports+=(5555) # Prisma Studio default
for p in "${ports[@]}"; do
kill_by_port "$p"
done
# Kill common dev processes by name
pkill -f "next dev" 2>/dev/null && log " Stopped Next.js dev server" || true
pkill -f "nest start --watch" 2>/dev/null && log " Stopped NestJS watch server" || true
pkill -f "next-server" 2>/dev/null && log " Stopped Next.js server process" || true
pkill -f "pnpm.*--parallel.*dev" 2>/dev/null && log " Stopped parallel dev processes" || true
pkill -f "prisma studio" 2>/dev/null && log " Stopped Prisma Studio" || true
sleep 1
log "✅ Development cleanup completed"
2025-08-21 15:24:40 +09:00
}
start_apps() {
preflight "apps"
cd "$PROJECT_ROOT"
cleanup_dev
if ! services_running; then
start_services
fi
load_env_exported
# Build shared package first
log "🔨 Building shared package..."
pnpm --filter @customer-portal/shared build
# Build BFF before watch (ensures dist exists)
log "🔨 Building BFF for initial setup..."
(cd "$PROJECT_ROOT/apps/bff" && pnpm tsc -p tsconfig.build.json)
local next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"
log "🎯 Starting development applications..."
log "🔗 BFF API: http://localhost:${bff}/api"
log "🔗 Frontend: http://localhost:${next}"
log "🔗 Database: postgresql://${POSTGRES_USER:-$DB_USER_DEFAULT}:${POSTGRES_PASSWORD:-${POSTGRES_PASSWORD:-dev}}@localhost:5432/${POSTGRES_DB:-$DB_NAME_DEFAULT}"
log "🔗 Redis: redis://localhost:6379"
log "📚 API Docs: http://localhost:${bff}/api/docs"
log "Starting apps with hot-reload..."
# Prisma Studio can be started manually with: pnpm db:studio
# Run portal + bff in parallel with hot reload
pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run dev
}
2025-08-21 15:24:40 +09:00
reset_env() {
preflight "reset"
cd "$PROJECT_ROOT"
log "🔄 Resetting development environment..."
compose down -v --remove-orphans
docker system prune -f
log "✅ Development environment reset"
2025-08-21 15:24:40 +09:00
}
migrate_db() {
preflight "migrate"
cd "$PROJECT_ROOT"
if ! compose ps postgres | grep -q "Up"; then
fail "Database service not running. Run '$0 start' or '$0 apps' first."
fi
load_env_exported
log "🗄️ Running database migrations..."
pnpm db:migrate
log "✅ Database migrations completed"
}
usage() {
cat <<EOF
🔧 Development Environment Manager
Usage: $0 {command}
Commands:
start - Start dev services (PostgreSQL + Redis)
stop - Stop all dev services
restart - Restart dev services
status - Show service status
logs - Tail logs (optionally: specify services, e.g. '$0 logs postgres redis')
tools - Start services with admin tools
apps - Start services + run dev apps (auto-cleanup)
cleanup - Clean up local dev processes and ports
migrate - Run database migrations
reset - Reset development environment
help - Show this help
Environment overrides:
PROJECT_ROOT, COMPOSE_FILE, ENV_FILE, ENV_EXAMPLE_FILE, PROJECT_NAME
POSTGRES_USER, POSTGRES_DB, POSTGRES_PASSWORD, DB_WAIT_SECS
NEXT_PORT, BFF_PORT
EOF
}
########################################
# Main
########################################
cmd="${1:-help}"
case "$cmd" in
start) start_services ;;
stop) stop_services ;;
restart) stop_services; start_services ;;
status) show_status ;;
logs) show_logs "$@" ;;
tools) start_with_tools ;;
apps) start_apps ;;
cleanup) cleanup_dev ;;
migrate) migrate_db ;;
reset) reset_env ;;
help|*) usage; exit 0 ;;
esac