Skip to content

ChristianSch/case

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Case

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.

Case terminal UI showing service status and logs

Installation

Install with go install github.com/ChristianSch/case/cmd/casectl@latest.

Features

  • Long-running services and one-shot setup tasks
  • Dependency conditions: started, healthy, and completed
  • 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

Build

Case currently targets Unix systems because process supervision relies on Unix process groups and signals.

go build -o casectl ./cmd/casectl
./casectl help

During development:

go run ./cmd/casectl validate -f examples/hiro.yaml
go test -race ./...

Quick start

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: healthy

Validate and start it:

casectl validate
casectl graph
casectl up

Case searches the current directory and its parents for case.yaml. Pass another file explicitly with -f.

Terminal UI

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-ui

Manifest reference

Project settings

settings:
  shutdown_timeout: 5s # SIGTERM grace period before SIGKILL
  debounce: 250ms      # file-event coalescing window
  log_history: 2000    # retained lines per service

Service fields

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

Dependency conditions

  • 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.

Health checks

Configure exactly one transport:

health:
  tcp: localhost:8080
  interval: 1s
  timeout: 30s
  request_timeout: 1s
  failure_threshold: 3
health:
  http: http://localhost:8080/health
  headers:
    Authorization: Bearer local-token
health:
  command: test -f /tmp/service-ready

The startup timeout applies until first readiness. Afterwards, Case continues polling and marks the service degraded after failure_threshold consecutive failures.

Restart policy

restart:
  policy: on-failure # no, on-failure, or always
  max_attempts: 5
  backoff: 1s

Backoff 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.

Commands

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.

Architecture

  • internal/config: Viper loading, exact unmarshalling, govalid schema checks
  • internal/graph: cross-service references, condition compatibility, cycle detection
  • internal/coordinator: single-owner lifecycle state machine and dependency scheduler
  • internal/supervisor: Unix process groups, graceful shutdown, stdout/stderr capture
  • internal/watch: fsnotify recursion, doublestar matching, debounce
  • internal/health: TCP, HTTP, and command probes
  • internal/logs: bounded history and live subscriptions
  • internal/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.

License

Case is licensed under the PolyForm Noncommercial License 1.0.0.

About

Run multiple services in one command. Brings startup dependency support and autoreload with change detection.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages