CLI reference
tsr <task> [options] [-- <args>...] run a task; args after -- are forwarded tsr --list list the tasks defined in tasks.toml tsr --config edit tasks.toml in an interactive TUI tsr --init create a starter tasks.toml here tsr --help | --version
The first argument is always a task name. Every builtin is a flag, so a task named list or init is never shadowed — tsr list runs your list task.
Options at a glance
These options may follow a task name. Each value-taking one accepts --flag value or --flag=value. Everything else still belongs after --.
| Option | Default | Summary |
|---|---|---|
--since <ref> | — | Run only in packages affected since a git ref. |
--resume-from <pkg> | — | Skip packages ordered before <pkg>. |
--no-bail | off | Run everything; don't stop at the first failure. |
--dry-run | off | Print what would run, and run nothing. |
--allow-unsafe-env | off | Let the config set guarded env variables. |
--reporter <fmt> | human | Terminal format: human or ndjson. |
--reporter-file <path> | — | Also write the NDJSON stream to a file. |
| Builtin | Summary |
|---|---|
--list | List the tasks in tasks.toml. |
--config | Edit tasks.toml in an interactive TUI. |
--init | Scaffold a starter tasks.toml. |
--help / -h · --version / -V | Usage and version. |
tsr <task>
Runs the named task. tsr finds the workspace root by walking up to the nearest tasks.toml, validates the config and the dependency subgraph, then executes.
tsr dev tsr build:prod tsr web#build # the 'build' task in package 'web'
No config? It still runs
tasks.toml is optional. With no config file, tsr <task> runs repo-aware by auto-detecting the ecosystem in the current directory (or a parent) and mapping the task to its native runner — so tsr dev becomes npm run dev, tsr build becomes cargo build, and so on, with -- passthrough working as usual.
# in a repo that only has a package.json — no tasks.toml tsr dev # → npm run dev tsr test -- --watch # → npm run test --watch
If neither a tasks.toml nor an ecosystem marker (package.json, Cargo.toml, go.mod, pyproject.toml) is found, tsr exits 64 and points you at tsr --init. A tasks.toml, when present, always wins — there's no fall-through from a defined config to auto-detection, so a mistyped task name stays an error. Package-qualified names (web#build) and the dependency graph need a tasks.toml.
Argument passthrough
Everything after -- is forwarded to the resolved command. If the task defines args, they are prepended before the passthrough:
[tasks.test] run = "vitest" args = ["--color"]
tsr test -- --watch # → vitest --color --watch
tsr <task> --since <ref>
Restricts every packages fan-out to the packages affected by changes since a git ref — the packages that changed, plus every package that transitively depends on them.
tsr build --since main tsr test --since HEAD~1
Changes are read from git, including untracked files. A change outside every package (root config, lockfile, CI workflow) runs everything, since it could affect anything. Upstream ^task dependencies are still built regardless, so a filtered run stays correct. Nothing affected is a clean exit 0.
A missing repository, unknown ref, or missing git exits 64 rather than silently running everything or nothing.
tsr <task> --resume-from <pkg>
Treats every package ordered before <pkg> as already built and runs the rest — for when a long run died two-thirds of the way through.
tsr build --resume-from packages/ui tsr build --resume-from @scope/ui # manifest name works too
The skipped prefix stays skipped even when a later package reaches it as an ^task upstream dependency — otherwise the resume would rebuild exactly what you told it to skip. The resume point itself runs, as does everything depending on it. A <pkg> matching nothing exits 64, since a typo would otherwise silently skip everything or nothing.
--since and --resume-from compose: a package must survive both filters.
tsr <task> --no-bail
By default tsr fails fast — the first non-zero child stops new work and kills running siblings. --no-bail runs every batch to completion instead, so one command tells you everything that is broken:
tsr test --no-bail
The propagated exit code is still the first failure's, so CI sees the same signal either way. This covers task failures only — a runner-level error still stops the run, because a missing delegate binary will be missing for every package too.
tsr <task> --dry-run
Walks the dependency graph and prints each unit of work — its label, directory and command — without running any of it. The way to read an unfamiliar tasks.toml before handing it a shell.
tsr ci --dry-run
· lint dir: . cmd: eslint . · build (packages/ui) dir: packages/ui cmd: vite build
Commands print as written, before $VAR expansion, so a plan pasted into an issue or a CI log cannot carry what your .env holds. The walk is always sequential — even for parallel = true batches — so the output stays readable, and a config that can't be resolved still fails with the same error a real run would give.
tsr <task> --allow-unsafe-env
Lets the config set the guarded environment variables — LD_PRELOAD, NODE_OPTIONS, GIT_SSH_COMMAND, … — and replace PATH outright.
tsr build --allow-unsafe-env
There is deliberately no [security] equivalent. These guards exist for the case where the tasks.toml is what you are wary of, and a guard the config could switch off would not survive that case.
tsr <task> --reporter <fmt>
Chooses what the terminal gets.
| Format | Terminal output |
|---|---|
human (default) | Nothing on success; a result table on failure. |
ndjson | One JSON object per line on stderr, always — success included. |
tsr ci --reporter ndjson
Both formats emit a task event as each unit of work finishes, then one summary:
{"durationMs":12.4,"exitCode":null,"label":"build (packages/ui)","status":"ok","type":"task"} {"durationMs":48.9,"exitCode":1,"failed":1,"ok":3,"runnerError":null,"skipped":2,"status":"failed","task":"build","type":"summary"}
status is ok, failed, or skipped; exitCode is null unless the unit failed.
Child processes inherit stdio, so --reporter ndjson shares stderr with whatever your tasks print. A child that logs JSON — pino, jest --json, Rust's tracing — emits lines indistinguishable from reporter events, type field and all. Filtering by "is this line JSON?" is not enough. Use --reporter-file for anything you intend to parse.
tsr <task> --reporter-file <path>
Writes the NDJSON stream to a file. Nothing else writes there, so it is the sink that is actually safe to parse.
tsr ci --reporter-file results.ndjson
It is independent of --reporter, so on its own you get the human summary on your terminal and a machine-readable record of the same run — which is usually what you want in CI:
tsr ci --no-bail --reporter-file results.ndjson
Pairing it with --no-bail makes the record cover every task rather than stopping at the first failure.
If the file cannot be created, tsr exits 64 before running anything — finding out the sink is unwritable after a long build would be useless.
tsr --list
Prints the tasks defined in tasks.toml, each with a one-line form descriptor.
Available tasks: build delegate: turbo ci deps: lint, test, build · parallel dev run: vite · dir: apps/web test packages: apps/*
With no tasks.toml, there is nothing declared to list, so --list instead reports the detected package and reminds you that scripts run directly (e.g. tsr dev).
tsr --config
Opens an interactive terminal UI for editing tasks.toml — the intended way to author tasks with all their options instead of hand-editing TOML.
It autosaves: there is no separate write step, no unsaved state, and no "discard changes?" prompt. Applying a task in the form (or confirming a delete) validates the whole config and writes tasks.toml immediately — and since an invalid form is never committed, an autosave can never leave a broken file.
Menu (home) — the TUI opens on a menu of workflows rather than a blank list, so there is always an obvious next step: Add a task, Edit a task, Delegate a task, Delete a task, Preview graph, and Quit.
↑↓move,⏎opens the selected workflow,qquits. Every sub-screen returns here withEsc.Add / Delegate — open the form for a new task. Delegate starts already on the
delegatetype so you land on the backend-bin field.Edit / Delete — open a task picker (the list of defined tasks).
↑↓move,⏎edits (or deletes, with ay/nconfirm) the selected task,gpreviews its graph,Escgoes back to the menu.Form view — fields for the full task model: name, form (
run/delegate/delegate(table)/ auto-detect),dirorpackages,deps,args,parallel,env, andenv_file. Irrelevant fields are hidden based on the chosen form.↑↓/Tabmove,←→/Spacechange a choice/toggle,⏎saves the task,Esccancels. A validation error keeps the form open and shows the problem inline, so nothing is written until it is fixed.⏎is the save key rather thanCtrl+Sdeliberately: editor and IDE terminals grabCtrl+Sfor "save file", and it is XOFF on terminals with flow control on.Ctrl+Sstill works as an alias where the terminal lets it through.Graph view — a read-only, connected dependency tree with each task's dry-run command (what
tsrwould actually execute, resolved by the same precedence:delegate→run→ auto-detect; a deps-only task shows "runs its deps only"). It shows every task rooted at the tasks nothing depends on, or a single task's subtree when opened from a picker. Parallel vs. sequentialdepsare tagged, and undefined deps or cycles in a mid-edit config are flagged inline.↑↓scroll,awiden to all,Esc/gback to the menu.
Edits go through the same format-preserving parser used everywhere else, so comments and unknown keys survive each autosave, and every change is validated before it is written. If no tasks.toml exists yet, --config starts a new one in the current directory — the file appears on your first saved task.
tsr --init
Scaffolds a starter tasks.toml in the current directory: reference comments only, showcasing all three task forms, [workspace], [env] and the dependency graph, with a link back to these docs. It refuses to overwrite an existing file, so it is always safe to run.
It deliberately defines no tasks. Because a present tasks.toml always wins over auto-detection, a scaffolded placeholder task would shadow what your repo already does — tsr dev would stop running your real npm run dev. Uncomment the example you want, or run tsr --config to add tasks interactively. A bare [tasks.<name>] (form 3) re-enables auto-detection for that task.
tsr --help · tsr --version
Print usage and the binary version, respectively.
Exit status
tsr follows a precise exit-code contract: 0 on success, the failing child's exact code on task failure, or 64 for any runner-level error.