Skip to content
vxvx
GitHubBlueskydev.toRSS

CLI reference

Every vx command, flag and output. Each verb has a section below.

Terminal window
# The prebuilt standalone binary; inside a project
# that installs @vzn/vx, it runs that one (§ Which vx runs):
bun add -g @vzn/vx
# From source (Bun ≥ 1.4.4):
bun src/bin.ts --version

On a Bun older than 1.4.4, vx still runs, and vx info’s bun row warns. What breaks is the output, not the start:

  • a large --format json write is cut off;
  • no task reports what it used;
  • a config syntax error shows as an internal error.

The released binary carries its own Bun, so the row never warns there. modules/util-bun-version.md has the measurements.

Page Covers
vx run vx run, Planning mode, Run artifacts, Sandbox
vx watch vx watch
vx show vx show
vx why vx why
vx last vx last
vx info vx info
vx lock vx lock, Runs read the lock
vx cache clean vx cache clean
vx init vx init, bunx @vzn/vx-migrate
vx upgrade vx upgrade
vx docs vx docs
vx completions vx completions
Plugin commands Plugin commands
Environment variables vx reads Environment variables vx reads
Output format Output format
Terminal window
# Core
vx run [OPTIONS] [TASK | PKG#TASK ...] [-- forwarded-args...]
vx watch [OPTIONS] [--poll] TASK [-- forwarded-args...]
vx cache clean [--older-than <duration>] [--max-size <size>] [--global] [--dry] [--format pretty|json] [--cache-dir <path>]
vx lock [--check] [--format pretty|json]
vx init [--dry [--format pretty|json]] [--force] [--mjs] [--native|--keep] [--plugin <hook>]
vx show [PROJECT[#TASK] | TASK] [--filter <pattern>] [--affected[=<ref>]] [--format pretty|json]
vx info [--format pretty|json] [--cache-dir <path>]
vx why (TASK | PKG#TASK) [--run RUNID] [--format pretty|json] [--cache-dir <path>]
vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--stats [<task>] [--branch <name>]] [--sql <query>] [--live [--follow]] [--format pretty|json] [--cache-dir <path>]
vx upgrade [TAG] [-g|--global]
vx docs <query> [--limit N] [--format pretty|json]
vx completions bash|zsh|fish
# Meta
vx # in a workspace, vx --help; outside one, says so and exits 1
vx help [VERB]
vx --help, -h
vx version
vx --version

Each verb’s lines are the usage lines vx <verb> --help prints, word for word (tests/cli-help-synopsis.test.ts); the flags a verb accepts are read from that line.

Several tasks run as one graph: vx run build lint test runs all three across the selected projects.

  • pkg#task names one project’s task.
  • A bare name runs in the cwd project, or wherever --all, --filter or --affected points.

The cwd project is the deepest project holding the directory, one reached through a link (packages/b -> ../ext/b) included.

Every requested name must resolve. When a name matches no project in scope, the run and --dry both refuse, exit 1, and print to stderr: vx run: no projects declare task(s): <name>.

The refusal adds what helps:

  • Did you mean <task>? when a declared task (or, for pkg#task, a runnable one) is within two edits. A pkg that is a project’s directory, relative to the root, names that project: p/a#build hints alpha#build for alpha at p/a.
  • Otherwise what exists: Tasks: build, test., No project is named zzz; projects: a, b., or a's tasks: build, test.
  • With no task declared anywhere (right after vx init): No project declares a task yet: add one under `tasks` in a vx.config, then run `vx lock`.
  • A pkg that matches a project only after Unicode normalization (café typed in NFC, the project’s directory in NFD) is said to differ in Unicode form, with the fix: set name in its vx.config.
  • Otherwise A run reads the lock: after editing a vx.config, run `vx lock`., since a task added since the last lock is not in it.
  • Only projects outside the selection declare &lt;name&gt; — pass --all, or a --filter that includes them. when the run was scoped and a project outside the scope declares it, then the lock line, since a task just added to a selected project’s config is not in the lock yet. A selected project whose build is the default one (a root with an empty config) is named instead: vx show counts that build, but no bare name runs it.

A name declared by only some projects is fine; the check fires only when a name matches nowhere. So vx run lint test typecheck fails on CI the day typecheck is renamed, instead of running two of three.

A name no config can declare is refused before the lookup: an empty one (vx run '') and one with surrounding whitespace (vx run 'build '), which the refusal quotes so the space shows.

Under --affected or a [ref] filter, a bare name is checked against the whole workspace, since what changed decides the projects:

  • A change that reached no project declaring test exits 0 with No affected project declares task(s): test. A name no project declares is still refused.
  • A change that reaches no requested task exits 0 with Nothing affected: the change reaches no test task.
  • A diff that touched no project exits 0 with nothing affected since <ref> on stderr, once each bare name is found declared somewhere: a name no project declares is still refused.

There is no -V; use vx --version, as in Turbo.

vx <verb> --help (also -h, vx help <verb> and vx --help <verb>) prints this reference for that verb: its usage lines and sections (of a section several verbs share, only its own entries), then Full reference: vx help. Every argument error points at it. vx help <plugin verb> is vx <plugin verb> --help: the plugin answers.

vx help <name> for a name that is no verb is refused like vx <name>: one line, a guess when one is close, exit 1. A second word after the verb is refused too.

After --, a flag belongs to the task: vx run build -- --help passes --help to it. A plugin verb handles its own arguments, --help included.

Releases are on demand. Dispatching auto-release.yml releases a commit on main whose CI went green: the sha input, or by default the newest such commit; one CI did not pass on is refused. Claude dispatches it at least once a day when main has changes, after merging that release’s blog post (.claude/skills/release/SKILL.md), and shares the post on Bluesky. The workflow tags the commit with the next version and creates the GitHub release, both from the Conventional Commits since the last tag (scripts/release-notes.ts, run by the release.auto task, scripts/auto-release.ts). The release is created as a draft: this repo’s releases are immutable and take assets only before they are published, so release.yml publishes it after its last upload. The version: below 0.1.0 always a patch (cutting 0.1.0 is the owner’s, by hand); then before 1.0 a feat or a breaking change (type!:, a BREAKING CHANGE: footer) is a minor (v0.4.2 → v0.5.0) and anything else a patch (v0.4.3); from 1.0 a breaking change is a major. The notes: breaking changes first, then feat, fix and perf, the rest counted. It then dispatches release.yml (with tag) and npm.yml (with version and ref). A release made with the workflow token fires no release event in other workflows, which is why the two are dispatched rather than triggered. A commit is released only when the last release is its ancestor, so an older tree never gets a higher version, and one that already carries a v* tag is a no-op.

A version can still be cut by hand: push its tag (say v1.0.0), create a draft release for it, and dispatch release.yml (tag) and npm.yml (version, ref); the next auto-release continues from it.

A GitHub release publishes everything: release.yml builds the four binaries, proves the darwin ones on macOS (re-signed ad hoc only where macOS refuses one) and attaches them; npm.yml builds the fifteen npm packages (@vzn/vx, one per platform, and every other public package: the plugins, the servers, @vzn/vx-devtools) and publishes them with npm trusted publishing — the job’s OIDC token is exchanged for a short-lived credential and provenance is attached, so no long-lived npm token exists anywhere. Both publish loops skip a package already on the registry, so a re-run (npm publish → Run workflow with the version) resumes where it stopped. That dispatch also takes a ref: it runs the workflow from the default branch against the code that ref names, so a release whose publish died on a workflow bug is completed by the FIXED workflow building the tag’s own source (version: 0.0.20, ref: v0.0.20) — re-running the failed run itself would replay the broken file, which is pinned to the tag.

Every step of the three workflows past the setup (checkout, Bun, Node, bun install, the runner action) is one vx task: CI runs no command of its own (2026-10-03), and tests/workflow-runner.unsafe.test.ts holds it. The tasks are @vzn/vx’s release.*, all uncached and in no ci graph:

Task Does
release.stamp writes VX_RELEASE_VERSION into packages/vx/package.json, the manifest the binary inlines
release.npm checks npm can publish with provenance (>= 11.5.1, sigstore loads); else installs npm into dist/npm-cli/
release.prove.<os> launches the host’s binary (re-signed only if macOS refuses it) and asserts vx <version>; on darwin, launches both (x64 under Rosetta)
release.assemble.<os> emits the platform packages under dist/npm (linux: also @vzn/vx and the plugins)
release.publish.<os> npm publish --provenance each in order, skipping one the registry holds
release.served waits (up to 10 min) until npm’s CDN answers 200 for every published tarball: it 404ed v0.0.646’s for minutes, failing installs
release.images pushes the three server images to ghcr.io/vznjs/<server> at the version (amd64 + arm64); latest follows npm’s
release.upload.<os> attaches dist/vx-<os>-* the draft release lacks; darwin’s then publishes it
release.auto decides the version, creates the draft release with its notes, dispatches the two workflows

The version reaches them as VX_RELEASE_VERSION (the tag or the dispatch input) and each refuses one that is not a version. Every one is sandboxed: the publish is granted the registry, the OIDC token host and Sigstore, the GitHub-Actions variables npm reads, and ~/.npm; release.auto reads .git and reaches api.github.com with GH_TOKEN; release.upload.<os> reaches it and uploads.github.com. A dispatch with ref builds that ref’s code, so it must hold these tasks.

Every Linux job that runs a task runs vx setup github (@vzn/vx-ci) before its first vx run: every task in this repo declares exec.sandbox, and a declared sandbox whose runtime is missing is a hard error, not a downgrade — the compile tasks fail in 0 ms saying which of bubblewrap, socat and ripgrep is absent. v0.0.20 shipped its darwin packages and then stopped exactly there.

One-time setup, per package, on npmjs.com → package → Settings → Trusted Publisher → GitHub Actions: owner vznjs, repository vx, workflow npm.yml, environment left blank. Do this for @vzn/vx, @vzn/vx-darwin-x64, @vzn/vx-darwin-arm64, @vzn/vx-linux-x64, @vzn/vx-linux-arm64 and the six plugins: @vzn/vx-ci, @vzn/vx-lockfile, @vzn/vx-migrate, @vzn/vx-otel, @vzn/vx-reapi and @vzn/vx-history.

Each platform package carries LICENSE and THIRD_PARTY_NOTICES.txt: the binary embeds Bun and npm code whose licenses must travel with it. The GitHub release carries the same file as an asset beside the binaries. tests/dependency-licenses.unsafe.test.ts holds every published package’s production dependency closure to permissive licenses (MIT, ISC, BSD, Apache-2.0, 0BSD, or a dual license offering one). After a dependency or Bun bump, bun packages/vx/scripts/third-party-notices.ts regenerates the notices; tests/third-party-notices.unsafe.test.ts fails until it does.

The plugins ship as the TypeScript source Bun runs, at the release’s version, with @vzn/vx as a peer on the same minor (^<version>). scripts/build-npm.ts --only=plugins emits every public workspace package other than @vzn/vx, so a new plugin package is published without a workflow edit, and tests/build-npm.unsafe.test.ts holds the set. If npm will not add a trusted publisher to a name that has never been published, publish that plugin once by hand from an owner’s account (npm publish dist/npm-plugins/plugins/<dir> --access public after a build-npm.ts <version> --only=plugins --out=dist/npm-plugins build), then add the publisher. Then delete the NPM_TOKEN repository secret: the workflow no longer reads it, and npm restricts classic tokens for direct publishing (the E401 token is invalid that stopped v0.0.17). Every uses: in both workflows is pinned to a commit SHA with the version in a comment; bump the SHA and the comment together.

Every verb works on one workspace: the directory whose .vx holds the nearest .vx/workspace.ts (or .mts, .js, .mjs, .cts, .cjs) from the working directory up. vx never looks above the git top-level (a worktree is its own, a submodule’s too; a submodule, its .git a file or a directory, looks on into its superproject), and outside git it looks up to /. Nothing else marks the root: not a package.json, pnpm-workspace.yaml or a lockfile (2026-10-13). Any entry by one of those names marks the root: one that is a broken link or a directory, with no file beside it, is refused by name (<path>: is a broken link — fix or remove it), not walked past, as vx lock counts it a nested workspace. A workspace file a sparse checkout left out (a cone checkout keeps the root’s files, not its .vx) is refused as such, VX_E_NO_WORKSPACE with the fix git sparse-checkout add .vx, by every verb, vx init too: a new one would replace the tracked one. Git is asked only once no file was found. Without one, every verb but vx init stops with VX_E_NO_WORKSPACE; vx init writes one (@vzn/vx-migrate beside a turbo.json, nx.json, exported Nx graph or Lerna on Nx). A project inside the root is a directory holding a vx.config.*, which vx lock finds (see vx lock); a discover plugin adds others.

A global vx (installed with a package manager, or a compiled binary installed by hand) hands every call to the project’s own @vzn/vx when there is one, so a project runs the version its lockfile pins (2026-10-13). There is no opt-out. The workspace root’s node_modules/@vzn/vx runs, through its node_modules/.bin/vx. One hop: the receiving vx sees VX_HANDED_OFF and does not look again. A vx never hands off to itself (the same package, a pnpm symlink included), nor to a .bin/vx another user owns (one planted above your checkout would run as you). An npm install’s launcher does the walk in the shell, a test per level and no fork, and execs the local vx; the compiled binary checks one path per level before it loads anything, then runs the local vx with its stdio, passes on a signal sent to it alone, and exits with its code. From source (bun src/bin.ts) vx never hands off.

vx --version says when it was handed the call (vx 0.0.640 (/repo/node_modules/@vzn/vx-linux-x64/vx, handed off from /usr/local/bin/vx)), and vx info shows the file that ran and who handed off (vxPath, handedOffFrom).

vx show, vx info, vx why, vx last and vx cache clean take --format json. Each prints one JSON document on one line, whose shape is a checked-in JSON Schema (draft 2020-12) shipped with the package: schemas/show.json, schemas/info.json, schemas/why.json, schemas/last.json, schemas/cache.json (node_modules/@vzn/vx/schemas/ in an install). Every object closes its key set, so a field vx adds is a schema change, never a surprise. tests/cli-json-schemas.test.ts holds each verb’s output to its schema, each declared field to some output, and each object’s keys to the source type. tests/cli-json-doc.test.ts holds this page to the schemas: each { … } a verb’s section shows is an object its schema closes with those keys, each object the verb prints whole is shown, and vx info’s field list is its schema’s top level.

A run’s result as JSON is vx last --format json, read after the run from the run history: one row per task with its status, key and cache result. vx run --dry=json prints the plan.

A verb asked for JSON (--format json, --format=json, --dry=json, before any --) answers a refusal on stdout too, as one line that schemas/error.json describes: {"ok":false,"error":{"code","message","docs"}}. docs links the code’s section below, which says what to do; vx docs <code> prints that section. The exit code (1) and the prose on stderr do not change. Branch on the code; the message may change.

A flag, argument, selection or VX_* environment value the verb does not take. The message names it, and a close spelling the verb does take when there is one; vx <verb> --help lists every flag.

No verb of that name here, core’s or a declared plugin’s. vx --help lists core’s verbs. A plugin’s verb exists once its plugin is in .vx/workspace.ts plugins; a task runs as vx run <task>.

No project in scope declares a requested task. The message lists the tasks it found; vx show --format json lists each project’s. Check the spelling, and the scope: --filter, --all, or the project the working directory is in. A run reads its configs from the lock, so a task added to a vx.config since the last vx lock is not there yet: run vx lock.

vx show names a project the workspace does not have. A project is named by its config’s name, else by a plugin (a lockfile plugin names it from its package.json), else by its directory; vx show --format json lists them.

vx why / vx last find no recorded run for what they were asked, or vx last --live no run in progress. Run the task once (vx run <task>); history is this checkout’s own. Where nothing has run yet and no project declares the task, vx why says so and points at vx show instead.

vx why <task> matches the task in several projects. Name one: vx why <project>#<task>. A run id that is the start of several recorded runs (vx last <id>, --run <id>) is refused the same way: type more of it.

A vx.config.* or .vx/workspace.* that does not load, throws or is refused. The message names the file and the field, or what the config (or a plugin it called, such as a factory refusing an option) threw and the line it threw at, then each cause it wraps (; caused by: …); fix that field (vx docs <field> prints its reference). vx lock names every project config that does not load at once. A project config’s refusal is vx lock’s, not a run’s: a run never evaluates one.

The task graph has a cycle. The message names the loop; drop one of its dependsOn edges. vx lock refuses it, not a run: a run trusts the graph the lock holds.

A run reads its configs only from .vx/projects.lock.json, and that lock is missing, not one this vx reads (not JSON, another version, a hand-edited entry), holds no entry for a project the workspace has, has not seen a vx.config written since, or locks one that is gone. Or a plugin claims a lockfile (bun(), pnpm(), …) and .vx/dependencies.lock.json is missing, not one this vx reads, does not record that lockfile, or records bytes it no longer holds: it changed since vx lock (an install). Run vx lock, then the verb again. Commit the lock; vx lock --check on CI fails one that no longer matches the configs.

No .vx/workspace.* in the working directory or a parent, up to the git top-level (a file above the checkout never counts). The file marks the workspace root, so run vx init to create one. Outside git, one another user owns does not count (security.md); the message names it. One a sparse checkout left out names git sparse-checkout add .vx instead, and vx init refuses rather than write a second one.

A reading verb met a cache index from another vx version. It reads nothing from it and leaves it be; the next vx run resets it. Or run the vx version that wrote it.

Any other refusal (a plugin’s, a check’s) with no code of its own. The message is that plugin’s or check’s: do what it says. vx info --format json lists the plugins.

The file system refused a read or a write, or the cache index could not be opened: permissions, a full disk, a path longer than it allows (ENAMETOOLONG, an output past PATH_MAX), a file where the cache directory goes, a directory where cache.db goes. The message names the path. Free space (vx cache clean --max-size <size> trims the cache), grant the access, shorten the path, or move it away, then re-run.

Out of file descriptors. Raise the limit (ulimit -n 4096), or run fewer tasks at once (--concurrency), then re-run.

--affected (or --affected=<ref>) has no base to diff against: the ref does not resolve, is a range (A..B) or an exclusion (^A), names a path inside a commit, or the repository has no history to compare with. Pass a branch or commit you have locally, alone: --affected=origin/main. A shallow clone fetches its base from origin itself; when origin cannot give it, fetch it (git fetch --depth=1 origin <base>), and with no origin remote, from its URL (git fetch --depth=1 <url> <base>) or add one (git remote add origin <url>).

vx needs git and found none: git is not on PATH, or the workspace is not inside a git work tree. The message says which. Install git, or run git init in the workspace root, then re-run.

A git command vx runs to read the diff or list a run’s inputs failed; the message carries git’s exit and its own words. Run that command in the workspace to see why (a broken index, a missing object in a partial clone).

vx docs could not read its pages from GitHub: no network, a proxy that refuses, or GitHub’s limit on anonymous listings. The message carries what failed. Retry with a network, set GITHUB_TOKEN or GH_TOKEN to lift the limit, or read the same pages on the site, https://vznjs.github.io/vx/.

A task’s dependsOn names a task or project that is not declared, or uses a form vx does not take (a bare wildcard, a negation, a pattern in pkg#task). The message names the task and the entry: declare the target, fix its spelling, or drop the entry. vx lock refuses it, not a run: a run trusts the graph the lock holds.

A ^task with no edge source is refused too: &lt;id&gt;: ^build needs project edges; add pnpm()/bun()/npm()/yarn() and install so its lockfile exists, or a plugin that returns `deps` . A lockfile plugin whose lockfile is absent gives no edges. Core reads no edge from a manifest, so with no plugin returning deps every ^ would resolve to nothing and fold no upstream into the key. Declare the lockfile plugin in .vx/workspace.ts (or a plugin of your own that returns deps); a run refuses it as well when the plugin left the workspace after the lock.

Two tasks declare overlapping outputs with no dependsOn edge between them, so whichever runs second deletes the other’s files. Give each task its own output path, or make one depend on the other. vx lock refuses it, not a run: a run trusts the graph the lock holds.

A task’s inputs match outputs: its own (vx cleans outputs before a run, so it would delete its sources; files and workspaceFiles are compared across, read from the root) or another task’s (its key must not read them; dependsOn already cascades). Exclude the outputs from the inputs with a ! glob, as the message shows, or declare no outputs for a task that rewrites files in place. vx lock refuses it, not a run: a run trusts the graph the lock holds. A run refuses the one shape the lock cannot see: an output directory that is a symbolic link into a directory the task’s own inputs can match below (dist -> src under src/** or **/*.ts). Remove the link.

A task’s inputs.files entry is rooted inside a project nested in its own (dist/** where dist/ holds a config): a project’s globs never reach another project’s files, so it would key nothing. Depend on a task of the nested project, or name the files in inputs.workspaceFiles, as the message shows. A glob rooted in a nested workspace (dist/ holding its own .vx/workspace.*) is not refused: it keys that workspace’s files. vx lock refuses it, not a run: a run trusts the graph the lock holds.

A task’s outputs.files entry is rooted inside a project nested in its own (sub/dist/** where sub/ holds a config): a project’s globs never reach another project’s files, so it would save nothing. Let a task of the nested project write the files, or name them in outputs.workspaceFiles, as the message shows. A nested workspace (sub/ holding its own .vx/workspace.*) is refused the same way, though an input glob may reach into one. vx lock refuses it, not a run: a run trusts the graph the lock holds.

vx lock found a project depending on one a run.rules.dependencies rule denies or does not allow. A header counts the broken edges and each indented line under it names both projects and the rule. Drop the dependency, or change the project’s tags or the rule.

A defect in vx: stderr carries the stack, then each cause under it as a Caused by: line with its stack. (A config’s own throw is VX_E_CONFIG; a plugin hook’s is VX_E_REFUSED or the plugin’s code.) Report it with that stack at https://github.com/vznjs/vx/issues.

A plugin can give its refusal a code: new UserError(message, 'MY_CODE'). The code holds whether a hook, setup or a verb of the plugin throws it.

Streams. A verb whose stdout is a product (a --format json document, --dry / --dry=json, a completion script) writes that product alone there; a notice or warning it meets on the way (a cache index from another vx version, a plugin’s warning) goes to stderr, as every refusal does. vx run‘s stdout is the run’s frame, the tasks’ output it carries. tests/cli-streams.test.ts holds each product verb to it under a notice.

Core ships no remote-cache wire client — the remote cache is a plugin concern. A cache plugin returns a wire client (a RemoteCacheLayer) and core layers it over the local cache; @vzn/vx-reapi provides one for any Bazel REAPI server (ActionCache + CAS).

Reads try local first, then remote (hydrating local on remote hit), with a background prefetch pass overlapping remote GETs with execution. Writes go to local immediately; the remote upload is a fire-and-forget background task drained at end of run — failures are warned but never fail the build.

For any OTHER cache server (a Turbo-wire deployment, S3-direct, …), implement core’s RemoteCacheLayer interface in a plugin’s cache capability — the recipe lives in the plugins guide. Embedders holding a wire client can inject it per-run via RunOptions.remoteCache (explicit injection wins over the plugin consult).

vx info surfaces the aggregate cache stats (entry count, total size, runs + hits in the last 24 h). For anything deeper, vx records every task to a runs table in cache.db (UUIDv7 run_id, hrtime wallclock spans, cpu_ms, peak RSS, status, cache_hit flag) plus one invocations header row per run (command, git/CI context, counts). The SQLite file IS the API:

Terminal window
sqlite3 .vx/cache/cache.db "
SELECT project, task, status, duration_ms
FROM runs
WHERE run_id = (SELECT run_id FROM runs ORDER BY id DESC LIMIT 1)
ORDER BY duration_ms DESC;
"

The schema is documented in caching.md § SQLite tables.

Tracked in comparison.md. Nothing visible from the CLI is open: --output-logs hash-only, --continue=<mode> and --cache-dir <path> all shipped and are documented above. Remote-cache credentials are not core CLI flags at all: core carries no HTTP cache client — a remote cache arrives through a plugin’s cache capability, which owns its own configuration.

import { run, planRun, defineProject, defineWorkspace } from '@vzn/vx'
const summary = await run({
cwd: process.cwd(),
tasks: ['build', 'test'],
concurrency: 4,
// Optional 4-axis cache control; omit for everything-on.
cache: { localRead: true, localWrite: true, remoteRead: true, remoteWrite: true },
})
// summary.ok: boolean; summary.outcomes: TaskOutcome[]
const plan = await planRun({
cwd: process.cwd(),
tasks: ['build'],
})
// plan.tasks: PlannedTask[]

Surface:

  • run(options) — execute. Returns Promise<RunSummary>.
  • planRun(options) — predict, no execute. Returns Promise<RunPlan>. Used by --dry.
  • prepareRun(options, log) — the shared setup (discovery → configs → graph → cache). What an embedder builds on.
  • defineProject / defineWorkspace — identity helpers for type inference in user configs.
  • Every one of the three returns a type the façade exports, so an embedder can hold it: RunOptions / RunSummary / TaskOutcome, RunPlan / PlannedTask, PreparedRun — alongside the plugin (VxPlugin, TaskExecutor, CacheLayer) and telemetry (TelemetrySink, RunSummaryRecord) surfaces. See src/index.ts.

A log: Logger option lets embedders swap the default framed-block logger for a custom one (e.g. JSON-line emission). Custom loggers always see plain text (colors are off when a non-default logger is provided).

The CLI dispatcher (run(argv) in src/cli/index.ts) is not part of the public package exports; bin.ts calls it directly.

Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.