- Updated app.config.ts to use process.cwd() for resolving environment file paths, improving compatibility with pnpm. - Replaced ensure_env function with check_app_env in manage.sh to verify the existence of required environment files for BFF and optional files for Portal, enhancing error handling and user guidance.
467 lines
14 KiB
Bash
Executable File
467 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# 🔧 Development Environment Manager
|
|
# Clean, portable helper for local dev services & apps
|
|
|
|
set -Eeuo pipefail
|
|
IFS=$'\n\t'
|
|
|
|
########################################
|
|
# Config (override via env if you like)
|
|
########################################
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="${PROJECT_ROOT:-"$(cd "$SCRIPT_DIR/../.." && pwd)"}"
|
|
|
|
# Compose file selection
|
|
# Priority: explicit COMPOSE_FILE > DEV_STACK (full|services|auto) > default (full if present)
|
|
DEFAULT_COMPOSE_SERVICES="$PROJECT_ROOT/docker/dev/docker-compose.yml"
|
|
DEFAULT_COMPOSE_FULL="$PROJECT_ROOT/docker/dev/docker-compose.full.yml"
|
|
|
|
if [ -z "${COMPOSE_FILE:-}" ]; then
|
|
case "${DEV_STACK:-services}" in
|
|
full)
|
|
COMPOSE_FILE="$DEFAULT_COMPOSE_FULL"
|
|
;;
|
|
services)
|
|
COMPOSE_FILE="$DEFAULT_COMPOSE_SERVICES"
|
|
;;
|
|
auto)
|
|
if [ -f "$DEFAULT_COMPOSE_FULL" ]; then
|
|
COMPOSE_FILE="$DEFAULT_COMPOSE_FULL"
|
|
else
|
|
COMPOSE_FILE="$DEFAULT_COMPOSE_SERVICES"
|
|
fi
|
|
;;
|
|
*)
|
|
COMPOSE_FILE="$DEFAULT_COMPOSE_FULL"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
ENV_FILE="${ENV_FILE:-"$PROJECT_ROOT/.env"}"
|
|
ENV_EXAMPLE_FILE="${ENV_EXAMPLE_FILE:-"$PROJECT_ROOT/.env.example"}"
|
|
PROJECT_NAME="${PROJECT_NAME:-portal-dev}"
|
|
|
|
DB_USER_DEFAULT="dev"
|
|
DB_NAME_DEFAULT="portal_dev"
|
|
DB_WAIT_SECS="${DB_WAIT_SECS:-30}"
|
|
|
|
NEXT_PORT_DEFAULT=3000
|
|
BFF_PORT_DEFAULT=4000
|
|
|
|
########################################
|
|
# 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
|
|
|
|
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 (set DEV_STACK=services or COMPOSE_FILE to override)"
|
|
|
|
# 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
|
|
########################################
|
|
|
|
# Check that per-app env files exist
|
|
check_app_env() {
|
|
local missing=0
|
|
|
|
if [ ! -f "$PROJECT_ROOT/apps/bff/.env" ] && [ ! -f "$PROJECT_ROOT/apps/bff/.env.development" ]; then
|
|
warn "BFF env file not found. Create one with:"
|
|
warn " cp apps/bff/.env.example apps/bff/.env"
|
|
missing=1
|
|
fi
|
|
|
|
# Portal env is optional - Next.js has sensible defaults for dev
|
|
if [ ! -f "$PROJECT_ROOT/apps/portal/.env.local" ] && [ ! -f "$PROJECT_ROOT/apps/portal/.env.development" ]; then
|
|
log "💡 Portal env file not found (optional). Create one with:"
|
|
log " cp apps/portal/.env.example apps/portal/.env.local"
|
|
fi
|
|
|
|
if [ "$missing" -eq 1 ]; then
|
|
fail "Required env files missing. See messages above."
|
|
fi
|
|
}
|
|
|
|
# Load root .env for Docker Compose services (postgres, redis config)
|
|
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
|
|
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
|
|
fi
|
|
else
|
|
warn "Neither lsof nor fuser found; skipping port cleanup for $port."
|
|
fi
|
|
}
|
|
|
|
# Check if a port is free using Node (portable)
|
|
is_port_free() {
|
|
local port="$1"
|
|
if ! command -v node >/dev/null 2>&1; then
|
|
return 0 # assume free if node unavailable
|
|
fi
|
|
node -e "const net=require('net');const p=parseInt(process.argv[1],10);const s=net.createServer();s.once('error',()=>process.exit(1));s.once('listening',()=>s.close(()=>process.exit(0)));s.listen({port:p,host:'127.0.0.1'});" "$port"
|
|
}
|
|
|
|
# Find a free port starting from base, up to +50
|
|
find_free_port() {
|
|
local base="$1"
|
|
local limit=$((base+50))
|
|
local p="$base"
|
|
while [ "$p" -le "$limit" ]; do
|
|
if is_port_free "$p"; then
|
|
echo "$p"
|
|
return 0
|
|
fi
|
|
p=$((p+1))
|
|
done
|
|
echo "$base"
|
|
}
|
|
|
|
########################################
|
|
# Commands
|
|
########################################
|
|
start_services() {
|
|
preflight "start"
|
|
cd "$PROJECT_ROOT"
|
|
load_env_exported
|
|
|
|
log "🚀 Starting development services..."
|
|
log "🧩 Using compose: $COMPOSE_FILE"
|
|
compose up -d postgres redis
|
|
wait_for_postgres
|
|
|
|
local next="${NEXT_PORT:-$NEXT_PORT_DEFAULT}"
|
|
local bff="${BFF_PORT:-$BFF_PORT_DEFAULT}"
|
|
# Ensure desired ports are free; kill any listeners
|
|
kill_by_port "$next"
|
|
kill_by_port "$bff"
|
|
# If still busy, either auto-shift (if allowed) or fail
|
|
if ! is_port_free "$next"; then
|
|
if [ "${ALLOW_PORT_SHIFT:-0}" = "1" ]; then
|
|
local next_free
|
|
next_free="$(find_free_port "$next")"
|
|
warn "Port $next in use; assigning NEXT_PORT=$next_free"
|
|
export NEXT_PORT="$next_free"
|
|
next="$next_free"
|
|
else
|
|
fail "Port $next is in use. Stop the process or run '$0 cleanup'. Set ALLOW_PORT_SHIFT=1 to auto-assign another port."
|
|
fi
|
|
fi
|
|
if ! is_port_free "$bff"; then
|
|
if [ "${ALLOW_PORT_SHIFT:-0}" = "1" ]; then
|
|
local bff_free
|
|
bff_free="$(find_free_port "$bff")"
|
|
warn "Port $bff in use; assigning BFF_PORT=$bff_free"
|
|
export BFF_PORT="$bff_free"
|
|
bff="$bff_free"
|
|
else
|
|
fail "Port $bff is in use. Stop the process or run '$0 cleanup'. Set ALLOW_PORT_SHIFT=1 to auto-assign another port."
|
|
fi
|
|
fi
|
|
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}"
|
|
}
|
|
|
|
start_with_tools() {
|
|
preflight "tools"
|
|
cd "$PROJECT_ROOT"
|
|
load_env_exported
|
|
|
|
log "🛠️ Starting development services with admin tools..."
|
|
log "🧩 Using compose: $COMPOSE_FILE"
|
|
# Explicitly start only services + admin tools to avoid building app containers in the full compose
|
|
compose --profile tools up -d postgres redis adminer redis-commander mailhog
|
|
wait_for_postgres
|
|
log "🔗 Database Admin: http://localhost:8080"
|
|
log "🔗 Redis Commander: http://localhost:8081"
|
|
}
|
|
|
|
stop_services() {
|
|
preflight "stop"
|
|
cd "$PROJECT_ROOT"
|
|
log "⏹️ Stopping development services..."
|
|
compose down --remove-orphans
|
|
log "✅ Services stopped"
|
|
}
|
|
|
|
show_status() {
|
|
preflight "status"
|
|
cd "$PROJECT_ROOT"
|
|
log "📊 Development Services Status:"
|
|
log "🧩 Using compose: $COMPOSE_FILE"
|
|
compose ps
|
|
}
|
|
|
|
show_logs() {
|
|
preflight "logs"
|
|
cd "$PROJECT_ROOT"
|
|
# Pass-through any service names after "logs"
|
|
# e.g. ./dev.sh logs postgres redis
|
|
log "🧩 Using compose: $COMPOSE_FILE"
|
|
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
|
|
pkill -f "apps/bff/scripts/dev-watch.sh" 2>/dev/null && log " Stopped BFF dev-watch script" || true
|
|
pkill -f "tsc -p tsconfig.build.json --watch" 2>/dev/null && log " Stopped BFF TypeScript watcher" || true
|
|
pkill -f "tsc-alias.*tsconfig.build.json.*-w" 2>/dev/null && log " Stopped BFF tsc-alias watcher" || true
|
|
pkill -f "node --watch dist/main.js" 2>/dev/null && log " Stopped BFF Node watcher" || true
|
|
|
|
sleep 1
|
|
log "✅ Development cleanup completed"
|
|
}
|
|
|
|
start_apps() {
|
|
preflight "apps"
|
|
cd "$PROJECT_ROOT"
|
|
|
|
# Verify app env files exist before proceeding
|
|
check_app_env
|
|
|
|
cleanup_dev
|
|
|
|
if ! services_running; then
|
|
start_services
|
|
fi
|
|
|
|
load_env_exported
|
|
|
|
# Build shared package first
|
|
log "🔨 Building shared package..."
|
|
pnpm --filter @customer-portal/domain build
|
|
pnpm --filter @customer-portal/validation build
|
|
|
|
# Build BFF before watch (ensures dist exists). Use Nest build for correct emit.
|
|
log "🔨 Building BFF for initial setup (ts emit)..."
|
|
(
|
|
cd "$PROJECT_ROOT/apps/bff" \
|
|
&& pnpm clean \
|
|
&& rm -f tsconfig.build.tsbuildinfo \
|
|
&& pnpm build || pnpm exec tsc -b --force tsconfig.build.json
|
|
)
|
|
if [ ! -d "$PROJECT_ROOT/apps/bff/dist" ]; then
|
|
warn "BFF dist not found after build; forcing TypeScript emit..."
|
|
(cd "$PROJECT_ROOT/apps/bff" && pnpm exec tsc -b --force tsconfig.build.json)
|
|
fi
|
|
if [ ! -f "$PROJECT_ROOT/apps/bff/dist/main.js" ] && [ ! -f "$PROJECT_ROOT/apps/bff/dist/main.cjs" ]; then
|
|
warn "BFF main output not found; will rely on watch to produce it."
|
|
fi
|
|
|
|
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/domain \
|
|
--filter @customer-portal/validation \
|
|
--filter @customer-portal/portal \
|
|
--filter @customer-portal/bff run dev
|
|
}
|
|
|
|
reset_env() {
|
|
preflight "reset"
|
|
cd "$PROJECT_ROOT"
|
|
log "🔄 Resetting development environment..."
|
|
compose down -v --remove-orphans
|
|
docker system prune -f
|
|
log "✅ Development environment reset"
|
|
}
|
|
|
|
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"
|
|
}
|
|
|
|
studio_db() {
|
|
preflight "studio"
|
|
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 "🔍 Starting Prisma Studio..."
|
|
pnpm --filter @customer-portal/bff run db:studio
|
|
}
|
|
|
|
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
|
|
studio - Open Prisma Studio database browser
|
|
reset - Reset development environment
|
|
help - Show this help
|
|
|
|
Setup (first time):
|
|
cp apps/bff/.env.example apps/bff/.env
|
|
cp apps/portal/.env.example apps/portal/.env.local # optional
|
|
|
|
Environment overrides:
|
|
PROJECT_ROOT, COMPOSE_FILE, PROJECT_NAME
|
|
DEV_STACK (full|services|auto) # default: full
|
|
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 ;;
|
|
studio) studio_db ;;
|
|
reset) reset_env ;;
|
|
help|*) usage; exit 0 ;;
|
|
esac
|