Browse Source

feat(skills): Add portless-ops and process-compose-ops

Two new skills derived from the X:\00_Orchestration migration:

portless-ops:
- Wraps Vercel Labs portless CLI (named .localhost/.lab URLs, HTTPS/HTTP2)
- Mental model of name.tld constraint (and where it bites)
- Static-alias pattern for externally-managed services (PC/PM2/Docker)
- TLD selection guide (.lab/.test/.dev/.localhost — OAuth caveats)
- Reset procedure for TLD changes (wipe routes.json + re-register)
- Windows-specific gotchas (openssl PATH, curl vs browser cert handling)
- Boot persistence via portless service install
- References upstream skills (vercel-labs/portless ships its own)

process-compose-ops:
- Single-binary Go supervisor as PM2/supervisord/Foreman replacement
- Why-not-PM2 framing (15+ CVEs in PM2 5.x; PC immune to npm worms)
- Full process-compose.yaml schema with health/dependencies/restart
- TUI shortcuts (F4 maximize, Tab panes, r/s/t controls)
- MCP server integration for agent-driven process control
- Windows boot persistence via Task Scheduler S4U + PATH wrapper
- YAML gotchas (Windows backslash escape, quoted paths with spaces)
- When-to-use matrix vs PM2/Docker/Foreman/systemd

Both skills cross-reference each other and link to the orchestration repo
as a worked example.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0xDarkMatter 1 month ago
parent
commit
54dd06e488
2 changed files with 499 additions and 0 deletions
  1. 204 0
      skills/portless-ops/SKILL.md
  2. 295 0
      skills/process-compose-ops/SKILL.md

+ 204 - 0
skills/portless-ops/SKILL.md

@@ -0,0 +1,204 @@
+---
+name: portless-ops
+description: "Portless local-dev HTTPS proxy operations and integration. Use for: portless setup, named .localhost or custom-TLD URLs (axiom.lab, myapp.test), portless alias for externally-managed services, replacing Caddy/nginx for local dev, HTTP/2 dev servers, local CA generation and trust, portless service install (boot persistence on Windows/macOS/Linux), portless monorepo orchestration, Tailscale/Funnel dev sharing, git-worktree subdomain routing, portless.json configuration, agent-friendly URL discovery via portless get <name>, MCP-integration patterns, OAuth-with-portless TLD selection (.dev/.test for Google/Apple compliance), Vite/Next.js/Astro framework port injection, Windows openssl PATH gotcha, curl-vs-browser cert handling, custom TLD pitfalls (.local/.dev/.localhost), troubleshooting EADDRINUSE, /etc/hosts auto-sync, portless trust system store integration."
+license: MIT
+allowed-tools: "Read Write Bash Edit"
+metadata:
+  author: claude-mods
+  related-skills: process-compose-ops, mcp-ops, cli-ops
+  upstream: https://github.com/vercel-labs/portless
+---
+
+# Portless Operations
+
+Portless (Vercel Labs) is a local-dev HTTPS proxy that replaces port numbers with named URLs. Replacement for Caddy/nginx in the local-dev role; not for production.
+
+**Upstream:** [vercel-labs/portless](https://github.com/vercel-labs/portless) (Apache-2.0). The repo ships its own canonical [`SKILL.md`](https://github.com/vercel-labs/portless/blob/main/skills/portless/SKILL.md) and [oauth SKILL.md](https://github.com/vercel-labs/portless/blob/main/skills/oauth/SKILL.md) — consult those for full CLI reference. This skill adds operational patterns we've validated in production.
+
+## Mental Model
+
+| Layer | Portless owns | Portless does NOT own |
+|---|---|---|
+| Routing | hostname → port mapping, HTTPS termination, HTTP/2, CA trust | process supervision (use Process Compose or PM2) |
+| Naming | `<name>.<tld>` shape — one TLD per proxy | per-service distinct TLDs (not supported) |
+| Process spawning | when invoked as `portless myapp <cmd>` | crash recovery, restart policy, health checks |
+
+**Key shape constraint:** portless always renders `<alias-name>.<tld>`. You can't have `0x.axiom` and `axiom.lab` in the same proxy because TLD is per-instance. Aliases like `portless alias 0x.axiom 8108` get the TLD appended → `0x.axiom.lab`.
+
+## Install
+
+```bash
+# Pin a specific version (zero runtime deps, low supply-chain surface)
+npm install -g portless@0.13.0
+
+# Verify
+portless --version
+```
+
+Record the pinned version in your repo. Upgrades are explicit PRs.
+
+## CLI Quick Reference
+
+```bash
+# Proxy lifecycle
+portless proxy start --tld lab --port 443   # HTTPS proxy on 443, *.lab routes
+portless proxy start --tld test --port 1355 # Non-privileged port for testing
+portless proxy stop
+portless trust                              # Add CA to system trust store
+
+# Aliases (for services portless didn't spawn — PM2, Process Compose, Docker, etc.)
+portless alias axiom 8108                   # https://axiom.lab → :8108
+portless alias axiom 8108 --force           # Overwrite existing
+portless alias --remove axiom               # Note: appends TLD! be careful
+
+# Spawn-mode (portless manages the process)
+portless myapp next dev                     # https://myapp.lab, auto port 4000-4999
+portless run pnpm dev                       # Auto-infer name from package.json
+
+# Discovery (agent-friendly)
+portless list                               # Active routes
+portless get axiom                          # Returns: https://axiom.lab
+
+# Boot persistence
+portless service install                    # OS-native startup task
+portless service status
+portless service uninstall
+```
+
+## The Static-Alias Pattern (portless + external process supervisor)
+
+The common pattern: a process supervisor (Process Compose, PM2, Docker) runs your dev servers on fixed ports. Portless just routes named URLs to those ports.
+
+```bash
+# Started by Process Compose, listening on 8108
+# Now make it reachable at https://axiom.lab
+portless alias axiom 8108
+```
+
+Decoupling means:
+- Restart the dev server (`pm2 restart axiom`, `process-compose process restart axiom`) → portless keeps routing transparently
+- Swap one supervisor for another → portless layer is untouched
+
+**Source of truth pattern:** keep alias registration in your supervisor config. Example `scripts/install.ps1`:
+
+```powershell
+$services = (yq '.processes | keys | .[]' process-compose.yaml)
+foreach ($svc in $services) {
+  $port = (yq ".processes.$svc.readiness_probe.http_get.port" process-compose.yaml)
+  if ($port -and $port -ne "null") {
+    portless alias $svc $port --force
+  }
+}
+```
+
+## TLD Selection
+
+| TLD | When to use | Caveats |
+|---|---|---|
+| `.localhost` (default) | Quickest start | Auto-resolves to 127.0.0.1 on most systems |
+| `.lab` | Personal/distinctive | Not IANA-reserved (no DNS collision in practice for local) |
+| `.test` | OAuth-friendly | IANA-reserved; safe |
+| `.dev` | OAuth (Google, Apple) | Google-owned, forces HTTPS — portless handles this fine |
+| `.local` | Avoid | mDNS/Bonjour conflict |
+
+OAuth providers reject `.localhost` subdomains (not in Public Suffix List). Switch to `--tld test` or `--tld dev` for OAuth dev work. See [upstream oauth SKILL.md](https://github.com/vercel-labs/portless/blob/main/skills/oauth/SKILL.md).
+
+## Reset (clean slate)
+
+```bash
+# Stop proxy
+portless proxy stop
+
+# Wipe all aliases (routes.json)
+rm ~/.portless/routes.json    # Linux/macOS
+Remove-Item "$env:USERPROFILE\.portless\routes.json"   # PowerShell
+
+# Start fresh with desired TLD
+portless proxy start --tld <tld> --port 443
+
+# Re-register aliases from your supervisor config
+```
+
+This is the right pattern when you change TLD — `portless alias --remove` appends the active TLD which makes it fight you.
+
+## Windows-Specific Notes
+
+### `openssl` required on PATH
+
+Portless uses OpenSSL to generate the local CA. Git for Windows ships it:
+
+```powershell
+# Persistent: add to user PATH
+$gitBin = "C:\Program Files\Git\usr\bin"
+$current = [Environment]::GetEnvironmentVariable("PATH", "User")
+if ($current -notlike "*$gitBin*") {
+    [Environment]::SetEnvironmentVariable("PATH", "$gitBin;$current", "User")
+}
+```
+
+Without it: `Error: openssl failed: spawnSync openssl ENOENT`
+
+### Boot persistence
+
+`portless service install` registers a Task Scheduler entry. Pair it with your supervisor's own boot task (e.g., for Process Compose, register a separate task via `scripts/boot-task-install.ps1`).
+
+Verify both registered:
+
+```powershell
+Get-ScheduledTask | Where-Object {
+    $_.TaskName -like "*ortless*" -or $_.TaskName -like "*ompose*"
+}
+```
+
+### curl vs browser cert handling
+
+curl on Windows uses its own bundled CA store, not the system one. So `curl https://axiom.lab/` returns code 000 (cert untrusted) even after `portless trust`. Browsers work fine because they use the system store.
+
+Test from curl with `-k` (skip verify), or `--cacert ~/.portless/ca.pem`:
+
+```bash
+curl -k https://axiom.lab/        # quick test
+curl --cacert ~/.portless/ca.pem https://axiom.lab/   # proper
+```
+
+## Common Errors
+
+| Error | Cause | Fix |
+|---|---|---|
+| `openssl failed: spawnSync openssl ENOENT` | OpenSSL not on PATH | Add Git's `usr/bin` to PATH |
+| `Error: No alias found for "foo.lab"` (you asked for `foo`) | `--remove` appends TLD; sometimes adds an extra | Wipe `routes.json` and re-register |
+| Browser shows cert warning | CA not in system trust store | Re-run `portless trust` (may need admin) |
+| `https://name.lab` shows "No app registered" | Alias not set or proxy stopped | `portless list` to confirm; re-register if needed |
+| Safari can't resolve `*.lab` | Safari uses system DNS, not Node's resolver | `portless hosts sync` to write /etc/hosts |
+| Port 443 conflict on `portless proxy start` | Another service bound (Caddy, IIS) | Stop the other service, or use `--port 1355` for testing |
+
+## Worked Example: Replacing Caddy with portless
+
+See `~/X/00_Orchestration/compose-portless/` for a worked migration from PM2+Caddy to Process Compose+portless. Key files:
+
+- `process-compose.yaml` — supervisor config with health-checked services
+- `scripts/cutover.ps1` — stops PM2/Caddy, starts portless+PC, registers aliases
+- `docs/MIGRATION-LOG.md` — every issue hit during cutover and how it was solved
+- `docs/SUPPLY-CHAIN.md` — pinning + verification procedures
+
+## Anti-Patterns
+
+```
+BAD:  portless alias name 8000; portless alias name 8001   # second silently fails without --force
+GOOD: portless alias name 8001 --force
+
+BAD:  use portless as production reverse proxy
+GOOD: keep portless as dev-only; production = nginx/Caddy/cloud LB
+
+BAD:  rely on portless for crash recovery (it has none for spawned processes)
+GOOD: pair portless with Process Compose / PM2 / supervisord for supervision
+
+BAD:  change TLD by stopping/starting with different --tld and hoping aliases update
+GOOD: stop proxy, wipe routes.json, start with new TLD, re-register from supervisor config
+```
+
+## Related Skills
+
+- `process-compose-ops` — the supervisor we pair with portless
+- `mcp-ops` — agent-friendly tooling; portless `get <name>` provides URL discovery for agents
+- `cli-ops` — general CLI tool patterns

+ 295 - 0
skills/process-compose-ops/SKILL.md

@@ -0,0 +1,295 @@
+---
+name: process-compose-ops
+description: "Process Compose orchestration for non-containerized local services. Use for: process-compose.yaml schema, replacing PM2/supervisord/Foreman, health checks (readiness_probe, liveness_probe), restart policies (always/exit_on_failure/no), process dependencies (depends_on conditions), TUI navigation and shortcuts (F4 maximize, Tab panes, r/s/t process control), REST API and MCP server integration, headless mode (-t=false for daemons), per-process and consolidated logging (log_location), cron and interval scheduling (availability.schedule), namespace grouping for multi-stack composition, environment variable handling (env files, secrets), Windows Task Scheduler boot persistence, supply-chain verified single-binary install, multi-replica processes, foreground/serial execution patterns, dry-run validation, project update (hot reload without restart), process restart/stop/start via CLI or TUI, log tailing and follow modes, shutdown timeouts and signals, agent-friendly MCP tools for process control."
+license: MIT
+allowed-tools: "Read Write Bash Edit"
+metadata:
+  author: claude-mods
+  related-skills: portless-ops, docker-ops, cli-ops
+  upstream: https://github.com/F1bonacc1/process-compose
+---
+
+# Process Compose Operations
+
+Process Compose is a Go-based supervisor for non-containerized services. Single binary, YAML config, built-in TUI, REST API, **MCP server**, and proper Windows support. Replacement for PM2/supervisord/Foreman in the local-dev role.
+
+**Why not PM2:** PM2 5.x has 15+ known CVEs (axios/lodash/tar/minimist transitive npm exposure). PC compiles all deps in at build time with `go.sum` hashes — structurally resistant to TanStack-style npm worm attacks.
+
+**Why not Docker Compose:** Container overhead is unnecessary for local Python/Node/Go dev servers running directly. PC gives you health checks, dependencies, and restart policies without the container layer.
+
+## Install (verified)
+
+```bash
+# Pin a specific version, verify SHA-256 against upstream checksums
+VER="v1.110.0"
+BASE="https://github.com/F1bonacc1/process-compose/releases/download/$VER"
+
+curl -fsSL -o pc.zip "$BASE/process-compose_windows_amd64.zip"
+curl -fsSL -o checksums.txt "$BASE/process-compose_checksums.txt"
+
+EXPECTED=$(grep "process-compose_windows_amd64.zip" checksums.txt | awk '{print $1}')
+ACTUAL=$(sha256sum pc.zip | awk '{print $1}')
+[ "$EXPECTED" = "$ACTUAL" ] || { echo "HASH MISMATCH"; exit 1; }
+
+unzip pc.zip
+# Commit process-compose.exe to your repo's bin/ directory
+```
+
+Record the binary's hash in your repo's `SUPPLY-CHAIN.md` for re-verification on next upgrade.
+
+## process-compose.yaml Quick Reference
+
+```yaml
+version: "0.5"
+
+log_level: info
+log_length: 1000
+
+processes:
+
+  my-service:
+    command: "pythonw -m uvicorn main:app --host 127.0.0.1 --port 8000"
+    working_dir: "X:/path/to/repo"
+    environment:
+      - "DJANGO_SETTINGS_MODULE=myapp.settings"
+      - "PYTHONUNBUFFERED=1"
+    readiness_probe:
+      http_get:
+        host: localhost
+        port: 8000
+        path: /
+      initial_delay_seconds: 5
+      period_seconds: 10
+      timeout_seconds: 3
+      failure_threshold: 3
+    availability:
+      restart: always           # always | exit_on_failure | on_failure | no
+      backoff_seconds: 5
+      max_restarts: 20
+    depends_on:
+      database:
+        condition: process_healthy   # process_started | process_healthy | process_completed
+    shutdown:
+      signal: 15                # SIGTERM
+      timeout_seconds: 30
+    log_location: "logs/my-service.log"
+
+  scheduled-job:
+    command: "python backup.py"
+    schedule: "0 2 * * *"       # 2am daily cron
+    availability:
+      restart: exit_on_failure
+```
+
+## Restart Policies
+
+| Policy | Restarts on... |
+|---|---|
+| `always` | Any exit (success or failure) — best for long-running daemons |
+| `on_failure` | Non-zero exit codes only |
+| `exit_on_failure` | Stops PC entirely if this process fails — use for critical deps |
+| `no` | Never restart |
+
+## Dependency Conditions
+
+| Condition | Wait until... |
+|---|---|
+| `process_started` | Dependency spawned (PID exists). Fastest, weakest guarantee. |
+| `process_healthy` | Dependency's readiness_probe passes. Strong guarantee. |
+| `process_completed` | Dependency exited successfully (for init/setup processes). |
+
+## CLI Reference
+
+```bash
+# Lifecycle
+process-compose up -f config.yaml          # Start (foreground TUI by default)
+process-compose up -f config.yaml -t=false # Headless (no TUI)
+process-compose up -f config.yaml --dry-run  # Validate config without starting
+process-compose down                       # Stop all processes + project
+
+# Inspection (against running PC)
+process-compose -p 8888 process list       # all processes + status
+process-compose -p 8888 process logs <name> --follow
+process-compose -p 8888 attach             # TUI for running project
+
+# Process control
+process-compose -p 8888 process restart <name>
+process-compose -p 8888 process stop <name>
+process-compose -p 8888 process start <name>
+
+# Reload config without stopping (hot update)
+process-compose -p 8888 project update -f config.yaml
+
+# Standalone inspection (no running PC)
+process-compose info                       # config home info
+process-compose graph -f config.yaml       # dependency graph
+process-compose analyze -f config.yaml     # startup timing analysis
+```
+
+**Key flag gotcha:** there's no `--detached` flag. To run in background:
+- Linux/Mac: `process-compose up -t=false &` (shell backgrounding)
+- Windows: launch via Task Scheduler or `Start-Process` with `-WindowStyle Hidden`
+
+## TUI Navigation
+
+Launch: `process-compose attach` (or `up` without `-t=false`).
+
+| Key | Action |
+|---|---|
+| `↑` `↓` or `j` `k` | Navigate process list |
+| `Tab` | Switch focus between process list and log pane |
+| `F4` | Maximize current pane (toggle) |
+| `F5` | Unfollow logs (lets you scroll history) |
+| `F6` | Unwrap log lines |
+| `r` | Restart selected process |
+| `s` | Stop selected process |
+| `t` | Start selected process |
+| `/` | Filter process list |
+| `?` | Help overlay |
+| `q` | Quit TUI (PC keeps running in background) |
+
+## MCP Server Integration
+
+PC ships a built-in MCP server exposing processes as tools for AI agents. Enable via the config or CLI flag. With the MCP server on, a Claude Code agent can directly:
+
+- List running processes
+- Get process status/health
+- Restart/stop/start processes
+- Read process logs
+
+This replaces shell-based glue scripts (the old PM2-broker pattern).
+
+## API Port Selection
+
+Default API port is 8080. Common collisions:
+
+| Port 8080 user | Workaround |
+|---|---|
+| Dagu dashboard | Use `-p 8888` until Dagu decommissioned |
+| Tomcat / Spring Boot dev | Use `-p 8888` |
+| Other dev tool defaults | Pick anything free in 8000–9999 range |
+
+If you change the API port, every subsequent CLI call needs `-p <port>`:
+
+```bash
+process-compose -p 8888 process list
+process-compose -p 8888 process logs axiom --follow
+```
+
+## Windows Boot Persistence Pattern
+
+Task Scheduler runs with minimal PATH. Use a wrapper script that sets PATH explicitly before launching PC.
+
+```powershell
+# scripts/boot-start.ps1
+$root = "X:\00_Orchestration\compose-portless"
+$pcExe = "$root\bin\process-compose.exe"
+
+# Explicit PATH for managed services (Python, uv, Git tools, cloudflared, etc.)
+$env:PATH = (@(
+    "$root\bin"
+    "C:\Program Files\Git\usr\bin"          # openssl, bash
+    "C:\Users\<user>\AppData\Local\Programs\Python\Python313\Scripts"
+    "$env:PATH"
+) -join ';')
+
+# Optional: source secrets from gitignored .env
+$envFile = "$root\.env"
+if (Test-Path $envFile) {
+    Get-Content $envFile | ForEach-Object {
+        if ($_ -match '^\s*([A-Z_]+)\s*=\s*(.+?)\s*$') {
+            [Environment]::SetEnvironmentVariable($matches[1], $matches[2], 'Process')
+        }
+    }
+}
+
+# Launch headless
+& $pcExe -p 8888 -t=false -L "$root\logs\process-compose.log" up -f "$root\process-compose.yaml"
+```
+
+Register as a Task Scheduler entry with `LogonType S4U` (runs at boot, no password, no interactive logon needed):
+
+```powershell
+$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType S4U -RunLevel Highest
+$action = New-ScheduledTaskAction -Execute "powershell.exe" `
+    -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$root\scripts\boot-start.ps1`""
+$trigger = New-ScheduledTaskTrigger -AtStartup
+Register-ScheduledTask -TaskName "ProcessCompose-Boot" `
+    -Action $action -Trigger $trigger -Principal $principal -Force
+```
+
+## YAML Gotchas
+
+| Gotcha | Symptom | Fix |
+|---|---|---|
+| Windows PATH with backslashes in double-quoted YAML | `yaml: found unknown escape character` | Use single quotes: `- 'PATH=C:\Program Files\Git\usr\bin;...'` |
+| `command` with quoted paths containing spaces | First arg eaten | Wrap whole command in single quotes, inner paths in double: `'"C:/Program Files/foo.exe" arg1 arg2'` |
+| Forgot `working_dir` | Process starts in PC's cwd, can't find files | Always specify absolute `working_dir` |
+| Health probe wrong port | Process restart-loops with `Not Ready` | Match `readiness_probe.http_get.port` to where the process actually binds |
+| Secrets in YAML | Committed to git | Use `environment` to pass-through; set in shell env or gitignored `.env` |
+
+## Common Operations
+
+```bash
+# Validate config before applying
+process-compose up --dry-run -f process-compose.yaml
+
+# Hot-reload after editing config
+process-compose -p 8888 project update -f process-compose.yaml
+
+# Restart one service after code change
+process-compose -p 8888 process restart axiom
+
+# Watch logs of a misbehaving service
+process-compose -p 8888 process logs axiom --follow
+
+# Stop one service temporarily for debugging
+process-compose -p 8888 process stop axiom
+# Now run it manually with your debugger, then:
+process-compose -p 8888 process start axiom
+```
+
+## When to Use Process Compose vs Alternatives
+
+| Need | Tool |
+|---|---|
+| Local non-containerized services with health/dependencies/MCP | **Process Compose** |
+| Production node.js process supervision | PM2 (despite age) |
+| Container-based stack | Docker Compose |
+| Job queue with cron + DAGs | Dagu, Temporal, Airflow |
+| System service supervision | systemd (Linux), Windows Services |
+| One-shot Procfile run | Foreman / Overmind / Hivemind (Unix-only) |
+
+## Worked Example
+
+See `X:\00_Orchestration\compose-portless\` for an 11-process production stack:
+- `process-compose.yaml` — health-checked services with depends_on chains
+- `scripts/boot-start.ps1` — PATH-aware boot wrapper
+- `docs/MIGRATION-LOG.md` — full migration from PM2 + Caddy, every gotcha documented
+- `docs/SUPPLY-CHAIN.md` — binary verification procedure
+
+## Anti-Patterns
+
+```
+BAD:  process-compose up --detached       # flag does not exist
+GOOD: process-compose up -t=false &       # background via shell
+
+BAD:  put secrets in process-compose.yaml (commits to git)
+GOOD: source from gitignored .env in boot wrapper
+
+BAD:  use API port 8080 (clashes with Dagu, Tomcat, others)
+GOOD: -p 8888 (or any free port), document the choice
+
+BAD:  ignore readiness_probe and just hope services come up
+GOOD: configure http_get probe on a real endpoint; depends_on uses process_healthy
+
+BAD:  upgrade PC by running an installer (npm install -g, scoop install, brew install)
+GOOD: download specific version, verify SHA-256 against upstream checksums.txt, commit binary
+```
+
+## Related Skills
+
+- `portless-ops` — the routing layer we pair with PC (replaces Caddy)
+- `docker-ops` — container alternative for the same role
+- `mcp-ops` — PC's MCP server fits this ecosystem
+- `cli-ops` — general CLI tool patterns