tsr

tsr is a lightweight, polyglot, repo-aware task runner. It is a command runner, not a build system: it provides one unified interface over the native runners already in your repo (npm, bun, cargo, go, uv, …), adds a task dependency graph and opt-in parallelism, and delegates caching to specialist tools (Turbo, Nx) rather than reimplementing it.

Shell
tsr dev            # run the 'dev' task
tsr ci             # run the 'ci' task
tsr test -- --watch
  • Binary: tsr

  • Config file: tasks.toml (also the workspace-root anchor)

  • Written in: Rust, single static binary

Design principles

  1. Lightweight — a thin unifying layer, not a replacement for native runners.

  2. Delegate, don't reimplement — execution is handed to native runners; caching is handed to Turbo/Nx.

  3. Polyglot — one entry point across every ecosystem in the repo.

  4. Predictable by default — sequential execution unless parallelism is explicitly requested; fail fast.

  5. Hand-edit-safe — the config stays valid and legible when edited by hand, and unknown keys survive a round-trip.

Explicitly out of scope

Content-hash caching, remote caching, and inputs/outputs tracking are never implemented in tsr — they are ceded to delegated backends (Turbo, Nx). Adding them would contradict the "lightweight, delegate" principle.

Run your first task

From an empty repo to a running task in four steps.

Install

tsr is a single static binary. Install it using the official installer or build from source:

sh
curl -fsSL https://raw.githubusercontent.com/Open-Tech-Foundation/tsr/main/install.sh | bash
powershell
irm https://raw.githubusercontent.com/Open-Tech-Foundation/tsr/main/install.ps1 | iex
sh
cargo build --release

Scaffold a config

From your repo root, generate a commented starter tasks.toml showcasing all three task forms. It refuses to overwrite an existing file, so it is always safe to run:

Shell
tsr --init

Define a task

Open tasks.toml and add a task — or author it interactively with tsr --config:

TOML
[tasks.dev]
run = "vite"
dir = "apps/web"

Run it

Shell
tsr dev

Because run = "vite" has no shell metacharacters, tsr splits it and spawns vite directly — no npm run / Node startup tax.

tasks.toml is the workspace anchor

The location of tasks.toml defines your workspace root. tsr finds it by walking up from the current directory to the nearest tasks.toml, so you can run tasks from anywhere in the repo.

No config? tsr still runs your scripts

tasks.toml is optional. In a repo that only has a package.json (or Cargo.toml, go.mod, pyproject.toml), tsr dev just runs npm run dev / cargo dev / … by auto-detecting the ecosystem — no config to write. Add a tasks.toml when you want a dependency graph, monorepo fan-out, or delegate.