Skip to content

Repository files navigation

vx

The fastest way to build a monorepo.

vx runs your task graph, remembers every result, and never does the same work twice.

The runner adds seconds to a cold build where others add minutes. On a 1,090-package graph of 3,270 tasks whose ideal schedule is 3m 38s, vx finishes in 3m 46s (+0:08), Turborepo in 5m 13s (+1:35) and Nx in 34m 44s (+31:06) — 8 ms of overhead per package against 88 ms and 1,712 ms, so the graph can grow and the runner stays in seconds. The cold build burns 35 s of CPU in vx, 73 s in Turborepo and 114 minutes in Nx; a fully cached run replays the graph in 510 ms.

On a real Turbo monorepo (solidjs/solid, its own turbo.json, no config rewritten) vx replays a cached build in 51 ms where Turbo takes 95, restores every output in 66 ms against 127, and builds cold in 40.6 s against 45.5 s. Measured, reproducible, on hardware you own (benchmarks).

One binary. No daemon, no cloud, no account, no paywall. Sandboxed tasks, a plugin at every stage, seconds of overhead at a thousand packages.

vx does one thing — run and cache a task graph, correctly — and stops there. Remote caches, remote execution, telemetry, AI agents, learned scheduling and your own verbs are plugins on documented seams, not features inside: what Nx would be if it weren't a product.

📖 Documentation site → — guides, architecture, caching, and the full CLI / config reference.

# From npm — ships the prebuilt standalone binary (no Bun required):
npm install -g @vzn/vx      # or: pnpm add -g @vzn/vx · bun add -g @vzn/vx
// vx.config.ts
import { defineProject } from '@vzn/vx'

export default defineProject({
  tasks: {
    build: {
      exec: { command: 'tsc -b' },
      dependsOn: ['^build'],
      cache: {
        inputs: { files: ['src/**'] },
        outputs: { files: ['dist/**'] },
      },
    },
    test: {
      exec: { command: 'bun test' },
      dependsOn: ['build'],
      cache: { inputs: { files: ['src/**', 'tests/**'] }, outputs: { files: [] } },
    },
    ci: { dependsOn: ['lint', 'test'] }, // umbrella; runs both
  },
})
vx init                   # scaffold vx.workspace.ts + a vx.config.ts per package from scripts
vx run build              # cwd project + its workspace deps
vx run test --all         # every project that declares `test`
vx run ci --affected      # only what changed since origin/HEAD
vx watch lint             # re-run on file changes
vx run build --dry        # show the plan, don't execute

A cache that actually understands your build

Every task runner caches. vx caches correctly — and stops work others would redo:

  • Config is code, and the cache knows it. vx.config.ts is evaluated before hashing, so imports, presets, and computed values all participate in cache identity. Change a shared preset, and exactly the right tasks re-run.
  • Outputs are owned. Declared outputs are wiped before every execution and every restore. Your tree ends each run bit-identical to the cached snapshot — stale files cannot exist.
  • Hashes come from git. On a clean tree, deriving every cache key costs zero file reads, zero stats, zero database lookups. At 15,000 files that's a 3.2× faster warm path.

Speed is a design discipline

Exact bitset graph algorithms for scheduling. One bulk git enumeration per run, partitioned by binary search. Restores that skip extraction entirely when the tree already matches. In-process tar (no subprocess on the hot path). Atomic artifact publishes. Single-transaction metadata writes. Every optimization is recorded with the invariant that keeps it valid — packages/vx/docs/optimizations.md is the ledger, and packages/vx-bench/ reproduces the numbers.

Built for trust

  • Corruption can't go live. A remote artifact is verified against its content digest and validated before it enters the store (zstd-bomb and oversize downloads refused); bad bytes degrade to a cache miss, never a wrong hit and never a crash.
  • Clean exits. SIGINT/SIGTERM reap every child process — no orphaned dev servers in CI.
  • Readiness you can bound. Persistent tasks gate downstream work on a readyWhen signal; exec.timeout bounds any task (with --timeout / workspace defaults), exec.retries + --retry absorb flakes.
  • Flaky tasks are found, not guessed. The same inputs both passing and failing is the definition, and vx holds every hash and outcome locally: a run names them under its footer, --summarize types them per task, vx info lists them. No service.
  • Kernel-level sandboxing, opt-in per task, that fails the build on violation instead of hiding it.

Reproducible graphs, when you want them

Configs are TypeScript — powerful, but a program's output can vary with its environment. vx lock freezes the fully-resolved task graph into a committed vx-lock.json, pnpm-style:

vx lock                      # evaluate everything once, write vx-lock.json
vx lock --check && vx run ci --frozen     # CI: audit, then run EXACTLY that graph
Command Evaluates configs Uses lock
vx run always, live never — local truth has no asterisks
vx run --frozen never yes; refuses if absent or a config file changed since locking
vx lock --check full graph compares — catches env and import drift that byte hashes cannot

Env values read at lock time are frozen by design — cache keys become reproducible across machines. Bonus: --frozen runs skip config evaluation entirely (~120 ms back per 1,000 packages). No other runner has an equivalent.

Everything you need, nothing to configure twice

TypeScript config with real imports · task graph with ^task resolution that bridges packages without the task · multi-task runs with one shared graph · pnpm-style filters and --affected · watch mode · --dry / --graph plans · persistent dev servers · retries, timeouts, --continue modes · per-layer cache control (--cache=local:r,remote:) · vx why explains a re-run from the persisted input fingerprints · vx last replays a recorded run · vx info, --summarize, --profile Chrome traces, --report · vx cache prune with TTL and size caps · bunx @vzn/vx-migrate from turbo.json or an Nx graph.

A pipeline with seams — plugins decide what happens

Core is the pipeline: discover projects, evaluate configs, build the task graph, derive keys, schedule, execute, cache, observe. Plugins declared in vx.workspace.ts hook each stage, Vite-style, on one VxPlugin object:

Stage Hook A plugin can…
workspace config(ws, ctx) edit the workspace config before it is used
project project(config, ctx) add, remove or rewrite a project's tasks (keyed like yours)
graph graph(nodes, ctx) add or drop edges, mark tasks requested
key key(task, ctx) fold extra material into the cache key (named in vx why)
schedule schedule(nodes, ctx) decide which ready task runs first
execute executor(ctx) decide WHERE a task's command runs (local, a REAPI worker)
store cache(ctx) decide where artifacts live (local, a shared remote)
observe telemetry(ctx) / setup receive every run record, or the raw event bus
cli commands add verbs to vx

Core applies no plugin by default and names none. Running here and caching here are its floor — the tail of every executor list and cache chain — so a workspace with no vx.workspace.ts runs and caches, and a plugin that declines a task hands it back to this machine. First-party plugins: @vzn/vx-reapi (Bazel Remote Execution API — remote cache and remote execution against NativeLink, BuildBuddy, Buildbarn or bazel-remote), @vzn/vx-otel (OpenTelemetry traces + metrics + logs, zero SDK deps), @vzn/vx-github (Actions job summary + Checks API), @vzn/vx-mcp (vx mcp — a read-only Model Context Protocol server for AI coding agents, no SDK), @vzn/vx-migrate (adoption in one package: turbo() runs a Turbo repo under vx with nothing written, bunx @vzn/vx-migrate writes configs from turbo.json or an Nx graph, and turboCache() / nxCache() keep a remote cache speaking Turbo's /v8/artifacts API or Nx's self-hosted spec — the wire is theirs, the artifacts are vx's), @vzn/vx-lockfile (pnpm(), bun(), npm(), yarn(): the lockfile keyed per project, so one install re-keys only the projects it reaches, and --affected follows), and @vzn/vx-schedule-history (order by learned critical path) and @vzn/vx-migrate (bunx @vzn/vx-migrate: turbo.json or an Nx graph → vx.config.ts, through core's migration seam). Core ships no plugin and reads no other runner's format; nothing distributed ships in this repo; the seams are how you build it.

How it compares

vx Turborepo Nx
Fully cached, 100 pkgs¹ 144 ms 279 ms 583+ ms
Config TypeScript, evaluated into the cache key JSON (static) JSON (static)
Output ownership Strict — wiped before exec AND restore Additive (stale files survive) Additive
Clean-tree hashing Zero reads (git index OIDs) git OIDs re-hash / daemon
Daemon required for speed No Optional Yes
Per-task sandbox Yes — kernel-level, opt-in No No
Plugin API Yes — executor / cache / telemetry seams No Yes (TS-tied)
OTel CI/CD spans Yesotel() plugin, zero OTel-SDK deps No Paid
Install Single binary — npm or 1 curl line, no Node/Bun needed npm + Node npm + Node

¹ Wall-clock, direct binaries, same machine and workspace — full methodology and more scenarios in packages/vx/docs/benchmarks.md.

Switching from another runner

Most projects can move in an afternoon. The mapping is mechanical:

// turbo.json (before)
{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["src/**"],
      "outputs": ["dist/**"],
      "env": ["NODE_ENV"],
    },
  },
}
// vx.config.ts (after)
import { defineProject } from '@vzn/vx'
export default defineProject({
  tasks: {
    build: {
      // Name the command (Turbo reads package.json scripts). The child
      // env is ISOLATED: a cache-input env var must also be passed
      // through, or the key would vary while the task can't see it.
      exec: { command: 'tsc -b', env: { passThrough: ['NODE_ENV'] } },
      dependsOn: ['^build'],
      cache: {
        inputs: { files: ['src/**'], env: ['NODE_ENV'] },
        outputs: { files: ['dist/**'] },
      },
    },
  },
})

Differences to know:

  • vx requires exec.command in the config — we don't read package.json scripts implicitly.
  • vx requires cache.inputs.files when caching is enabled (no default $TURBO_DEFAULT$).
  • vx defaults caching off; opt in per task by adding the cache block.
  • Persistent tasks: persistent: { readyWhen: 'regex' } (Turbo uses just persistent: true).
  • Remote caching is a plugin, not a built-in — connect one and every vx run reads through it.

Side-by-side feature matrix + every known gap: packages/vx/docs/comparison.md. What a Turbo or Nx user relies on, spelled in vx and pinned by a test: packages/vx/docs/parity.md.

Architecture (one paragraph)

bin.ts → cli/index.ts dispatches subcommands. orchestrator/run.ts:run() calls prepareRun() which discovers the workspace, loads configs, builds the package + task graph, opens the cache (local SQLite + an optional remote layer), and installs plugins from vx.workspace.ts. The two-tier scheduler runs the graph in topological order with bounded concurrency (confirmed cache hits restore ahead of their deps); each task hits the cache (hash → get → restore on hit; spawn → save on miss) or short-circuits as a group / persistent. Every observation flows through one event bus — the terminal renderer subscribes directly, and plugins receive the versioned telemetry contract (TelemetryRecord / RunSummaryRecord) — that's how telemetry and cache plugins export without core knowing them. Core never imports a plugin; the arrow only points plugin → core. Every module has a docs page; every interface is a swappable seam.

Read packages/vx/docs/architecture.md for the module map; the design record lives under packages/vx/docs/design/.

Documentation

Full technical docs live under packages/vx/docs/ and on the documentation site:

The design record lives under packages/vx/docs/design/; the maintainers' handoff is packages/vx/docs/STATUS.md.

Status

Pre-alpha. The schema is settling; we bump CACHE_VERSION rather than maintain back-compat. ~2,500 core tests plus the package suites; CI green on every commit; the project dogfoods itself (vx run ci). Published on npm: @vzn/vx (a prebuilt standalone binary).

Production readiness for the core task runner: the semantics are solid; it is dogfooded continuously. Linux and macOS, x64 and arm64; Windows runs vx under WSL (POSIX shell is the API), with no native build.

Surface Maturity Notes
Core task runner + caching production-ready dogfooded continuously; ~2,500 core tests + the package suites, green
Plugin pipeline (9 hooks, commands included) shippable crash-isolated, re-validated; the local executor + cache are the floor under every plugin
vx init / @vzn/vx-migrate (scripts; Turbo, Nx) shippable one config per package, TODOs where a source cannot say
REAPI remote cache + execution (@vzn/vx-reapi) shippable Bazel AC + CAS + Execute; NativeLink / BuildBuddy / Buildbarn / bazel-remote
OTel export (@vzn/vx-otel) shippable OTLP traces + metrics + logs, zero SDK deps
GitHub Actions (@vzn/vx-github) shippable job summary + Checks API run
MCP server (@vzn/vx-mcp) shippable vx mcp — read-only tools for AI agents, no SDK
Adoption (@vzn/vx-migrate) shippable turbo(): a turbo.json workspace runs with no vx.config written; turboCache() / nxCache(): any /v8/artifacts or Nx /v1/cache server; the migrate CLI
Lockfile keys (@vzn/vx-lockfile) shippable pnpm() bun() npm() yarn(): per-project dependency-closure keys; --affected follows

Development

git clone https://github.com/vznjs/vx && cd vx
bun install
bun packages/vx/src/bin.ts run ci --all     # lint + test + docs build, every package
bun packages/vx/src/bin.ts run build --filter @vzn/vx   # cross-target binaries → packages/vx/dist/

vx is self-hosted: every dev task routes through bun packages/vx/src/bin.ts run <task> per each package's own vx.config.ts. No package.json scripts; CI invokes vx directly. Start with packages/vx/docs/STATUS.md — the living handoff — and CLAUDE.md.

License

MIT — see LICENSE.

About

Resources

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages