diff --git a/.cursor/environment.docker-compose.json b/.cursor/environment.docker-compose.json deleted file mode 100644 index 255af62235b..00000000000 --- a/.cursor/environment.docker-compose.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Setting up Docker Compose environment...' && cd packages/twenty-docker && cp -n docker-compose.yml docker-compose.dev.yml || true && echo 'Dependencies installed and docker-compose prepared'", - "start": "sudo service docker start && echo 'Docker service started' && cd packages/twenty-docker && echo 'Installing yq for YAML processing...' && sudo apt-get update -qq && sudo apt-get install -y wget && wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq && echo 'Patching docker-compose for local development...' && yq eval 'del(.services.server.image)' -i docker-compose.dev.yml && yq eval '.services.server.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.server.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && yq eval 'del(.services.worker.image)' -i docker-compose.dev.yml && yq eval '.services.worker.build.context = \"../../\"' -i docker-compose.dev.yml && yq eval '.services.worker.build.dockerfile = \"./packages/twenty-docker/twenty/Dockerfile\"' -i docker-compose.dev.yml && echo 'Setting up .env file with database configuration...' && echo 'SERVER_URL=http://localhost:3000' > .env && echo 'APP_SECRET='$(openssl rand -base64 32) >> .env && echo 'PG_DATABASE_PASSWORD='$(openssl rand -hex 16) >> .env && echo 'PG_DATABASE_URL=postgres://postgres:password@localhost:5432/postgres' >> .env && echo 'SIGN_IN_PREFILLED=true' >> .env && echo 'Building and starting services...' && docker-compose -f docker-compose.dev.yml up -d --build && echo 'Waiting for services to initialize...' && sleep 30 && echo 'Checking service health...' && docker-compose -f docker-compose.dev.yml ps && echo 'Environment setup complete!'", - "terminals": [ - { - "name": "Database Setup & Seed", - "command": "sleep 40 && cd packages/twenty-docker && echo 'Waiting for PostgreSQL to be ready...' && until docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres; do echo 'Waiting for PostgreSQL...'; sleep 5; done && echo 'PostgreSQL is ready!' && echo 'Waiting for Twenty server to be healthy...' && until docker-compose -f docker-compose.dev.yml exec -T server curl --fail http://localhost:3000/healthz 2>/dev/null; do echo 'Waiting for server...'; sleep 5; done && echo 'Server is healthy!' && echo 'Running database setup and seeding...' && docker-compose -f docker-compose.dev.yml exec -T server npx nx database:reset twenty-server && echo 'Database seeded successfully!' && bash" - }, - { - "name": "Application Logs", - "command": "sleep 35 && cd packages/twenty-docker && echo 'Following application logs...' && docker-compose -f docker-compose.dev.yml logs -f server worker" - }, - { - "name": "Service Monitor", - "command": "sleep 15 && cd packages/twenty-docker && echo '=== Service Status Monitor ===' && while true; do clear; echo '=== Service Status at $(date) ===' && docker-compose -f docker-compose.dev.yml ps && echo '\\n=== Health Status ===' && (docker-compose -f docker-compose.dev.yml exec -T server curl -s http://localhost:3000/healthz 2>/dev/null && echo '✅ Twenty Server: Healthy') || echo '❌ Twenty Server: Not Ready' && (docker-compose -f docker-compose.dev.yml exec -T db pg_isready -U postgres 2>/dev/null && echo '✅ PostgreSQL: Ready') || echo '❌ PostgreSQL: Not Ready' && echo '\\n=== Database Connection Test ===' && docker-compose -f docker-compose.dev.yml exec -T server node -e \"const { Client } = require('pg'); const client = new Client({connectionString: process.env.PG_DATABASE_URL}); client.connect().then(() => {console.log('✅ Database Connection: OK'); client.end();}).catch(e => console.log('❌ Database Connection: Failed -', e.message));\" || echo 'Connection test failed' && sleep 45; done" - } - ] -} \ No newline at end of file diff --git a/.cursor/environment.json b/.cursor/environment.json index 49baf882897..55a886c302b 100644 --- a/.cursor/environment.json +++ b/.cursor/environment.json @@ -1,6 +1,6 @@ { "install": "yarn install", - "start": "sudo service docker start && sleep 2 && (docker start twenty_pg 2>/dev/null || make -C packages/twenty-docker postgres-on-docker) && (docker start twenty_redis 2>/dev/null || make -C packages/twenty-docker redis-on-docker) && until docker exec twenty_pg pg_isready -U postgres -h localhost 2>/dev/null; do sleep 1; done && echo 'PostgreSQL ready' && until docker exec twenty_redis redis-cli ping 2>/dev/null | grep -q PONG; do sleep 1; done && echo 'Redis ready' && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server", + "start": "(sudo service docker start || service docker start || true) && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server", "terminals": [ { "name": "Development Server", diff --git a/CLAUDE.md b/CLAUDE.md index 070905e9e81..6fe99438691 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -188,13 +188,22 @@ IMPORTANT: Use Context7 for code generation, setup or configuration steps, or li - Descriptive test names: "should [behavior] when [condition]" - Clear mocks between tests with `jest.clearAllMocks()` -## CI Environment (GitHub Actions) +## Dev Environment Setup -When running in CI, the dev environment is **not** pre-configured. Dependencies are installed but builds, env files, and databases are not set up. +All dev environments (Claude Code web, Cursor, local) use one script: -- **Before running tests, builds, lint, type checks, or DB operations**, run: `bash packages/twenty-utils/setup-dev-env.sh` +```bash +bash packages/twenty-utils/setup-dev-env.sh +``` + +This handles everything: starts Postgres + Redis (auto-detects local services vs Docker), creates databases, and copies `.env` files. Idempotent — safe to run multiple times. + +- `--docker` — force Docker mode (uses `packages/twenty-docker/docker-compose.dev.yml`) +- `--down` — stop services +- `--reset` — wipe data and restart fresh - **Skip the setup script** for tasks that only read code — architecture questions, code review, documentation, etc. -- The script is idempotent and safe to run multiple times. + +**Note:** CI workflows (GitHub Actions) manage services via Actions service containers and run setup steps individually — they don't use this script. ## Important Files - `nx.json` - Nx workspace configuration with task definitions diff --git a/packages/twenty-docker/docker-compose.dev.yml b/packages/twenty-docker/docker-compose.dev.yml new file mode 100644 index 00000000000..ea4aa8d91e3 --- /dev/null +++ b/packages/twenty-docker/docker-compose.dev.yml @@ -0,0 +1,42 @@ +# Development infrastructure services only (Postgres + Redis). +# Use this when developing locally against the source code. +# +# Usage: +# docker compose -f docker-compose.dev.yml up -d +# docker compose -f docker-compose.dev.yml down # stop +# docker compose -f docker-compose.dev.yml down -v # stop + wipe data + +name: twenty-dev + +services: + db: + image: postgres:16 + volumes: + - dev-db-data:/var/lib/postgresql/data + ports: + - "5432:5432" + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: default + healthcheck: + test: pg_isready -U postgres -h localhost -d postgres + interval: 5s + timeout: 5s + retries: 10 + restart: unless-stopped + + redis: + image: redis:7 + ports: + - "6379:6379" + command: ["--maxmemory-policy", "noeviction"] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 10 + restart: unless-stopped + +volumes: + dev-db-data: diff --git a/packages/twenty-utils/setup-dev-env.sh b/packages/twenty-utils/setup-dev-env.sh index c1ba8a00100..817a00d661d 100755 --- a/packages/twenty-utils/setup-dev-env.sh +++ b/packages/twenty-utils/setup-dev-env.sh @@ -1,12 +1,251 @@ #!/bin/bash -set -e +# ============================================================================= +# Twenty CRM — Development Environment Setup +# ============================================================================= +# Single entry point for setting up a dev environment. Idempotent. +# +# What it does: +# 1. Starts Postgres + Redis (local services or Docker, auto-detected) +# 2. Creates 'default' and 'test' databases +# 3. Copies .env.example -> .env for front and server +# +# Usage (from repo root): +# bash packages/twenty-utils/setup-dev-env.sh # start + configure +# bash packages/twenty-utils/setup-dev-env.sh --down # stop services +# bash packages/twenty-utils/setup-dev-env.sh --reset # wipe data + restart +# bash packages/twenty-utils/setup-dev-env.sh --docker # force Docker mode +# ============================================================================= +set -euo pipefail -echo "Setting up dev environment..." +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +COMPOSE_FILE="$REPO_ROOT/packages/twenty-docker/docker-compose.dev.yml" -npx nx reset:env twenty-front -npx nx reset:env twenty-server +info() { echo "=> $*"; } +ok() { echo " done: $*"; } +fail() { echo " FAIL: $*" >&2; } -PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";' 2>/dev/null || true -PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";' 2>/dev/null || true +# --------------- detection helpers --------------- +has_local_pg() { + command -v pg_ctlcluster &>/dev/null && pg_lsclusters 2>/dev/null | grep -q "16" +} +has_local_redis() { + command -v redis-server &>/dev/null +} + +can_use_docker() { + docker compose version &>/dev/null 2>&1 +} + +pg_is_up() { + if command -v pg_isready &>/dev/null; then + pg_isready -h localhost -p 5432 -U postgres -q 2>/dev/null + elif command -v psql &>/dev/null; then + PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -c "SELECT 1" &>/dev/null + elif can_use_docker && docker compose -f "$COMPOSE_FILE" ps --quiet db 2>/dev/null | grep -q .; then + docker compose -f "$COMPOSE_FILE" exec -T db pg_isready -U postgres -q 2>/dev/null + else + return 1 + fi +} + +redis_is_up() { + if command -v redis-cli &>/dev/null; then + redis-cli -h localhost -p 6379 ping 2>/dev/null | grep -q PONG + elif can_use_docker && docker compose -f "$COMPOSE_FILE" ps --quiet redis 2>/dev/null | grep -q .; then + docker compose -f "$COMPOSE_FILE" exec -T redis redis-cli ping 2>/dev/null | grep -q PONG + else + # Portable fallback using bash /dev/tcp (no nc -q dependency) + timeout 2 bash -c 'exec 3<>/dev/tcp/localhost/6379; echo PING >&3; read -r reply <&3; exec 3>&-; echo "$reply"' 2>/dev/null | grep -q PONG + fi +} + +wait_for_pg() { + local retries=30 + while ! pg_is_up; do + retries=$((retries - 1)) + if [ "$retries" -le 0 ]; then fail "PostgreSQL did not start in time"; exit 1; fi + sleep 1 + done +} + +wait_for_redis() { + local retries=30 + while ! redis_is_up; do + retries=$((retries - 1)) + if [ "$retries" -le 0 ]; then fail "Redis did not start in time"; exit 1; fi + sleep 1 + done +} + +# --------------- parse flags --------------- +USE_DOCKER=false +ACTION="up" + +while [ $# -gt 0 ]; do + case "$1" in + --docker) USE_DOCKER=true ;; + --down) ACTION="down" ;; + --reset) ACTION="reset" ;; + *) echo "Unknown flag: $1"; exit 1 ;; + esac + shift +done + +# --------------- stop --------------- +stop_docker() { + if can_use_docker && docker compose -f "$COMPOSE_FILE" ps -a --quiet 2>/dev/null | grep -q .; then + docker compose -f "$COMPOSE_FILE" down "$@" + fi +} + +stop_local() { + if has_local_pg; then sudo pg_ctlcluster 16 main stop 2>/dev/null || true; fi + if has_local_redis && pgrep -x redis-server &>/dev/null; then + sudo service redis-server stop 2>/dev/null || true + fi +} + +stop_services() { + if [ "$USE_DOCKER" = true ]; then + stop_docker "$@" + else + stop_docker "$@" + stop_local + fi +} + +if [ "$ACTION" = "down" ]; then + info "Stopping dev services..." + stop_services + ok "Services stopped" + exit 0 +fi + +if [ "$ACTION" = "reset" ]; then + info "Resetting dev services (wiping data)..." + # Wipe local Redis data while it's still running + if [ "$USE_DOCKER" = false ] && has_local_redis && pgrep -x redis-server &>/dev/null; then + info "Flushing local Redis data..." + redis-cli flushall 2>/dev/null || true + fi + # Wipe local PostgreSQL data while it's still running + if [ "$USE_DOCKER" = false ] && has_local_pg; then + info "Dropping local databases..." + sudo pg_ctlcluster 16 main start 2>/dev/null || true + wait_for_pg + sudo -u postgres psql -c 'DROP DATABASE IF EXISTS "default";' 2>/dev/null || true + sudo -u postgres psql -c 'DROP DATABASE IF EXISTS "test";' 2>/dev/null || true + fi + # Stop Docker with -v to remove volumes + stop_docker -v 2>/dev/null || stop_docker + # Stop local services + if [ "$USE_DOCKER" = false ]; then + stop_local + fi +fi + +# ============================================================================= +# 1. Start services (auto-detect: local > Docker) +# ============================================================================= +start_pg() { + if pg_is_up; then + ok "PostgreSQL already running" + return + fi + + if [ "$USE_DOCKER" = false ] && has_local_pg; then + info "Starting local PostgreSQL..." + sudo pg_ctlcluster 16 main start + wait_for_pg + sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';" 2>/dev/null || true + elif can_use_docker; then + info "Starting PostgreSQL via Docker..." + docker compose -f "$COMPOSE_FILE" up -d db + wait_for_pg + else + fail "No PostgreSQL available. Install PostgreSQL 16 or Docker." + exit 1 + fi +} + +start_redis() { + if redis_is_up; then + ok "Redis already running" + return + fi + + if [ "$USE_DOCKER" = false ] && has_local_redis; then + info "Starting local Redis..." + sudo service redis-server start 2>/dev/null || redis-server --daemonize yes 2>/dev/null || true + wait_for_redis + elif can_use_docker; then + info "Starting Redis via Docker..." + docker compose -f "$COMPOSE_FILE" up -d redis + wait_for_redis + else + fail "No Redis available. Install Redis or Docker." + exit 1 + fi +} + +if [ "$USE_DOCKER" = true ]; then + info "Starting services via Docker Compose..." + docker compose -f "$COMPOSE_FILE" up -d + wait_for_pg + wait_for_redis +else + start_pg + start_redis +fi + +ok "PostgreSQL on localhost:5432" +ok "Redis on localhost:6379" + +# ============================================================================= +# 2. Create databases +# ============================================================================= +info "Creating databases..." +run_psql() { + if command -v psql &>/dev/null; then + PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c "$1" 2>/dev/null || true + elif can_use_docker && docker compose -f "$COMPOSE_FILE" ps --quiet db 2>/dev/null | grep -q .; then + docker compose -f "$COMPOSE_FILE" exec -T db psql -U postgres -d postgres -c "$1" 2>/dev/null || true + else + fail "No psql client available and no Docker db container running" + return 1 + fi +} +run_psql 'CREATE DATABASE "default";' +run_psql 'CREATE DATABASE "test";' +ok "Databases 'default' and 'test' ready" + +# ============================================================================= +# 3. Environment files (via Nx when available, fallback to cp) +# ============================================================================= +info "Setting up .env files..." +cd "$REPO_ROOT" + +if command -v npx &>/dev/null && [ -d node_modules ]; then + npx nx reset:env twenty-front + npx nx reset:env twenty-server +else + for pkg in twenty-front twenty-server; do + src="packages/$pkg/.env.example" + dst="packages/$pkg/.env" + if [ -f "$src" ] && [ ! -f "$dst" ]; then + cp "$src" "$dst" + ok "$pkg/.env created" + fi + done +fi + +# ============================================================================= +echo "" echo "Dev environment ready." +echo "" +echo " yarn start # start everything" +echo " npx nx start twenty-front # frontend -> http://localhost:3001" +echo " npx nx start twenty-server # backend -> http://localhost:3000" +echo ""