process-compose-ops
Process Compose orchestration for non-containerized local services: process-compose.yaml schema, health checks, restart policies, dependencies, TUI/REST/MCP control, scheduling, and boot persistence. Use as a PM2/supervisord/Foreman replacement for local dev service management.
Install
npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/process-compose-ops
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
git clone https://github.com/0xDarkMatter/claude-mods.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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)
# 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
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
# 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) (example values — substitute your own port/dir)
process-compose -p <your-pc-port> process list # all processes + status
process-compose -p <your-pc-port> process logs <name> --follow
process-compose -p <your-pc-port> attach # TUI for running project
# Process control
process-compose -p <your-pc-port> process restart <name>
process-compose -p <your-pc-port> process stop <name>
process-compose -p <your-pc-port> process start <name>
# Reload config without stopping (hot update)
process-compose -p <your-pc-port> 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-Processwith-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 <your-pc-port> until Dagu decommissioned |
| Tomcat / Spring Boot dev | Use -p <your-pc-port> |
| Other dev tool defaults | Pick anything free in 8000–9999 range |
If you change the API port, every subsequent CLI call needs -p <port>:
process-compose -p <your-pc-port> process list
process-compose -p <your-pc-port> 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.
# scripts/boot-start.ps1
$root = "<your-process-compose-dir>"
$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 <your-pc-port> -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):
$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
# Validate config before applying
process-compose up --dry-run -f process-compose.yaml
# Hot-reload after editing config
process-compose -p <your-pc-port> project update -f process-compose.yaml
# Restart one service after code change
process-compose -p <your-pc-port> process restart axiom
# Watch logs of a misbehaving service
process-compose -p <your-pc-port> process logs axiom --follow
# Stop one service temporarily for debugging
process-compose -p <your-pc-port> process stop axiom
# Now run it manually with your debugger, then:
process-compose -p <your-pc-port> 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 <your-process-compose-dir>\ for an 11-process production stack:
process-compose.yaml— health-checked services with depends_on chainsscripts/boot-start.ps1— PATH-aware boot wrapperdocs/MIGRATION-LOG.md— full migration from PM2 + Caddy, every gotcha documenteddocs/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 <your-pc-port> (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
Resources in this skill
references/
schema-reference.md— full process-compose.yaml schema with field semantics, defaults, and command-quoting gotchasprobe-patterns.md— readiness probe recipes by stack (Python, Go, Node, TCP-only, daemons)dependency-patterns.md—depends_onpatterns: companion daemons, DB-before-app, tunnel-after-service, one-shot inittui-shortcuts.md— TUI cheatsheet (keys, status legend, search/sort/filter)boot-persistence-windows.md— Task Scheduler setup with S4U logon, PATH-aware wrappersupply-chain-verification.md— full SHA-256 verification procedure for the binary
scripts/
install-process-compose.ps1— download + verify + extract a pinned version, writes VERIFICATION.mdverify-binary.ps1— re-verify committed binary hash (monthly / pre-commit)boot-start.template.ps1— PATH-aware boot wrapper (copy + adapt per machine)boot-task-install.template.ps1— Task Scheduler entry registration (S4U logon)
assets/
python-uvicorn.yaml— uvicorn/FastAPI/Django basic service templatedjango-with-companions.yaml— Django + queue daemon + audit watcher chaingo-binary-service.yaml— Go binary with HTTP or TCP probetunnel-with-dependency.yaml— Cloudflare tunnel waiting on its target servicecron-job.yaml— scheduled task patterns
Related Skills
portless-ops— the routing layer we pair with PC (replaces Caddy)docker-ops— container alternative for the same rolemcp-ops— PC's MCP server fits this ecosystemcli-ops— general CLI tool patterns
Files (claude-mods)
-
assets
-
cron-job.yaml 968 B
# process-compose.yaml — Scheduled (cron) job # # Pattern: Periodic batch task that PC runs on schedule, not as a daemon. version: "0.5" processes: daily-backup: command: "python scripts/backup.py --target s3://my-bucket" working_dir: "X:/path/to/scripts" environment: - "PYTHONUNBUFFERED=1" # AWS creds, etc., from boot-start .env loading availability: restart: exit_on_failure # Stop the whole project if this fails schedule: "0 2 * * *" # cron syntax: 02:00 every day # Or interval-based: # schedule: "@every 6h" log_location: "logs/daily-backup.log" hourly-cleanup: command: "python scripts/cleanup.py" working_dir: "X:/path/to/scripts" environment: - "PYTHONUNBUFFERED=1" availability: restart: no # One-shot per schedule run schedule: "0 * * * *" # Every hour on the hour log_location: "logs/hourly-cleanup.log" -
django-with-companions.yaml 2.6 KB
# process-compose.yaml — Django web app + queue daemon + audit watcher # # Pattern: A main HTTP service with two long-running companion daemons # that depend on the main service being up. # # Notes: # - depends_on ensures startup ordering, not runtime coupling. # If the web app restarts, the daemons keep running. # - The audit watcher uses Git Bash for a wrapper script that needs # coreutils on PATH (single-quoted YAML to escape backslashes). # - The daemon enforces an OAuth-only policy: ANTHROPIC_API_KEY must # be unset (handled by the boot-start wrapper). version: "0.5" processes: webapp: command: "uv run python manage.py serve --host 127.0.0.1 --port 8000 --no-reload" working_dir: "X:/path/to/myapp" environment: - "DJANGO_SETTINGS_MODULE=myapp.settings.local" - "PYTHONUNBUFFERED=1" readiness_probe: http_get: host: localhost port: 8000 path: / initial_delay_seconds: 30 # Django w/ migrations is slow to come up period_seconds: 15 timeout_seconds: 5 failure_threshold: 3 availability: restart: always backoff_seconds: 5 max_restarts: 20 log_location: "logs/webapp.log" webapp-daemon: command: "uv run python -m myapp.worker.daemon-start" working_dir: "X:/path/to/myapp" environment: - "PYTHONUNBUFFERED=1" # NOTE: ANTHROPIC_API_KEY must be UNSET — daemon enforces OAuth-only. # The boot-start wrapper handles this; for manual runs, unset it first. depends_on: webapp: condition: process_started # Just needs webapp's pid to exist; # daemon polls webapp itself for readiness availability: restart: always backoff_seconds: 5 max_restarts: 20 shutdown: signal: 15 # SIGTERM timeout_seconds: 35 # Allow daemon's 30s graceful shutdown_grace_s log_location: "logs/webapp-daemon.log" webapp-feedback: # Bash wrapper script — paths in single quotes to avoid YAML escape interpretation command: '"C:/Program Files/Git/usr/bin/bash.exe" --login X:/path/to/myapp/scripts/feedback-wrapper.sh' working_dir: "X:/path/to/myapp" environment: - "PYTHONUNBUFFERED=1" # Belt-and-braces PATH for the bash wrapper: - 'PATH=C:\Program Files\Git\usr\bin;C:\Program Files\Git\bin;C:\Users\me\AppData\Local\Programs\Python\Python313\Scripts;C:\Windows\System32' depends_on: webapp: condition: process_started availability: restart: always backoff_seconds: 15 max_restarts: 20 log_location: "logs/webapp-feedback.log" -
go-binary-service.yaml 848 B
# process-compose.yaml — Go binary service # # Pattern: Fast-startup Go service with TCP probe. # Use HTTP probe if it serves HTTP; tcp_socket is the fallback for # protocols that aren't HTTP. version: "0.5" processes: mysvc: command: "X:/path/to/mysvc/bin/mysvc.exe serve" working_dir: "X:/path/to/mysvc" readiness_probe: http_get: # Use http_get if the service is HTTP host: localhost port: 8080 path: / # OR for non-HTTP protocols: # tcp_socket: # host: localhost # port: 5432 initial_delay_seconds: 1 # Go services usually come up in < 1s period_seconds: 5 timeout_seconds: 3 failure_threshold: 3 availability: restart: always backoff_seconds: 10 max_restarts: 10 log_location: "logs/mysvc.log" -
python-uvicorn.yaml 1.1 KB
# process-compose.yaml — Python uvicorn/FastAPI/Django service # # Pattern: Python web service with health endpoint, always-restart, dedicated log file. # Copy + adapt to your stack. version: "0.5" processes: myapp: # Wrap pythonw exe path in single quotes if it contains spaces; use forward # slashes inside or single-quote backslash-paths to avoid YAML escapes. command: "pythonw -m uvicorn myapp.main:app --host 127.0.0.1 --port 8000" working_dir: "X:/path/to/myapp" environment: - "PYTHONUNBUFFERED=1" - "DJANGO_SETTINGS_MODULE=myapp.settings.local" # Don't put secrets here — source from gitignored .env via boot-start wrapper readiness_probe: http_get: host: localhost port: 8000 path: / # bare root if no /health endpoint; redirects count as healthy initial_delay_seconds: 5 period_seconds: 10 timeout_seconds: 3 failure_threshold: 3 availability: restart: always backoff_seconds: 5 max_restarts: 20 log_location: "logs/myapp.log" -
tunnel-with-dependency.yaml 1.5 KB
# process-compose.yaml — Cloudflare tunnel exposing a local service # # Pattern: A web service + a Cloudflare tunnel that forwards external # traffic to it. The tunnel must depend on the service being HEALTHY, # not just started, to avoid Cloudflare seeing repeated connection # refused errors during service warmup. version: "0.5" processes: internal-svc: command: "myservice serve --port 8000" working_dir: "X:/path/to/myservice" readiness_probe: http_get: host: localhost port: 8000 path: / initial_delay_seconds: 5 period_seconds: 15 timeout_seconds: 3 failure_threshold: 3 availability: restart: always backoff_seconds: 3 max_restarts: 20 log_location: "logs/internal-svc.log" tunnel: # Single-quoted YAML string with embedded paths-with-spaces. # Tunnel UUID + cert paths should come from a gitignored .env. command: '"C:/Program Files (x86)/cloudflared/cloudflared.exe" tunnel --origincert C:/Users/me/.cloudflared/cert.pem --credentials-file C:/Users/me/.cloudflared/<TUNNEL_UUID>.json run --url http://localhost:8000 my-tunnel' working_dir: "C:/Users/me/.cloudflared" depends_on: internal-svc: condition: process_healthy # Critical: don't open tunnel until service is ready availability: restart: always backoff_seconds: 5 max_restarts: 50 # Tunnels can disconnect; allow many retries log_location: "logs/tunnel.log"
-
-
references
-
boot-persistence-windows.md 7 KB
# Boot Persistence on Windows Process Compose has no built-in `service install` (unlike portless). On Windows, register a Task Scheduler entry. ## Key Constraints 1. Task Scheduler runs with a **minimal PATH** — Python, uv, Git tools, custom binaries won't be found unless we set PATH explicitly 2. Tasks running at boot-before-logon need **LogonType S4U** (no stored password, no interactive logon) 3. Hidden window style avoids console flash on login ## Two-File Pattern Use a wrapper script that sets the environment, then have Task Scheduler launch the wrapper. Keeps task definition simple and lets you tweak env without re-registering. ### File 1 — `boot-start.ps1` (wrapper) ```powershell <# .SYNOPSIS Boot-time launcher for Process Compose. Sets PATH and launches headless. #> [CmdletBinding()] param() $ErrorActionPreference = 'Continue' $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $root = (Resolve-Path (Join-Path $scriptDir '..')).Path $pcExe = Join-Path $root 'bin\process-compose.exe' $pcYaml = Join-Path $root 'process-compose.yaml' $logFile = Join-Path $root 'logs\process-compose.log' $bootLog = Join-Path $root 'logs\boot-start.log' New-Item -ItemType Directory -Force -Path (Join-Path $root 'logs') | Out-Null "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] boot-start invoked. User: $env:USERNAME" | Out-File -FilePath $bootLog -Append # Build PATH explicitly. Tune for your machine. $pathParts = @( "$root\bin" # PC + any committed binaries "C:\Program Files\Git\usr\bin" # openssl, bash, coreutils "C:\Program Files\Git\bin" # git "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python313" # python, pythonw "C:\Users\$env:USERNAME\AppData\Local\Programs\Python\Python313\Scripts" # uv, pip, etc. "C:\Program Files (x86)\cloudflared" # optional: cloudflared "C:\Windows\System32" "C:\Windows" $env:PATH ) $env:PATH = ($pathParts -join ';') # Optional: load secrets from gitignored .env (e.g. API keys) $envFile = Join-Path $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') } } } # Ensure incompatible env vars are unset (example: OAuth-only services that # refuse to start with stale API keys) # [Environment]::SetEnvironmentVariable('SOME_API_KEY', $null, 'Process') "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] Starting process-compose..." | Out-File -FilePath $bootLog -Append # -p 8888 API port (pick something free, avoid 8080 if you have other tools there) # -t=false no TUI (headless daemon mode) # -L PC's own log file & $pcExe -p 8888 -t=false -L $logFile up -f $pcYaml "[$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssK')] process-compose exited code $LASTEXITCODE" | Out-File -FilePath $bootLog -Append ``` ### File 2 — `boot-task-install.ps1` (registers the task) ```powershell [CmdletBinding()] param() $ErrorActionPreference = 'Stop' # Must be admin to create scheduled tasks $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw "Run as Administrator." } $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $root = (Resolve-Path (Join-Path $scriptDir '..')).Path $bootScript = Join-Path $scriptDir 'boot-start.ps1' $taskName = "ProcessCompose-MyStack" # rename per project # Idempotent: remove existing if present Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false $action = New-ScheduledTaskAction ` -Execute "powershell.exe" ` -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$bootScript`"" ` -WorkingDirectory $root $trigger = New-ScheduledTaskTrigger -AtStartup $settings = New-ScheduledTaskSettingsSet ` -ExecutionTimeLimit (New-TimeSpan -Seconds 0) ` -AllowStartIfOnBatteries ` -DontStopIfGoingOnBatteries ` -RestartCount 3 ` -RestartInterval (New-TimeSpan -Minutes 1) # S4U: run at boot as user without interactive logon or stored password $taskPrincipal = New-ScheduledTaskPrincipal ` -UserId $env:USERNAME ` -LogonType S4U ` -RunLevel Highest Register-ScheduledTask -TaskName $taskName ` -Action $action -Trigger $trigger -Settings $settings -Principal $taskPrincipal ` -Description "Starts Process Compose at boot." ``` ## LogonType Trade-offs | LogonType | Runs at boot before logon? | Needs password? | Capability | |---|---|---|---| | `Interactive` | No — waits for user logon | No | Full user context (UI, network shares) | | `S4U` | Yes | No | User context but no UI, no network shares | | `Password` | Yes | Yes (stored encrypted) | Full user context | | `ServiceAccount` | Yes (as Local System / Network Service) | No | Limited to service account perms — typically can't read user files | For Process Compose managing user-scoped dev services, **S4U** is usually the right choice: services run as the user (can read `C:\Users\<user>\...`) without requiring an interactive logon. ## Verify After Registration ```powershell # Check task exists Get-ScheduledTask -TaskName "ProcessCompose-MyStack" | Format-List TaskName, State, Triggers, Principal # Manually run the task to test before reboot Start-ScheduledTask -TaskName "ProcessCompose-MyStack" # Wait, then check PC is up Start-Sleep -Seconds 10 process-compose -p 8888 process list ``` ## Troubleshooting Boot Failures After a reboot, if services don't come up: 1. **Check the boot log:** `<root>/logs/boot-start.log` — confirm the wrapper actually ran 2. **Check PC's log:** `<root>/logs/process-compose.log` — confirm PC started and look for process-spawn errors 3. **Check Task Scheduler history:** Right-click the task → History tab. Look for failure reasons. 4. **Reproduce manually:** open elevated PS, run `.\scripts\boot-start.ps1` and watch what happens. Common failures: - PATH missing a tool → add to `pathParts` array - Working dir not absolute → ensure all paths in `process-compose.yaml` are absolute - Secrets not loaded → `.env` file not in expected location - Port collision (PC API port 8888 occupied) → check `netstat -ano | findstr :8888` ## Pair with portless service install portless has its own boot task. The two are independent — register both: ```powershell portless service install # registers portless's task .\scripts\boot-task-install.ps1 # registers PC's task # Verify both Get-ScheduledTask | Where-Object { $_.TaskName -like "*ortless*" -or $_.TaskName -like "*ompose*" } ``` ## Uninstall ```powershell # In the same script: Get-ScheduledTask -TaskName "ProcessCompose-MyStack" -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm:$false portless service uninstall ``` -
dependency-patterns.md 4.9 KB
# Dependency Patterns (depends_on) How to express startup ordering and runtime dependencies between processes. ## The Four Conditions | Condition | Meaning | Best for | |---|---|---| | `process_started` | Dependency has spawned (PID exists, may not be ready) | Coarse ordering when readiness doesn't matter | | `process_healthy` | Dependency's `readiness_probe` passes | Runtime services that must be queryable | | `process_completed` | Dependency exited (any code) | One-shot tasks that may fail | | `process_completed_successfully` | Dependency exited with code 0 | One-shot init that must succeed | ## Pattern 1 — Web app + companion daemon A common pattern: a web service + a worker daemon that talks to the same DB or queue. Daemon should start AFTER the web app has its DB connection pool warm. ```yaml processes: webapp: command: "uv run python manage.py serve --port 8000" working_dir: "D:/code/MyApp" readiness_probe: http_get: { host: localhost, port: 8000, path: / } initial_delay_seconds: 10 availability: { restart: always } worker: command: "uv run python -m myapp.worker" working_dir: "D:/code/MyApp" depends_on: webapp: condition: process_healthy availability: { restart: always } ``` Result: `worker` doesn't start until `webapp`'s readiness probe passes. If `webapp` restarts, `worker` keeps running (depends_on is a startup ordering rule, not a runtime tether). ## Pattern 2 — Three-tier chain Web app + background daemon + audit watcher (Axiom pattern): ```yaml processes: app: command: "..." readiness_probe: { ... } app-daemon: command: "..." depends_on: app: condition: process_healthy app-feedback: command: "..." depends_on: app: condition: process_started # weaker — just needs app's pid to exist ``` ## Pattern 3 — Database before app Postgres in the same PC stack, app depends on it: ```yaml processes: postgres: command: "postgres -D /var/lib/pg" readiness_probe: tcp_socket: { host: localhost, port: 5432 } initial_delay_seconds: 3 migrate: command: "alembic upgrade head" working_dir: "X:/MyApp" depends_on: postgres: condition: process_healthy availability: restart: exit_on_failure # one-shot; if it fails, the whole stack fails app: command: "uvicorn main:app" working_dir: "X:/MyApp" depends_on: migrate: condition: process_completed_successfully postgres: condition: process_healthy availability: { restart: always } ``` `migrate` runs once, must succeed. `app` waits for both `migrate` (success) and `postgres` (healthy). ## Pattern 4 — Tunnel that depends on the service it tunnels E.g. Cloudflare tunnel exposing a local service: ```yaml processes: mcp-server: command: "fastmcp serve --port 8000" readiness_probe: http_get: { host: localhost, port: 8000, path: / } initial_delay_seconds: 5 mcp-tunnel: command: '"C:/Program Files/cloudflared/cloudflared.exe" tunnel run my-tunnel' depends_on: mcp-server: condition: process_healthy # don't open tunnel until server is ready availability: restart: always backoff_seconds: 5 max_restarts: 50 # tunnels can disconnect, allow many retries ``` ## Pattern 5 — Static (one-time) setup task ```yaml processes: fetch-secrets: command: "python scripts/fetch_secrets.py" availability: restart: exit_on_failure # must complete; stop the project if it fails # No readiness_probe — task either completes or doesn't app: command: "..." depends_on: fetch-secrets: condition: process_completed_successfully ``` ## Cycle Detection PC detects cycles at startup. This fails immediately: ```yaml processes: a: { depends_on: { b: { condition: process_started } } } b: { depends_on: { a: { condition: process_started } } } # Error: dependency cycle detected: a -> b -> a ``` ## What `depends_on` Does NOT Do - **Does not** restart dependents when a dependency restarts. If `webapp` crashes and recovers, `worker` doesn't automatically restart. - **Does not** stop a dependent when the dependency stops. You'll need to model this with `restart: exit_on_failure` and probes. - **Does not** enforce shutdown order (PC shuts down in any order unless `--ordered-shutdown` flag is used). For runtime coupling, the dependent process needs application-level reconnect/retry logic. ## Shutdown Ordering By default PC shuts processes down in any order. For services with stateful deps, use: ```bash process-compose down --ordered-shutdown # Stops in reverse dependency order: dependents first, then dependencies ``` ## See Also - `probe-patterns.md` for crafting good `readiness_probe`s (without these, `process_healthy` is useless) - `schema-reference.md` for full availability/shutdown field semantics -
probe-patterns.md 4 KB
# Readiness Probe Patterns Concrete probe recipes by stack. Used in `process-compose.yaml`'s `readiness_probe` field. ## Python web servers (Django, Flask, FastAPI) ### Has a health endpoint ```yaml readiness_probe: http_get: host: localhost port: 8000 path: /health/ initial_delay_seconds: 5 period_seconds: 10 timeout_seconds: 3 failure_threshold: 3 ``` ### No health endpoint (use any 200-returning path) ```yaml readiness_probe: http_get: host: localhost port: 8000 path: / # bare root; whatever returns 200 initial_delay_seconds: 10 # Django often takes 5-15s to come up period_seconds: 10 failure_threshold: 3 ``` ### Auth-required app If `/` returns 302 redirecting to login, that's still healthy (server is up). Probe a path that handles redirects: ```yaml readiness_probe: http_get: host: localhost port: 8000 path: / # PC follows redirects; 200/302/301 all count as healthy initial_delay_seconds: 10 ``` ### Long-running startup (DB migrations, model loading) ```yaml readiness_probe: http_get: host: localhost port: 8000 path: /ready initial_delay_seconds: 30 # give Django apps with migrations ~30s period_seconds: 15 timeout_seconds: 5 failure_threshold: 5 # tolerant during warmup availability: restart: always backoff_seconds: 10 # longer backoff matching startup cost ``` ## Go binaries Most Go web servers come up in < 1 second: ```yaml readiness_probe: http_get: host: localhost port: 8080 path: / initial_delay_seconds: 1 period_seconds: 5 failure_threshold: 3 ``` ## Node.js / Express / Next.js ```yaml readiness_probe: http_get: host: localhost port: 3000 path: / initial_delay_seconds: 5 # Next.js cold start period_seconds: 10 timeout_seconds: 3 ``` For dev servers (`next dev`), the initial route may not be ready immediately. Use a known static asset path or `_next/static/` if the home route is dynamic. ## Static file servers (`python -m http.server`) ```yaml readiness_probe: http_get: host: localhost port: 8000 path: / initial_delay_seconds: 1 period_seconds: 5 ``` ## TCP-only services (databases, message queues, custom protocols) ```yaml readiness_probe: tcp_socket: host: localhost port: 5432 initial_delay_seconds: 5 period_seconds: 10 failure_threshold: 3 ``` ## Stuff that doesn't expose ports (daemons, watchers, cron-like) Use `exec` probe with a custom check, or skip probes entirely: ```yaml # Option A — exec probe checks daemon's pid file or self-reported status readiness_probe: exec: command: "test -f /var/run/myd.pid" initial_delay_seconds: 5 period_seconds: 30 # Option B — no probe at all; depends_on only uses process_started # (no readiness_probe block, just availability config) availability: restart: always ``` ## When the probe is failing — debugging 1. **Check the actual port:** does the service really bind that port? `netstat -ano | grep :8000` (Linux/Mac: `lsof -i :8000`) 2. **Check the actual path:** does `curl -i http://localhost:8000/health` return 2xx/3xx? PC follows redirects but doesn't accept 4xx/5xx as healthy. 3. **Check initial_delay_seconds:** does the service take longer than this to come up? 4. **Check failure_threshold:** is the service flaky, returning 5xx intermittently? ## Anti-patterns ```yaml # BAD: probing a path that requires auth and returns 401 readiness_probe: http_get: { port: 8000, path: /api/users/me } # BAD: probing a path that 404s during startup but eventually 200s # (the probe sees 404 → marks Not Ready → never recovers) readiness_probe: http_get: { port: 8000, path: /admin/some-resource } # BAD: zero initial_delay_seconds — guarantees first probe sees connection refused readiness_probe: http_get: { port: 8000, path: / } initial_delay_seconds: 0 # don't do this ``` ## See Also - `dependency-patterns.md` for using readiness probes with `depends_on` - Upstream docs: https://f1bonacc1.github.io/process-compose/health/ -
schema-reference.md 6 KB
# process-compose.yaml Schema Reference Comprehensive reference for the YAML schema (`version: "0.5"`). Annotated with field semantics, defaults, and gotchas. ## Top-level ```yaml version: "0.5" # required; current schema version log_level: info # debug | info | warn | error (default: info) log_length: 1000 # lines retained in TUI's in-memory log buffer log_no_color: false # disable ANSI colour in log file log_timestamps: true # prepend ISO-8601 timestamp to each line log_truncate: false # truncate logs on startup instead of appending processes: # required; map of process-name → spec <name>: ... # see Process Spec below environment: # OPTIONAL global env vars applied to every process - "GLOBAL_VAR=value" ``` ## Process Spec ### Required ```yaml command: "string" # shell command to execute (no shell interpolation # unless explicitly wrapped — see "command quoting" # below) ``` ### Recommended ```yaml working_dir: "/abs/path" # cwd; otherwise inherits PC's cwd environment: # array of KEY=value strings - "ENV_VAR=value" - "PYTHONUNBUFFERED=1" ``` ### Lifecycle ```yaml availability: restart: always # always | exit_on_failure | on_failure | no backoff_seconds: 5 # delay before next restart attempt max_restarts: 20 # absolute cap; 0 = unlimited exit_on_skipped: false # treat skipped-by-dep failure as exit schedule: "0 2 * * *" # cron schedule for periodic execution # (mutually exclusive with restart: always) shutdown: signal: 15 # SIGTERM (default) or 9 for SIGKILL timeout_seconds: 30 # SIGKILL escalation deadline command: "graceful-cli stop" # optional pre-stop command ``` ### Health checks ```yaml readiness_probe: # marks process "Ready" for depends_on http_get: host: localhost port: 8000 path: /health # bare "/" is fine if no health endpoint scheme: HTTP # HTTP (default) or HTTPS # OR alternative probe types: # exec: # command: "curl -f http://localhost:8000/health" # tcp_socket: # host: localhost # port: 8000 initial_delay_seconds: 5 # wait before first probe period_seconds: 10 # interval between probes timeout_seconds: 3 # per-probe timeout success_threshold: 1 # consecutive successes to mark Ready failure_threshold: 3 # consecutive failures to mark Not Ready liveness_probe: # restarts process if probe fails ... # same shape as readiness_probe ``` ### Dependencies ```yaml depends_on: database: condition: process_healthy # wait until database's readiness_probe passes migrations: condition: process_completed_successfully # wait for one-shot init ``` Conditions: | Condition | Wait until... | Use when | |---|---|---| | `process_started` | Dependency spawned (PID exists) | Weakest; use only when ordering matters but readiness doesn't | | `process_healthy` | Dependency's readiness_probe passes | Strongest; preferred for runtime services | | `process_completed` | Dependency exited (any code) | One-shot init that may fail | | `process_completed_successfully` | Dependency exited 0 | One-shot init that must succeed | ### Logging ```yaml log_location: "logs/myapp.log" # relative to PC's cwd or absolute path log_max_size_kb: 0 # rotation threshold; 0 = no rotation log_max_backups: 0 # rotated files to retain log_max_age_days: 0 # age-based rotation log_compress: false # gzip rotated logs ``` ### Identity / grouping ```yaml namespace: "backend" # group processes; --namespace flag filters by it replicas: 3 # spawn N independent copies (named myapp@0, @1, @2) disabled: false # exclude from `up` without deleting the spec is_daemon: false # set true for processes that fork and exit # (e.g. systemd-style daemons) ``` ### Visibility ```yaml is_foreground: false # show full output in TUI immediately is_tty: false # allocate a PTY (interactive processes) disable_ansi_colors: false # strip ANSI from this process's logs ``` ## Command Quoting YAML loves to surprise here. Three reliable patterns: ```yaml # Pattern 1 — simple command, no quotes anywhere command: pythonw manage.py runserver 0.0.0.0:8000 # Pattern 2 — single-quoted (literal, no escapes processed) command: 'pythonw "C:\Program Files\foo\app.py" --port 8000' # Pattern 3 — double-quoted (escapes processed, watch the backslashes) command: "pythonw -m my_module" # Pattern 4 — wrap the exe path in double quotes inside a single-quoted string command: '"C:/Program Files/Git/usr/bin/bash.exe" --login script.sh' ``` **Windows PATH env vars must be single-quoted** to escape backslashes: ```yaml environment: # WRONG: double quotes try to interpret \P, \U, etc. as escape codes # - "PATH=C:\Program Files\Git\usr\bin;..." # RIGHT: - 'PATH=C:\Program Files\Git\usr\bin;C:\Users\me\AppData\Local\Programs\Python\Python313' ``` ## File Composition You can split config across files and merge: ```bash process-compose up -f base.yaml -f overrides.yaml -f local.yaml ``` Later files override earlier ones. Useful pattern: base config in repo + per-machine overrides in gitignored `local.yaml`. ## Validation ```bash process-compose up -f process-compose.yaml --dry-run # → "Validated N configured processes from M files." ``` Run before committing. Catches: - YAML parse errors (escape issues, indentation) - Missing required fields - Invalid restart policy values - Circular depends_on chains ## Hot Reload ```bash process-compose -p 8888 project update -f process-compose.yaml ``` Reloads the config in a running PC instance. Added processes start; removed processes stop; changed processes restart. -
supply-chain-verification.md 4.1 KB
# Supply-Chain Verification for Process Compose Process Compose ships as a single Go binary via GitHub Releases with SHA-256 checksums. This is structurally safer than npm/PyPI packages but still requires verification before use. ## Why bother Even with Go's `go.sum` model: - The compiled binary is what runs, not the source — must verify the binary matches what was built from verified source - GitHub Releases artifacts can theoretically be tampered if repo permissions are compromised - Checksums file is on GitHub Releases too, so without a separate signature, you're trusting GitHub auth integrity - No GPG signing on Process Compose releases (as of v1.110.0) — relies entirely on GitHub Release access controls ## The Procedure ### 1. Download from official release ```bash VER="v1.110.0" # pin a specific tag BASE="https://github.com/F1bonacc1/process-compose/releases/download/$VER" curl -fsSL -o pc.zip "$BASE/process-compose_windows_amd64.zip" curl -fsSL -o process-compose_checksums.txt "$BASE/process-compose_checksums.txt" ``` ### 2. Verify hash BEFORE extraction ```bash EXPECTED=$(grep "process-compose_windows_amd64.zip" process-compose_checksums.txt | awk '{print $1}') ACTUAL=$(sha256sum pc.zip | awk '{print $1}') [ "$EXPECTED" = "$ACTUAL" ] || { echo "HASH MISMATCH - ABORT"; exit 1; } ``` **Never** extract or run the binary before this check passes. ### 3. Extract, record the binary's own hash ```bash unzip pc.zip EXE_HASH=$(sha256sum process-compose.exe | awk '{print $1}') echo "$EXE_HASH" > bin/EXE_HASH ``` This is the hash you re-verify on future installs to confirm the binary in your repo hasn't been tampered. ### 4. Commit binary + checksums to repo ```bash git add bin/process-compose.exe \ bin/process-compose_checksums.txt \ bin/VERSION # contains "v1.110.0" git commit -m "feat: pin process-compose $VER, verified SHA-256" ``` ### 5. Document the verification Write a `bin/VERIFICATION.md`: ```markdown # Binary Verification ## process-compose.exe — v1.110.0 - Pinned: 2026-MM-DD - Source: https://github.com/F1bonacc1/process-compose/releases/tag/v1.110.0 - ZIP SHA-256: 018c660f... (matched checksums.txt) - EXE SHA-256: 2e2a09a9...858637 (recorded for re-verification) - Runtime check: "Process Compose v1.110.0, Commit cd7f6af" Trust anchor: GitHub Releases (HTTPS, requires repo write access to tamper). Limitation: No GPG signing on Process Compose releases. ``` ## Re-verification Periodically — or as part of CI — re-hash the committed binary and confirm it matches the recorded value: ```powershell # Windows $expected = (Get-Content bin/EXE_HASH).Trim() $actual = (Get-FileHash bin/process-compose.exe -Algorithm SHA256).Hash.ToLower() if ($expected -ne $actual) { throw "binary tampered" } ``` ```bash # Unix expected=$(cat bin/EXE_HASH | tr -d '[:space:]') actual=$(sha256sum bin/process-compose.exe | awk '{print $1}') [ "$expected" = "$actual" ] || { echo "binary tampered"; exit 1; } ``` ## Upgrade Procedure To bump versions: 1. Run the download + verify procedure above with the new version tag 2. Replace `bin/process-compose.exe`, `bin/process-compose_checksums.txt`, `bin/VERSION`, `bin/EXE_HASH` 3. Update `VERIFICATION.md` with new hashes + date 4. Run a parallel test (non-prod port) before cutting over 5. Single PR with all of the above; review before merge ## What's NOT Covered The verification confirms **you got the binary the project intended to publish**. It does NOT cover: - Compromise of the project's source code (would need full source audit) - Compromise of the build environment (GoReleaser + GitHub Actions infrastructure) - The Go modules the binary was compiled with (transitive dependency risk) For deeper supply-chain analysis: tools like `osv-scanner`, `govulncheck`, or commercial tools (Socket.dev, Snyk) inspect the **source** dependency tree. Use those upstream of the verification step. ## See Also - `boot-persistence-windows.md` for how to launch the verified binary at boot - The repo's own AGENTS.md should document the pinned version policy -
tui-shortcuts.md 4.5 KB
# TUI Shortcuts Cheatsheet Launch: ```bash process-compose attach # connect to the default port (8080) process-compose -p 8888 attach # connect to a non-default API port ``` If you launched PC with `-t=false` (headless), `attach` is how you bring up the TUI later. Quitting the TUI with `q` does **not** stop PC. ## Layout ``` ┌──────────────────────────────────────────────────────────────┐ │ Version + Project info │ │ Resources (RAM, CPU) │ ├──────────────────────────────────────────────────────────────┤ │ PROCESS LIST (focused by default) │ │ PID NAME NS STATUS AGE HEALTH RESTARTS EX │ │ ... │ ├──────────────────────────────────────────────────────────────┤ │ LOG PANE (logs for selected process) │ │ [timestamp] log line │ │ [timestamp] log line │ ├──────────────────────────────────────────────────────────────┤ │ F1 Shortcuts F2 Scale F3 Find F4 Maximize ... │ └──────────────────────────────────────────────────────────────┘ ``` ## Navigation | Key | Action | |---|---| | `↑` / `↓` | Move process selection up/down | | `k` / `j` | Same (vim-style) | | `Tab` | Move focus between process list and log pane | | `Home` / `End` | Jump to first / last process | | `Page Up` / `Page Down` | Page through process list | ## Pane controls | Key | Action | |---|---| | `F4` | Maximize current pane (toggle — second press un-maximizes) | | `F5` | Toggle log follow (unfollow lets you scroll history) | | `F6` | Toggle log wrap | | `Ctrl-S` | Toggle "select on" — clicks select process | ## Process control (selected process) | Key | Action | |---|---| | `r` | Restart | | `s` | Stop (graceful — SIGTERM) | | `t` | Start (if stopped) | | `Ctrl-D` | Disable the process (won't auto-restart) | | `Ctrl-E` | Re-enable a disabled process | ## Search / filter | Key | Action | |---|---| | `/` | Open filter input (filter process list by name) | | `F3` | Find in logs (search current log pane) | | `n` / `N` | Next / previous match in logs | | `Esc` | Clear filter / cancel input | ## Sorting | Key | Action | |---|---| | `Ctrl-N` | Sort by Name | | `Ctrl-T` | Sort by Status | | `Ctrl-A` | Sort by Age | | `Ctrl-H` | Sort by Health | | `R` (uppercase) | Reverse current sort | ## Status column legend | Status | Meaning | |---|---| | `Running` | Process is up | | `Ready` | `readiness_probe` passing (only shown if probe defined) | | `Not Ready` | Probe failing; PC will keep checking | | `Restarting` | Between restart attempts (`backoff_seconds`) | | `Completed` | Exited successfully (only for non-restart processes) | | `Failed` | Exited with error and out of restart budget | | `Pending` | Waiting on `depends_on` to be satisfied | | `Disabled` | Manually disabled or `disabled: true` in YAML | | `Skipped` | A dependency failed so this process was skipped | ## Exit | Key | Action | |---|---| | `q` | Quit TUI (PC keeps running headless) | | `Ctrl-C` | Same | | `?` | Show help overlay | ## Headless workflow (no TUI) If you don't want the TUI but need to peek at state: ```bash # Process list process-compose -p 8888 process list # Logs for one process process-compose -p 8888 process logs my-service --follow process-compose -p 8888 process logs my-service # one-shot, no follow # Control without TUI process-compose -p 8888 process restart my-service process-compose -p 8888 process stop my-service process-compose -p 8888 process start my-service ``` ## See Also - Schema reference for `is_foreground`, `is_tty`, `disable_ansi_colors` which affect log rendering in the TUI - Upstream docs: https://f1bonacc1.github.io/process-compose/tui/
-
-
scripts
-
boot-start.template.ps1 3.7 KB · in bundle
-
boot-task-install.template.ps1 3.3 KB · in bundle
-
install-process-compose.ps1 3.8 KB · in bundle
-
verify-binary.ps1 1.7 KB · in bundle
-
-
tests
-
run.sh 4.4 KB
#!/usr/bin/env bash # Self-test for process-compose-ops — fully offline: no network, no download. # # Wraps the skill's binary-integrity verifier (scripts/verify-binary.ps1), which # re-checks the committed process-compose.exe SHA-256 against the recorded # EXE_HASH. Unlike the three §7 staleness scripts this verifier is PowerShell and # platform-locked to a Windows binary, so the suite has two layers: # * contract / structural (run anywhere, no PowerShell host required): the .ps1 # exists and encodes the Get-FileHash/EXE_HASH compare plus the mismatch # throw — proving it is a real check, not a vacuous rubber stamp; # * behavioural happy + negative against a temp bin/ — exercised only when a # PowerShell host (pwsh | powershell) is present. A dummy .exe and a # hand-written EXE_HASH stand in for the real binary, so no download and no # network ever occur. SKIP'd (suite stays green) on a PowerShell-less Linux # runner — the structural layer still runs. # # Usage: bash tests/run.sh # Exit: 0 all pass, 1 one or more failures set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL="$(dirname "$HERE")" V="$SKILL/scripts/verify-binary.ps1" # A PowerShell host, if any (pwsh = PowerShell 7 cross-platform; powershell = # Windows PowerShell 5.1). Absent on most Linux CI runners. PSHOST="" for c in pwsh powershell; do if command -v "$c" >/dev/null 2>&1 && "$c" -NoProfile -Command 'exit 0' >/dev/null 2>&1; then PSHOST="$c"; break; fi done SB="$(mktemp -d)"; trap 'rm -rf "$SB"' EXIT PASS=0; FAIL=0 ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$1"; } no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; } expect_exit() { [[ "$2" == "$3" ]] && ok "$1 (exit $3)" || no "$1 (want $2 got $3)"; } expect_has() { case "$3" in *"$2"*) ok "$1";; *) no "$1 (missing '$2')";; esac; } echo "=== process-compose-ops self-test ===" # ── contract / structural (runs anywhere, no PowerShell host needed) ────────── echo "-- contract --" [[ -f "$V" ]] && ok "verify-binary.ps1 present" || no "verify-binary.ps1 present" [[ -s "$V" ]] && ok "verify-binary.ps1 non-empty" || no "verify-binary.ps1 non-empty" # The verifier must encode the integrity check it claims to run: a SHA-256 # compare of the binary against EXE_HASH that throws on mismatch. If these # markers were gutted the verifier would be a vacuous rubber stamp. vtxt="$(cat "$V")" expect_has "computes SHA-256 of the binary" "Get-FileHash" "$vtxt" expect_has "reads recorded EXE_HASH" "EXE_HASH" "$vtxt" expect_has "fails loud on mismatch" "MISMATCH" "$vtxt" # ── behavioural (happy + negative) under a PowerShell host, if present ───────── echo "-- behavioural (${PSHOST:-no powershell host}) --" if [[ -z "$PSHOST" ]]; then echo " SKIP happy/negative (no pwsh/powershell on this runner — structural layer above still ran)" else # happy: a dummy .exe whose recorded EXE_HASH matches its actual SHA-256. mkdir -p "$SB/ok/bin" printf 'process-compose-fake-bytes' > "$SB/ok/bin/process-compose.exe" # sha256sum and Get-FileHash compute the identical digest; the verifier # compares lowercased + trimmed, so this matches what it recomputes. sha256sum "$SB/ok/bin/process-compose.exe" | awk '{print $1}' > "$SB/ok/bin/EXE_HASH" "$PSHOST" -NoProfile -File "$V" -BinDir "$SB/ok/bin" >/dev/null 2>&1 expect_exit "matching EXE_HASH -> 0" 0 $? # negative: a recorded EXE_HASH that disagrees with the actual SHA-256. mkdir -p "$SB/bad/bin" printf 'process-compose-fake-bytes' > "$SB/bad/bin/process-compose.exe" printf '0000000000000000000000000000000000000000000000000000000000000000' > "$SB/bad/bin/EXE_HASH" "$PSHOST" -NoProfile -File "$V" -BinDir "$SB/bad/bin" >"$SB/neg.out" 2>&1 rc=$? [[ "$rc" -ne 0 ]] && ok "mismatched EXE_HASH -> nonzero (exit $rc)" || no "mismatched EXE_HASH -> nonzero (got 0)" expect_has "verifier reports MISMATCH" "MISMATCH" "$(cat "$SB/neg.out")" fi # ── SKILL.md sanity ─────────────────────────────────────────────────────────── echo "-- SKILL.md --" grep -q '^name: process-compose-ops$' "$SKILL/SKILL.md" && ok "frontmatter name" || no "frontmatter name" grep -q 'verify-binary.ps1' "$SKILL/SKILL.md" && ok "verifier cited from SKILL.md" || no "verifier cited from SKILL.md" echo "" echo "=== $PASS passed, $FAIL failed ===" [[ "$FAIL" -eq 0 ]] || exit 1 exit 0
-
-
SKILL.md 12.2 KB
--- name: process-compose-ops description: "Process Compose orchestration for non-containerized local services: process-compose.yaml schema, health checks, restart policies, dependencies, TUI/REST/MCP control, scheduling, and boot persistence. Use as a PM2/supervisord/Foreman replacement for local dev service management." 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) (example values — substitute your own port/dir) process-compose -p <your-pc-port> process list # all processes + status process-compose -p <your-pc-port> process logs <name> --follow process-compose -p <your-pc-port> attach # TUI for running project # Process control process-compose -p <your-pc-port> process restart <name> process-compose -p <your-pc-port> process stop <name> process-compose -p <your-pc-port> process start <name> # Reload config without stopping (hot update) process-compose -p <your-pc-port> 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 <your-pc-port>` until Dagu decommissioned | | Tomcat / Spring Boot dev | Use `-p <your-pc-port>` | | 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 <your-pc-port> process list process-compose -p <your-pc-port> 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 = "<your-process-compose-dir>" $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 <your-pc-port> -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 <your-pc-port> project update -f process-compose.yaml # Restart one service after code change process-compose -p <your-pc-port> process restart axiom # Watch logs of a misbehaving service process-compose -p <your-pc-port> process logs axiom --follow # Stop one service temporarily for debugging process-compose -p <your-pc-port> process stop axiom # Now run it manually with your debugger, then: process-compose -p <your-pc-port> 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 `<your-process-compose-dir>\` 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 <your-pc-port> (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 ``` ## Resources in this skill ### `references/` - `schema-reference.md` — full process-compose.yaml schema with field semantics, defaults, and command-quoting gotchas - `probe-patterns.md` — readiness probe recipes by stack (Python, Go, Node, TCP-only, daemons) - `dependency-patterns.md` — `depends_on` patterns: companion daemons, DB-before-app, tunnel-after-service, one-shot init - `tui-shortcuts.md` — TUI cheatsheet (keys, status legend, search/sort/filter) - `boot-persistence-windows.md` — Task Scheduler setup with S4U logon, PATH-aware wrapper - `supply-chain-verification.md` — full SHA-256 verification procedure for the binary ### `scripts/` - `install-process-compose.ps1` — download + verify + extract a pinned version, writes VERIFICATION.md - `verify-binary.ps1` — re-verify committed binary hash (monthly / pre-commit) - `boot-start.template.ps1` — PATH-aware boot wrapper (copy + adapt per machine) - `boot-task-install.template.ps1` — Task Scheduler entry registration (S4U logon) ### `assets/` - `python-uvicorn.yaml` — uvicorn/FastAPI/Django basic service template - `django-with-companions.yaml` — Django + queue daemon + audit watcher chain - `go-binary-service.yaml` — Go binary with HTTP or TCP probe - `tunnel-with-dependency.yaml` — Cloudflare tunnel waiting on its target service - `cron-job.yaml` — scheduled task patterns ## 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
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.