A declarative, fast and lightweight monorepo tool that runs multiple dev servers and build tasks concurrently.
fyrer reads a fyrer.yml file describing the packages in a monorepo and
their tasks, resolves the dependency graph between tasks, runs each level of
the graph concurrently, and streams every task's output through a colorized,
prefixed logger. Long-running dev servers keep running until you quit, and
cached builds are skipped when their inputs haven't changed.
curl -fsSL https://raw.githubusercontent.com/07calc/fyrer/main/install.sh | shThe script downloads the prebuilt binary for your OS/architecture from the
latest GitHub release, verifies its SHA-256 checksum, and installs it to
~/.local/bin (override with FYRER_INSTALL_DIR, or pin a version with
sh install.sh v0.5.0).
powershell -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/07calc/fyrer/main/install.ps1 | iex"Installs fyrer.exe to $HOME\.local\bin (override with -InstallDir).
cargo install fyrergit clone https://github.com/07calc/fyrer
cd fyrer
cargo build --release
cargo install --path .A runnable demo monorepo lives in examples/acme-corp:
four languages (Bun/TS, Rust, Go, Python), shared packages, dev servers,
cached builds, .env parsing, cwd, and a timeout demo.
cd examples/acme-corp
fyrer list # see every package and task
fyrer plan build # show the execution plan without running
fyrer run build # build everything (second run is cached)
fyrer run dev # all dev servers at once; q / Ctrl+C to stopfyrer looks for a fyrer.yml file in the current directory. A full example
is available at fyrer.example.yml. Package roots and
task globs are resolved relative to the current working directory, so run
fyrer from the directory that contains the config.
fyrer list # list every package and its tasks
fyrer plan <task?> # print the topological execution plan
fyrer run <task?> # execute tasks and stream their logsThe <task> specifier comes in three forms:
| Specifier | Meaning |
|---|---|
| (empty) | run every task in every package |
build |
run the build task of every package that has one |
web:dev |
run exactly one task, web:dev (package:task) |
Flags:
fyrer --config path/to/fyrer.yml run # point at a different config file
fyrer run build -n # plain (non-TUI) output, for CI/pipesversion: 1 # config format version (must be 1)
cache: # build cache configuration
provider: local # only "local" is available for now
env: # variables shared by every package
NODE_ENV: development
packages: # packages that make up the monorepo
- name: web # unique package name
root: ./apps/web # relative package root (must exist)
env: # variables shared by every task
PORT: "3000"
env_file: .env # .env file path, relative to root
tasks: # map of task name -> task config
dev:
cmd: bun run dev # shell command ($SHELL -c) to run
depends_on: # tasks that must finish first
- ui:build # "package:task" ...
- check # ... or a bare "task" in this package
inputs: # globs of files that define the task's inputs
- src/**
outputs: # globs of files the task produces
- dist/**
ignore: # globs excluded from inputs/outputs
- node_modules/**
cache: false # allow skipping when inputs haven't changed
persistent: true # long-running: keep alive until quit
watch: true # watched: restart on input changes (see note)
timeout: 30s # kill the task after this duration
cwd: subdir # run the command from a subdirectory of root
env: # per-task variables (highest precedence)
PORT: "8080"
env_file: .env.local # per-task .env file, relative to root| Option | Description |
|---|---|
cmd |
Required. Command run via sh -c (Unix) or cmd /C (Windows). |
depends_on |
Task IDs that must finish first. Accepts package:task or a bare task. |
inputs |
Globs (relative to the package root) included in the cache key. |
outputs |
Globs (relative to the package root) of files produced by the task. |
ignore |
Globs excluded from inputs/outputs. |
cache |
If true, a successful run is cached and skipped on future identical runs. |
persistent |
Marks a long-running task (used with dev servers). Cannot combine with cache. |
watch |
Marks a task that restarts when watched inputs change. Cannot combine with cache. |
timeout |
Kill the task (SIGKILL to its process group) after this duration. |
cwd |
Working directory for the command, relative to the package root. |
env |
Per-task environment variables (highest precedence). |
env_file |
Per-task .env file, relative to the package root. |
Validation rules: package names and task names must be unique; package roots
must exist and be relative; env_file must exist and be relative; cwd must
exist inside the package root; timeout must be greater than zero; inputs,
outputs and ignore must be valid glob patterns; and cache cannot be
combined with persistent or watch.
From lowest to highest:
- root-level
env - package
env_file - package-level
env - task
env_file - task-level
env
.env files are plain KEY=VALUE files; blank lines and lines starting with
# are ignored.
Tasks with cache: true are keyed by a blake3 hash of the task id,
command, working directory, resolved environment, the contents of every file
matched by inputs, and the cache keys of every dependency. Outputs are
archived under .fyrer/cache/ (a local provider for now).
On a later run, a task whose cache key matches a previous successful run is
reported as ⚡ Cached instead of executing again. If its outputs are
missing or stale, they are restored from the cache before the run is skipped.
By default fyrer run opens a full-screen TUI with a task list on the left
and the selected task's logs on the right:
qorCtrl+C— quit (kills all spawned process groups)j/kor↑/↓— select the previous / next tasku/dorPageUp/PageDown— scroll the log paneg— jump to the top of the log;G— tail (follow)- mouse wheel — scroll the log
Enter— after the run finishes, browse each task's logs
When a run completes, a summary popup shows success / failure / cached /
skipped counts and total duration. Pass -n (--no-tui) for plain,
prefixed, colorized log output instead.
- Run multiple development servers and build tasks concurrently
- Declarative YAML-based configuration with validation
- Dependency-aware topological ordering, with failure propagation (tasks whose dependencies failed are skipped)
- Automatic
.envfile parsing at the package and task level - Content-addressed local build cache with output restore
- Per-task
timeoutand working directory (cwd) support - Colorized, prefixed log output in plain mode, or a full TUI
- Graceful shutdown on
SIGINT/SIGTERM, killing spawned process groups
- Tasks run in streaming DAG order: a task starts as soon as its dependencies succeed, up to the
concurrencylimit. Apersistenttask only blocks its transitive dependents — unrelated branches still run concurrently. Keep dev servers on leaves of the graph. watch: trueis implemented via polling: input globs are checked every 300ms (debounced 300ms) and the task restarts automatically when inputs change.watchcannot be combined withcache.
Releases are triggered by merging to main. The version in [workspace.package].version (root Cargo.toml) is the single source of truth — npm/package.json and the version pins in [workspace.dependencies] are kept in sync by cargo xtask bump.
# 1. bump versions
cargo xtask bump 0.5.1
# 2. commit + PR -> merge to main
git commit -am "chore: release v0.5.1"
gh pr create --fill
# (or push a branch and merge via GitHub UI)On merge, .github/workflows/release.yml:
- Checks whether tag
v0.5.1already exists — if so, the run no-ops. - Otherwise builds the
fyrerbinary for 6 targets (Linux / macOS / Windows × x64 / arm64). - Publishes crates to crates.io in dependency order via
cargo xtask publish(skips already-published versions; retries on sparse-index lag). - Creates the GitHub Release
v0.5.1with the binaries and*.sha256checksums (tag is created last, so a failed publish never leaves a blocking tag).
For a local dry-run without publishing:
cargo xtask publish --dry-run --allow-dirtyNote on the first publish of new crate names: crates.io rate-limits new crates (~1 per 10 min). The initial bootstrap of the 9 workspace crates may take ~90 min in CI; subsequent releases publish new versions of existing crates and are not subject to that limit.
MIT