CLI reference

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

OptionDefaultSummary
--since <ref>Run only in packages affected since a git ref.
--resume-from <pkg>Skip packages ordered before <pkg>.
--no-bailoffRun everything; don't stop at the first failure.
--dry-runoffPrint what would run, and run nothing.
--allow-unsafe-envoffLet the config set guarded env variables.
--reporter <fmt>humanTerminal format: human or ndjson.
--reporter-file <path>Also write the NDJSON stream to a file.
BuiltinSummary
--listList the tasks in tasks.toml.
--configEdit tasks.toml in an interactive TUI.
--initScaffold a starter tasks.toml.
--help / -h · --version / -VUsage 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.

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

Shell
# 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:

TOML
[tasks.test]
run = "vitest"
args = ["--color"]
Shell
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.

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

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

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

Shell
tsr ci --dry-run
TEXT
· 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 variablesLD_PRELOAD, NODE_OPTIONS, GIT_SSH_COMMAND, … — and replace PATH outright.

Shell
tsr build --allow-unsafe-env
A flag, never a config key

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.

FormatTerminal output
human (default)Nothing on success; a result table on failure.
ndjsonOne JSON object per line on stderr, always — success included.
Shell
tsr ci --reporter ndjson

Both formats emit a task event as each unit of work finishes, then one summary:

JSON
{"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.

Don't parse the terminal stream

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.

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

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

TEXT
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, q quits. Every sub-screen returns here with Esc.

  • Add / Delegate — open the form for a new task. Delegate starts already on the delegate type so you land on the backend-bin field.

  • Edit / Delete — open a task picker (the list of defined tasks). ↑↓ move, edits (or deletes, with a y/n confirm) the selected task, g previews its graph, Esc goes back to the menu.

  • Form view — fields for the full task model: name, form (run / delegate / delegate(table) / auto-detect), dir or packages, deps, args, parallel, env, and env_file. Irrelevant fields are hidden based on the chosen form. ↑↓/Tab move, ←→/Space change a choice/toggle, saves the task, Esc cancels. 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 than Ctrl+S deliberately: editor and IDE terminals grab Ctrl+S for "save file", and it is XOFF on terminals with flow control on. Ctrl+S still 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 tsr would actually execute, resolved by the same precedence: delegaterun → 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. sequential deps are tagged, and undefined deps or cycles in a mid-edit config are flagged inline. ↑↓ scroll, a widen to all, Esc/g back 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.