Every vx verb now speaks in one shape and says how to fix what it refuses, a CI run keeps the logs and outputs you need to debug it, a new branch forecasts from main's timings, and a run your local cache answers stays off the network.
Every vx verb now speaks in one shape and tells you how to fix what it
refuses. A CI run keeps the logs and outputs you need to debug it, a new
branch forecasts from main’s timings, and a run your local cache answers
stays off the network.
There are breaking changes, mostly in what vx prints, so read the
list at the end if a script reads vx’s output.
A refusal used to be one long vx run: … line, with the suggestion and
the help pointer packed into parentheses at the end. Each verb also
printed its own way, so vx info, vx lock and a migration report
looked like three different tools.
Now every verb draws from one set of parts. A refusal is a red ✗ with
the verb and the problem, then the facts one per line, and the fix in
bold:
Terminal window
$vxrunbuild--concurency4
✗vxrununknownflag:--concurency
seevxrun--help
fix--concurrency
$vxbuild--all
✗vx`build`isataskhere,notacommand
fixvxrunbuild--all
A config typo names the file at its line and shows the source, from any
verb:
Terminal window
$vxrunbuild--all
✗vxrunconfigrefused
.vx/workspace.ts:4:3
unknownfield"concurrency"
allowed:cache,lock,plugins,run,watch
2|
3|exportdefaultdefineWorkspace({
>4|concurrency:4,
|^
Success is one green line, and a verb with nothing to do says so instead
of refusing:
Terminal window
$vxlock
✓vxlock1projectconfig→.vx/projects.lock.json
$vxwhyapp#build
›vxwhynorunsofapp#buildyet
fixvxrunapp#build
vx info, vx show, vx upgrade, vx cache clean, vx last,
vx init and bunx @vzn/vx-migrate use the same parts: a header
naming the verb or task, then the facts in one aligned column, and the
next command last. vx show lists projects as a table:
Plugins draw with the same parts: @vzn/vx exports them as ui
(ui.notice, ui.status, ui.report, ui.fields, ui.table), so a
plugin’s output looks like vx’s (Draw the terminal).
--format json keeps its flat message.
When a task failed on CI, its log and the reports it wrote, a junit file
or a coverage dump, died with the runner. Nothing could show you that
run later.
Now a run that writes a remote cache (CI by default) keeps each failed
task’s log and its declared outputs, and each uncached task’s log, in
that remote. Nothing extra goes to your local cache, which already has
them. At the end, each plugin’s teardown(run) gets a report: the run
id, commit, CI run URL and job, and every task with its status,
duration, key and the names of what vx kept. ctx.cache.getRun(name)
reads those back on any machine.
vx writes no manifest of its own; a plugin decides what to record. This
one keeps every run and adds a verb that prints its logs:
import { definePlugin, type PluginCache, type VxPlugin } from'@vzn/vx'
exportfunctionrunLog():VxPlugin {
let cache:PluginCache
const text = (b:Uint8Array|null) => newTextDecoder().decode(b ?? newUint8Array())
returndefinePlugin(import.meta, {
setup(ctx) {
cache = ctx.cache
},
asyncteardown(run) {
if (run) await cache.put(`run/${run.runId}`,newTextEncoder().encode(JSON.stringify(run)))
Last release taught vx how long each task usually takes, from the
median of its last five timings on your branch. A fresh CI runner or a
branch you just made had no timings yet, so its first run guessed.
With a remote cache declared, scheduleHistoryPlugin() from
@vzn/vx-schedule-history now carries timings there, with nothing to
configure. A CI run on main writes each task’s usual time under its
commit. When a task has no timing on your branch, the plugin reads the
timings at your branch’s merge-base with main, or up to four commits
before it. It waits at most 500 ms, and a run whose tasks all have
timings asks the cache nothing.
A forecast now reads this branch first, then main’s, then any branch’s.
defaultBranch names your main branch if it is not main. Plugins get
the same store: ctx.cache has get, put, has and hasMany, and
an entry, once written, is never replaced.
The run’s footer said only how many tasks missed. It did not say what
the local and the remote cache each did, or whether results went up for
the next run. Now each layer gets its own counts:
↑ is saved to that layer, ↓ restored from it, ● already on disk
and ○ a task with no cache. With a remote, a second half follows, such
as · remote 3↓ 1↑. N not saved, N not downloaded and N ↑ failed
show only when they are not zero.
That line also shows a fix: a run whose every task was in the local
cache still asked the remote which keys it held before starting. vx now
asks the local cache first and the remote only for what it lacks, so a
run the local cache answers makes no network call.
One edit could start a cycle and then one or two more, re-running tasks
for nothing. Three things the run itself did were read as your edits: an
output a cache hit restored, an output another task read through a glob
like **/*, and, on macOS, a write at a project’s top level. Watch now
tells all three apart from an edit, so an edit runs one cycle.
The second case was a real gap in the configs too: a task reading
**/* beside another’s **/dist/** hashed files it did not own.
turbo() and nx() now take those outputs back out of the reader’s
inputs, and vx lock refuses a hand-written config like that and names
the ! entry to add.
cache.outputs.clean: false keeps a task’s outputs on disk before a
miss, for tools such as tsc that read their last output. Until now vx
refused to save the entry if a file from before the run was left
unchanged, so a task like that often never cached at all.
Setting clean: false is now a plain opt-out: vx saves everything the
output globs match, leftovers included, and a hit restores all of it on
every machine. Set it on a task, or for every task in
.vx/workspace.ts:
vx run --affected --dry names the workspace file that selected every
task: affected: .vx/workspace.ts changed (workspace-wide), and
--dry=json gives { "kind": "workspace", "file": … }.
In a Turbo or Nx repo, what vx cannot map is reported once by
vx lock, not before every run.
bunx @vzn/vx-migrate maps an Nx repo with no package.json, with the
edges written out.
Every worktree of a repo now shares the flaky record.
A live run now shows what vx itself is doing and what each idle worker waits on, the footer counts remote failures and uploads in flight, --force no longer re-uploads what the cache holds, vx lock refuses fewer configs that were fine, and vx run sheds nine flags.
vx now runs projects in any language, every run starts from one reviewed lock, pull requests build on what main cached, and a run tells you how long it has left.
Inputs move onto the task, so every task is keyed whether it caches or not. A gitignored file or a symlink you name is an input. A test can read what its build wrote, vx upgrade moves your plugins with it, and a run waits on less.
Turborepo, Nx and other product names are trademarks of their owners. vx is not affiliated with or endorsed by them.