{ lib, stdenv, mkShell, writeShellScriptBin, # Elixir/Erlang elixir, # Databases and caches postgresql_16, redis, # SNMP tools net-snmp, # Build tools for C NIF gcc, gnumake, pkg-config, # Development tools git-lfs, inotify-tools, # LSPs and formatters elixir-ls, nixfmt, shellcheck, # Pre-commit hooks pre-commit-hooks, }: let # PostgreSQL data directory (local to project) pgDataDir = ".nix-postgres"; pgPort = "5432"; pgHost = "localhost"; # Redis data directory (local to project) redisDataDir = ".nix-redis"; redisPort = "6379"; # Service management flag servicesFlag = ".nix-services-started"; # Script to start PostgreSQL and Redis startServices = writeShellScriptBin "start-services" '' set -e echo "🚀 Starting development services..." # Check if already started if [ -f "${servicesFlag}" ]; then echo "✓ Services already running (remove ${servicesFlag} to force restart)" exit 0 fi # Initialize PostgreSQL if needed if [ ! -d "${pgDataDir}" ]; then echo "📦 Initializing PostgreSQL database..." ${postgresql_16}/bin/initdb -D ${pgDataDir} -U $USER --encoding=UTF8 --locale=en_US.UTF-8 # Configure PostgreSQL cat >> ${pgDataDir}/postgresql.conf << EOF # Development configuration max_connections = 100 shared_buffers = 128MB fsync = off # Faster for development (DO NOT use in production) synchronous_commit = off full_page_writes = off EOF fi # Check if PostgreSQL is already running on this port if ${postgresql_16}/bin/pg_isready -h ${pgHost} -p ${pgPort} > /dev/null 2>&1; then echo "✓ PostgreSQL already running on port ${pgPort}" else # Start PostgreSQL echo "🐘 Starting PostgreSQL on port ${pgPort}..." ${postgresql_16}/bin/pg_ctl -D ${pgDataDir} -l ${pgDataDir}/logfile -o "-p ${pgPort}" start # Wait for PostgreSQL to be ready for i in {1..30}; do if ${postgresql_16}/bin/pg_isready -h ${pgHost} -p ${pgPort} > /dev/null 2>&1; then break fi if [ $i -eq 30 ]; then echo "❌ PostgreSQL failed to start within 30 seconds" echo " Check ${pgDataDir}/logfile for details" exit 1 fi sleep 1 done echo "✓ PostgreSQL started successfully" fi # Create postgres role if it doesn't exist (needed by some tools) ${postgresql_16}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d postgres -tc \ "SELECT 1 FROM pg_roles WHERE rolname='postgres'" | grep -q 1 || \ ${postgresql_16}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d postgres -c \ "CREATE ROLE postgres WITH SUPERUSER LOGIN;" # Create development and test databases for db in towerops_dev towerops_test; do ${postgresql_16}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d postgres -tc \ "SELECT 1 FROM pg_database WHERE datname='$db'" | grep -q 1 || \ ${postgresql_16}/bin/createdb -h ${pgHost} -p ${pgPort} -U $USER $db done # Enable TimescaleDB extension (dev database only, test uses plain PostgreSQL) ${postgresql_16}/bin/psql -h ${pgHost} -p ${pgPort} -U $USER -d towerops_dev -c \ "CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;" 2>/dev/null || \ echo "⚠️ TimescaleDB not available (optional for development)" echo "✓ PostgreSQL started successfully" # Check if Redis is already running on this port if ${redis}/bin/redis-cli -p ${redisPort} ping > /dev/null 2>&1; then echo "✓ Redis already running on port ${redisPort}" else # Start Redis echo "📮 Starting Redis on port ${redisPort}..." mkdir -p ${redisDataDir} ${redis}/bin/redis-server --daemonize yes \ --port ${redisPort} \ --dir ${redisDataDir} \ --save "" \ --appendonly no # Wait for Redis to be ready for i in {1..10}; do if ${redis}/bin/redis-cli -p ${redisPort} ping > /dev/null 2>&1; then break fi if [ $i -eq 10 ]; then echo "❌ Redis failed to start within 10 seconds" exit 1 fi sleep 1 done echo "✓ Redis started successfully" fi # Mark services as started touch ${servicesFlag} echo "" echo "✨ Development environment ready!" echo "" echo "PostgreSQL: ${pgHost}:${pgPort} (databases: towerops_dev, towerops_test)" echo "Redis: ${pgHost}:${redisPort}" echo "" ''; # Script to stop PostgreSQL and Redis stopServices = writeShellScriptBin "stop-services" '' set -e echo "🛑 Stopping development services..." # Stop PostgreSQL if [ -d "${pgDataDir}" ]; then ${postgresql_16}/bin/pg_ctl -D ${pgDataDir} stop -m fast 2>/dev/null || true echo "✓ PostgreSQL stopped" fi # Stop Redis ${redis}/bin/redis-cli -p ${redisPort} shutdown 2>/dev/null || true echo "✓ Redis stopped" # Remove services flag rm -f ${servicesFlag} echo "✓ Services stopped successfully" ''; # Pre-commit hooks configuration pre-commit-check = pre-commit-hooks.run { src = ../.; hooks = { # Elixir formatting mix-format = { enable = true; name = "mix format"; entry = "mix format --check-formatted"; files = "\\.(ex|exs)$"; pass_filenames = false; }; # Credo linting credo = { enable = true; name = "credo"; entry = "mix credo --strict"; files = "\\.(ex|exs)$"; pass_filenames = false; }; # Nix formatting nixfmt = { enable = true; entry = "${nixfmt}/bin/nixfmt"; }; # Shellcheck for shell scripts shellcheck.enable = true; }; }; in mkShell { name = "towerops-dev"; # Development tools buildInputs = [ # Elixir/Erlang elixir # Databases postgresql_16 redis # SNMP tools net-snmp # C NIF build tools gcc gnumake pkg-config # Development tools git-lfs # LSPs and formatters elixir-ls nixfmt shellcheck # Service management scripts startServices stopServices ] ++ lib.optionals stdenv.isLinux [ # Linux-only tools inotify-tools # For Mix file watching ]; # Environment variables shellHook = '' # Color codes GREEN='\033[0;32m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Set Mix and Hex home to local directories (avoid polluting $HOME) export MIX_HOME="$PWD/.nix-mix" export HEX_HOME="$PWD/.nix-hex" mkdir -p "$MIX_HOME" "$HEX_HOME" # PostgreSQL connection export PGHOST="${pgHost}" export PGPORT="${pgPort}" export PGUSER="$USER" export DATABASE_URL="ecto://\$USER@${pgHost}:${pgPort}/towerops_dev" # Redis connection export REDIS_URL="redis://${pgHost}:${redisPort}/0" # Development secrets (DO NOT use in production) export SECRET_KEY_BASE="dev_secret_key_base_at_least_64_bytes_long_for_phoenix_to_accept_it_safely" export CLOAK_KEY="dev_cloak_key_base64_encoded_32_bytes_long=" export RELEASE_COOKIE="dev_release_cookie_for_distributed_erlang" # Development environment export MIX_ENV=dev export PHX_HOST=localhost export PORT=4000 # Ensure Erlang can find NIF libraries export ERL_AFLAGS="-kernel shell_history enabled" # Install Hex and Rebar3 if not already installed mix local.hex --force --if-missing > /dev/null 2>&1 mix local.rebar --force --if-missing > /dev/null 2>&1 # Auto-start services on first shell entry if [ ! -f "${servicesFlag}" ]; then ${startServices}/bin/start-services # Run migrations if databases exist and Mix deps are available if [ -d "deps" ] && mix ecto.migrate 2>/dev/null; then echo "✓ Database migrations applied" fi fi # Register trap to stop services on shell exit trap '${stopServices}/bin/stop-services' EXIT # Install pre-commit hooks ${pre-commit-check.shellHook} # Welcome message echo "" echo -e "''${BLUE}╔═══════════════════════════════════════════════════════════╗''${NC}" echo -e "''${BLUE}║ ║''${NC}" echo -e "''${BLUE}║''${NC} ''${GREEN}Towerops Development Environment''${NC} ''${BLUE}║''${NC}" echo -e "''${BLUE}║ ║''${NC}" echo -e "''${BLUE}╚═══════════════════════════════════════════════════════════╝''${NC}" echo "" echo -e "''${GREEN}Available commands:''${NC}" echo " mix phx.server - Start Phoenix development server" echo " mix test - Run test suite" echo " mix precommit - Run pre-commit checks (format, credo, test)" echo " start-services - Start PostgreSQL and Redis" echo " stop-services - Stop PostgreSQL and Redis" echo " mix ecto.reset - Reset database (drop, create, migrate, seed)" echo "" echo -e "''${GREEN}Services:''${NC}" echo " PostgreSQL: ${pgHost}:${pgPort}" echo " Redis: ${pgHost}:${redisPort}" echo "" echo -e "''${GREEN}Documentation:''${NC}" echo " See CLAUDE.md and docs/nix.md for more information" echo "" ''; }