home / skills / steveclarke / dotfiles / outport

outport skill

/ai/skills/outport

This skill helps you manage dev ports and derived URLs with Outport, ensuring deterministic, non-conflicting ports across worktrees and services.

npx playbooks add skill steveclarke/dotfiles --skill outport

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
13.3 KB
---
name: outport
description: Manage dev ports with Outport. Use when setting up a new project, adding services, resolving port conflicts, configuring monorepo cross-service URLs, or working with worktrees and multiple instances. Triggers on "outport", "port conflict", "port allocation", "dev ports", ".outport.yml", "port management", "env var ports", "derived values", "cross-service URLs", "CORS origins from ports", ".test domains", "local DNS", "reverse proxy", "cookie isolation". Also use when the user mentions running multiple instances of a project, worktree port setup, or when services need to discover each other's URLs.
---

# Outport — Dev Port Manager

Outport allocates deterministic, non-conflicting ports for dev services,
assigns `.test` hostnames, and writes everything to `.env` files. Every
framework reads `.env` — Rails, Nuxt, Django, Docker Compose — so ports and
URLs just work without manual configuration.

## Quick Reference

```bash
# Core workflow
outport init              # Create .outport.yml (interactive)
outport apply             # Allocate ports, assign hostnames, write .env
outport a                 # Short alias for apply
outport apply --force     # Clear and re-allocate all ports from scratch
outport unapply           # Remove ports and clean .env files

# Inspect
outport ports             # Show ports for current project
outport ports --derived   # Show ports and derived values
outport ports --json      # Machine-readable output
outport open              # Open HTTP services in browser
outport open web          # Open a specific service
outport status            # Show all registered projects
outport status --check    # Show with health checks (up/down)
outport gc                # Remove stale registry entries

# .test domain routing (macOS, one-time setup)
outport setup             # Install DNS resolver and proxy daemon
outport teardown          # Remove DNS resolver and daemon
outport up                # Start the daemon manually
outport down              # Stop the daemon manually

# Instance management
outport rename <old> <new>  # Rename the current instance
outport promote             # Promote the current instance to main
```

All commands support `--json` for machine-readable output.

## Setting Up a New Project

### 1. Create `.outport.yml`

Run `outport init` for interactive setup, or create manually:

```yaml
name: my-project
services:
  web:
    env_var: PORT
    protocol: http
  postgres:
    env_var: DB_PORT
  redis:
    env_var: REDIS_PORT
```

### 2. Run `outport apply`

Allocates deterministic ports (hashed from project name + service name) and
writes them to `.env`. Same inputs always produce the same ports.

### 3. Wire up your project to read from `.env`

Most frameworks read `.env` natively or with minimal setup:

- **Docker Compose** — reads `.env` automatically. Use `${DB_PORT:-5432}` in
  `compose.yml`
- **Rails** — use `dotenv-rails` gem, or reference env vars in config:
  `port: ENV.fetch("DB_PORT", 5432)`
- **Nuxt** — reads `.env` natively. Runtime config values can be overridden
  via `NUXT_*` env vars
- **Foreman** — reads `.env` automatically
- **Overmind** — does NOT auto-load `.env`. Source it in your start script:
  ```bash
  if [ -f .env ]; then set -a; source .env; set +a; fi
  ```

### 4. Commit `.outport.yml`, gitignore `.env`

`.outport.yml` is project config — commit it so worktrees and teammates
inherit it. `.env` contains allocated ports — gitignore it. Each checkout
gets its own.

## .test Domains (DNS Proxying)

Running `outport setup` once enables friendly `.test` hostnames for your
services. After setup, `http://myapp.test` routes to your app instead of
`http://localhost:24920`.

### How it works

`outport setup` installs two components (macOS, requires sudo for DNS step):

1. **DNS resolver** — `/etc/resolver/test` points `*.test` queries to a
   local DNS server on port 15353, which resolves all `*.test` names to
   `127.0.0.1`.
2. **HTTP reverse proxy** — a LaunchAgent runs on port 80, routes requests
   by `Host` header to the correct service port, and auto-updates when you
   run `outport apply`. WebSocket connections are proxied transparently.

```bash
outport setup     # Install DNS + daemon (one-time, prompts for sudo)
outport teardown  # Remove everything — reverse of setup
outport up        # Start the daemon (if it was stopped)
outport down      # Stop the daemon
```

### Configuring .test hostnames

Add `hostname` and `protocol: http` to a service to assign it a `.test`
URL:

```yaml
name: myapp
services:
  web:
    env_var: PORT
    protocol: http
    hostname: myapp.test       # → http://myapp.test
  postgres:
    env_var: DB_PORT           # no hostname: port allocation only
```

Hostname rules:
- Must include the project name (e.g., `myapp.test`, `app.myapp.test`)
- Requires `protocol: http` or `https`
- Non-main instances get the instance code appended automatically (see
  [Multiple Instances](#multiple-instances))

### Cookie isolation

Each instance gets its own `.test` hostname, so browser cookies and
sessions are isolated automatically — no incognito windows needed:

```text
myapp [main]   web → 24920   http://myapp.test
myapp [bkrm]   web → 28104   http://myapp-bkrm.test
```

## Config Reference

### Service Fields

| Field | Required | Description |
|-------|----------|-------------|
| `env_var` | yes | Environment variable name written to `.env` |
| `protocol` | no | `http`, `https`, `smtp`, `postgres`, `redis`, etc. HTTP/HTTPS services show URLs in output and work with `outport open` |
| `hostname` | no | `.test` hostname for this service (e.g., `myapp.test`). Requires `protocol: http` or `https`. Non-main instances get the instance code appended. |
| `preferred_port` | no | Port to try first. Falls back to hash-based allocation if already in use |
| `env_file` | no | Where to write. String or array. Defaults to `.env` in project root |

### Writing to Multiple `.env` Files

For monorepos, a port often needs to appear in multiple `.env` files. Use an
array for `env_file`:

```yaml
services:
  rails:
    env_var: RAILS_PORT
    protocol: http
    env_file:
      - backend/.env
      - frontend/.env          # Frontend needs this to construct API URLs
```

## Derived Values

Applications don't just need port numbers — they need URLs. Derived values
compute environment variables from your service map and write finished values
to `.env`.

### Basic syntax

```yaml
derived:
  API_URL:
    value: "${rails.url:direct}/api/v1"   # http://localhost:24920/api/v1
    env_file: frontend/.env
  CORS_ORIGINS:
    value: "${web.url}"                   # http://myapp.test (or localhost:PORT)
    env_file: backend/.env
```

- `${service_name.field}` references service fields
- `env_file` is required (no default — you must be explicit)
- Derived names must not collide with service `env_var` names

### Template Fields

| Template | Resolves to | Use case |
|----------|------------|----------|
| `${rails.port}` | `24920` | Raw port number |
| `${rails.hostname}` | `myapp.test` (or `localhost` if no hostname set) | Hostname only |
| `${rails.url}` | `http://myapp.test` | Browser-facing URLs (CORS, asset hosts), routed via proxy |
| `${rails.url:direct}` | `http://localhost:24920` | Server-to-server calls that bypass the proxy |

**When to use `url` vs `url:direct`:**
- `${service.url}` — for values the browser sends (CORS origins, asset
  hosts, OAuth redirect URIs). Uses the `.test` hostname when configured.
- `${service.url:direct}` — for server-to-server calls (API base URLs a
  backend fetches, WebSocket connections from a Node server). Always uses
  `localhost` — no proxy hop.

### Per-file value overrides

When the same env var needs different values in different files (common in
monorepos where multiple apps share a framework convention), use the object
syntax for `env_file` entries:

```yaml
derived:
  NUXT_API_BASE_URL:
    env_file:
      - file: frontend/apps/main/.env
        value: "${rails.url:direct}/api/v1"
      - file: frontend/apps/portal/.env
        value: "${rails.url:direct}/portal/api/v1"
```

You can mix plain string entries (which use the top-level `value`) with
object entries in the same list.

### Real-world monorepo example

Rails backend with two Nuxt frontends. Backend needs CORS origins from
frontend `.test` URLs. Frontends need the Rails API URL for server-side
fetches:

```yaml
name: myapp
services:
  rails:
    env_var: RAILS_PORT
    protocol: http
    hostname: myapp.test
    env_file: backend/.env
  frontend_main:
    env_var: MAIN_PORT
    protocol: http
    hostname: app.myapp.test
    env_file:
      - frontend/apps/main/.env
      - backend/.env               # Backend needs this for CORS
  frontend_portal:
    env_var: PORTAL_PORT
    protocol: http
    hostname: portal.myapp.test
    env_file:
      - frontend/apps/portal/.env
      - backend/.env               # Backend needs this for CORS

derived:
  # Server-to-server API URLs (bypass proxy — use direct localhost)
  NUXT_API_BASE_URL:
    env_file:
      - file: frontend/apps/main/.env
        value: "${rails.url:direct}/api/v1"
      - file: frontend/apps/portal/.env
        value: "${rails.url:direct}/portal/api/v1"

  # Backend CORS (browser-facing — use .test hostnames)
  CORE_CORS_ORIGINS:
    value: "${frontend_main.url},${frontend_portal.url}"
    env_file: backend/.env

  # Backend asset host (browser-facing)
  SHRINE_ASSET_HOST:
    value: "${rails.url}"
    env_file: backend/.env
```

After `outport apply`, every service has the right ports AND the right URLs.
No hardcoded values survive.

## Framework Env Var Conventions

When setting up derived values, knowing how frameworks map env vars to
config is essential:

| Framework | Convention | Example |
|-----------|-----------|---------|
| **Nuxt** | `NUXT_` prefix maps to `runtimeConfig` | `NUXT_API_BASE_URL` overrides `runtimeConfig.apiBaseUrl` |
| **Rails (AnyWayConfig)** | `PREFIX_ATTR` maps to config class | `CORE_CORS_ORIGINS` overrides `CoreConfig.cors_origins` |
| **Rails (Shrine)** | Same AnyWayConfig pattern | `SHRINE_ASSET_HOST` overrides `ShrineConfig.asset_host` |
| **Django** | Typically reads `os.environ` directly | Name vars however your `settings.py` expects |
| **Docker Compose** | Reads `.env` automatically | `${DB_PORT:-5432}` in `compose.yml` |

The derived values feature is most powerful when it writes env vars that
match these framework conventions — the framework reads the value natively
and no config code changes are needed.

## .env File Format

Outport writes managed variables in a fenced block at the bottom of each
`.env` file:

```env
# Your own variables — Outport never touches these
SECRET_KEY=abc123
RAILS_ENV=development

# --- begin outport.dev ---
DB_PORT=21536
RAILS_PORT=24920
NUXT_API_BASE_URL=http://localhost:24920/api/v1
# --- end outport.dev ---
```

On each `outport apply`, the fenced block is replaced with current values.
Variables removed from `.outport.yml` disappear from the block. Everything
outside the block is preserved.

## Multiple Instances

Outport detects git worktrees automatically. Each worktree gets unique ports
and its own `.test` hostname — no configuration needed:

```bash
# Main checkout
$ outport apply
my-app [main]
  rails  RAILS_PORT → 24920  http://my-app.test
  web    MAIN_PORT  → 21349

# Worktree — different ports, different hostname, zero conflicts
$ cd ../my-app-feature && outport apply
  Registered as my-app-bkrm. Use 'outport rename bkrm <name>' to rename.
my-app [bkrm]
  rails  RAILS_PORT → 20192  http://my-app-bkrm.test
  web    MAIN_PORT  → 21133
```

Derived values are recomputed per instance — CORS origins, API URLs, and
all other derived values automatically use that instance's ports and
hostnames. Two full instances run simultaneously with no port collisions,
no hostname collisions, and no manual configuration.

Manage instances with:

```bash
outport rename bkrm my-feature   # Rename an instance
outport promote                   # Promote current instance to main
```

## Integrating with Setup Scripts

Run `outport apply` early in your project's setup flow — after `.env` file
creation but before services start. Make it optional so developers without
Outport aren't blocked:

```bash
# In bin/setup or similar
if command -v outport > /dev/null 2>&1; then
  outport apply
else
  echo "Outport not found — using default ports"
  echo "Install: brew install steveclarke/tap/outport"
fi
```

## Common Tasks

### Port conflict with another project
Run `outport apply` in both projects. Outport's registry ensures no
collisions across all registered projects.

### Ports are stale from an old allocation
Run `outport apply --force` to clear and re-allocate.

### Freeing ports from a project you're done with
Run `outport unapply` to remove from registry and free all ports.

### Adding a new service to an existing project
Add it to `.outport.yml` and run `outport apply`. Existing allocations
are preserved — only the new service gets a port.

### Agent needs to know the project's URLs
Run `outport ports --json` for structured output with ports, protocols,
and URLs.

### Services moved to different ports than expected
Check `outport status` to see all allocations. If another project holds
the ports you want, unapply it first, then `outport apply --force`.

### .test domain not resolving
Run `outport status` to verify the daemon is configured. If `outport setup`
was never run, run it now. If the daemon is stopped, run `outport up`.

Overview

This skill manages development ports, deterministic hostnames, and .env wiring for multi-service projects. It allocates non-conflicting ports, assigns .test hostnames, and writes env variables and derived URLs so services discover each other without manual port juggling. It supports multiple instances, monorepos, and a local DNS + reverse proxy for friendly hostnames.

How this skill works

It hashes project and service names to produce deterministic ports, records allocations in a registry, and writes a fenced block into one or more .env files. Optional .test DNS + proxy setup routes browser traffic to services by hostname. Derived values compute URLs (browser-facing vs direct localhost) and write them to specified files so frameworks pick them up automatically.

When to use it

  • Setting up a new project to avoid manual port choices
  • Adding services or changing service names to get new allocations
  • Running multiple instances or git worktrees of the same project
  • Configuring monorepo cross-service URLs and CORS origins
  • Resolving port conflicts between projects or machines
  • Enabling friendly .test hostnames and cookie isolation for local web apps

Best practices

  • Commit .outport.yml to the repo; gitignore generated .env files
  • Run outport apply early in setup scripts before services start
  • Use env_file arrays to propagate ports to every component that needs them
  • Prefer ${service.url} for browser-facing values and ${service.url:direct} for server-to-server calls
  • Use outport apply --force only to reallocate when ports are stale or corrupt the registry
  • Install the .test DNS/proxy on development machines for consistent hostnames

Example use cases

  • Monorepo: write Rails API port to multiple frontend .env files so frontends build correct API URLs
  • Worktree feature branch: run two instances of the app with isolated cookies and different .test hostnames
  • Conflict resolution: discover which project holds a port via outport status and unapply to free it
  • Derived values: generate CORS_ORIGINS from frontend .test URLs and NUXT_API_BASE_URL for server-side fetches
  • Local development: use outport setup to route myapp.test to the correct local service without modifying /etc/hosts

FAQ

How do I share port config with teammates?

Commit .outport.yml to the repo. Each developer runs outport apply locally; the same service names produce consistent allocations per machine.

When should I run outport setup?

Run outport setup once per machine to install the DNS resolver and proxy daemon if you want .test hostnames and browser routing; use outport up/down to manage the daemon.

What if ports are stale or collide?

Use outport apply --force to clear and re-allocate, or outport unapply on the other project to free ports. outport status shows current allocations and health.