Compose Spec Alignment: Where it makes sense, we align with the Compose Specification for naming and behavior of new features.
rfc/- Contains work tasks and planning documents. Do not read unless specifically asked.
Stateless over stateful: tmux IS the state. Don't add state files. Query reality.
Idempotent operations: Every command should be safe to run multiple times. stop loops through all processes even if none are running. start checks if already running.
Let processes be independent: When one process dies, don't kill others. User decides lifecycle (like docker-compose).
- Session naming convention
{group}-{name}enables discovery via prefix filtering remain-on-exit onpreserves crash output for debuggingkill-sessionsends SIGHUP → process exits → port released- In raw mode, Ctrl+C is byte 3, not SIGINT - must handle explicitly
lsof -p PID -idoesn't filter properly on macOS with-iflag - must filter output by PID column- Process trees need recursive
pgrep -Pto find all children - Sum RSS across tree for accurate memory (child processes matter)
rig psshould be instant (~30ms) - don't collect metrics by defaultrig ps -ffor full metrics (~800ms acceptable)rig topuses smart refresh: high CPU processes refresh more often
- Uses clap with derive macros for argument parsing
- Custom help text (not clap's auto-generated help)
- POSIX
shonly, no bashisms — the script is piped viacurl | sh curlis required and checked immediately — hard error if missing (curl must exist to fetch the installer anyway)- Downloads prebuilt binary from GitHub Releases (
rig-{platform}-{arch}.tar.gz) - tmux is required (hard error if missing, no auto-install)
- watchexec is optional — installed via
curl https://webi.sh/watchexec | sh(their own installer handles all platform/arch logic); macOS prefersbrew, falls back to webi - Do not use
.tar.xzfor anything — watchexec has no.tar.gzrelease so we delegate to webi instead of handling archives ourselves - Transparency pattern: check deps → show plan → ask user → download binary
- Read user input when stdin is a pipe: redirect from
/dev/tty - Install location:
~/.local/binon Linux,/usr/local/binon macOS - Version check: skips download if already up to date
- The installer doubles as upgrader — safe to re-run
- Supported platforms: macOS (arm64, x86_64), Linux (amd64, arm64)
- Watch wraps service commands with watchexec for file-triggered restarts
- Paths in
watch.pathsresolve relative toworking_dir(same as other path fields) - Glob patterns need shell quoting (
shellQuote()) to prevent shell expansion - Full delegation to watchexec - no rig-specific defaults or remapping
- Check watchexec installed before starting service (
requireWatchexec()) - All runtime dependency errors (tmux, watchexec) point to the installer URL as first option
- Services can declare
requirements— check/command pairs evaluated before start - Each requirement runs a
checkcommand (exit 0 = met). If check fails, runscommand(remediation) - If remediation fails (non-zero exit), service start is aborted with an error
- Module-level
remediatedChecksSet deduplicates remediation across services in oneriginvocation - Check commands run with
stdout/stderr: "null"(only exit code matters) - Remediation commands run with
stdout/stderr: "inherit"(user sees output) - Commands run in the service's
working_dirwith itsenvironment - Requirements do not apply to tasks
- Simple text output (
rig config) skips complex nested fields (tasks,watch) to avoid[object Object]serialization - JSON output (
rig config --json) includes full details - Use
skipSet to control which fields appear in text output
- Path normalization uses URL class:
new URL(path, "file:///").pathname- cleaner than manual string manipulation - Circular import detection needs normalized absolute paths to work correctly
- File discovery respects .gitignore (via
ignorecrate) to avoid pulling in rig files from dependencies - Deduplication must happen by absolute normalized path, not relative path
- Path expansion must happen per-file before merging (each config's paths relative to its own location)
LoadContextpattern works well: trackconfigPath,configDir,loadedSet, andimportChainfor recursion
Config uses multi-group format with services and tasks nested under groups:
imports:
- db/rig.yaml
- backend/rig.yaml
groups:
backend:
services:
api: { command: ..., working_dir: ... }
db: { command: ..., working_dir: ... }
tasks:
deploy: { command: ..., working_dir: ... }
frontend:
services:
web: { command: ..., working_dir: ... }Key rules:
- Service names must be unique across all groups (even across imported files)
- Groups can have services, tasks, or both
- Groups are targeted with
-g/--groupflag:rig start -g backend - Default CLI targets are services:
rig start api db - Tasks are run via
rig run group.taskorrig run group.service.task - One SessionManager instance per group (tmux sessions:
{group}-{service})
Configs can import other configs to compose a service graph:
imports:
- shared/db/rig.yaml # Relative path from this config's location
- ../common/rig.yaml # Parent directory traversal supported
- infra.rig.yaml # *.rig.yaml naming pattern supportedKey rules:
rigsearches upward from CWD to find the nearest config file- Each file's paths (
working_dir,env_file) are expanded relative to its own location - Same file imported multiple times = loaded once (deduped by absolute path)
- Circular imports = error
- Duplicate group or service names = error
rig discoverscans for rig files and suggests imports
src/
main.rs → Entry point: CLI parsing (clap derive) and command dispatch
lib.rs → Library root, re-exports modules
output.rs → Terminal output: colors, logging, display helpers
config.rs → Types, schema validation, config loading/parsing/querying
process.rs → SessionManager, process tree/metrics, tmux checks, log streaming
commands.rs → CLI command implementations (start/stop/ps/top/logs/tasks/discover)
Module dependency graph (strict DAG):
output ← config ← process ← commands ← main
install.sh:
Platform detection → OS, arch
Dependency check → tmux (required), watchexec (optional)
Binary download → GitHub Releases tarball
PATH verification → check install dir is in PATH
-
Orphan processes: Old processes from different systems won't be in tmux. Port-based cleanup was removed - tmux handles lifecycle properly now.
-
Path resolution:
working_dirin config is relative to config file location, not CWD. With multi-file imports, each config expands paths relative to its own location. -
Raw mode stdin: Intercepts Ctrl+C. Must check for byte 3 explicitly.
-
lsof on macOS:
-pflag doesn't filter with-i. Parse output and filter by PID. -
Test session cleanup: When testing multi-file configs, sessions may be created in different groups. The
cleanupSessions()helper must track all possible test group prefixes.
Developers should set up local development as described in CONTRIBUTING.md.
Build and run from source:
cargo build # Build debug binary
cargo install --path . # Install to ~/.cargo/bin
./rig-dev ps # Build + run in one step (dev wrapper)Before committing, run cargo fmt to auto-format all source files. CI enforces cargo fmt --check and will reject unformatted code.
The rig-dev script builds from the source tree and runs the debug binary, so your working directory's rig.yaml is used while the binary comes from the source checkout.
Keep README.md in sync with command output: When modifying commands or their output, always run rig -h and update the Commands section in README.md to match the exact output. The README should reflect what users see when they run the help command.
Keep install.sh in sync with dependencies: When adding new runtime dependencies, update install.sh (detection, plan display), the README install section, and the error messages in src/main.rs that guide users when a tool is missing.
Tests are end-to-end Rust integration tests that spawn the rig binary and interact with tmux.
During development, test locally:
cargo test -- --test-threads=1Tests must run single-threaded because they share tmux state.
After completing any task, always validate on Linux via Docker:
cargo test -- --test-threads=1 # Local
docker build -f test/Dockerfile -t rig-test . && docker run --rm rig-test # DockerBoth must pass before considering the task complete.
tests/
common/mod.rs # TestContext: temp dirs, rig binary, tmux helpers, cleanup
e2e_help.rs # Help/version output tests
e2e_tasks.rs # Task execution tests
e2e_env.rs # Environment variable tests
e2e_deps.rs # depends_on tests
e2e_watch.rs # File watching tests
e2e_services.rs # Service lifecycle tests
e2e_multifile.rs # Multi-file config tests
e2e_requirements.rs # Pre-start requirement tests
e2e_schema.rs # Config validation tests
test/
Dockerfile # Linux test environment for Docker
Each test file uses TestContext from common/mod.rs which handles temp directory creation, writing test configs, running the rig binary, and tmux session cleanup.
- Group-level settings (shared env, working_dir defaults)
- Import globs:
imports: ["services/*/rig.yaml"] rig discover --watchfor continuous import updates