@vzn/vx
v0.0.16
Published
An open, extensible monorepo task runner.
Maintainers
Readme
vx
The fastest way to build a monorepo.
vx runs your task graph, remembers every result, and never does the same work twice. Fully cached runs finish in milliseconds — 144 ms across 100 packages, 0.62 s across a 1090-package graph of 3,270 tasks. Measured, reproducible, on hardware you own.
One binary. No daemon. No Node. Nothing to babysit.
📖 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 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 executeA 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.tsis 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 —
docs/optimizations.md is the ledger, and
bench/ reproduces the numbers.
Built for trust
- Provable cache correctness.
vx run --verifyre-runs each cacheable task and byte-compares outputs (determinism);--verify=inputssandboxes it against the declared inputs and fails loud naming any undeclared read (completeness). No other runner can prove a cache entry safe. - 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
readyWhensignal;exec.timeoutbounds any task (with--timeout/ workspace defaults),exec.retries+--retryabsorb flakes — and retried-then-passed tasks are flagged flaky. - 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 info, --summarize, --profile
Chrome traces, --report · vx cache prune with TTL and size caps ·
vx migrate from turbo.json or an Nx graph.
Extensible by design — the core is provider-neutral
vx runs tasks and nothing else. A dashboard, a remote cache,
distributed execution, and telemetry export all arrive through
plugins declared in vx.workspace.ts, filling three seams the
core exposes — backend (where a task runs), cache (a remote
layer behind the local one), and telemetry (a read-only stream of
the versioned TelemetryRecord / RunSummaryRecord contract). Core
depends on none of them; the arrow only ever points plugin → core.
First-party plugins ship the OpenTelemetry exporter
(@vzn/vx-otel, OTLP
traces + metrics with zero OTel-SDK deps) and vx Cloud (below) —
and the same seams are open to anyone.
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 |
| Provable cache safety | Yes — --verify (determinism) + --verify=inputs (completeness) | No | No |
| MCP server for AI agents | Yes — vx mcp (stdio, reads your local cache) | No | No |
| Plugin API | Yes — backend / cache / telemetry seams | No | Yes (TS-tied) |
| Predictive scheduling | Yes (opt-in: predictive: true) | No | No |
| OTel CI/CD spans | Yes — otel() plugin, zero OTel-SDK deps | No | Paid |
| Self-hosted platform² | Yes — dashboard, distributed CI, remote cache; Docker compose | Vercel-only cache | Paid (Nx Cloud) |
| 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
docs/benchmarks.md. ² Optional, self-hosted,
OSS — see vx Cloud.
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.commandin the config — we don't readpackage.jsonscripts implicitly. - vx requires
cache.inputs.fileswhen caching is enabled (no default$TURBO_DEFAULT$). - vx defaults caching off; opt in per task by adding the
cacheblock. - Persistent tasks:
persistent: { readyWhen: 'regex' }(Turbo uses justpersistent: true). - Remote caching is a plugin, not a built-in — connect one and every
vx runreads through it.
Side-by-side feature matrix + every known gap: docs/comparison.md.
vx Cloud
An optional, self-hosted CI platform — the first-party service you
run yourself. It is a fully independent app (accounts, roles,
organizations, teams, multiple workspaces), with Postgres as the
system of record and an S3-compatible bucket for artifacts; the
controller keeps zero artifact bytes at rest. Core vx never depends
on it — it connects to a deployment through the plugin seams above.
What a connected platform adds:
- Dashboard. Runs, flamegraphs, a live run cockpit with the task DAG, per-task logs + artifacts, cache-key diffs ("why did this re-run?"), and flaky-task detection with the concrete fix.
- Remote cache. The vx-native
/v1/cachewire, tenant-partitioned per org/workspace, with trusted / untrusted tiers derived from the token — a fork-PR artifact can never feed a trusted build — and an always-on integrity digest the client verifies. - Distributed CI (DTE). Agents form a session-keyed pool; a
vx runfans tasks out across same-commit agents, outputs propagate through the shared artifact store, and a standing pool multiplexes concurrent runs with fair scheduling. - MCP over the platform.
POST /mcp(Postgres-backed, behind the token) exposes the analytics as typed tools for AI agents — alongside the corevx mcpstdio server that reads your local cache.
Deploy the stack (app + Postgres + object storage) with Docker Compose, then connect any workspace with one URL and a minted token:
# Deploy — open the URL, register the first admin, mint a CI token under Admin → Tokens:
VX_CLOUD_SECRET=$(openssl rand -hex 32) \
docker compose -f packages/cloud/deploy/docker-compose.yml up
# Connect a workspace (remote cache + analytics + distribution):
vx-cloud connect https://ci.acme.dev --token vxc_…
# …or set VX_CLOUD_URL + VX_CLOUD_TOKEN in the environment.Install the platform CLI with npm i -g @vzn/vx-cloud (a standalone
binary, or the ghcr.io/vznjs/vx-cloud image). Full setup, RBAC,
distributed CI, and the wire protocol live in the vx Cloud section
of the documentation site.
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 docs/architecture.md for the module
map; the design record lives under docs/design/.
Documentation
Full technical docs live under docs/ and on the
documentation site:
docs/architecture.md— module map + data flowdocs/schema.md— every config fielddocs/caching.md— cache-key derivation + invalidation tabledocs/execution.md—vx runlifecycledocs/cli.md— every flagdocs/comparison.md— Turbo / Nx / vite-task feature matrixdocs/modules/— one reference page per source module- The vx Cloud section of the docs site — deploy, RBAC, dashboard, distributed CI, wire protocol
The full design record (including the platform-pivot notes) lives
under docs/design/.
Status
Pre-alpha. The schema is settling; we bump CACHE_VERSION rather
than maintain back-compat. 1,600+ tests (core + packages); CI green
on every commit; the project dogfoods itself (vx run ci).
Published on npm: @vzn/vx
(a prebuilt standalone binary) and @vzn/vx-cloud.
Production readiness for the core task runner: the semantics are solid; it is dogfooded continuously. The main operational rough edge is Windows (unsupported).
| Surface | Maturity | Notes |
| ---------------------------------------------- | ------------------------------ | ----------------------------------------------------------------- |
| Core task runner + caching | production-ready | dogfooded continuously; 1,200+ core tests, all green |
| vx run --verify (provable cache correctness) | shippable | determinism + input-completeness proofs; CI-gate recipe in docs |
| vx mcp | shippable | live cache.db tools over stdio |
| Plugin API (backend / cache / telemetry) | shippable | crash-isolated; the cloud + OTel plugins are ordinary plugins |
| Predictive scheduling | shippable as opt-in | gated on predictive: true + observed data |
| OTel export (@vzn/vx-otel) | shippable | declare otel() in vx.workspace.ts; OTLP traces + metrics |
| vx Cloud platform (self-hosted) | shippable for self-hosting | accounts/RBAC/orgs on Postgres, S3 artifacts, dashboard, DTE, MCP |
Development
git clone https://github.com/vznjs/vx && cd vx
bun install
bun src/bin.ts run ci # format-check + lint + test
bun src/bin.ts run build # cross-target binaries → dist/vx is self-hosted: every dev task routes through bun src/bin.ts run <task> per the repo's own vx.config.ts. No package.json scripts; CI invokes vx directly.
License
MIT — see LICENSE.
