# Zapper documentation This raw document is generated from the Markdown files that power the Zapper VitePress documentation site. # Source: docs/index.md # Zapper A lightweight dev environment runner for local multi-service projects. ## Install ```bash npm install -g pm2 @mp-lb/zapper ``` ## Create `zap.yaml` ```yaml project: myapp env: [.env] native: backend: cmd: pnpm dev env: "*" frontend: cmd: pnpm dev cwd: ./frontend env: "*" docker: postgres: image: postgres:15 ports: - 5432:5432 ``` ## Run ```bash zap up zap status zap down ``` See the [full reference](usage.md) for every `zap.yaml` field and command. # Source: docs/usage.md # Zapper Reference Complete reference for `zap.yaml` syntax and all CLI commands. --- ## Table of Contents - [Installation](#installation) - [Project Configuration](#project-configuration) - [CLI Commands](#cli-commands) - [Native Processes](#native-processes) - [Docker Services](#docker-services) - [Environment Variables](#environment-variables) - [Instances](#instances) - [Resource Management](resource-management.md) - [Tasks](#tasks) - [Dependencies](#dependencies) - [Profiles](#profiles) - [Links](#links) - [Notes](#notes) - [Git Cloning](#git-cloning) --- ## Installation ```bash npm install -g pm2 @mp-lb/zapper ``` For VS Code/Cursor, install the extension: `felixsebastian.zapper-vscode` Docker-backed services require Docker CLI. On macOS, Zapper now attempts to auto-install Docker Desktop via Homebrew (`brew install --cask docker`) when Docker is missing. If Homebrew is unavailable or install fails, Zapper exits with manual install instructions. --- ## Project Configuration ### Minimal config ```yaml project: myapp native: api: cmd: pnpm dev ``` ### Full config structure ```yaml project: myapp # Required. Used as PM2/Docker namespace env: # Load env vars from these files default: [.env.base, .env] prod_dbs: [.env.base, .env.prod-dbs] ports: # Port names to assign random values - FRONTEND_PORT - BACKEND_PORT init_task: seed # Optional task to run after `zap init` git_method: ssh # ssh | http | cli (for repo cloning) native: # ... process definitions docker: # ... container definitions tasks: # ... task definitions homepage: http://localhost:3000 # Optional default URL for `zap launch` notes: "API: http://localhost:${API_PORT}" # Optional note text for `zap notes` links: # ... quick reference links ``` --- ## CLI Commands ### Global Options Available with any command: ```bash --config # Use a specific config file (default: zap.yaml) -v, --verbose # Increase logging verbosity -q, --quiet # Reduce logging output -d, --debug # Enable debug logging ``` Examples: ```bash zap --config prod.yaml up zap --config staging.yaml status zap --debug restart zap --verbose --config custom.yaml task build ``` ### Starting and stopping ```bash zap up # Start all services zap up backend # Start one service (and its dependencies) zap up api worker db # Start multiple services zap up --json # Output command result as JSON zap down # Stop all services zap down backend # Stop one service zap down api worker db # Stop multiple services zap down backend --json # Output command result as JSON zap restart # Restart all services zap restart api # Restart one service (does not restart its dependencies) zap restart api worker db # Restart multiple services zap r api worker # Short alias for: zap restart api worker ``` ### Status and logs ```bash zap status # Show status of all services zap status api db # Show status for specific services zap ls # List services/containers plus assigned ports zap ls --extended # Include instance, dangling, and alien resource inventory zap ls --all # Alias for: zap ls --extended zap ls api db # List details for specific services zap ls --json # Output detailed list as JSON zap logs api # Follow logs for one service zap logs api worker --no-follow # Show logs for multiple services and exit zap startup-log api # Show saved startup output for one service ``` When passing multiple services to `zap logs`, use `--no-follow`. If a service fails during startup, Zapper saves the last startup attempt output under `.zap/logs/`. Use `zap startup-log ` to inspect that saved startup output. ### Tasks ```bash zap task # List all tasks zap task # Run a task zap run # Alias for: zap task zap task seed zap task build --target=prod # Run with named parameters zap task test -- --coverage # Run with pass-through args zap task build --list-params # Show task parameters as JSON ``` ### Utilities ```bash zap reset # Stop all services and delete .zap folder zap reset --json # Output command result as JSON zap kill # Kill all PM2 processes and containers for current project, across all instances zap kill my-old-project # Kill all PM2 processes and containers for a specific project, across all instances zap kill --force # Skip the interactive confirmation zap kill --json # Output kill result as JSON zap clone # Clone all repos defined in config zap clone api # Clone one repo zap clone api web # Clone multiple repos zap clone --json # Output command result as JSON zap init # Ensure local state exists for the default instance (and run init_task if configured) zap init --instance e2e # Initialize/create a named instance zap init -R # Force full port re-randomization zap init --json # Output as JSON zap volume prune # Delete stale generated Docker volumes for the selected instance zap volume reset # Forget generated volume assignments for the selected instance zap launch # Open homepage (if configured) zap launch "API Docs" # Open a configured link by name zap launch "API Docs" --json # Output command result as JSON zap links # List homepage and configured links zap links --json # Output links as JSON zap home # Print homepage URL (if configured) zap home --json # Output homepage value as JSON zap notes # Print notes (if configured) zap notes --json # Output notes value as JSON zap open # Alias for: zap launch zap o "API Docs" # Short alias for: zap launch "API Docs" ``` ### Profiles ```bash zap profile dev # Enable a profile zap profile --disable # Disable active profile zap profile dev --json # Output profile action result as JSON ``` ### Environments ```bash zap env --list # List available environment sets zap env prod_dbs # Switch env file set zap env --disable # Reset to default env set zap env prod_dbs --json # Output environment action result as JSON ``` Aliases: ```bash zap environment --list zap envset prod_dbs ``` ### JSON Output Most non-streaming commands support `--json` and will print machine-readable JSON to stdout. Examples: `up`, `down`, `restart`, `clone`, `reset`, `kill`, `status`, `ls`, `task` (list/params), `profile`, `env`, `state`, `config`, `launch`, `links`, `home`, `notes`, `init`, and git subcommands. Streaming commands keep stream output and are not JSON-encoded: ```bash zap logs [more-services...] [--no-follow] zap startup-log [more-services...] zap task ``` Service aliases configured with `aliases` on `native` or `docker` entries are resolved before service filtering or execution. The same alias works with service-targeting commands such as `up`, `down`, `restart`, `status`, `ls`, `logs`, `startup-log`, and `clone`. `zap kill ` does not require a local `zap.yaml`; it targets resources by prefix (`zap..*`). --- ## Native Processes Native processes run via PM2 on your local machine. ### Basic process ```yaml native: api: cmd: pnpm dev ``` ### All options ```yaml native: api: cmd: pnpm dev # Required. Command to run aliases: [be, backend] # Alternate service names accepted by commands cwd: ./backend # Working directory (relative to zap.yaml) env: "*" # Pass all values from the root env stack depends_on: [postgres] # Start these first profiles: [dev, test] # Only start when profile matches repo: myorg/api-repo # Git repo (for zap clone) healthcheck: 10 # Seconds to wait before considering "up" # OR healthcheck: http://localhost:3000/health # URL to poll for readiness ``` ### Working directory ```yaml native: frontend: cmd: pnpm dev cwd: ./packages/frontend # Relative to project root ``` ### Multiple processes ```yaml native: api: cmd: pnpm dev cwd: ./api worker: cmd: pnpm worker cwd: ./api frontend: cmd: pnpm dev cwd: ./web ``` --- ## Docker Services Containers managed via Docker CLI. ### Basic container ```yaml docker: redis: image: redis:latest ports: - 6379:6379 ``` ### All options ```yaml docker: postgres: image: postgres:15 # Required. Docker image aliases: [db, pg] # Alternate service names accepted by commands ports: # Port mappings (host:container) - 5432:5432 env: .zap/env/postgres.yaml # Strict whitelist file for root env values volumes: # Volume mounts - /var/lib/postgresql/data - postgres-logs:/var/log/postgresql - ./init.sql:/docker-entrypoint-initdb.d/init.sql depends_on: [other] # Start dependencies first profiles: [dev] # Profile filtering healthcheck: 10 # Seconds to wait before considering "up" # OR healthcheck: http://localhost:5432 # URL to poll for readiness ``` ### Common database setups #### PostgreSQL ```yaml docker: postgres: image: postgres:15 ports: - 5432:5432 env: .env.postgres volumes: - /var/lib/postgresql/data ``` #### MongoDB ```yaml docker: mongodb: image: mongo:7 ports: - 27017:27017 volumes: - /data/db ``` ### Docker volumes Zapper supports Compose-style volume mounts and adds a higher-level managed volume form. ```yaml docker: postgres: image: postgres:15 volumes: - /var/lib/postgresql/data # Zapper-managed volume - /var/lib/postgresql/wal:ro # Zapper-managed volume with mode - postgres-logs:/var/log/postgresql # Explicit named Docker volume - ./init.sql:/docker-entrypoint-initdb.d/init.sql # Bind mount - internal_dir: /var/lib/postgresql/wal # Zapper-managed volume mode: ro - name: postgres-config internal_dir: /etc/postgresql # Explicit named Docker volume ``` When a volume entry is only a container path, or an object without `name`, Zapper generates a Docker volume name and stores it under the selected instance in `.zap/state.json`. Each instance gets its own generated volume name for the same service/path pair, using names like `zap.myapp.a1b2c3.vol1`. Explicit names keep Compose-style behavior and are shared anywhere that name is reused. Use `zap volume prune` to delete generated Docker volumes that are still in state but no longer appear in `zap.yaml`. Use `zap volume reset` to forget the generated assignments for the selected instance without deleting Docker volumes. #### Redis ```yaml docker: redis: image: redis:7-alpine ports: - 6379:6379 ``` #### MySQL ```yaml docker: mysql: image: mysql:8 ports: - 3306:3306 env: .env.mysql volumes: - mysql-data:/var/lib/mysql ``` --- ## Environment Variables Zapper uses one `env` field for environment variable source and routing: - Root `env` defines the global env file stack. - Service `env: "*"` passes all values from the root env stack. - Service `env: [files...]` uses a service-specific env file stack instead. - Service `env: path/to/whitelist.yaml` filters the root env stack through a strict whitelist file. Inline variable whitelists are not supported in `zap.yaml`. Arrays under service `env` are file stacks. ### Loading from files ```yaml env: [.env] # Single file env: [.env.base, .env] # Multiple files (later files override) ``` Env file names are intentionally flexible. Any non-empty filename or path is accepted, including names such as `.env.something`, `service-env`, or `config/local.env`. Values that look like uppercase variable names, such as `DATABASE_URL`, are rejected because service `env` arrays define file stacks, not inline variable lists. Root `env_files` is still accepted as a compatibility alias for root `env`. ### Environment sets (recommended) You can define multiple env file sets and switch between them with `zap env `. The `default` set is optional; if omitted and no environment is active, no env files are loaded. ```yaml env: default: [.env.base, .env] prod_dbs: [.env.base, .env.prod-dbs] ``` Compatibility alias: ```yaml env_files: [.env.base, .env] ``` ### Recommended pattern Split into two files: - `.env.base` — non-secrets (ports, URLs), **committed** to git - `.env` — secrets (API keys, passwords), **gitignored** ```yaml env: [.env.base, .env] ``` ### Passing all env to a service Use `env: "*"` when a service can receive every value from the root env stack: ```yaml env: [.env.base, .env] native: backend: cmd: pnpm dev env: "*" frontend: cmd: pnpm dev env: "*" ``` ### Service-specific file stacks Use a service-level env file stack when a service should not use the global stack: ```yaml native: frontend: cmd: pnpm dev env: [.env.common, .env.frontend, .env.frontend.user] backend: cmd: pnpm dev env: [.env.common, .env.db, .env.backend, .env.backend.user] ``` The service stack replaces the root stack for that service. ### Whitelist files Use a whitelist file when you want central env files but explicit routing: ```yaml env: [.env.common, .env.db, .env.user] native: api: cmd: pnpm dev env: .zap/env/api.yaml ``` `.zap/env/api.yaml`: ```yaml vars: - DATABASE_URL - JWT_SECRET ``` ### Port assignment Define port variable names in your config and initialize values with `zap init`: ```yaml project: myapp ports: - FRONTEND_PORT - BACKEND_PORT - DB_PORT env: [.env] native: frontend: cmd: pnpm dev env: "*" backend: cmd: pnpm dev env: "*" ``` Then run: ```bash zap init # Ensures ports/volumes/state exist for default instance zap init --instance e2e # Ensures ports/volumes/state exist for named instance zap init -R # Re-randomizes all configured ports in selected instance ``` Most config-backed commands now perform this initialization step automatically if the target instance has not been created yet, so your first command no longer needs to be `zap up`. If `init_task` is set, `zap init` runs that task after initialization completes. This is equivalent to running `zap init` first and then `zap task `. The assigned ports have **highest precedence** - they override values from any `.env` files. This is useful for: - Avoiding port conflicts when running multiple instances - Dynamic port assignment in development - Sharing configurations with different port needs `zap ls` always shows assigned port variables in a separate Ports table, even without `--extended`, because they are part of the active instance's key runtime state. **Interpolation works with assigned ports:** ```txt # .env FRONTEND_PORT=3000 FRONTEND_URL=http://localhost:${FRONTEND_PORT} ``` After initialization: ```bash # If FRONTEND_PORT was assigned 54321 FRONTEND_URL will be http://localhost:54321 ``` ### Docker env vars Docker services can use env vars too: ```yaml docker: postgres: image: postgres:15 env: "*" ``` Docker `ports` mappings also support interpolation, including values initialized by `ports:`: ```yaml ports: - MONGO_PORT docker: mongodb: image: mongo:latest ports: - ${MONGO_PORT}:27017 ``` ### Inspecting resolved env vars ```bash zap env --service api # Show resolved env vars for a service zap env api # Works if no environment set named 'api' ``` --- ## Instances Instances let you run multiple stacks for the same project without name, port, or managed-volume collisions. ```bash zap up # Ensures default instance exists on first run zap up --instance e2e # Run a named instance zap init --instance e2e # Explicitly create/init named instance state ``` If you omit `--instance`, Zapper targets `default`. Instance keys must use lowercase letters and hyphens only. See [Instances](instances.md) for full details. --- ## Tasks One-off commands that can use your env vars and accept parameters. ### Basic task ```yaml tasks: seed: cmds: - pnpm db:seed ``` ### All options ```yaml tasks: seed: desc: Seed the database # Description (shown in help) aliases: [s] # Alternate task names accepted by zap task cwd: ./backend # Working directory env: .zap/env/backend.yaml # Strict whitelist file params: # Named parameters - name: count default: "10" desc: Number of records - name: env required: true desc: Target environment cmds: # Commands to run (in order) - pnpm db:migrate - "pnpm db:seed --count={{count}}" ``` ### Running tasks ```bash zap task seed zap task lint ``` ### Running a task automatically after init Set `init_task` to any defined task name: ```yaml init_task: seed tasks: seed: cmds: - pnpm db:seed ``` When you run `zap init`, Zapper performs normal initialization and then runs that task. ### Parameters Tasks can accept named parameters and pass-through arguments. #### Named parameters Define parameters with defaults and validation: ```yaml tasks: build: desc: Build for target environment params: - name: target default: development desc: Build target - name: minify desc: Enable minification cmds: - 'echo "Building for {{target}}"' - "npm run build -- --env={{target}}" ``` Run with parameters: ```bash zap task build --target=production --minify=true ``` #### Required parameters Mark parameters as required (task fails if not provided): ```yaml tasks: deploy: params: - name: env required: true desc: Deployment environment cmds: - "deploy.sh {{env}}" ``` ```bash zap task deploy --env=staging # Works zap task deploy # Error: Required parameter 'env' not provided ``` #### Pass-through arguments (REST) Use {{REST}} to forward extra CLI arguments: ```yaml tasks: test: desc: Run tests with optional args cmds: - "pnpm vitest {{REST}}" ``` Everything after `--` is passed through: ```bash zap task test -- --coverage src/ # Runs: pnpm vitest --coverage src/ ``` #### Custom delimiters If your commands contain {{ and }}, use custom delimiters: ```yaml project: myapp task_delimiters: ["<<", ">>"] tasks: build: cmds: - 'echo "Building <>"' ``` ### Listing task parameters For tooling integration (VS Code extension), get parameter info as JSON: ```bash zap task build --list-params ``` Output: ```json { "name": "build", "params": [ { "name": "target", "default": "development", "required": false, "desc": "Build target" } ], "acceptsRest": false } ``` ### Common task patterns #### Database operations ```yaml tasks: db:migrate: desc: Run database migrations env: .zap/env/database.yaml cmds: - pnpm prisma migrate dev db:seed: desc: Seed the database env: .zap/env/database.yaml cmds: - pnpm prisma db seed db:reset: desc: Reset and reseed database env: .zap/env/database.yaml cmds: - pnpm prisma migrate reset --force ``` #### Code quality ```yaml tasks: lint: cmds: - pnpm eslint . --fix - pnpm prettier --write . typecheck: cmds: - pnpm tsc --noEmit test: env: .zap/env/database.yaml cmds: - pnpm vitest run checks: desc: Run all checks before committing cmds: - pnpm eslint . - pnpm tsc --noEmit - pnpm vitest run ``` --- ## Dependencies Control startup order with `depends_on`. ### Basic dependency ```yaml docker: postgres: image: postgres:15 ports: - 5432:5432 native: api: cmd: pnpm dev depends_on: [postgres] # Postgres starts first ``` ### Dependency chain ```yaml docker: postgres: image: postgres:15 redis: image: redis:7 native: api: cmd: pnpm dev depends_on: [postgres, redis] worker: cmd: pnpm worker depends_on: [api] # API (and its deps) start first frontend: cmd: pnpm dev depends_on: [api] ``` When you run `zap up frontend`, Zapper starts: postgres → redis → api → frontend. `depends_on` affects start order only. - `zap up` / `zap restart` start waves are dependency-aware. - `zap down` stops targeted services in a single wave. - `zap restart ` restarts only the targeted service(s), not their dependencies. --- ## Profiles Run different subsets of services. ### Defining profiles ```yaml native: api: cmd: pnpm dev profiles: [dev, test] api-prod: cmd: pnpm start profiles: [prod] frontend: cmd: pnpm dev profiles: [dev] docker: postgres: image: postgres:15 profiles: [dev, test] postgres-test: image: postgres:15 env: .env.test-db profiles: [test] ``` ### Using profiles ```bash zap up # Starts only services with no `profiles` field zap profile dev # Enables 'dev' profile and starts matching services zap restart # Restarts all services using active profile filtering zap profile --disable # Disables active profile ``` ### Default behavior Services without a `profiles` field run regardless of profile state. Services with a `profiles` field run only when an active profile matches. --- ## Links Quick reference links for your project. These are for your own reference and can be displayed by tooling. You can also set a top-level `homepage` URL as the default target for `zap launch` with no arguments. Use `zap home` to print just the homepage URL, or `zap links` to list the homepage alongside your configured links. ### Homepage ```yaml homepage: http://localhost:3000 ``` ### Basic usage ```yaml links: - name: API Docs url: https://api.example.com/docs - name: Staging url: https://staging.example.com - name: Figma url: https://figma.com/file/abc123 ``` ### Environment variable interpolation Link URLs support `${VAR}` syntax to reference environment variables from your root `env` files: ```yaml env: [.env] links: - name: API url: http://localhost:${API_PORT} - name: Frontend url: http://localhost:${FRONTEND_PORT} ``` ### Opening links ```bash zap launch # Open homepage zap launch "API Docs" # Open by link name (quote if spaces) zap links # List homepage + configured links zap home # Print homepage zap open # Alias for: zap launch zap o "API Docs" # Short alias for: zap launch "API Docs" ``` ### Properties | Property | Required | Description | | -------- | -------- | ------------------------------------- | | `name` | Yes | Display name (max 100 characters) | | `url` | Yes | URL (supports `${VAR}` interpolation) | --- ## Notes Top-level project notes you can print with `zap notes`. The notes string supports `${VAR}` interpolation from your root `env` files. ### Configuration ```yaml env: [.env] notes: | Frontend: http://localhost:${FRONTEND_PORT} API: http://localhost:${API_PORT} ``` ### Usage ```bash zap notes # Print interpolated notes zap notes --json # JSON output ``` --- ## Git Cloning For multi-repo setups, Zapper can clone repositories. ### Configuration ```yaml project: myapp git_method: ssh # ssh | http | cli native: api: cmd: pnpm dev cwd: ./api repo: myorg/api-service web: cmd: pnpm dev cwd: ./web repo: myorg/web-app ``` ### Git methods | Method | URL Format | Notes | | ------ | ----------------------------------- | ------------------- | | `ssh` | `git@github.com:myorg/repo.git` | Requires SSH key | | `http` | `https://github.com/myorg/repo.git` | May prompt for auth | | `cli` | Uses `gh repo clone` | Requires GitHub CLI | ### Cloning ```bash zap clone # Clone all repos zap clone api # Clone one repo zap clone api web # Clone multiple repos ``` Repos are cloned to the path specified in `cwd`. --- ## Full Example A complete example for a typical full-stack app: ```yaml project: myapp env: [.env.base, .env] git_method: ssh native: api: cmd: pnpm dev cwd: ./api repo: myorg/api env: .zap/env/api.yaml depends_on: [postgres, redis] worker: cmd: pnpm worker cwd: ./api env: .zap/env/worker.yaml depends_on: [api] frontend: cmd: pnpm dev cwd: ./web repo: myorg/web env: .zap/env/web.yaml depends_on: [api] docker: postgres: image: postgres:15 ports: - 5432:5432 env: .zap/env/postgres.yaml volumes: - /var/lib/postgresql/data redis: image: redis:7-alpine ports: - 6379:6379 tasks: db:migrate: desc: Run migrations env: .zap/env/database.yaml cmds: - pnpm --filter api prisma migrate dev db:seed: desc: Seed database env: .zap/env/database.yaml params: - name: count default: "10" desc: Number of seed records cmds: - "pnpm --filter api prisma db seed --count={{count}}" test: desc: Run tests with optional args env: .zap/env/database.yaml cmds: - "pnpm vitest {{REST}}" deploy: desc: Deploy to environment params: - name: env required: true desc: Target environment (staging, production) cmds: - "deploy.sh {{env}}" lint: cmds: - pnpm eslint . --fix - pnpm tsc --noEmit homepage: http://localhost:5173 notes: "Docs: http://localhost:3000/docs" links: - name: API Docs url: http://localhost:3000/docs - name: Storybook url: http://localhost:6006 ``` # Source: docs/instances.md # Instances Zapper is instance-first. A project can have multiple stack instances, and each instance has: - Its own random `id` (used in PM2/Docker names) - Its own assigned `ports` map - Its own generated Docker `volumes` map for path-only volume mounts This prevents collisions across separate checkouts and also supports multiple stacks from one repo (for example, E2E runs). ## Defaults - If `--instance` is omitted, Zapper resolves the default instance key from `state.json` (`defaultInstance`, fallback: `default`). - Instance keys must contain lowercase letters and hyphens only. ## Initialization - Any config-backed command ensures the target instance exists before running. - `zap init` is the explicit/idempotent way to force that setup and then run `init_task` if configured. - `zap init -R` re-randomizes all configured ports for the selected instance. - `zap volume prune` deletes generated Docker volumes whose service/path no longer exists in the current config. - `zap volume reset` clears generated volume assignments in state without deleting Docker volumes. Examples: ```bash zap status zap up zap up --instance e2e zap init --instance e2e ``` ## Naming PM2 and Docker names are always namespaced: - `zap...` ## State file Zapper stores instance state in `.zap/state.json`: ```json { "defaultInstance": "default", "instances": { "default": { "id": "a1b2c3", "ports": { "FRONTEND_PORT": "54321" }, "volumes": { "zap.myapp.a1b2c3.vol1": { "service": "postgres", "internal_dir": "/var/lib/postgresql/data" } } }, "e2e": { "id": "k9m2pq", "ports": { "FRONTEND_PORT": "61234" }, "volumes": { "zap.myapp.k9m2pq.vol1": { "service": "postgres", "internal_dir": "/var/lib/postgresql/data" } } } } } ``` # Source: docs/resource-management.md # Resource Management Zapper names the resources it creates so they can be discovered later: - PM2 processes and Docker containers: `zap...` - Generated Docker volumes: `zap...volN` `zap ls` shows configured services and assigned ports by default. Use `zap ls --extended` (or `zap ls --all`) for the local inventory view: configured services first, then recognized instances from the local `.zap/state.json` and resources that look related to the project but no longer line up with the current config or state. ## Resource Types ### Current resources Current resources belong to the selected instance and still match a service or managed volume path in the current `zap.yaml`. ### Dangling resources Dangling resources belong to an instance recorded in this repo, but no longer match the current `zap.yaml` or current state. Common causes: - A service was renamed or removed while its PM2 process or Docker container still exists. - A generated Docker volume exists but is no longer tracked in `.zap/state.json`. - A managed volume path changed, leaving the old generated volume assignment stale. Use `zap ls --extended` to see these. The usual repair is to stop/delete the stale resources rather than hand-editing state. ### Unrecognized resources Unrecognized resources match the current project name but do not belong to any instance recorded in the local `.zap/state.json`. They usually come from another checkout, older state, or manual resource creation. Use `zap global list ` or `zap global kill ` when you want a project-wide view or cleanup across checkouts. ## Cleanup Commands - `zap down` stops resources for the selected instance and current config. - `zap kill` deletes all PM2 processes and Docker containers for the current project across instances. - `zap global kill ` deletes PM2 processes and Docker containers for a named project. - `zap volume prune` deletes stale generated Docker volumes for the selected instance. - `zap volume reset` forgets generated volume assignments in `.zap/state.json` without deleting Docker volumes. For one-off cleanup, Docker and PM2 commands are still valid escape hatches: ```bash docker rm -f docker volume rm pm2 delete ``` ## Practical Recovery If a config change leaves old resources around: ```bash zap ls --extended zap volume prune zap kill zap up ``` If generated volume state is confusing but you want to keep the Docker volumes for manual inspection: ```bash zap volume reset zap init ``` # Source: docs/env-var-mgmt.md # Environment Variable Management This document explains the environment variable model and the reasoning behind it. For concise syntax reference, see [usage.md](usage.md). ## Goals Zapper should make local development environment variables easy to share across services without forcing users to copy the same values into many places. The original model is still sound: 1. Load environment variables from a central source. 2. Treat that source as the local source of truth. 3. Decide what each service receives. The problem is that explicit routing is too much ceremony for many local projects. Zapper should support careful routing, but the common path should be small enough that most projects can understand it at a glance. ## Recommended Model Use one field: `env`. At the root level, `env` defines the global environment file stack: ```yaml env: [.env.local, .env.user] ``` Root-level `env_files` remains as a compatibility alias: ```yaml env_files: [.env.local, .env.user] ``` At the service level, `env` chooses how that service receives environment variables: ```yaml env: "*" # Pass all values from the global env stack env: [.env.api] # Replace global env with this service file stack env: api.env.yaml # Route global env through this strict whitelist file ``` There is no inline whitelist array in `zap.yaml`. Arrays in `zap.yaml` are file stacks. Variable allowlists live only in external whitelist files. This gives Zapper one concept with three levels of power: 1. `env: "*"` for the default local developer. 2. `env: [files...]` for the power user who wants direct file assignment. 3. `env: whitelist.yaml` for the large-team user who needs central storage plus explicit routing. ## Resolution Rules Root `env` and root `env_files` both mean "load these environment files as the global stack." If both are present, Zapper rejects the config instead of guessing which one wins. Service-level `env` resolves as follows: 1. `env: "*"` passes every value from the resolved global env stack. 2. `env: [files...]` loads those files for that service and exposes every value from that service stack. This replaces the global stack for that service. 3. `env: path/to/whitelist.yaml` loads a strict whitelist file and exposes only the listed variables from the global env stack. 4. Missing `env` means no Zapper-managed env for that service. The service file-stack rule is an override, not a merge. That keeps precedence straightforward: - Root `env` defines the default source. - Service `env: "*"` uses the default source. - Service `env: [files...]` replaces the default source. - Service `env: whitelist.yaml` filters the default source. Generated Zapper values, such as assigned ports, are part of the resolved environment source before either `*` or whitelist filtering is applied. ## Persona Stress Test The model is useful only if it handles the common case without ceremony and still has credible answers for more demanding setups. ### Persona 1: Default Local Developer This user has a few services and a manageable number of variables. Most values are local-only coordination values: ports, local URLs, feature toggles, and container credentials that are not meaningful outside the dev machine. They want: - One obvious place to put shared values. - One gitignored place to put personal overrides. - No per-service env bookkeeping. They should use root `env` and service `env: "*"`: ```yaml project: myapp env: [.env.local, .env.user] native: frontend: cmd: pnpm dev env: "*" backend: cmd: pnpm dev env: "*" docker: postgres: image: postgres:15 env: "*" ``` This is intentionally permissive. It is the right default because local dev often values alignment more than isolation. The star is visible enough to signal broad access. How the model handles it: - Strong fit. - Small `zap.yaml`. - No variable duplication. - No extra routing files. - Easy migration path if one service later needs a custom file stack. ### Persona 2: Security-Conscious Power User This user has enough secrets that they do not want every service receiving the same gitignored user file. They also prefer ordinary env files over Zapper-owned whitelist policy. They want: - Direct file assignment per service. - Shared non-sensitive files where useful. - Separate user secret files for sensitive services. - No central whitelist registry in `zap.yaml`. They should use service-level file stacks: ```yaml project: myapp native: frontend: cmd: pnpm dev env: [.env.common, .env.frontend, .env.frontend.user] backend: cmd: pnpm dev env: [.env.common, .env.db, .env.backend, .env.backend.user] worker: cmd: pnpm worker env: [.env.common, .env.db, .env.worker, .env.worker.user] ``` This is direct file assignment. The service's `env` array is the source stack for that service, and all values from that stack are exposed to that service. How the model handles it: - Strong fit. - Security boundaries are represented by file boundaries. - `zap.yaml` stays readable because it names file stacks rather than individual variables. - The main weakness is that file composition becomes the routing system. If the project grows to hundreds or thousands of variables, this can become hard to maintain. ### Persona 3: Large-Team Platform Owner This user has a large environment surface, possibly hundreds or thousands of variables. Many values need to line up across services. Copying variables into service-specific files would be risky and tedious. They want: - Central env files as the source of truth. - Explicit routing so services receive only what they need. - Routing policy outside the core service config. - A strict schema for routing files. They should use a global stack and service-level whitelist files: ```yaml project: enterprise-app env: [.env.company, .env.local, .env.user] native: frontend: cmd: pnpm dev env: .zap/env/frontend.yaml backend: cmd: pnpm dev env: .zap/env/backend.yaml worker: cmd: pnpm worker env: .zap/env/worker.yaml ``` With `.zap/env/backend.yaml`: ```yaml vars: - DATABASE_URL - REDIS_URL - JWT_SECRET ``` This is the most complex setup, but it earns that complexity. The environment values remain centralized, while routing policy moves into dedicated files that can be reviewed separately from service definitions. How the model handles it: - Good fit for very large projects. - Avoids copying values across service files. - Keeps `zap.yaml` from being polluted by long whitelist definitions. - The complexity is isolated to projects that actually need it. ## Whitelist Files A service string other than `*` should be interpreted as a whitelist file path, not a named whitelist embedded in `zap.yaml`. Whitelist files have a strict schema: ```yaml vars: - DATABASE_URL - REDIS_URL - JWT_SECRET ``` Rules: - The top level must be an object. - `vars` must be an array of non-empty variable names. - Unknown keys are rejected. - `*` is not a valid whitelist file path or variable name. - Whitelist files require a global env stack. If root `env` or `env_files` is missing, service `env: some-whitelist.yaml` errors because there is no central source to filter. The last rule is important: a whitelist does not load values. It only selects values from the global env source. ## Weird Cases ### Root `env` and Root `env_files` Invalid: ```yaml env: [.env.local] env_files: [.env.local] ``` Both fields mean the same thing. Supporting both at once creates unnecessary precedence rules, so this should be a validation error. ### Service `env: "*"` Without Global Env Valid, but usually empty unless generated values such as assigned ports exist: ```yaml native: api: cmd: pnpm dev env: "*" ``` Because `*` means "all currently available values", this produces an empty env when there is no root env source and no generated values. Whitelist files are stricter: they require a root env source because they filter central values. ### Service File Stack With Global Env Valid: ```yaml env: [.env.local, .env.user] native: api: cmd: pnpm dev env: [api/.env.local, api/.env.user] ``` The service stack replaces the global stack for this service. It does not merge with the global stack. Users who want shared values can include the shared file directly: ```yaml native: api: cmd: pnpm dev env: [.env.common, api/.env.local, api/.env.user] ``` ### Inline Variable Arrays Invalid: ```yaml env: - DATABASE_URL - JWT_SECRET ``` There is no inline whitelist array in `zap.yaml`. A service `env` array is a file stack, so entries are interpreted as file names or paths. Zapper accepts ordinary file names such as `.env.something` and `service-env`, but rejects entries that look like uppercase variable names such as `DATABASE_URL`. Explicit variable routing belongs in a whitelist file: ```yaml native: api: cmd: pnpm dev env: .zap/env/api.yaml ``` With `.zap/env/api.yaml`: ```yaml vars: - DATABASE_URL - JWT_SECRET ``` ### Mixing File Stacks and Whitelist Files Invalid: ```yaml native: api: cmd: pnpm dev env: [.env.common, .zap/env/api.yaml] ``` An `env` array is a file stack. A string is a whitelist file path. Mixing those concepts in one value makes resolution unclear and should be rejected. ## Why This Is the Middle Ground This is technically three capabilities, but only one needs to be common: - Common path: root `env`, service `env: "*"`. - Power-user path: service `env: [files...]`. - Large-team path: root `env`, service `env: whitelist.yaml`. The benefit is that all three use one field and one idea: - Root `env` defines the default source. - Service `env` defines how the service receives env. - `*` is shorthand for "all values from the default source." - A service file stack is an explicit source override. - A whitelist file filters the default source. ## Current State The implemented model is: - Most projects use root `env` and service `env: "*"`. - Projects with existing env-file conventions use service `env: [files...]`. - Security-conscious large projects use service `env: whitelist.yaml`. - Root `env_files` remains a compatibility alias. - Inline `whitelists` and inline service variable arrays disappear from the core YAML spec.