CLI reference
tsr <task> [options] [-- <args>...] run a task; args after -- are forwarded tsr [-- <args>...] run the [workspace] default task 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 --completions <shell> print a bash / zsh / fish completion script 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. |
--expand | off | With --dry-run: show expanded commands and env. |
-F, --filter <pkg> | — | Run package fan-outs only in <pkg>. Repeatable. |
-j, --jobs <n> | unlimited | Run at most <n> commands at once. |
--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. |
--completions <shell> | Print a shell completion script. |
--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 task and everything it depends on, then executes. A broken task elsewhere in the file doesn't block it.
tsr dev tsr build:prod tsr web#build # the 'build' task in package 'web' tsr # the [workspace] default task, if one is set
An unknown name exits 64 and suggests the closest defined task: unknown task 'tset' — did you mean 'test'?.
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 is found, tsr exits 64 and points you at tsr --init. A build system with no task-name convention (CMake, Meson, Bazel, a bare Dockerfile, R, Julia, Perl) is named in that error, along with a run line to start your tasks.toml from. 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> --filter <pkg>
Runs every packages fan-out only in the matching packages, so you can run one package without defining a task for it.
tsr build --filter web # manifest name tsr test -F apps/admin -F 'packages/*' # path, glob; repeatable
Patterns match the way packages entries do. A package's ^upstream work still runs, because the package needs it. A pattern that matches no package exits 64 before anything runs. --filter composes with --since and --resume-from: a package has to pass all of them.
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> --jobs <n>
Caps how many commands run at once, across the whole run. Every deps batch and packages fan-out counts against the same limit, including nested ones, so --jobs 4 means four processes at most, however wide the graph is.
tsr build -j 4 # a 200-package parallel fan-out, four at a time
--jobs never adds parallelism: a task without parallel = true still runs its batch one at a time. A command still waiting for a slot when the run fails fast never starts, and shows as skipped.
Debugging with --expand
Plain --dry-run never shows variable values. When you need to see exactly what will run, add --expand:
$ tsr deploy --dry-run --expand ⚠ --expand prints variable values, including any from .env — don't paste this output anywhere public · deploy dir: . cmd: deploy --to $TARGET src/*.js run: deploy --to prod src/a.js src/b.js env: TARGET=prod
run: is the command after $VAR and glob expansion. Each env: line is a variable that tsr sets or changes for the task. --expand works only together with --dry-run.
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. A task's desc comes first when it has one, and the [workspace] default task is marked.
Available tasks: build delegate: turbo ci deps: lint, test, build · parallel dev (default) Start the dev server (run: vite · dir: apps/web) test packages: apps/*
A task with a config error doesn't stop the listing. It shows in place, marked ✗ with its error, and a note on stderr counts the broken tasks. Running the broken task, or any task that depends on it, exits 64 before anything starts. Tasks that can't reach it run normally.
Available tasks: bad ✗ 'run' string uses '|' (pipe), which is unsupported — set `shell = true` on the task, or use a script file ok run: echo fine
With no tasks.toml, --list reports the detected package and lists what you can run by name: the package.json scripts, or a Python project's [project.scripts]. For Cargo and Go, where any subcommand runs as a task, it says so instead.
No tasks.toml — tsr runs your package scripts directly. Detected an npm package (package.json) at /work/app. Available scripts: build vite build dev vite Run one with: tsr <script>
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 --completions <shell>
Prints a completion script for bash, zsh or fish. It completes flags, option values, and your task names, which it reads fresh from the current directory each time, so new tasks show up without regenerating anything.
# bash — add to ~/.bashrc (eval works on the bash 3.2 macOS ships) eval "$(tsr --completions bash)" # zsh — add to ~/.zshrc (after compinit) source <(tsr --completions zsh) # fish — add to ~/.config/fish/config.fish tsr --completions fish | source
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.