numux
v2.7.0
Published
Terminal multiplexer with dependency orchestration
Maintainers
Readme
numux
Terminal multiplexer with dependency orchestration. Run multiple processes in a tabbed TUI with a dependency graph controlling startup order, readiness detection, and output capture between processes.
Works with zero configuration — pass commands as arguments, run a script across monorepo workspaces with -w, or match multiple scripts with glob patterns like 'dev:*'. For advanced setups, define a typed config with conditional processes, file watching with auto-restart, error detection, log persistence, and output capture — e.g. extract a port from one process's stdout and pass it to another process's command or env.
Inspired by sst dev and concurrently
Install
Requires Bun >= 1.0.
bun install -g numuxUsage
Quick start
numux initThis creates a starter numux.config.ts with commented-out examples. Edit it, then run numux.
Config file
Create numux.config.ts (or .js):
import { defineConfig } from 'numux'
export default defineConfig({
processes: {
db: {
command: 'docker compose up postgres',
readyPattern: 'ready to accept connections',
},
migrate: {
command: 'bun run migrate',
dependsOn: ['db'],
},
api: {
command: 'bun run dev:api',
dependsOn: ['migrate'],
readyPattern: 'listening on port 3000',
},
// String shorthand for simple processes
web: 'bun run dev:web',
// Interactive process — keyboard input is forwarded
confirm: {
command: 'sh -c "printf \'Deploy to staging? [y/n] \' && read answer && echo $answer"',
interactive: true,
},
},
})The defineConfig() helper is optional — it provides type checking for your config.
Processes can be a string (shorthand for { command: "..." }), true or {} (auto-resolves to a matching package.json script), or a full config object.
Then run:
numuxSubcommands
numux init # Create a starter numux.config.ts
numux validate # Validate config and show process dependency graph
numux exec <name> [--] <command> # Run a command in a process's environment
numux completions <shell> # Generate shell completions (bash, zsh, fish)validate respects --only/--exclude filters and shows processes grouped by dependency tiers.
exec runs a one-off command using a process's configured cwd, env, and envFile — useful for migrations, scripts, or any command that needs the same environment:
numux exec api -- npx prisma migrate
numux exec web npm run buildSet up completions for your shell:
# Bash (add to ~/.bashrc)
eval "$(numux completions bash)"
# Zsh (add to ~/.zshrc)
eval "$(numux completions zsh)"
# Fish
numux completions fish | source
# Or save permanently:
numux completions fish > ~/.config/fish/completions/numux.fishWorkspaces
Run a package.json script across all workspaces in a monorepo:
numux -w devReads the workspaces field from your root package.json, finds which workspaces have the given script, and spawns <pm> run <script> in each. The package manager is auto-detected from packageManager field or lockfiles.
Composes with other flags:
numux -w dev -n redis="redis-server" --colorsAd-hoc commands
# Unnamed (name derived from command)
numux "bun dev:api" "bun dev:web"
# Named
numux -n api="bun dev:api" -n web="bun dev:web"Script patterns
Run package.json scripts by name — any colon-containing name is automatically recognized as a script reference:
numux 'lint:eslint --fix' # runs: yarn run lint:eslint --fix
numux 'dev:*' # all scripts matching dev:*
numux 'npm:*:dev' # explicit npm: prefix (same behavior)* does not match across : separators (like / in file paths), so format:* matches format:store but not format:check:store. Use format:*:* to match two levels deep.
Append ^ to skip scripts that act as group runners — scripts that have sub-scripts beneath them. For example, if format:check runs numux 'format:check:*' internally, then format:*^ excludes it (because format:check:store and format:check:odoo exist as sub-scripts), avoiding duplicate runs.
Extra arguments after the pattern are forwarded to each matched command:
numux 'lint:* --fix' # → bun run lint:js --fix, bun run lint:ts --fixIn a config file, use the pattern as the process name:
export default defineConfig({
processes: {
'dev:*': { color: ['green', 'cyan'] },
'lint:* --fix': {},
},
})Template properties (color, env, dependsOn, etc.) are inherited by all matched processes. Colors given as an array are distributed round-robin.
When a process has no command and its name matches a package.json script, the command is auto-resolved:
export default defineConfig({
processes: {
lint: true, // → bun run lint
typecheck: { dependsOn: ['db'] }, // → bun run typecheck (with dependency)
db: 'docker compose up postgres', // explicit command, not resolved
},
})Options
| Flag | Description |
|------|-------------|
| -w, --workspace <script> | Run a script across all workspaces |
| -c, --config <path> | Explicit config file path |
| -n, --name <name=cmd> | Add a named process (repeatable) |
| -p, --prefix | Prefixed output mode (no TUI, for CI/scripts) |
| --only <a,b,...> | Only run these processes (+ their dependencies) |
| --exclude <a,b,...> | Exclude these processes |
| --kill-others | Kill all processes when any exits (regardless of exit code) |
| --kill-others-on-fail | Kill all processes when any exits with a non-zero exit code |
| --max-restarts <n> | Max auto-restarts for crashed processes |
| -s, --sort <mode> | Tab display order: config (default), alphabetical, topological |
| --no-watch | Disable file watching even if config has watch patterns |
| -t, --timestamps [format] | Add timestamps (default HH:mm:ss). Works in both prefix and TUI mode. Pass a format string for custom output (e.g. HH:mm:ss.SSS). Toggle in TUI with T |
| --log-dir <path> | Persist logs to timestamped subdirs (<path>/<timestamp>/<name>.log) with a latest symlink. Path is printed on exit |
| --debug | Log to .numux/debug.log |
| -h, --help | Show help |
| -v, --version | Show version |
Prefix mode
Use --prefix (-p) for CI or headless environments. Output is printed with colored [name] prefixes instead of the TUI:
numux --prefixAuto-exits when all processes finish. Exit code 1 if any process failed.
Config reference
Global options
Top-level options apply to all processes (process-level settings override):
| Field | Type | Description |
|-------|------|-------------|
| cwd | string | Working directory for all processes (process cwd overrides) |
| env | Record<string, string> | Environment variables merged into all processes (process env overrides per key) |
| envFile | string \| string[] \| false | .env file(s) for all processes (process envFile replaces if set; false disables) |
| showCommand | boolean | Print the command being run as the first line of output (default: true) |
| maxRestarts | number | Restart limit for all processes (default: 0) |
| readyTimeout | number | Ready timeout in ms for all processes |
| stopSignal | 'SIGTERM' \| 'SIGINT' \| 'SIGHUP' | Stop signal for all processes (default: 'SIGTERM') |
| errorMatcher | boolean \| string | Error detection for all processes (true = ANSI red, string = regex) |
| watch | string \| string[] | Watch patterns for all processes (process watch replaces if set) |
| sort | 'config' \| 'alphabetical' \| 'topological' | Tab display order (default: 'config' — definition order) |
export default defineConfig({
cwd: './packages/backend',
env: { NODE_ENV: 'development' },
envFile: '.env',
processes: {
api: { command: 'node server.js' }, // inherits cwd, env, envFile
web: { command: 'vite', cwd: './packages/web' }, // overrides cwd
},
})Process options
Each process accepts:
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| command | string | required | Shell command to run. Supports $dep.group references from dependency capture groups |
| cwd | string | process.cwd() | Working directory |
| env | Record<string, string> | — | Extra environment variables. Values support $dep.group references from dependency capture groups |
| envFile | string \| string[] \| false | — | .env file path(s) to load (relative to cwd); false disables inherited envFile |
| dependsOn | string[] | — | Processes that must be ready first |
| readyPattern | string \| RegExp | — | Regex matched against stdout to signal readiness. Use RegExp to capture groups (see below) |
| readyTimeout | number | — | Milliseconds to wait for readyPattern before failing |
| maxRestarts | number | 0 | Max auto-restart attempts on non-zero exit (0 = no restarts) |
| delay | number | — | Milliseconds to wait before starting the process |
| condition | string | — | Env var name; process skipped if falsy. Prefix with ! to negate |
| platform | string \| string[] | — | OS(es) this process runs on (e.g. 'darwin', 'linux'). Non-matching processes are removed; dependents still start |
| stopSignal | string | SIGTERM | Signal for graceful stop (SIGTERM, SIGINT, or SIGHUP) |
| color | string \| string[] | auto | Hex (e.g. "#ff6600") or basic name: black, red, green, yellow, blue, magenta, cyan, white, gray, orange, purple |
| watch | string \| string[] | — | Glob patterns — restart process when matching files change |
| interactive | boolean | false | When true, keyboard input is forwarded to the process |
| errorMatcher | boolean \| string | — | true detects ANSI red output, string = regex pattern — shows error indicator on tab |
| showCommand | boolean | true | Print the command being run as the first line of output |
| workspaces | boolean \| string \| string[] | — | Run command in monorepo workspaces (see below) |
Workspace expansion
Use workspaces on a process to expand it into per-workspace processes. Reads the workspaces field from your root package.json.
export default defineConfig({
processes: {
// All workspaces — filters by script availability for PM run commands
lint: { command: 'npm run lint', workspaces: true },
// Specific workspace by package name
validate: { command: 'npm run validate', workspaces: '@repo/image-worker' },
// Multiple specific workspaces
dev: { command: 'npm run dev', workspaces: ['@repo/api', '@repo/web'] },
},
})Each entry expands into {name}:{wsName} processes (e.g. lint:api, lint:web) with cwd set to the workspace directory. All other config (env, dependsOn, color, etc.) is inherited from the template.
When workspaces: true is used with a PM run command (npm run lint), only workspaces that have the matching script are included. Raw commands (eslint .) run in all workspaces.
String values resolve by package name first (with or without scope), then fall back to relative path. Cannot be combined with cwd.
File watching
Use watch to automatically restart a process when source files change:
export default defineConfig({
processes: {
api: {
command: 'node server.js',
watch: 'src/**/*.ts',
},
styles: {
command: 'sass --watch src:dist',
watch: ['src/**/*.scss', 'src/**/*.css'],
},
},
})Patterns are matched relative to the process's cwd (or the project root). Changes in node_modules and .git are always ignored. Rapid file changes are debounced (300ms) to avoid restart storms.
A watched process is only restarted if it's currently running, ready, or failed — manually stopped processes are not affected.
Environment variable interpolation
Config values support ${VAR} syntax for environment variable substitution:
export default defineConfig({
processes: {
api: {
command: 'node server.js --port ${PORT:-3000}',
env: {
DATABASE_URL: '${DATABASE_URL:?DATABASE_URL must be set}',
},
},
},
})| Syntax | Behavior |
|--------|----------|
| ${VAR} | Value of VAR, or empty string if unset |
| ${VAR:-default} | Value of VAR, or default if unset |
| ${VAR:?error} | Value of VAR, or error with message if unset |
Interpolation applies to all string values in the config (command, cwd, env, envFile, readyPattern, etc.).
Conditional processes
Use condition to run a process only when an environment variable is set:
export default defineConfig({
processes: {
seed: {
command: 'bun run seed',
condition: 'SEED_DB', // only runs when SEED_DB is set and truthy
},
storybook: {
command: 'bun run storybook',
condition: '!CI', // skipped in CI environments
},
},
})Falsy values: unset, empty string, "0", "false", "no", "off" (case-insensitive). If a conditional process is skipped, its dependents are also skipped.
Dependency orchestration
Each process starts as soon as its declared dependsOn dependencies are ready — it does not wait for unrelated processes. If a process fails, its dependents are skipped.
A process becomes ready when:
- Has
readyPattern— the pattern matches in stdout (long-running server) - No
readyPattern— exits with code 0 (one-shot task)
Processes that crash (non-zero exit) can be auto-restarted by setting maxRestarts (default: 0). Restarts use exponential backoff (1s–30s), which resets after 10s of uptime.
Dependency output capture
When readyPattern is a RegExp (not a string), capture groups are extracted on match and expanded into dependent process command and env values using $process.group syntax:
export default defineConfig({
processes: {
db: {
command: 'docker compose up postgres',
readyPattern: /ready to accept connections on port (?<port>\d+)/,
},
api: {
command: 'node server.js --db-port $db.port',
dependsOn: ['db'],
env: { DB_PORT: '$db.port' },
},
},
})Both named ($db.port) and positional ($db.1) references work. Named groups also populate positional slots, so $db.port and $db.1 both resolve to the same value above.
Unmatched references are left as-is (the shell will expand $db as empty + .port literal, making the issue visible). String readyPattern values work as before — readiness detection only, no capture extraction.
Keybindings
Keybindings are shown in the status bar at the bottom of the app. Panes are readonly by default — keyboard input is not forwarded to processes. Set interactive: true on processes that need stdin (REPLs, shells, etc.).
| Key | Action |
|-----|--------|
| ←/→ or 1-9 | Switch tabs |
| F | Search current pane |
| Tab (in search) | Toggle between single-pane and all-process search |
| Enter/Shift+Enter | Next/previous match |
| Esc | Exit search |
| R | Restart current process |
| Shift+R | Restart all processes |
| S | Stop/start current process |
| Y | Copy all output |
| L | Clear pane |
| G/Shift+G | Scroll to top/bottom |
| PageUp/PageDown | Scroll by page |
| Ctrl+Click | Open link |
| Ctrl+C | Quit |
Tab icons
| Icon | Status | |------|--------| | ○ | Pending | | ◐ | Starting | | ◉ | Running | | ● | Ready | | ◑ | Stopping | | ■ | Stopped | | ✖ | Failed | | ⊘ | Skipped |
Dependencies
ghostty-opentui
Despite the name, ghostty-opentui is not a compatibility layer for the Ghostty terminal. It uses Ghostty's Zig-based VT parser as the ANSI terminal emulation engine for OpenTUI's terminal renderable. It works in any terminal emulator (iTerm, Kitty, Alacritty, WezTerm, etc.) and adds ~8MB to install size due to native binaries.
License
MIT
