diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..78ad11a --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,297 @@ +#!/usr/bin/env bash +# ============================================================================= +# GoodGo Platform — One-Command Bootstrap +# ============================================================================= +# Sets up a complete local development environment from a clean clone. +# +# Usage: +# ./scripts/bootstrap.sh # Full setup +# ./scripts/bootstrap.sh --skip-seed # Skip database seeding +# ./scripts/bootstrap.sh --no-docker # Skip Docker services (BYO database) +# +# Idempotent — safe to run multiple times. +# Works on macOS and Linux. +# ============================================================================= + +set -euo pipefail + +# ── Colours & Helpers ──────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' # No Color + +info() { printf "${BLUE}ℹ${NC} %s\n" "$1"; } +success() { printf "${GREEN}✔${NC} %s\n" "$1"; } +warn() { printf "${YELLOW}⚠${NC} %s\n" "$1"; } +fail() { printf "${RED}✖${NC} %s\n" "$1" >&2; exit 1; } +step() { printf "\n${BOLD}── %s ──${NC}\n" "$1"; } + +# ── Parse Arguments ────────────────────────────────────────────────────────── + +SKIP_SEED=false +NO_DOCKER=false + +for arg in "$@"; do + case "$arg" in + --skip-seed) SKIP_SEED=true ;; + --no-docker) NO_DOCKER=true ;; + --help|-h) + echo "Usage: ./scripts/bootstrap.sh [--skip-seed] [--no-docker]" + echo "" + echo "Options:" + echo " --skip-seed Skip database seeding (pnpm db:seed)" + echo " --no-docker Skip Docker Compose services (bring your own DB)" + echo " --help, -h Show this help message" + exit 0 + ;; + *) + warn "Unknown argument: $arg (ignored)" + ;; + esac +done + +# ── Navigate to Project Root ───────────────────────────────────────────────── + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_ROOT" + +echo "" +printf "${BOLD}╔══════════════════════════════════════════════════╗${NC}\n" +printf "${BOLD}║ GoodGo Platform — Developer Bootstrap ║${NC}\n" +printf "${BOLD}╚══════════════════════════════════════════════════╝${NC}\n" +echo "" +info "Project root: $PROJECT_ROOT" + +# ── Step 1: Check Prerequisites ────────────────────────────────────────────── + +step "Checking prerequisites" + +MISSING=() + +# Node.js >= 22 +if command -v node &>/dev/null; then + NODE_VERSION=$(node -v | sed 's/v//') + NODE_MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1) + if [ "$NODE_MAJOR" -ge 22 ]; then + success "Node.js $NODE_VERSION (>= 22 required)" + else + fail "Node.js $NODE_VERSION found but >= 22 is required. Install via nvm: nvm install 22" + fi +else + MISSING+=("Node.js >= 22 — https://nodejs.org or 'nvm install 22'") +fi + +# pnpm +if command -v pnpm &>/dev/null; then + PNPM_VERSION=$(pnpm -v) + success "pnpm $PNPM_VERSION" +else + MISSING+=("pnpm — install with 'corepack enable' or 'npm install -g pnpm'") +fi + +# Docker (only if not --no-docker) +if [ "$NO_DOCKER" = false ]; then + if command -v docker &>/dev/null; then + DOCKER_VERSION=$(docker --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + success "Docker $DOCKER_VERSION" + else + MISSING+=("Docker — https://docs.docker.com/get-docker/") + fi + + # Docker Compose (v2 plugin or standalone) + if docker compose version &>/dev/null 2>&1; then + COMPOSE_VERSION=$(docker compose version --short 2>/dev/null || docker compose version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + success "Docker Compose $COMPOSE_VERSION" + elif command -v docker-compose &>/dev/null; then + COMPOSE_VERSION=$(docker-compose --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) + success "Docker Compose $COMPOSE_VERSION (standalone)" + else + MISSING+=("Docker Compose — https://docs.docker.com/compose/install/") + fi + + # Check Docker daemon is running + if ! docker info &>/dev/null 2>&1; then + fail "Docker daemon is not running. Please start Docker Desktop or the Docker service." + fi +fi + +if [ ${#MISSING[@]} -gt 0 ]; then + echo "" + printf "${RED}Missing prerequisites:${NC}\n" + for dep in "${MISSING[@]}"; do + echo " • $dep" + done + echo "" + fail "Install the missing tools above and re-run this script." +fi + +success "All prerequisites met" + +# ── Step 2: Environment File ───────────────────────────────────────────────── + +step "Setting up environment" + +if [ -f .env ]; then + success ".env already exists (skipping copy)" +else + if [ -f .env.example ]; then + cp .env.example .env + + # Generate secure defaults for development + if command -v openssl &>/dev/null; then + JWT_SECRET_VAL=$(openssl rand -base64 48) + JWT_REFRESH_VAL=$(openssl rand -base64 48) + KYC_KEY_VAL=$(openssl rand -hex 32) + + # Platform-safe sed in-place (macOS vs Linux) + if [[ "$OSTYPE" == "darwin"* ]]; then + SED_INPLACE=(sed -i '') + else + SED_INPLACE=(sed -i) + fi + + "${SED_INPLACE[@]}" "s|DB_PASSWORD=CHANGE_ME|DB_PASSWORD=goodgo_secret|" .env + "${SED_INPLACE[@]}" "s|TYPESENSE_API_KEY=CHANGE_ME|TYPESENSE_API_KEY=ts_dev_key_change_me|" .env + "${SED_INPLACE[@]}" "s|MINIO_ACCESS_KEY=CHANGE_ME|MINIO_ACCESS_KEY=minioadmin|" .env + "${SED_INPLACE[@]}" "s|MINIO_SECRET_KEY=CHANGE_ME|MINIO_SECRET_KEY=minioadmin|" .env + "${SED_INPLACE[@]}" "s|PGBOUNCER_ADMIN_PASSWORD=CHANGE_ME|PGBOUNCER_ADMIN_PASSWORD=pgbouncer_admin|" .env + "${SED_INPLACE[@]}" "s|PGBOUNCER_STATS_PASSWORD=CHANGE_ME|PGBOUNCER_STATS_PASSWORD=pgbouncer_stats|" .env + "${SED_INPLACE[@]}" "s|JWT_SECRET=.*|JWT_SECRET=$JWT_SECRET_VAL|" .env + "${SED_INPLACE[@]}" "s|JWT_REFRESH_SECRET=.*|JWT_REFRESH_SECRET=$JWT_REFRESH_VAL|" .env + "${SED_INPLACE[@]}" "s|KYC_ENCRYPTION_KEY=.*|KYC_ENCRYPTION_KEY=$KYC_KEY_VAL|" .env + else + warn "openssl not found — .env copied but secrets are placeholders. Update them manually." + fi + + success ".env created from .env.example with generated dev secrets" + else + fail ".env.example not found. Is this the project root?" + fi +fi + +# ── Step 3: Install Dependencies ───────────────────────────────────────────── + +step "Installing dependencies" + +pnpm install --frozen-lockfile 2>/dev/null || pnpm install +success "Dependencies installed" + +# ── Step 4: Start Docker Services ──────────────────────────────────────────── + +if [ "$NO_DOCKER" = false ]; then + step "Starting Docker services" + + # Start only core dev services (not monitoring/backup stack) + CORE_SERVICES=(postgres redis typesense minio) + + info "Starting: ${CORE_SERVICES[*]}" + docker compose up -d "${CORE_SERVICES[@]}" + + # Wait for services to be healthy + info "Waiting for services to be healthy..." + + MAX_WAIT=120 + ELAPSED=0 + INTERVAL=5 + + wait_for_service() { + local service="$1" + local container="goodgo-$service" + + while [ "$ELAPSED" -lt "$MAX_WAIT" ]; do + local health + health=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "not_found") + + case "$health" in + healthy) + success "$service is healthy" + return 0 + ;; + unhealthy) + fail "$service is unhealthy. Check logs: docker compose logs $service" + ;; + not_found) + fail "Container $container not found. Check: docker compose ps" + ;; + *) + # starting or no status yet + ;; + esac + + sleep "$INTERVAL" + ELAPSED=$((ELAPSED + INTERVAL)) + done + + fail "$service did not become healthy within ${MAX_WAIT}s. Check logs: docker compose logs $service" + } + + for svc in "${CORE_SERVICES[@]}"; do + ELAPSED=0 + wait_for_service "$svc" + done + + success "All Docker services are healthy" +else + warn "Skipping Docker services (--no-docker). Ensure PostgreSQL, Redis, Typesense, and MinIO are available." +fi + +# ── Step 5: Generate Prisma Client ─────────────────────────────────────────── + +step "Setting up database" + +info "Generating Prisma client..." +pnpm db:generate +success "Prisma client generated" + +# ── Step 6: Run Migrations ─────────────────────────────────────────────────── + +info "Running database migrations..." +pnpm db:migrate:dev --name init 2>/dev/null || pnpm db:migrate:dev 2>/dev/null || { + # If migrate dev fails (e.g., migrations already applied), try deploy instead + warn "migrate dev had issues, trying migrate deploy..." + pnpm db:migrate:deploy +} +success "Database migrations applied" + +# ── Step 7: Seed Database ──────────────────────────────────────────────────── + +if [ "$SKIP_SEED" = false ]; then + info "Seeding database..." + pnpm db:seed || { + warn "Seed command returned non-zero (data may already exist). Continuing..." + } + success "Database seeded" +else + info "Skipping database seed (--skip-seed)" +fi + +# ── Done! ──────────────────────────────────────────────────────────────────── + +echo "" +printf "${BOLD}${GREEN}╔══════════════════════════════════════════════════╗${NC}\n" +printf "${BOLD}${GREEN}║ Setup Complete! ║${NC}\n" +printf "${BOLD}${GREEN}╚══════════════════════════════════════════════════╝${NC}\n" +echo "" +echo "Next steps:" +echo "" +echo " ${BOLD}pnpm dev${NC} Start all apps (API :3001, Web :3000)" +echo " ${BOLD}pnpm db:studio${NC} Open Prisma Studio to browse data" +echo " ${BOLD}pnpm test${NC} Run unit tests" +echo " ${BOLD}pnpm lint${NC} Run ESLint" +echo "" +echo "Optional services (monitoring stack):" +echo "" +echo " ${BOLD}docker compose up -d loki promtail prometheus grafana${NC}" +echo " Grafana: http://localhost:3002 (admin/admin)" +echo "" +echo "Documentation:" +echo "" +echo " README.md Project overview" +echo " CONTRIBUTING.md Contribution guidelines" +echo " CLAUDE.md AI assistant instructions" +echo ""