{ pkgs ? import {} }: pkgs.mkShell { buildInputs = with pkgs; [ # PHP and tools php84 php84Packages.composer symfony-cli # Node.js and npm (React/Vite SPA) nodejs_22 # Container tools podman podman-compose # Database client (for direct DB access) postgresql # Utilities git curl gnumake ]; shellHook = '' export USER_ID=$(id -u) export GROUP_ID=$(id -g) export PODMAN_USERNS=keep-id # Compose files (symfony-docker convention: root compose.yaml + compose.override.yaml). # Both must be passed explicitly so the dev override is merged. # NOTE: $COMPOSE is invoked unquoted (word-split into args) — assumes the repo path # contains no spaces. Fine here; would break if checked out under a path with spaces. COMPOSE="podman-compose -f $PWD/compose.yaml -f $PWD/compose.override.yaml" # =================== # ALIASES # =================== alias pc="$COMPOSE" # =================== # DEV COMMANDS # =================== dev-up() { echo "Starting services..." PODMAN_USERNS=keep-id $COMPOSE up -d "$@" echo "" $COMPOSE ps echo "" echo "App available at: http://localhost:8100" } dev-down() { if [[ "$1" == "-v" ]]; then echo "Stopping services and removing volumes..." $COMPOSE down -v else echo "Stopping services..." $COMPOSE down fi } dev-restart() { echo "Restarting services..." $COMPOSE restart "$@" } dev-rebuild() { echo "Rebuilding services (down -v + up)..." $COMPOSE down -v PODMAN_USERNS=keep-id $COMPOSE up -d "$@" echo "" $COMPOSE ps echo "" echo "App available at: http://localhost:8100" } dev-logs() { $COMPOSE logs -f php "$@" } dev-logs-db() { $COMPOSE logs -f database "$@" } dev-shell() { $COMPOSE exec php sh } dev-console() { $COMPOSE exec php php bin/console "$@" } dev-composer() { $COMPOSE exec php composer "$@" } # =================== # QA COMMANDS (run in the php container) # =================== dev-test() { $COMPOSE exec php php bin/phpunit "$@" } dev-coverage() { # Code coverage as a plain text table in the terminal (needs Xdebug, which is installed). # --colors=never keeps the table readable (the ANSI colors garble it). # Pass-through args, e.g. scope to one class: dev-coverage --filter BucketRoomCalculator $COMPOSE exec -e XDEBUG_MODE=coverage php php bin/phpunit --coverage-text --colors=never "$@" } dev-coverage-check() { # Backend coverage gate: PHPUnit 13 has no native "fail under N%" option, # so run the report and fail if the Lines % drops below the floor (95%). # Floor set a touch below current (~99.7%) to catch regressions, not game. local floor=95 local out pct out=$($COMPOSE exec -e XDEBUG_MODE=coverage php php bin/phpunit --coverage-text --colors=never "$@") echo "$out" pct=$(echo "$out" | grep -oE 'Lines:[[:space:]]+[0-9]+\.[0-9]+%' | grep -oE '[0-9]+\.[0-9]+' | head -1) if [ -z "$pct" ]; then echo "✗ could not parse coverage %"; return 1; fi # integer compare (drop decimals) against the floor if [ "''${pct%.*}" -lt "$floor" ]; then echo "✗ backend line coverage $pct% is below the $floor% floor"; return 1 fi echo "✓ backend line coverage $pct% (floor $floor%)" } dev-stan() { # Warm the dev cache so phpstan-symfony can read the compiled container, then analyse. $COMPOSE exec php php bin/console cache:warmup --quiet $COMPOSE exec php vendor/bin/phpstan analyse --memory-limit=512M "$@" } dev-cs() { # Dry-run code-style check (no changes). Use for CI / verification. $COMPOSE exec php vendor/bin/php-cs-fixer check --diff "$@" } dev-cs-fix() { # Apply code-style fixes in place. $COMPOSE exec php vendor/bin/php-cs-fixer fix "$@" } # =================== # FRONTEND COMMANDS (run in the frontend/Vite container) # =================== dev-fe-test() { # Run the Vitest suite once. Pass-through args, e.g.: dev-fe-test src/auth $COMPOSE exec frontend npm test "$@" } dev-fe-coverage() { # Vitest run with the v8 coverage report. $COMPOSE exec frontend npm run test:coverage "$@" } dev-fe-test-watch() { $COMPOSE exec frontend npm run test:watch "$@" } dev-fe-lint() { $COMPOSE exec frontend npm run lint "$@" } dev-fe-cs() { # Prettier formatting check (no changes). $COMPOSE exec frontend npm run format:check "$@" } dev-fe-cs-fix() { $COMPOSE exec frontend npm run format "$@" } dev-fe-build() { $COMPOSE exec frontend npm run build "$@" } dev-e2e() { # Run the Playwright e2e smoke suite in the `playwright` compose service # (browsers preinstalled). Needs the stack up (dev-up). NOT the Vitest # suite — that's dev-fe-test. Pass-through args, e.g.: dev-e2e --headed PODMAN_USERNS=keep-id $COMPOSE --profile e2e run --rm playwright npm test -- "$@" } dev-check() { # Fast pre-commit gate: all static/unit checks, back + front. Fail-fast # (stops at the first failure). No coverage, no e2e — use dev-check-all # for the thorough version. echo "▶ backend: tests" && dev-test \ && echo "▶ backend: phpstan" && dev-stan \ && echo "▶ backend: cs" && dev-cs \ && echo "▶ frontend: tests" && dev-fe-test \ && echo "▶ frontend: lint" && dev-fe-lint \ && echo "▶ frontend: cs" && dev-fe-cs \ && echo "▶ frontend: build" && dev-fe-build \ && echo "✓ dev-check passed" } dev-check-all() { # Thorough gate: everything in dev-check, but with COVERAGE instead of the # plain test runs, plus the Playwright e2e. Slow; needs the stack up. echo "▶ backend: coverage" && dev-coverage-check \ && echo "▶ backend: phpstan" && dev-stan \ && echo "▶ backend: cs" && dev-cs \ && echo "▶ frontend: coverage" && dev-fe-coverage \ && echo "▶ frontend: lint" && dev-fe-lint \ && echo "▶ frontend: cs" && dev-fe-cs \ && echo "▶ frontend: build" && dev-fe-build \ && echo "▶ e2e" && dev-e2e \ && echo "✓ dev-check-all passed" } # =================== # DATABASE / MIGRATION COMMANDS # =================== # The Doctrine migration workflow, simplified. Typical loop: # 1. change an entity mapping # 2. dev-migrate-diff -> generate ONE migration from the mapping (review it!) # 3. dev-migrate -> apply to BOTH dev and test DBs # If you generated/applied a wrong migration: dev-migrate-prev rolls back BOTH DBs, # then delete the bad migrations/Version*.php file and re-run dev-migrate-diff. dev-migrate-diff() { # Generate a migration from the diff between entity mappings and the dev DB schema. # ALWAYS read the generated migrations/Version*.php before applying it. echo "Generating migration from entity mapping diff..." $COMPOSE exec php php bin/console doctrine:migrations:diff "$@" echo "" echo ">> Review the new file in migrations/ before running dev-migrate." } dev-migrate() { # Apply pending migrations to BOTH the dev and test databases. # Interactive: Doctrine will prompt before destructive/unregistered changes — read them. echo "=== Migrating DEV database (buckets) ===" $COMPOSE exec php php bin/console doctrine:migrations:migrate "$@" echo "" echo "=== Migrating TEST database (buckets_test) ===" $COMPOSE exec php php bin/console doctrine:migrations:migrate --env=test "$@" } dev-migrate-prev() { # Roll back the most recent migration on BOTH the dev and test databases. echo "=== Rolling back DEV database (buckets) one step ===" $COMPOSE exec php php bin/console doctrine:migrations:migrate prev "$@" echo "" echo "=== Rolling back TEST database (buckets_test) one step ===" $COMPOSE exec php php bin/console doctrine:migrations:migrate prev --env=test "$@" } # =================== # BUILD COMMANDS # =================== base-build() { local image="forge.lvl0.xyz/lvl0/buckets:latest" # Check if logged in, prompt if not if ! podman login --get-login forge.lvl0.xyz &>/dev/null; then echo "Not logged in to forge.lvl0.xyz" podman login forge.lvl0.xyz || return 1 fi echo "Building image: $image" if ! podman build -t "$image" -f Dockerfile .; then echo "Build failed!" return 1 fi echo "" echo "Pushing to registry..." if ! podman push "$image"; then echo "Push failed!" return 1 fi echo "" echo "Done! Image pushed: $image" } # =================== # WELCOME MESSAGE # =================== echo "" echo "=================================================" echo " Buckets Dev Environment " echo "=================================================" echo "" echo " PHP: $(php --version | head -1 | cut -d' ' -f2)" echo " Symfony: $(symfony version 2>/dev/null | head -1 || echo 'symfony-cli')" echo " Podman: $(podman --version | cut -d' ' -f3)" echo "" echo "Commands:" echo " dev-up [services] Start all or specific services" echo " dev-down [-v] Stop services (-v removes volumes)" echo " dev-rebuild Fresh start (down -v + up)" echo " dev-restart Restart services" echo " dev-logs Tail app logs" echo " dev-logs-db Tail database logs" echo " dev-shell Shell into app container" echo " dev-console Run Symfony console command" echo " dev-composer Run composer in the container" echo " dev-test [args] Run PHPUnit (bin/phpunit)" echo " dev-stan [args] Run PHPStan static analysis" echo " dev-cs [args] Check code style (dry-run)" echo " dev-cs-fix [args] Apply code style fixes" echo " dev-fe-test [args] Run frontend Vitest suite" echo " dev-fe-lint [args] Lint frontend (oxlint)" echo " dev-fe-cs [args] Check frontend formatting (Prettier)" echo " dev-fe-cs-fix Apply frontend formatting" echo " dev-fe-build Build the frontend (tsc + vite build)" echo " dev-fe-coverage Frontend Vitest run with coverage report" echo " dev-e2e [args] Run Playwright e2e smoke suite (container)" echo " dev-check Fast gate: all back+front tests/stan/cs/build" echo " dev-check-all Thorough gate: dev-check + coverage + e2e" echo " dev-migrate-diff Generate a migration from entity mappings" echo " dev-migrate Apply migrations to dev + test DBs" echo " dev-migrate-prev Roll back one migration on dev + test DBs" echo " base-build Build and push image" echo "" echo "Services:" echo " php Symfony/FrankenPHP http://localhost:8100" echo " frontend React/Vite SPA http://localhost:5173" echo " database Postgres 16 localhost:5433" echo "" ''; }