--- name: gen-env description: Creates, updates, or reviews a project's gen-env command for running multiple isolated instances on localhost. Handles instance identity, port allocation, data isolation, browser state separation, and cleanup. --- # gen-env Skill Generate or review a `gen-env` command that enables running **multiple isolated instances** of a project on localhost simultaneously (e.g., multiple worktrees, feature branches, or versions). ## The Problem Without isolation, multiple instances of the same project: - Fight for hardcoded ports (3000, 5432, 8080) - Share Docker volumes → data corruption - Share browser cookies/localStorage → auth confusion - Have ambiguous container names → can't tell which is which - Risk catastrophic cleanup → `docker down -v` nukes everything ## The Solution: Instance Identity Everything flows from a **workspace name**: ``` name = "feature-x" ↓ ┌─────────────────────────────────────────────────────┐ │ COMPOSE_PROJECT_NAME = localnet-feature-x │ │ DOCKER_NETWORK = localnet-feature-x │ │ VOLUME_PREFIX = localnet-feature-x │ │ CONTAINER_PREFIX = localnet-feature-x- │ │ TILT_HOST = feature-x.localhost │ │ Ports = dynamically allocated │ │ URLs = derived from host + ports │ └─────────────────────────────────────────────────────┘ ``` ## Isolation Dimensions ### 1. Port Isolation Each instance gets unique ports from ephemeral range (49152-65535). ### 2. Data Isolation Docker Compose project name controls volume naming: - Instance A: `localnet-main_postgres_data` - Instance B: `localnet-feature-x_postgres_data` No cross-contamination. Independent databases. ### 3. Network Isolation Separate Docker networks per instance. Containers reference each other by service name without collision. ### 4. Browser State Isolation **Critical**: Different ports on `localhost` still share cookies! ``` http://localhost:3000 ─┐ ├─ SAME cookies, localStorage http://localhost:3001 ─┘ ``` Solution: subdomain isolation via `*.localhost`: ``` http://main.localhost:3000 ─ separate cookies http://feature-x.localhost:3001 ─ separate cookies ``` Chrome/Edge treat `*.localhost` as `127.0.0.1` automatically. No `/etc/hosts` needed. ### 5. Auth Isolation Each instance can have its own auth realm/audience, preventing token confusion. ### 6. Resource Naming Clear prefixes on containers, volumes, Tilt resources, logs → know exactly which instance you're looking at. ## Implementation Checklist When creating or reviewing gen-env: **Identity & Naming:** - [ ] Requires `--name ` argument - [ ] Validates name (alphanumeric + dashes, max 63 chars for DNS) - [ ] Generates `COMPOSE_PROJECT_NAME` from name - [ ] Generates `DOCKER_NETWORK`, `VOLUME_PREFIX`, `CONTAINER_PREFIX` - [ ] Generates `*_HOST` for browser isolation (`name.localhost`) **Port Allocation:** - [ ] Allocates from ephemeral range (49152-65535) - [ ] Checks port availability before assignment - [ ] Uses short timeout (100ms) for CI compatibility - [ ] Handles IPv6-disabled environments gracefully **Persistence:** - [ ] Lockfile stores name + ports (`.gen-env.lock`) - [ ] Reuses ports when lockfile exists and name matches - [ ] `--force` regenerates all - [ ] `--clean` removes generated files **Output:** - [ ] Generates `.localnet.env` (or project-specific name) - [ ] Clear header with generation timestamp - [ ] All derived URLs use correct host + port **Integration:** - [ ] Script added to PATH via `.envrc` - [ ] Generated env sourced by `.envrc` - [ ] Works with Docker Compose (`--env-file`) - [ ] Works with Tilt (Starlark reads env file) ## Generated Environment Structure ```bash # .localnet.env - generated by gen-env # Instance: feature-x # Generated: 2024-01-15T10:30:00Z # === Instance Identity === WORKSPACE_NAME=feature-x COMPOSE_NAME=localnet-feature-x COMPOSE_PROJECT_NAME=localnet-feature-x DOCKER_NETWORK=localnet-feature-x VOLUME_PREFIX=localnet-feature-x CONTAINER_PREFIX=localnet-feature-x- # === Host (for browser isolation) === APP_HOST=feature-x.localhost TILT_HOST=feature-x.localhost # === Allocated Ports === POSTGRES_PORT=51234 REDIS_PORT=51235 API_PORT=51236 WEB_PORT=51237 # ... more ports # === Derived URLs === DATABASE_URL=postgres://user:pass@localhost:51234/dev WEB_URL=http://feature-x.localhost:51237 API_URL=http://feature-x.localhost:51236 ``` ## direnv Integration ```bash # .envrc PATH_add bin # or scripts dotenv_if_exists .localnet.env ``` ## Reference Implementation (TypeScript/Bun) See @IMPLEMENTATION.md for full implementation. Key types: ```typescript interface InstanceConfig { name: string; // Workspace identity composeName: string; // Docker Compose project name dockerNetwork: string; // Docker network name volumePrefix: string; // Docker volume prefix containerPrefix: string; // Container name prefix host: string; // Browser hostname (name.localhost) ports: Record; // Allocated ports urls: Record; // Derived URLs } interface LockfileData { version: 1; generatedAt: string; instance: InstanceConfig; } ``` ## Cleanup Patterns Surgical cleanup per instance: ```bash # Clean only feature-x (containers + volumes + networks) docker compose -p localnet-feature-x down -v # Or via gen-env gen-env --clean # removes .localnet.env and .gen-env.lock # List all localnet instances docker ps -a --filter "name=localnet-" --format "table {{.Names}}\t{{.Status}}" # Nuclear option (all instances) - DANGEROUS docker ps -a --filter "name=localnet-" -q | xargs docker rm -f docker volume ls --filter "name=localnet-" -q | xargs docker volume rm ``` ## Common Patterns ### Pattern 1: Worktree-Based Naming ```bash # Derive name from git worktree directory WORKTREE_NAME=$(basename "$(git rev-parse --show-toplevel)") gen-env --name "$WORKTREE_NAME" ``` ### Pattern 2: Branch-Based Naming ```bash # Derive name from branch BRANCH=$(git branch --show-current | tr '/' '-') gen-env --name "$BRANCH" ``` ### Pattern 3: Explicit Naming ```bash # User specifies (recommended for clarity) gen-env --name bb-dev gen-env --name testing-v2 ``` ## Review Checklist When reviewing an existing gen-env: 1. **Does it create instance identity?** (not just ports) 2. **Does it set COMPOSE_PROJECT_NAME?** (controls Docker naming) 3. **Does it generate a browser-safe host?** (`*.localhost`) 4. **Are URLs derived with correct host?** (not hardcoded `localhost`) 5. **Is cleanup surgical?** (can remove one instance without affecting others) 6. **Does the lockfile store the name?** (for consistency across runs) 7. **Does it validate name conflicts?** (warn if lockfile has different name) ## Anti-Patterns ❌ **Hardcoded `localhost` in URLs** ```bash WEB_URL=http://localhost:${WEB_PORT} # BAD: shares cookies ``` ✅ **Use instance host** ```bash WEB_URL=http://${APP_HOST}:${WEB_PORT} # GOOD: isolated cookies ``` ❌ **No COMPOSE_PROJECT_NAME** ```bash # BAD: uses directory name, may conflict docker compose up ``` ✅ **Explicit project name** ```bash COMPOSE_PROJECT_NAME=localnet-feature-x docker compose up # Uses project name for all resources ``` ❌ **Shared cleanup** ```bash docker compose down -v # BAD: which instance? ``` ✅ **Instance-specific cleanup** ```bash docker compose -p localnet-feature-x down -v # GOOD: explicit ``` ## References - @IMPLEMENTATION.md - Full TypeScript implementation - @ADVANCED_PATTERNS.md - Complex scenarios (monorepos, CI, Tilt integration)