Case is a local development process supervisor. It starts related services in dependency order, waits for readiness, restarts services when their source files change, and keeps status and logs together in a terminal UI.
Install with go install github.com/ChristianSch/case/cmd/casectl@latest.
- Long-running services and one-shot setup tasks
- Dependency conditions:
started,healthy, andcompleted - Recursive file watching with doublestar globs and per-service debounce
- Graceful process-group replacement without orphaning
go run,uv, or shell children - TCP, HTTP, and command health checks
- Ongoing degradation and recovery reporting after initial readiness
- Bounded crash restart with exponential backoff
- Colored Bubble Tea interface using Bubbles table and viewport components
- Headless output for pipes, CI, and
casectl up --no-ui - Strict Viper decoding and govalid field validation
Case currently targets Unix systems because process supervision relies on Unix process groups and signals.
go build -o casectl ./cmd/casectl
./casectl helpDuring development:
go run ./cmd/casectl validate -f examples/hiro.yaml
go test -race ./...Create case.yaml in a project root:
version: 1
project: example
services:
database:
run: docker compose up postgres
health:
command: pg_isready -h localhost -p 5432
interval: 2s
timeout: 30s
migrations:
type: task
run: ./scripts/migrate
depends_on:
database: healthy
api:
run: go run ./cmd/api
watch:
- "**/*.go"
ignore:
- vendor/**
depends_on:
migrations: completed
health:
tcp: localhost:8080
worker:
path: worker
run: uv run python -m worker
watch:
- "**/*.py"
- pyproject.toml
depends_on:
database: healthyValidate and start it:
casectl validate
casectl graph
casectl upCase searches the current directory and its parents for case.yaml. Pass another file explicitly with -f.
Case uses Bubble Tea, Bubbles, and Lip Gloss rather than implementing terminal mechanics itself.
| Key | Action |
|---|---|
↑ / ↓, j / k |
Select a service |
Tab / Shift+Tab |
Select next or previous log tab |
r |
Gracefully restart the selected service |
/ |
Search the selected service's logs |
n / N |
Jump to the next or previous search match |
Esc |
Cancel search input or clear the active search |
PgUp / PgDown, Ctrl+U / Ctrl+D |
Scroll logs |
q, Ctrl+C |
Shut down and exit |
Status, stderr, supervisor messages, reloads, and failures use distinct colors. The status text and symbols remain visible alongside color.
When a service exhausts its restart attempts, Case restores the primary screen and prints the last 40 captured lines for every service currently in failed or backoff state. This keeps the active failure context visible after the TUI exits.
When stdout is not a terminal, Case automatically uses line-oriented output. Force that mode with:
casectl up --no-uisettings:
shutdown_timeout: 5s # SIGTERM grace period before SIGKILL
debounce: 250ms # file-event coalescing window
log_history: 2000 # retained lines per service| Field | Meaning |
|---|---|
type |
service (default) or one-shot task |
path |
Working directory relative to case.yaml |
run |
Required shell command |
env |
Environment overrides |
watch |
Reload-triggering doublestar globs relative to path |
ignore |
Globs excluded from reloads |
depends_on |
Map of service names to dependency conditions |
health |
One TCP, HTTP, or command readiness check |
restart |
Crash restart policy and backoff |
started: the upstream process has started.healthy: its readiness check currently passes.completed: the upstream one-shot task exited successfully.
Case validates unknown services, incompatible conditions, and dependency cycles before starting anything.
A dependency becoming degraded does not restart an already-running dependent. It only prevents waiting dependents from starting until the dependency recovers.
Configure exactly one transport:
health:
tcp: localhost:8080
interval: 1s
timeout: 30s
request_timeout: 1s
failure_threshold: 3health:
http: http://localhost:8080/health
headers:
Authorization: Bearer local-tokenhealth:
command: test -f /tmp/service-readyThe startup timeout applies until first readiness. Afterwards, Case continues polling and marks the service degraded after failure_threshold consecutive failures.
restart:
policy: on-failure # no, on-failure, or always
max_attempts: 5
backoff: 1sBackoff doubles after each crash and is capped at 32 times the configured base delay. Set max_attempts: 0 to disable crash retries explicitly. When omitted, it defaults to 5. Tasks default to policy: no; services default to on-failure.
casectl up [--no-ui] [-f case.yaml]
casectl validate [-f case.yaml]
casectl graph [-f case.yaml] [--dot]
casectl help
casectl graph --dot emits Graphviz DOT for external visualization.
internal/config: Viper loading, exact unmarshalling, govalid schema checksinternal/graph: cross-service references, condition compatibility, cycle detectioninternal/coordinator: single-owner lifecycle state machine and dependency schedulerinternal/supervisor: Unix process groups, graceful shutdown, stdout/stderr captureinternal/watch: fsnotify recursion, doublestar matching, debounceinternal/health: TCP, HTTP, and command probesinternal/logs: bounded history and live subscriptionsinternal/ui: Bubble Tea model composed from Bubbles table and viewport
Asynchronous process, probe, and watcher results carry a process generation number. Results from an old generation are ignored after a reload, preventing stale exits or probes from mutating the replacement process.
Case is licensed under the PolyForm Noncommercial License 1.0.0.
