@vzn/vx 0.0.635 → 0.0.637
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -19
- package/docs/caching.md +222 -210
- package/docs/cli.md +699 -664
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +31 -32
- package/docs/schema.md +418 -202
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +2 -2
- package/schemas/show.json +13 -4
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -61
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +123 -144
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- package/src/config.ts +191 -71
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +25 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +192 -70
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +43 -28
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +114 -73
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +127 -88
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +251 -241
- package/src/workspace/dependency-rules.ts +57 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +22 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +242 -146
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +362 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/docs/cli.md
CHANGED
|
@@ -8,7 +8,8 @@ in `src/cli/index.ts` dispatches to per-subcommand handlers under
|
|
|
8
8
|
so existing Turbo users can swap in with minimal muscle-memory churn.
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
# Standalone binary via npm (no Bun required on target)
|
|
11
|
+
# Standalone binary via npm (no Bun required on target); inside a project
|
|
12
|
+
# that installs @vzn/vx, it runs that one (§ Which vx runs):
|
|
12
13
|
npm install -g @vzn/vx
|
|
13
14
|
|
|
14
15
|
# From source (Bun ≥ 1.4):
|
|
@@ -28,15 +29,15 @@ released binary carries its own Bun and the row never says it.
|
|
|
28
29
|
```
|
|
29
30
|
# Core
|
|
30
31
|
vx run [OPTIONS] [TASK | PKG#TASK ...] [-- forwarded-args...]
|
|
31
|
-
vx watch [OPTIONS] TASK [-- forwarded-args...]
|
|
32
|
-
vx cache
|
|
32
|
+
vx watch [OPTIONS] [--poll] TASK [-- forwarded-args...]
|
|
33
|
+
vx cache clean [--older-than <duration>] [--max-size <size>] [--global] [--dry-run] [--format pretty|json] [--cache-dir <path>]
|
|
33
34
|
vx lock [--check] [--format pretty|json]
|
|
34
35
|
vx init [--dry [--format pretty|json]] [--force] [--mjs] [--native|--keep] [--plugin <seam>]
|
|
35
36
|
vx show [PROJECT[#TASK] | TASK] [--filter <pattern>] [--affected[=<ref>]] [--format pretty|json]
|
|
36
37
|
vx info [--format pretty|json] [--cache-dir <path>]
|
|
37
38
|
vx why (TASK | PKG#TASK) [--run RUNID] [--format pretty|json] [--cache-dir <path>]
|
|
38
|
-
vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--format pretty|json] [--cache-dir <path>]
|
|
39
|
-
vx upgrade [TAG] #
|
|
39
|
+
vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--stats [<task>] [--branch <name>]] [--format pretty|json] [--cache-dir <path>]
|
|
40
|
+
vx upgrade [TAG] # upgrade vx the way it was installed
|
|
40
41
|
vx docs <query> [--limit N] [--format pretty|json]
|
|
41
42
|
vx completions bash|zsh|fish
|
|
42
43
|
|
|
@@ -128,11 +129,11 @@ If no task name is given:
|
|
|
128
129
|
|
|
129
130
|
Exit codes:
|
|
130
131
|
|
|
131
|
-
| Code | When
|
|
132
|
-
| --------------------- |
|
|
133
|
-
| `0` | Every task finished `success` or `cache-hit` (local or remote); or `--affected` left no project that declares the task.
|
|
134
|
-
| `1` | At least one task ended `failed` or `skipped`; a persistent task exited non-zero after it was ready; a task name no project declares; or parse/setup error.
|
|
135
|
-
| `130` / `143` / `129` | Interrupted (SIGINT / SIGTERM / SIGHUP): each task's process group (the task and what it forked) gets vx's signal once (a SIGHUP as a SIGTERM), `
|
|
132
|
+
| Code | When |
|
|
133
|
+
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
134
|
+
| `0` | Every task finished `success` or `cache-hit` (local or remote); or `--affected` left no project that declares the task. |
|
|
135
|
+
| `1` | At least one task ended `failed` or `skipped`; a persistent task exited non-zero after it was ready; a task name no project declares; or parse/setup error. |
|
|
136
|
+
| `130` / `143` / `129` | Interrupted (SIGINT / SIGTERM / SIGHUP): each task's process group (the task and what it forked) gets vx's signal once (a SIGHUP as a SIGTERM), `run.killGrace` (2 s) to go, then SIGKILL; a second signal skips the grace. vx then dies of the signal, so a shell script running it stops too (a run that handed a task the terminal exits with the code, so the terminal is restored). |
|
|
136
137
|
|
|
137
138
|
A task runs in its own session, so a terminal's Ctrl-C reaches vx alone,
|
|
138
139
|
and each task hears it once: from vx, as SIGINT.
|
|
@@ -182,7 +183,7 @@ facing summary.
|
|
|
182
183
|
A filter that matches nothing refuses the run (`no projects matched
|
|
183
184
|
filter(s): …`) with `Did you mean <name>?` when a project name is within
|
|
184
185
|
two edits, or when exactly one scoped project's name after its `/` is
|
|
185
|
-
(`--filter vx-
|
|
186
|
+
(`--filter vx-otel` hints `@vzn/vx-otel`), and `Projects: a, b` otherwise
|
|
186
187
|
(M-56). A list names eight, then a count. An unmatched `tag:` filter
|
|
187
188
|
hints the nearest tag instead (`Did you mean tag:scope:web?`), else
|
|
188
189
|
lists the tags. When every pattern matched and an exclusion took back
|
|
@@ -208,19 +209,15 @@ selected: !app excluded every project the other filters matched`).
|
|
|
208
209
|
| `!<pattern>` | Exclude packages matching `<pattern>`, from everything the includes select, in any order. |
|
|
209
210
|
| `[<git-ref>]` | Projects whose files changed since `<git-ref>` (`main`, `HEAD~5`, …). |
|
|
210
211
|
|
|
211
|
-
An edge is a `
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
cross-project `dependsOn` entry (`e2e`'s `test: { dependsOn:
|
|
221
|
-
['app#build'] }` makes `e2e` a dependent of `app`). The task graph knows
|
|
222
|
-
both, so selection follows both: `vx run test --filter '...app'` runs
|
|
223
|
-
`e2e#test` even though `e2e` has no manifest dependency on `app`. There
|
|
212
|
+
An edge is one a plugin supplies — a `discover` entry's `deps` (order
|
|
213
|
+
and selection) or `reach` (selection only); the lockfile plugins
|
|
214
|
+
(`pnpm()`, `bun()`, `npm()`, `yarn()` from `@vzn/vx-lockfile`) supply
|
|
215
|
+
what the package manager installed, and core reads no edge from a
|
|
216
|
+
`package.json` — OR a cross-project `dependsOn` entry (`e2e`'s `test: {
|
|
217
|
+
dependsOn: ['app#build'] }` makes `e2e` a dependent of `app`). The task
|
|
218
|
+
graph knows both, so selection follows both: `vx run test --filter
|
|
219
|
+
'...app'` runs `e2e#test` even though no plugin gives `e2e` an edge to
|
|
220
|
+
`app`. There
|
|
224
221
|
is no `implicitDependencies` field — declare the edge where the task
|
|
225
222
|
needs it.
|
|
226
223
|
|
|
@@ -244,7 +241,7 @@ vx run build --filter '[origin/main]' # projects with files changed sinc
|
|
|
244
241
|
|
|
245
242
|
Run the task only in projects whose files changed since `<base>`.
|
|
246
243
|
|
|
247
|
-
- `--affected` (no value) uses the workspace's `affectedBase` when it
|
|
244
|
+
- `--affected` (no value) uses the workspace's `run.affectedBase` when it
|
|
248
245
|
names one (`nx()` and `turbo()` set it from `NX_BASE` / nx.json's
|
|
249
246
|
`defaultBase` and `TURBO_SCM_BASE`, `turbo()` on GitHub Actions from
|
|
250
247
|
the pull request's base or the push's `before`, as Turbo does;
|
|
@@ -301,12 +298,13 @@ change seeds tasks in the projects it touches:
|
|
|
301
298
|
(`files`, `workspaceFiles`), or a changed submodule or embedded
|
|
302
299
|
repository is one its `workspaceFiles` may reach into (git reports it
|
|
303
300
|
as one path; a glob whose fixed prefix is above or inside it counts);
|
|
304
|
-
- every task of a project whose `
|
|
301
|
+
- every task of a project whose `vx.config.*`
|
|
305
302
|
changed, or that holds a changed path no cached task of its declares
|
|
306
303
|
(vx cannot prove that path re-keys nothing), or that holds a changed
|
|
307
304
|
submodule or embedded repository (git reports it as one path, and no
|
|
308
305
|
task's globs can say whether they reach inside), or that a lockfile
|
|
309
|
-
claim
|
|
306
|
+
claim or a config import names (a dependency an install added or
|
|
307
|
+
removed reaches here as the lockfile's change);
|
|
310
308
|
- an uncached task when a changed path lies in its project or the
|
|
311
309
|
project is reached whole; a root file another task of its declares
|
|
312
310
|
(`workspaceFiles`) is no change there. A group seeds nothing.
|
|
@@ -354,7 +352,7 @@ change to it, and every project under it is selected, the project that
|
|
|
354
352
|
holds it included. A repository's own request to hide submodules from a diff
|
|
355
353
|
(`diff.ignoreSubmodules`, `submodule.<name>.ignore`) does not apply:
|
|
356
354
|
the key sees the change whatever git is told to show.
|
|
357
|
-
|
|
355
|
+
`.vx/projects.lock.json` is filtered out of the changed set — a `vx lock`
|
|
358
356
|
re-write never marks every project affected.
|
|
359
357
|
|
|
360
358
|
**A lockfile change selects everything.** The root lockfiles,
|
|
@@ -370,7 +368,9 @@ plugin CLAIMS (`VxPlugin.fingerprint`, e.g. `pnpm-lock.yaml` under
|
|
|
370
368
|
`@vzn/vx-lockfile`) is the exception on both sides: the key folds what the
|
|
371
369
|
plugin says per project, so `--affected` asks the plugin which projects
|
|
372
370
|
the change touches — given the bytes at the base ref and in the working
|
|
373
|
-
tree — and selects those; only a plugin that cannot tell widens
|
|
371
|
+
tree — and selects those; only a plugin that cannot tell widens, as
|
|
372
|
+
`@vzn/vx-lockfile` does for the install settings it claims (`.npmrc`,
|
|
373
|
+
`bunfig.toml`, `pnpm-workspace.yaml`, `.yarnrc.yml`).
|
|
374
374
|
|
|
375
375
|
**A dropped edge selects its dependent.** A package the change deleted
|
|
376
376
|
(its `package.json` was there at the base and is gone) is no project
|
|
@@ -393,8 +393,7 @@ too. "New" is judged at the base: no manifest there, or one with no
|
|
|
393
393
|
`name`.
|
|
394
394
|
|
|
395
395
|
**A workspace config change selects everything.** An edit to
|
|
396
|
-
|
|
397
|
-
every project: the `config` and `project` stages its plugins install
|
|
396
|
+
`.vx/workspace.*` selects every project: the `config` and `project` stages its plugins install
|
|
398
397
|
shape every resolved config, so the edit can re-key any task, and
|
|
399
398
|
selection cannot tell which. It is not in the fingerprint (a key moves
|
|
400
399
|
only when a stage's output does). A root file a plugin's stages read
|
|
@@ -470,32 +469,37 @@ than run with the args unheard.
|
|
|
470
469
|
|
|
471
470
|
### Flags
|
|
472
471
|
|
|
473
|
-
| Flag
|
|
474
|
-
|
|
|
475
|
-
| `--filter <pattern>`
|
|
476
|
-
| `--all`
|
|
477
|
-
| `--affected[=<base>]`
|
|
478
|
-
| `--exclude-dependencies[=<names>]`
|
|
479
|
-
| `--concurrency <n>`
|
|
480
|
-
| `--
|
|
481
|
-
| `--
|
|
482
|
-
| `--cache
|
|
483
|
-
| `--
|
|
484
|
-
| `--
|
|
485
|
-
| `--
|
|
486
|
-
| `--
|
|
487
|
-
| `--
|
|
488
|
-
| `--
|
|
489
|
-
| `--
|
|
490
|
-
| `--
|
|
491
|
-
| `--
|
|
492
|
-
| `--
|
|
493
|
-
| `--
|
|
494
|
-
| `--
|
|
495
|
-
| `--
|
|
496
|
-
| `--
|
|
497
|
-
| `--
|
|
498
|
-
| `--
|
|
472
|
+
| Flag | Type | Default | Description |
|
|
473
|
+
| ------------------------------------------ | -------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
474
|
+
| `--filter <pattern>` | repeatable | (none) | pnpm-style filter DSL (see above). `--filter=<pattern>` form too. |
|
|
475
|
+
| `--all` | boolean | off | Select every project that declares the task. |
|
|
476
|
+
| `--affected[=<base>]` | optional value | off | Select the tasks a change since `<base>` reaches: the ones it touches and those whose `dependsOn` closure holds one (default `run.affectedBase`, else `origin/HEAD`, else `main` or `master`, else `HEAD~1`); candidates are `--filter "...[<base>]"`'s. |
|
|
477
|
+
| `--exclude-dependencies[=<names>]` | optional value | off | Drop `dependsOn` edges. No value = all (just the requested task runs; a group's members run as the group); comma-list = drop only those names, each of which some project must declare (a typo is refused with the nearest name, item 1026). An edge to a task the run schedules anyway (`--all` requests it) stays, so the two still run in order, and so does the order through a dropped task: with `gen` dropped from `test → gen → build` and `build` requested, `test` still waits for `build`. An empty `=` value is a parse error (ambiguous — see below). A dropped dependency does not run but is still keyed, so every key is the one a full run derives; a task keyed on one may hit but does not save (`caching.md` step 10). |
|
|
478
|
+
| `--concurrency <n>` | int or `<n>%` | `run.concurrency`, else the cores (cgroup-capped) | Maximum parallel tasks that EXECUTE; confirmed cache-hit restores are disk work and run on their own lane, up to twice this. `1` serializes both; `50%` is half the CPUs (rounded, never below 1; over 100% is allowed for I/O-bound work). `--concurrency=<n>` form too. |
|
|
479
|
+
| `--schedule <rule>` | value | `run.schedule`, else `most-work` | Which ready task starts first when more are ready than workers: `most-work`, `critical-path`, `direct-dependents`, `ready-order` (`docs/schema.md` § `run.schedule`). |
|
|
480
|
+
| `--branch <name>` | value | `VX_RUN_BRANCH`, else git's branch, then the CI provider's | The branch the run records in its history and forecasts from. No config key: a committed branch would mislabel every other branch's runs. |
|
|
481
|
+
| `--no-cache` | boolean | off | Disable caching entirely (no reads, no writes); output globs are NOT cleaned. |
|
|
482
|
+
| `--force` | boolean | off | Re-execute everything (skip cache reads) but still REFRESH the cache (writes stay on). Output globs are cleaned (so the saved snapshot is clean). |
|
|
483
|
+
| `--cache-policy <spec>` (`--cache-policy`) | value | `VX_CACHE_POLICY` / `cache.policy` | Per-layer read/write control. See below. An EMPTY spec (`--cache-policy=`) is a parse error — it applied nothing and left every axis on; pass `--no-cache` to disable them all. |
|
|
484
|
+
| `--cache-scope <name>` | value | `VX_CACHE_SCOPE` / workspace `cache.scope` | This run's `cache.scope` (`pr-123`), above `VX_CACHE_SCOPE`, `.vx/workspace`'s and `github()`'s. A name `cache.scope` would refuse is refused. `--cache-scope=<name>` form too. Never folded into a cache key. |
|
|
485
|
+
| `--cache-dir <path>` | value | `VX_CACHE_DIR` / `cache.dir` / `~/.vx/<id>/cache` | Cache directory override, resolved relative to cwd (absolute paths used as-is). Beats `VX_CACHE_DIR`, the workspace's `cache.dir` and the default; like them, the directory then holds the whole cache, shared through no store, for every cache the run opens, so the workspace's default dir is not created beside it. A per-run knob — never folded into a cache key. `--cache-dir=<path>` form too; the space form rejects a value starting with `-`. A directory this user cannot write into fails the run before any task with `cache directory <path> is not writable (EACCES: …)` — every run records its history there. |
|
|
486
|
+
| `--retries <n>` | value | `run.retries`, else `0` | Re-run a failed task up to `n` more times; never a persistent one. A task's own `exec.retries` wins (even an explicit `0`). Never affects cache keys. `--retries=<n>` form too. |
|
|
487
|
+
| `--continue[=<mode>]` | value | `deps-ok` | What a failed task takes down with it. `never` stops dispatch on the first failure; `deps-ok` (default) skips only its dependents; `always` (bare `--continue`) runs dependents anyway. See § Failure propagation. |
|
|
488
|
+
| `--timeout <duration>` | duration | `run.timeout`, else none | A task's time limit (`30s`, `10m`), for tasks without their own `exec.timeout`, which always wins. A runaway task is killed + `failed`. A bare number is refused naming the forms with units. Never affects cache keys. `--timeout=<duration>` form too. |
|
|
489
|
+
| `--kill-grace <duration>` | duration | `run.killGrace`, else `2s` | How long a task that ignores SIGTERM gets before SIGKILL: on a timeout, on a signal, and at the end-of-run shutdown of persistent tasks. |
|
|
490
|
+
| `--teardown-timeout <duration>` | duration | `run.teardownTimeout`, else `3s` | The bound on each plugin's end-of-run flush or teardown, and on its `telemetry()` setup, so a third party's I/O cannot hold the run. |
|
|
491
|
+
| `--ready-notice <duration>` | duration | `run.readyNotice`, else `10s` | How long a persistent task may take to match `readyWhen` before vx says once what it waits for. |
|
|
492
|
+
| `--output-logs <mode>` | value | `VX_RUN_OUTPUT_LOGS`, else flow-derived | `full` \| `errors-only` \| `hash-only` \| `none` — explicit output override. See § `--output-logs`. `--output-logs=<mode>` form too. |
|
|
493
|
+
| `--cache-download <mode>` | value | `cache.download`, else `all` | `all` \| `toplevel` \| `none` — where a REMOTELY-executed task's outputs land. `none` leaves them in the remote CAS and fetches lazily, only when a locally-placed task needs them. Never affects cache keys. See § `--cache-download`. `--cache-download=<mode>` form too. |
|
|
494
|
+
| `--verbosity <n>` | int (0+) | `0` | `1` or more prints a per-task table (groups left out) after the framed blocks, above the footer. `--verbosity=<n>` form too. |
|
|
495
|
+
| `--dry[=text\|json]` | optional value | off | Print the task graph + predicted cache hit/miss; skip execution. `VX_DEBUG_TIMING=1` prints the stage table here as it does for a run. |
|
|
496
|
+
| `--graph[=<path>]` | optional value | off | Emit Graphviz DOT (stdout if no path, its directory made if missing); skip execution. A path it cannot write is one line and exit 1. |
|
|
497
|
+
| `--format <fmt>` | value | `pretty` | `json` prints the `--summarize` document on stdout when the run ends and moves every other line to stderr ([Machine-readable output](#machine-readable-output)). `--format=<fmt>` form too. |
|
|
498
|
+
| `--summarize[=<path>]` | optional value | off | Write per-run JSON to `<cacheDir>/runs/<run_id>.json` (or the explicit path). |
|
|
499
|
+
| `--profile[=<path>]` | optional value | off (`profile.json` when set) | Write Chrome-trace JSON of the run's wallclock spans. |
|
|
500
|
+
| `--tag <k=v>` | repeatable | (none) | Label this invocation. Recorded on the run's `invocations` row so dashboards can filter runs. `--tag=k=v` form too. |
|
|
501
|
+
| `--report[=markdown]` | optional value | off | At the end of the run, print a markdown run report to stdout, above the footer. Only `markdown` is supported (`json` is reserved). |
|
|
502
|
+
| `--report-file <path>` | value | off | After the run, APPEND the same markdown report to `<path>`, making its directory as the other output paths do. Use this for `$GITHUB_STEP_SUMMARY` — redirecting stdout captures the whole run log too. `--report-file=<path>` form too. |
|
|
499
503
|
|
|
500
504
|
Mutual exclusion:
|
|
501
505
|
|
|
@@ -550,11 +554,13 @@ shell variable), never a path or task id. Use the `=` form for a literal
|
|
|
550
554
|
leading dash.
|
|
551
555
|
|
|
552
556
|
**Numeric flags take a plain decimal integer.** `--concurrency`,
|
|
553
|
-
`--
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
557
|
+
`--retries` and `--verbosity` reject hex (`0x10`), exponent (`1e3`),
|
|
558
|
+
fractional (`2.7`), signed (`+4`) and space-padded forms, plus anything
|
|
559
|
+
past `2^53` (it would parse to a number you did not type). These all
|
|
560
|
+
used to be silently reinterpreted — `--concurrency 0x10` ran 16
|
|
561
|
+
workers. A duration flag (`--timeout`, `--kill-grace`,
|
|
562
|
+
`--teardown-timeout`, `--ready-notice`) takes its unit (`500ms`, `30s`,
|
|
563
|
+
`10m`): a bare `500` is refused, naming both readings.
|
|
558
564
|
|
|
559
565
|
An empty `=` value means "no value" on four flags, which then act like
|
|
560
566
|
their bare forms: `--profile=` writes `profile.json`, `--summarize=`
|
|
@@ -562,25 +568,34 @@ writes `<cacheDir>/runs/<run_id>.json`, `--graph=` prints to stdout and
|
|
|
562
568
|
`--affected=` uses the default base. Every other flag refuses an empty
|
|
563
569
|
value: `--dry=`, `--report=`, `--continue=` and
|
|
564
570
|
`--exclude-dependencies=` as well as the ones with no bare form
|
|
565
|
-
(`--
|
|
571
|
+
(`--retries=`, `--timeout=`, `--cache-dir=`, `--filter=`, `--cache-policy=`).
|
|
566
572
|
|
|
567
|
-
#### Cache control: `--cache`, `--no-cache`, `--force`
|
|
573
|
+
#### Cache control: `--cache-policy`, `--no-cache`, `--force`
|
|
568
574
|
|
|
569
575
|
The cache has four independent axes — **localRead**, **localWrite**,
|
|
570
576
|
**remoteRead**, **remoteWrite** — and the three flags above resolve
|
|
571
577
|
them in this precedence order:
|
|
572
578
|
|
|
573
579
|
1. Start with every axis **on** (the default).
|
|
574
|
-
2. Apply each `--cache=<spec>` segment (the base)
|
|
580
|
+
2. Apply each `--cache-policy=<spec>` segment (the base), or, with no
|
|
581
|
+
`--cache-policy`, the `VX_CACHE_POLICY` environment variable's spec, or, with
|
|
582
|
+
neither, `.vx/workspace`'s `cache.policy` (`docs/schema.md`).
|
|
575
583
|
3. If `--no-cache` was passed, force **all four off**.
|
|
576
584
|
4. If `--force` was passed, force **both reads off** (writes stay
|
|
577
|
-
whatever the base / `--cache` left them).
|
|
585
|
+
whatever the base / `--cache-policy` left them).
|
|
586
|
+
5. Off CI (`CI` unset, `0` or `false`), unless `--cache-policy`,
|
|
587
|
+
`VX_CACHE_POLICY` or `cache.policy` names the remote, force
|
|
588
|
+
**remoteWrite off**: a key holds only declared inputs, so a laptop's
|
|
589
|
+
write could replay on CI. `cache.scope` never changes an axis.
|
|
578
590
|
|
|
579
591
|
A spec that names a remote axis (`remote:r`, `remote:w`, `remote:rw`)
|
|
580
592
|
in a workspace whose plugins supply no remote layer gets one status
|
|
581
593
|
line saying so — the axes are inert without a layer to serve them, and
|
|
582
594
|
a CI job that believes it is filling a shared cache should be told.
|
|
583
595
|
|
|
596
|
+
Whatever the axes, a lookup asks the local cache first, then each
|
|
597
|
+
remote in the order the plugins declare them.
|
|
598
|
+
|
|
584
599
|
So `--no-cache` always wins over `--force`. The common cases:
|
|
585
600
|
|
|
586
601
|
- `--no-cache` → nothing reads, nothing writes, and declared output
|
|
@@ -591,29 +606,30 @@ So `--no-cache` always wins over `--force`. The common cases:
|
|
|
591
606
|
before each task so the saved snapshot is clean. This is the "rebuild
|
|
592
607
|
and refresh the cache" flag.
|
|
593
608
|
|
|
594
|
-
`--cache=<spec>` is a comma-separated list of `<layer>:<flags>`
|
|
609
|
+
`--cache-policy=<spec>` is a comma-separated list of `<layer>:<flags>`
|
|
595
610
|
segments. `layer` is `local` or `remote`; `flags` is any subset of `r`
|
|
596
611
|
(read) and `w` (write), order-independent and possibly empty. A
|
|
597
612
|
**mentioned** layer is set EXACTLY to its flags; an **unmentioned**
|
|
598
|
-
layer keeps its current value. Both `--cache=<spec>` and the space form
|
|
599
|
-
`--cache <spec>` are accepted.
|
|
613
|
+
layer keeps its current value. Both `--cache-policy=<spec>` and the space form
|
|
614
|
+
`--cache-policy <spec>` are accepted.
|
|
600
615
|
|
|
601
|
-
| Spec
|
|
602
|
-
|
|
|
603
|
-
| `--cache=local:rw,remote:r` | remote read-only (won't upload); local full |
|
|
604
|
-
| `--cache=local:r` | local read-only; remote untouched (still full) |
|
|
605
|
-
| `--cache=remote:` | remote fully off; local untouched |
|
|
606
|
-
| `--cache=local:,remote:rw` | don't touch the local cache, but still upload to remote |
|
|
616
|
+
| Spec | Effect |
|
|
617
|
+
| ---------------------------------- | ------------------------------------------------------- |
|
|
618
|
+
| `--cache-policy=local:rw,remote:r` | remote read-only (won't upload); local full |
|
|
619
|
+
| `--cache-policy=local:r` | local read-only; remote untouched (still full) |
|
|
620
|
+
| `--cache-policy=remote:` | remote fully off; local untouched |
|
|
621
|
+
| `--cache-policy=local:,remote:rw` | don't touch the local cache, but still upload to remote |
|
|
607
622
|
|
|
608
623
|
`local:` means "don't serve hits out of the pre-existing local cache" —
|
|
609
624
|
a remote hit is still delivered (the artifact has to land on disk to be
|
|
610
625
|
extracted), it just never short-circuits the remote read.
|
|
611
626
|
|
|
612
627
|
Combine with `--force` for "re-execute and refresh only the remote":
|
|
613
|
-
`--cache=local: --force` (local off, reads off, remote write-only).
|
|
628
|
+
`--cache-policy=local: --force` (local off, reads off, remote write-only).
|
|
614
629
|
|
|
615
630
|
Invalid layers/flags are a parse error
|
|
616
|
-
(
|
|
631
|
+
(`--cache-policy: invalid layer 'disk'`, `--cache-policy: invalid flag 'x'`, …);
|
|
632
|
+
`VX_CACHE_POLICY` and `cache.policy` say their own name instead.
|
|
617
633
|
|
|
618
634
|
### Output
|
|
619
635
|
|
|
@@ -729,7 +745,7 @@ tracks the run live. Top to bottom:
|
|
|
729
745
|
persistent tasks it depends on, until it — or, with several, the
|
|
730
746
|
first of them — exits (one kept only as a dependency that exits 0,
|
|
731
747
|
a daemon that forked and returned, does not end it); the rest still up are then torn
|
|
732
|
-
down (SIGTERM, `
|
|
748
|
+
down (SIGTERM, `run.killGrace`, SIGKILL), one status line names
|
|
733
749
|
the task and its code (`vx: app#dev exited with code 1; stopping 1
|
|
734
750
|
other persistent task`), and a non-zero exit makes the run exit 1
|
|
735
751
|
and is what `vx last` records.
|
|
@@ -742,15 +758,18 @@ other persistent task`), and a non-zero exit makes the run exit 1
|
|
|
742
758
|
each one's id (`app#dev │ Local: http://localhost:5173`), except
|
|
743
759
|
under `--output-logs errors-only`, `hash-only` or `none`; a single
|
|
744
760
|
requested server in the focused flow streams raw from its frame.
|
|
745
|
-
3. **Worker rows** — one per worker slot (
|
|
761
|
+
3. **Worker rows** — one per worker slot that has been busy (at most
|
|
746
762
|
`min(concurrency, 10)`), no glyph and no spinner: the live ticking
|
|
747
763
|
elapsed time leads (` 568ms running <id>`). A task stays in
|
|
748
|
-
its row for its whole life;
|
|
749
|
-
nothing ever jumps; overflow shows as `+k more`. A second in, a row
|
|
764
|
+
its row for its whole life; a row whose task ended holds its place
|
|
765
|
+
dimmed (`idle`), so nothing ever jumps; overflow shows as `+k more`. A second in, a row
|
|
750
766
|
says what waits on it: `critical path` (yellow) on the running task
|
|
751
767
|
the longest unfinished chain waits on, by the same p50s as the
|
|
752
768
|
forecast below and only while two or more run, and a dim
|
|
753
|
-
`blocks N`, its unfinished dependents direct and transitive.
|
|
769
|
+
`blocks N`, its unfinished dependents direct and transitive. Where
|
|
770
|
+
local history knows the task, its row also shows its usual duration
|
|
771
|
+
(its executed p50) in the cache column, fixed while the elapsed time
|
|
772
|
+
counts up toward it (`3.33s running ~5.2s solid-js#types`).
|
|
754
773
|
4. **The live summary section** — the SAME meters the final footer
|
|
755
774
|
prints (`tasks` + `cache` bars with legends, `time`), filling in as
|
|
756
775
|
the run progresses, under a bare `vx` wordmark rule. A second in,
|
|
@@ -760,7 +779,10 @@ other persistent task`), and a non-zero exit makes the run exit 1
|
|
|
760
779
|
workers, a running task's spent time taken off its own. It says
|
|
761
780
|
nothing when history knows none of what is left, or once a running
|
|
762
781
|
task is 25% past its p50. A run that ends inside the second never
|
|
763
|
-
reads history for it, and the final summary never shows it.
|
|
782
|
+
reads history for it, and the final summary never shows it. A p50
|
|
783
|
+
here is the median of the task's last five timings on this branch,
|
|
784
|
+
else on any branch; the timings are shared by every worktree of the
|
|
785
|
+
repo, so a new one has a forecast on its first run.
|
|
764
786
|
|
|
765
787
|
In a terminal known to read them, the region also sends zero-width
|
|
766
788
|
escapes: tab or taskbar progress (OSC 9;4: Ghostty, iTerm2 3.6+,
|
|
@@ -781,7 +803,8 @@ text is the task's output unchanged; a successful task's frame is never
|
|
|
781
803
|
linked.
|
|
782
804
|
|
|
783
805
|
The region is redrawn in place (cursor-up + clear; not a TUI — no
|
|
784
|
-
alternate screen) and erased before the final summary prints.
|
|
806
|
+
alternate screen) and erased before the final summary prints. On a
|
|
807
|
+
terminal shorter than the region, it shows only the rows that fit. In the
|
|
785
808
|
focused flow it only lives while dependencies run; it disappears for
|
|
786
809
|
good the moment the requested task starts streaming.
|
|
787
810
|
|
|
@@ -848,7 +871,7 @@ down with it:
|
|
|
848
871
|
The mode is the local scheduler's: a task a plugin executor runs
|
|
849
872
|
elsewhere is one dispatch, failed or not, like any other.
|
|
850
873
|
|
|
851
|
-
### `--download <mode>`
|
|
874
|
+
### `--cache-download <mode>`
|
|
852
875
|
|
|
853
876
|
Where a **remotely-executed** task's outputs land. `all` (default)
|
|
854
877
|
downloads every task's outputs to this machine as it completes —
|
|
@@ -862,7 +885,7 @@ bytes"). A CI job that only wants the verdict moves no output bytes at
|
|
|
862
885
|
all.
|
|
863
886
|
|
|
864
887
|
Locally-executed tasks always write in place and ignore the flag, and
|
|
865
|
-
`exec.remote: 'only'` still means never — `--download` cannot override
|
|
888
|
+
`exec.remote: 'only'` still means never — `--cache-download` cannot override
|
|
866
889
|
it in either direction. **It never affects a cache key**: transfer
|
|
867
890
|
tuning cannot change what a command produces.
|
|
868
891
|
|
|
@@ -974,8 +997,9 @@ $ vx run build lint --affected=main --dry
|
|
|
974
997
|
```
|
|
975
998
|
|
|
976
999
|
`kind` is `input` (a changed `file` is a declared input), `project` (its
|
|
977
|
-
project changed and the task reads it whole: an uncached task, or
|
|
978
|
-
|
|
1000
|
+
project changed and the task reads it whole: an uncached task, or a changed
|
|
1001
|
+
`file` no cached task of the project declares, often an output git does not
|
|
1002
|
+
ignore; no `file` for a lockfile claim or a config import),
|
|
979
1003
|
`package` (a `^` edge passes a changed `project` with no such task),
|
|
980
1004
|
`named` (`pkg#task` beside a bare task) or `selected` (another filter
|
|
981
1005
|
selected its project). `via` is the `dependsOn` chain, nearest first, to
|
|
@@ -1087,7 +1111,7 @@ outcome bucket or `total`, but they make the run red, so they are
|
|
|
1087
1111
|
listed separately and counted as `summary.aborted`.
|
|
1088
1112
|
|
|
1089
1113
|
**`noCache: true`** marks a task no cache answered for: it declares no
|
|
1090
|
-
`cache` block (it executes every run by design), or the run's `--cache`
|
|
1114
|
+
`cache` block (it executes every run by design), or the run's `--cache-policy`
|
|
1091
1115
|
read and wrote nothing (`--no-cache`), so a hit rate should leave it out
|
|
1092
1116
|
of the denominator. The key is present only when true; every other row is
|
|
1093
1117
|
unchanged. Its `hash` is still set: dependents fold it.
|
|
@@ -1190,14 +1214,14 @@ or a timeout's reason, `failed (timed out, exit 143)`, a persistent
|
|
|
1190
1214
|
task's `never ready: …`, or a sandboxed task's violation count, or
|
|
1191
1215
|
`skipped`, naming what blocked it: `skipped (blocked by lib#build)`);
|
|
1192
1216
|
`Cache` is its provenance (`miss` / `no-cache` for a task no cache
|
|
1193
|
-
answered for — no `cache` block, or a `--no-cache` / `--cache=local:` run —
|
|
1217
|
+
answered for — no `cache` block, or a `--no-cache` / `--cache-policy=local:` run —
|
|
1194
1218
|
which never consulted one / `local` / `remote` / `up-to-date` /
|
|
1195
1219
|
`—`). A stopped run (a Ctrl-C teardown) reads `## vx run — interrupted`,
|
|
1196
1220
|
and its tasks keep the terminal's words: one the signal killed is
|
|
1197
1221
|
`aborted` with its time so far, one the stop reached before it ran is
|
|
1198
|
-
`not run` with no time.
|
|
1199
|
-
|
|
1200
|
-
|
|
1222
|
+
`not run` with no time. Both count in the header's task total, listed
|
|
1223
|
+
as `1 aborted · 1 not run`, so a red report with no failing row still
|
|
1224
|
+
says why. Group tasks (no `exec`) get neither — they are not
|
|
1201
1225
|
work, and the header's counts match the terminal summary and
|
|
1202
1226
|
`--summarize` exactly.
|
|
1203
1227
|
|
|
@@ -1319,7 +1343,7 @@ sandbox run as usual, and the failure's dependents follow `--continue`.
|
|
|
1319
1343
|
## `vx watch`
|
|
1320
1344
|
|
|
1321
1345
|
```
|
|
1322
|
-
vx watch [OPTIONS] TASK [-- forwarded-args...]
|
|
1346
|
+
vx watch [OPTIONS] [--poll] TASK [-- forwarded-args...]
|
|
1323
1347
|
```
|
|
1324
1348
|
|
|
1325
1349
|
Run the named task, then re-run it on every filesystem change in the
|
|
@@ -1345,11 +1369,11 @@ run...` precedes it.
|
|
|
1345
1369
|
`dependsOn` edges included — is watched recursively, less any project
|
|
1346
1370
|
nested inside it, with a config or not (a root project's key leaves a nested
|
|
1347
1371
|
project's files out, so an edit there is no cycle; X-42, X-57). The workspace root is
|
|
1348
|
-
watched (non-recursively) for lockfile
|
|
1349
|
-
|
|
1372
|
+
watched (non-recursively) for a claimed file (a lockfile), for
|
|
1373
|
+
`.vx/projects.lock.json` and for an edit to `.vx/workspace.*` — the one root file that
|
|
1350
1374
|
shapes a run (plugins, `config` stage, concurrency) without being any
|
|
1351
1375
|
task's input; the cycle after it re-evaluates the file. A `.gitignore`
|
|
1352
|
-
there or in a
|
|
1376
|
+
there or in a project's parent directory is a cycle too: inputs are
|
|
1353
1377
|
gitignore-aware, so it changes what keys read (X-136). A file a
|
|
1354
1378
|
config imports by relative path from outside the watched projects (a
|
|
1355
1379
|
shared preset) is watched too, and its edit is a cycle that re-reads
|
|
@@ -1357,20 +1381,20 @@ run...` precedes it.
|
|
|
1357
1381
|
config's own edit does (WD-14). A file the workspace config imports is loaded once per
|
|
1358
1382
|
process, so its edit is named with the restart it needs rather than
|
|
1359
1383
|
run stale (item 949). An import whose file is gone stays on the list
|
|
1360
|
-
by the name it would have, so its return is a cycle (X-133). The
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1384
|
+
by the name it would have, so its return is a cycle (X-133). The
|
|
1385
|
+
projects are the lock's: a new `.vx/projects.lock.json` is the
|
|
1386
|
+
event that adds or drops one, and its directory is watched
|
|
1387
|
+
from before that cycle runs, so an edit made during it queues
|
|
1388
|
+
(X-138). A config added and not locked is no project yet. With no
|
|
1389
|
+
lock at start the first cycle fails saying so, and the watch waits
|
|
1390
|
+
for one. A project directory, or one a config imports from,
|
|
1391
|
+
removed and made again (a checkout that restores `packages/`) is
|
|
1392
|
+
heard from its nearest directory that exists and each one above it,
|
|
1393
|
+
and a watch whose
|
|
1394
|
+
directory was replaced is armed again (an OS watch holds the deleted
|
|
1395
|
+
one; `watch-recreated-dirs.test.ts`). Under `--filter` or
|
|
1396
|
+
`--affected` the scope is the one resolved at start, and a new
|
|
1397
|
+
project joins it only as a dependency of it. A task's own declared outputs (`cache.outputs.files`,
|
|
1374
1398
|
`outputs.workspaceFiles`; a plugin's `project` stage counts, as in
|
|
1375
1399
|
a run) never trigger a re-run — a cycle that writes `dist/` is not
|
|
1376
1400
|
an edit, nor is the `dist` directory itself coming and going (a
|
|
@@ -1425,7 +1449,8 @@ run...` precedes it.
|
|
|
1425
1449
|
at the root, a `coverage/` or `.turbo/` tree, an editor's scratch
|
|
1426
1450
|
file are dropped before the trigger — so `vx watch … > build.log`
|
|
1427
1451
|
inside the repo settles instead of feeding itself a cycle per line
|
|
1428
|
-
of its own output.
|
|
1452
|
+
of its own output. The opening line, `watching N projects · M tasks`
|
|
1453
|
+
(after the initial run's rows), is
|
|
1429
1454
|
printed only after every watcher has reported a probe file written
|
|
1430
1455
|
under it (`.vx-watch-probe`, re-written on a short backoff until its
|
|
1431
1456
|
event arrives, then removed): on macOS a directory watcher can return
|
|
@@ -1437,10 +1462,14 @@ run...` precedes it.
|
|
|
1437
1462
|
watcher whose probe is not heard within 2 s is replaced by a poller
|
|
1438
1463
|
that checks every 250 ms, with the notice `vx watch: <dir>: no OS
|
|
1439
1464
|
watch events within 2000 ms; polling every 250 ms instead`.
|
|
1440
|
-
3. **On change.**
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1465
|
+
3. **On change.** A cycle opens with a blank line and
|
|
1466
|
+
`changed <project> <relpath>`, plus ` (+N more)` when more paths
|
|
1467
|
+
changed, then its task rows as `vx run` prints them, with no footer.
|
|
1468
|
+
The named path is the first changed one that still exists, so an
|
|
1469
|
+
editor's temporary file renamed away names nothing; a deletion names
|
|
1470
|
+
the cycle when nothing else changed. A cycle that re-read the watched
|
|
1471
|
+
set (a member came or went, the lock or a plugin changed) says the
|
|
1472
|
+
`watching` line again. Errors and refusals go to stderr. The
|
|
1444
1473
|
orchestrator is invoked again with the same options, save one: the
|
|
1445
1474
|
`--affected` diff is read once, for the initial run. A later cycle
|
|
1446
1475
|
runs the requested task in every project of the scope fixed at start,
|
|
@@ -1455,15 +1484,15 @@ watch events within 2000 ms; polling every 250 ms instead`.
|
|
|
1455
1484
|
|
|
1456
1485
|
Always ignored (no re-trigger):
|
|
1457
1486
|
|
|
1458
|
-
- `node_modules/`, `.git/`, `.vx/` anywhere in the path.
|
|
1487
|
+
- `node_modules/`, `.git/`, `.vx/cache/` anywhere in the path.
|
|
1459
1488
|
- A path git ignores (`git check-ignore`, asked once per debounce
|
|
1460
1489
|
window, never per event): invisible to every cache key — inputs are
|
|
1461
1490
|
tracked + untracked-not-ignored — so a cycle it started could change
|
|
1462
1491
|
nothing. A tracked file that matches a pattern is not ignored, by
|
|
1463
1492
|
git's own rule; outside a repository nothing is.
|
|
1464
1493
|
- Files ending in `.tsbuildinfo` or `~` (editor swap files).
|
|
1465
|
-
- The run's **resolved cache directory**, wherever it is. `.vx/` covers the
|
|
1466
|
-
default, but `defineWorkspace({
|
|
1494
|
+
- The run's **resolved cache directory**, wherever it is. `.vx/cache/` covers the
|
|
1495
|
+
default, but `defineWorkspace({ cache: { dir } })` and `--cache-dir` can put it
|
|
1467
1496
|
anywhere; watching it would let vx's own cache writes trigger the next
|
|
1468
1497
|
cycle, which writes again — a loop that never settles.
|
|
1469
1498
|
|
|
@@ -1492,19 +1521,20 @@ It samples each file's later clock of mtime and ctime, so a replacement
|
|
|
1492
1521
|
that carries the old file's mtime (`cp -p`, `rsync -a`, a `mv`) is an
|
|
1493
1522
|
edit to it as it is to the OS watcher.
|
|
1494
1523
|
|
|
1495
|
-
###
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
`pnpm-
|
|
1499
|
-
|
|
1500
|
-
Watch mode hears
|
|
1501
|
-
(non-recursively)
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
(`vx lock`) is a cycle that
|
|
1524
|
+
### Claimed root file changes
|
|
1525
|
+
|
|
1526
|
+
A root file a plugin claims — a lockfile or install setting under
|
|
1527
|
+
`@vzn/vx-lockfile` (`pnpm-lock.yaml`, `bun.lock`, `.npmrc`, …),
|
|
1528
|
+
`turbo.json` under `turbo()` (item 961) — triggers a cycle, and so does
|
|
1529
|
+
`.vx/projects.lock.json`. Watch mode hears them because it watches the
|
|
1530
|
+
workspace root (non-recursively); a plugin added to the workspace
|
|
1531
|
+
config mid-watch counts from the cycle that loads it. The keys then
|
|
1532
|
+
decide which projects actually re-run: core keys no package file, so a
|
|
1533
|
+
lockfile edit re-keys only through its claimant
|
|
1534
|
+
([cache key derivation](./caching.md#cache-key-derivation)). Every
|
|
1535
|
+
cycle's configs come from `.vx/projects.lock.json`, so a config edit
|
|
1536
|
+
alone changes no command, and a re-lock (`vx lock`) is a cycle that
|
|
1537
|
+
re-reads the watched set (item 971).
|
|
1508
1538
|
|
|
1509
1539
|
### Constraints
|
|
1510
1540
|
|
|
@@ -1539,7 +1569,7 @@ re-runs itself and what it depends on.
|
|
|
1539
1569
|
- `0` — clean Ctrl+C / SIGTERM exit. A cycle in flight is torn down
|
|
1540
1570
|
first — its children, and the dev server held between cycles, get the
|
|
1541
1571
|
signal watch received (a Ctrl-C as SIGINT; a SIGTERM or SIGHUP as SIGTERM),
|
|
1542
|
-
`
|
|
1572
|
+
`run.killGrace` (2 s), then SIGKILL — and the process leaves only
|
|
1543
1573
|
once it has returned, so a CI cancellation never orphans a task.
|
|
1544
1574
|
Also `0`, with no watch, when `--affected` selects nothing: watch
|
|
1545
1575
|
prints `vx watch: nothing affected since <ref>` and exits as `vx run`
|
|
@@ -1558,32 +1588,34 @@ the watch loop — a failed cycle just prints the framed FAILED block
|
|
|
1558
1588
|
and waits for the next change. This matches `turbo watch` / `nx
|
|
1559
1589
|
watch`.
|
|
1560
1590
|
|
|
1561
|
-
## `vx cache
|
|
1591
|
+
## `vx cache clean`
|
|
1562
1592
|
|
|
1563
1593
|
Evict old or oversized cache entries. Operates on the entries and
|
|
1564
1594
|
`<hash>.tar.zst` artifacts of the store the workspace's index records
|
|
1565
1595
|
(`~/.vx/<id>/cache`, shared by every checkout of the repository), or of
|
|
1566
|
-
`<cacheDir>` itself when the workspace names one (`
|
|
1596
|
+
`<cacheDir>` itself when the workspace names one (`cache.dir`,
|
|
1567
1597
|
`VX_CACHE_DIR`, `--cache-dir`).
|
|
1568
1598
|
|
|
1569
|
-
`
|
|
1599
|
+
`clean` is the only `vx cache` subcommand: the statistics other runners
|
|
1570
1600
|
put under a `cache` verb — the directory, the entry count, the size —
|
|
1571
1601
|
are part of [`vx info`](#vx-info), and `vx cache stats`, `list`, `ls`,
|
|
1572
|
-
`
|
|
1573
|
-
`vx
|
|
1602
|
+
`gc` and their neighbours say so (`list` also names `vx last --list`;
|
|
1603
|
+
`clear`, `purge`, `reset` and `wipe` name a bare `vx cache clean`) rather than printing a bare "unknown subcommand".
|
|
1574
1604
|
|
|
1575
1605
|
```
|
|
1576
|
-
vx cache
|
|
1577
|
-
vx cache
|
|
1578
|
-
vx cache
|
|
1579
|
-
vx cache
|
|
1580
|
-
vx cache
|
|
1606
|
+
vx cache clean --older-than <duration> # Drop entries last used before now - duration (up to 1h slack).
|
|
1607
|
+
vx cache clean --max-size <size> # After age-based pruning, evict LRU until under <size>.
|
|
1608
|
+
vx cache clean ... --dry-run # Say what either policy would reap; delete nothing.
|
|
1609
|
+
vx cache clean ... --format json # { dryRun, evicted, bytesFreed, orphans, orphanBytes }
|
|
1610
|
+
vx cache clean ... --cache-dir <path> # The cache a run with the same flag uses.
|
|
1611
|
+
vx cache clean # No bound: clear the whole cache (below).
|
|
1612
|
+
vx cache clean --global # Also clear every other repository's store.
|
|
1581
1613
|
```
|
|
1582
1614
|
|
|
1583
|
-
|
|
1584
|
-
be combined: age-based eviction runs first, then LRU eviction if the
|
|
1615
|
+
With no bound it clears the cache ([below](#with-no-bound-clear-the-cache)).
|
|
1616
|
+
Both bounds may be combined: age-based eviction runs first, then LRU eviction if the
|
|
1585
1617
|
total is still over the size cap. The same policy runs unattended at
|
|
1586
|
-
the end of every run when
|
|
1618
|
+
the end of every run when `.vx/workspace.ts` declares
|
|
1587
1619
|
`cacheRetention: { olderThan, maxSize }` (same spellings; see
|
|
1588
1620
|
[schema](./schema.md)).
|
|
1589
1621
|
|
|
@@ -1599,7 +1631,7 @@ vx version sharing the store may still use it. A temp goes once it is an
|
|
|
1599
1631
|
hour old; nothing younger than an hour is taken. Only the names vx writes are taken:
|
|
1600
1632
|
`<hash>` is the key's 16 lowercase hex digits and the temp suffix is
|
|
1601
1633
|
the one a save makes, so a `release.tar.zst` beside the index in a
|
|
1602
|
-
`
|
|
1634
|
+
`cache.dir` you share is never touched (item 968). The converse, an
|
|
1603
1635
|
index row whose artifact is gone (deleted by hand), is dropped by the
|
|
1604
1636
|
same prune, and its bytes count toward neither `--max-size` nor the
|
|
1605
1637
|
evicted total: they are on no disk, and counting them evicted real
|
|
@@ -1607,7 +1639,7 @@ entries to make room for them (item 975). A row used within the hour
|
|
|
1607
1639
|
is kept until a later prune, but its bytes are left out all the same
|
|
1608
1640
|
(item 1081). The sweep runs on every prune, under
|
|
1609
1641
|
either flag, and reports separately from the policy's evictions. A
|
|
1610
|
-
workspace's `
|
|
1642
|
+
workspace's `cache.retention` runs it too, at the end of a run, at most
|
|
1611
1643
|
once an hour even when nothing the index holds is due (the policy sums
|
|
1612
1644
|
index rows, so orphans alone never make it due).
|
|
1613
1645
|
|
|
@@ -1624,22 +1656,20 @@ write `10G`, or `10B` when bytes really are the unit.
|
|
|
1624
1656
|
|
|
1625
1657
|
**A zero bound is rejected.** `--max-size 0` and `--older-than 0d` would
|
|
1626
1658
|
evict every entry in the cache, which is far more often a
|
|
1627
|
-
computed-to-zero retention than an intent
|
|
1628
|
-
|
|
1629
|
-
full prune). Delete the cache store `vx info` names when that is really
|
|
1630
|
-
what you want: the entries live there, not in the workspace's `.vx`.
|
|
1659
|
+
computed-to-zero retention than an intent. `vx cache clean` with no
|
|
1660
|
+
bound clears the whole cache when that is really what you want.
|
|
1631
1661
|
|
|
1632
1662
|
```
|
|
1633
|
-
$ vx cache
|
|
1663
|
+
$ vx cache clean --older-than 30d
|
|
1634
1664
|
Pruned 42 entries (1.3 GB freed)
|
|
1635
1665
|
|
|
1636
|
-
$ vx cache
|
|
1666
|
+
$ vx cache clean --older-than 7d --max-size 500M
|
|
1637
1667
|
Pruned 18 entries (320 MB freed)
|
|
1638
1668
|
|
|
1639
|
-
$ vx cache
|
|
1669
|
+
$ vx cache clean --older-than 30d # after a SCHEMA_VERSION bump
|
|
1640
1670
|
Pruned 0 entries (0 B freed), reaped 42 orphaned artifacts (1.3 GB)
|
|
1641
1671
|
|
|
1642
|
-
$ vx cache
|
|
1672
|
+
$ vx cache clean --older-than 30d --dry-run
|
|
1643
1673
|
Would prune 42 entries (1.3 GB), would reap 3 orphaned artifacts (12 MB)
|
|
1644
1674
|
```
|
|
1645
1675
|
|
|
@@ -1668,49 +1698,128 @@ Exit codes:
|
|
|
1668
1698
|
nothing is created.
|
|
1669
1699
|
- `1` — parse error, missing policy, or workspace-discovery error.
|
|
1670
1700
|
|
|
1671
|
-
`vx cache
|
|
1672
|
-
`defineWorkspace({
|
|
1701
|
+
`vx cache clean` resolves the workspace root from cwd and honors a
|
|
1702
|
+
`defineWorkspace({ cache: { dir } })` override — it prunes the same
|
|
1673
1703
|
directory a run would use. `--cache-dir <path>` (cwd-relative, as on
|
|
1674
1704
|
`vx run`) names another one, so a run that wrote elsewhere can be
|
|
1675
1705
|
pruned there.
|
|
1676
1706
|
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
`vx
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1707
|
+
### With no bound: clear the cache
|
|
1708
|
+
|
|
1709
|
+
`vx cache clean` with neither `--older-than` nor `--max-size` clears
|
|
1710
|
+
the whole cache: the cache directory (`.vx/cache`, or `cache.dir` /
|
|
1711
|
+
`--cache-dir`: the index and this workspace's run history, so `vx last`
|
|
1712
|
+
and `vx why` start over) and the store its index names, where the
|
|
1713
|
+
entries live (`~/.vx/<id>/cache`, shared by this repository's
|
|
1714
|
+
checkouts, so their entries go too). `--global` also clears every other
|
|
1715
|
+
repository's store under `~/.vx` (`~/.vx/*/cache`); it takes no bound
|
|
1716
|
+
and no `--cache-dir`. `.vx/workspace.ts`, the lock and `.vx/.gitignore`
|
|
1717
|
+
sit outside them and stay; a `cache.dir` that holds them (`.vx`, the
|
|
1718
|
+
workspace root) is refused. A `cache.dir` you named may be shared
|
|
1719
|
+
(`build/`, `~/.cache`), so there only what vx wrote goes (the index,
|
|
1720
|
+
its artifacts, run history, its `*` `.gitignore`); your own files stay,
|
|
1721
|
+
and the directory goes only if that empties it. A directory with no
|
|
1722
|
+
`cache.db` is not a vx cache and is left alone, and the store an index
|
|
1723
|
+
names goes only if it is one (`.vx/<id>/cache` holding a `store.db`): the
|
|
1724
|
+
index is a file in the checkout, which a commit or a restored CI cache
|
|
1725
|
+
could point at any directory. A workspace that never ran says
|
|
1726
|
+
`Nothing to clear: no cache here`. The next run misses every task once
|
|
1727
|
+
and saves again.
|
|
1728
|
+
|
|
1729
|
+
```
|
|
1730
|
+
$ vx cache clean
|
|
1731
|
+
Cleared this repository's store, shared by its checkouts, and this workspace's run history (1.4 GB freed):
|
|
1732
|
+
.vx/cache
|
|
1733
|
+
/home/me/.vx/3f2a9c01b4d5e6f7/cache
|
|
1734
|
+
|
|
1735
|
+
$ vx cache clean --global --dry-run
|
|
1736
|
+
Would clear every repository's store and this workspace's run history (3.1 GB):
|
|
1737
|
+
.vx/cache
|
|
1738
|
+
/home/me/.vx/3f2a9c01b4d5e6f7/cache
|
|
1739
|
+
/home/me/.vx/9b8e7d6c5a4f3e2d/cache
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
It waits for a `vx run` on this workspace to finish first (the run's
|
|
1743
|
+
lock), as a trim does. With `--format json` it prints
|
|
1744
|
+
`{ dryRun, cleared, bytesFreed }` (`schemas/cache.json`), `cleared`
|
|
1745
|
+
listing the directories.
|
|
1746
|
+
|
|
1747
|
+
## Runs read the lock
|
|
1748
|
+
|
|
1749
|
+
Every run, `vx watch` cycle, `--affected` selection and the picker takes
|
|
1750
|
+
its projects and their configs only from the committed
|
|
1751
|
+
`.vx/projects.lock.json` (owner, 2026-10-14). A run never walks for
|
|
1752
|
+
configs, never evaluates one, never checks the graph rules
|
|
1753
|
+
(`run.rules`) and never writes the lock: only `vx lock` does. With no
|
|
1754
|
+
lock, a run fails with `no lock: run \`vx lock\``. A run trusts the
|
|
1755
|
+
lock: it performs NO staleness check, so after adding or editing a
|
|
1756
|
+
config, run `vx lock`. In a pipeline, `vx lock --check` first re-evaluates every
|
|
1757
|
+
config and checks the rules.
|
|
1687
1758
|
|
|
1688
1759
|
## `vx lock`
|
|
1689
1760
|
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
vx
|
|
1761
|
+
Find every project, and freeze each one's **resolved** config into
|
|
1762
|
+
`.vx/projects.lock.json` at the workspace root. A project is a directory
|
|
1763
|
+
holding a `vx.config.*`, in any language: `vx lock` lists them with git
|
|
1764
|
+
(`git ls-files`, tracked and untracked, git's ignore rules applied; a
|
|
1765
|
+
nested repository's own configs too), or walks the tree outside git,
|
|
1766
|
+
and leaves out any under a nested `.vx/workspace.*`. Configs are
|
|
1767
|
+
programs; `vx lock` evaluates them in the current environment and
|
|
1768
|
+
stores the post-evaluation objects, by directory (version 3; a lock an
|
|
1769
|
+
earlier vx wrote is refused, "re-run `vx lock`"). A project is named by
|
|
1770
|
+
its config's `name`, else by a plugin's `discover` stage (vx-lockfile
|
|
1771
|
+
gives `package.json`'s `name`), else by its root-relative path (`//` for
|
|
1772
|
+
the root); two projects of one name are refused naming both
|
|
1773
|
+
directories. The plugin `discover` stage runs here as in a run, and may
|
|
1774
|
+
add directories (X-143). Before writing, it
|
|
1775
|
+
builds the graph of every task the workspace declares, `graph` stage
|
|
1776
|
+
included, and checks the graph rules (`run.rules`), which no run
|
|
1777
|
+
checks, `dependencies` (which projects a project may depend on,
|
|
1778
|
+
`VX_E_DEPENDENCY_RULE`) among them: a broken one prints `vx lock: .vx/projects.lock.json not written:
|
|
1779
|
+
<the rule's refusal>` and writes nothing.
|
|
1780
|
+
|
|
1781
|
+
```
|
|
1782
|
+
vx lock # Evaluate all vx.config.* now; write .vx/projects.lock.json.
|
|
1783
|
+
vx lock --check # Audit: full re-evaluation vs the lock. Exit 1 on drift.
|
|
1699
1784
|
vx lock --check --format json # The audit as {upToDate, audited, notAudited, problems}.
|
|
1700
1785
|
```
|
|
1701
1786
|
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1787
|
+
**When to run it.** Run `vx lock` and commit `.vx/projects.lock.json` after
|
|
1788
|
+
editing any `vx.config.*` or `.vx/workspace.*`, after updating
|
|
1789
|
+
dependencies or plugins, and after changing a file a config imports.
|
|
1790
|
+
Runs read their configs from the lock, so until then they keep the old
|
|
1791
|
+
ones. `vx lock --check` as CI's first step fails a stale lock.
|
|
1792
|
+
The full workflow: the site's CI guide and `examples/ci`.
|
|
1793
|
+
|
|
1794
|
+
`.vx/projects.lock.json` is minified: it is the one file vx reads, and it loads
|
|
1795
|
+
faster that way (10,000 projects: 15.6 ms against 22.5 ms pretty). For
|
|
1796
|
+
review, `vx lock` also writes each project's resolved config (plugins
|
|
1797
|
+
applied), pretty, to `vx.lock.json` beside its `vx.config`; vx never reads
|
|
1798
|
+
it, it is no task's input, and `lock.reviewFiles: false`
|
|
1799
|
+
(`VX_LOCK_REVIEW_FILES`) turns it off. `vx lock` also writes
|
|
1800
|
+
`.vx/.gitattributes` (`projects.lock.json linguist-generated=true`) when it is
|
|
1801
|
+
missing, so GitHub collapses the machine lock in a diff; `vx init` does
|
|
1802
|
+
too, and an existing one is never edited.
|
|
1803
|
+
|
|
1804
|
+
Every run consumes it: configs come from the
|
|
1805
|
+
lock with no evaluation, no staleness check and no schema validation
|
|
1806
|
+
of its entries, which `vx lock` validated when it wrote them (frozen-env
|
|
1705
1807
|
semantics: env reads in a config keep their lock-time values; a
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1808
|
+
locked directory whose config file is gone (X-144) or that this user
|
|
1809
|
+
may not search (EACCES, named as such), or a missing lock, is a hard error; a config added since the lock is no project until
|
|
1810
|
+
`vx lock`). An entry edited by
|
|
1811
|
+
hand into a shape `vx lock` could not write fails the run with
|
|
1812
|
+
`.vx/projects.lock.json is invalid, run \`vx lock\``, checked only once
|
|
1813
|
+
something has crashed, so a normal run pays nothing for it.
|
|
1814
|
+
|
|
1815
|
+
`--check` is the audit: it re-evaluates every config in the current
|
|
1816
|
+
environment and `Bun.deepEquals`-compares it with the frozen object. A
|
|
1817
|
+
project that differs gets one message, whether its config, a file it
|
|
1818
|
+
imports or the environment it reads changed. A lock an earlier vx wrote
|
|
1819
|
+
fails the shape check and asks for `vx lock`. It also checks the graph rules over the configs as they
|
|
1820
|
+
evaluate now, and that each `vx.lock.json` is what its project resolves to
|
|
1821
|
+
here, and that none is left beside a deleted `vx.config` (delete that one
|
|
1822
|
+
yourself: vx never deletes a committed file), unless `lock.reviewFiles` is off. The CI recipe is `vx lock --check && vx run …`.
|
|
1714
1823
|
`--format json` prints the audit as one document
|
|
1715
1824
|
(`schemas/lock.json`, exit code unchanged; a missing lock is one of
|
|
1716
1825
|
its `problems`); it answers `--check` only.
|
|
@@ -1720,18 +1829,18 @@ Full design: `docs/design/config-lock-2026-06.md`. A lock written by a
|
|
|
1720
1829
|
A project with no `vx.config.*` — its tasks from a plugin's `project`
|
|
1721
1830
|
stage (`turbo()` from `@vzn/vx-migrate`), or none at all — has nothing
|
|
1722
1831
|
to freeze: the lock records nothing for it, `--check` does not audit
|
|
1723
|
-
it, and
|
|
1724
|
-
|
|
1725
|
-
vx
|
|
1726
|
-
|
|
1727
|
-
|
|
1832
|
+
it, and a run still loads its tasks live. Both verbs name such
|
|
1833
|
+
projects that have tasks (`locked 0 project configs → .vx/projects.lock.json (no
|
|
1834
|
+
vx.config, so not locked: app, lib)`, five names then `and N more`),
|
|
1835
|
+
so an empty lock on a plugin-only workspace never reads like an audit.
|
|
1836
|
+
One with no task (core's default `build` aside) is not named.
|
|
1728
1837
|
|
|
1729
1838
|
The lock is committed, so `vx lock` refuses to write one that holds a
|
|
1730
1839
|
secret: a config that evaluated to the value of a secret-named variable
|
|
1731
1840
|
(or one a task lists in `exec.env.secret`), say
|
|
1732
1841
|
`` `--token ${process.env.API_TOKEN}` ``. It names each place
|
|
1733
1842
|
(`a: tasks.deploy.exec.command holds $API_TOKEN`) and writes nothing.
|
|
1734
|
-
Masking it instead would freeze a `***` that
|
|
1843
|
+
Masking it instead would freeze a `***` that every run runs. Let the
|
|
1735
1844
|
shell expand it: `$API_TOKEN` in the command, the name in
|
|
1736
1845
|
`exec.env.passThrough` (L-42).
|
|
1737
1846
|
|
|
@@ -1822,8 +1931,8 @@ One-time setup, per package, on npmjs.com → package → Settings →
|
|
|
1822
1931
|
Trusted Publisher → GitHub Actions: owner `vznjs`, repository `vx`,
|
|
1823
1932
|
workflow `npm.yml`, environment left blank. Do this for `@vzn/vx`,
|
|
1824
1933
|
`@vzn/vx-darwin-x64`, `@vzn/vx-darwin-arm64`, `@vzn/vx-linux-x64`,
|
|
1825
|
-
`@vzn/vx-linux-arm64` and the
|
|
1826
|
-
`@vzn/vx-lockfile`, `@vzn/vx-
|
|
1934
|
+
`@vzn/vx-linux-arm64` and the six plugins: `@vzn/vx-ci`,
|
|
1935
|
+
`@vzn/vx-lockfile`, `@vzn/vx-migrate`, `@vzn/vx-otel`,
|
|
1827
1936
|
`@vzn/vx-reapi` and `@vzn/vx-schedule-history`.
|
|
1828
1937
|
|
|
1829
1938
|
Each platform package carries `LICENSE` and `THIRD_PARTY_NOTICES.txt`:
|
|
@@ -1852,308 +1961,173 @@ direct publishing (the `E401 token is invalid` that stopped v0.0.17).
|
|
|
1852
1961
|
Every `uses:` in both workflows is pinned to a commit SHA with the
|
|
1853
1962
|
version in a comment; bump the SHA and the comment together.
|
|
1854
1963
|
|
|
1964
|
+
## The workspace root
|
|
1965
|
+
|
|
1966
|
+
Every verb works on one workspace: the directory whose `.vx` holds the
|
|
1967
|
+
nearest `.vx/workspace.ts` (or `.mts`, `.js`, `.mjs`, `.cts`, `.cjs`) from
|
|
1968
|
+
the working directory up. vx never looks above the git top-level (a
|
|
1969
|
+
worktree is its own; a submodule looks on into its superproject), and
|
|
1970
|
+
outside git it looks up to `/`. Nothing else marks the root: not a
|
|
1971
|
+
`package.json`, `pnpm-workspace.yaml` or a lockfile (owner, 2026-10-13).
|
|
1972
|
+
Without one, every verb but `vx init` stops with `VX_E_NO_WORKSPACE`;
|
|
1973
|
+
`vx init` writes one (`@vzn/vx-migrate` at the package manager's root,
|
|
1974
|
+
in a nested workspace the outer one does not list too). A project inside the
|
|
1975
|
+
root is a directory holding a `vx.config.*`, which `vx lock` finds
|
|
1976
|
+
(see [`vx lock`](#vx-lock)); a `discover` plugin adds others.
|
|
1977
|
+
|
|
1978
|
+
## Which vx runs
|
|
1979
|
+
|
|
1980
|
+
A global vx (installed with a package manager, or a compiled binary
|
|
1981
|
+
installed by hand) hands every call to the project's own `@vzn/vx` when
|
|
1982
|
+
there is one, so a project runs the version its lockfile pins (owner,
|
|
1983
|
+
2026-10-13). There is no opt-out. The workspace root's
|
|
1984
|
+
`node_modules/@vzn/vx` runs, through its
|
|
1985
|
+
`node_modules/.bin/vx`. One hop: the receiving vx sees `VX_HANDED_OFF`
|
|
1986
|
+
and does not look again. A vx never hands off to itself (the same
|
|
1987
|
+
package, a pnpm symlink included), nor to a `.bin/vx` another user owns
|
|
1988
|
+
(one planted above your checkout would run as you). An npm install's launcher does the
|
|
1989
|
+
walk in the shell, a test per level and no fork, and `exec`s the local
|
|
1990
|
+
vx; the compiled binary checks one path per level before it loads
|
|
1991
|
+
anything, then runs the local vx with its stdio, passes on a signal sent
|
|
1992
|
+
to it alone, and exits with its code. From source (`bun src/bin.ts`) vx
|
|
1993
|
+
never hands off.
|
|
1994
|
+
|
|
1995
|
+
`vx --version` says when it was handed the call (`vx 0.0.640
|
|
1996
|
+
(/repo/node_modules/@vzn/vx-linux-x64/vx, handed off from
|
|
1997
|
+
/usr/local/bin/vx)`), and `vx info` shows the file that ran and who handed off
|
|
1998
|
+
(`vxPath`, `handedOffFrom`).
|
|
1999
|
+
|
|
1855
2000
|
## `vx upgrade`
|
|
1856
2001
|
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
`
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
2002
|
+
Brings vx to a newer release (`vx upgrade TAG` pins one, `v0.4.0` or
|
|
2003
|
+
`0.4.0`; default latest; a second tag is refused, not dropped), the way
|
|
2004
|
+
it was installed. Named `upgrade` per CLI convention (`bun upgrade`,
|
|
2005
|
+
`deno upgrade`). Refuses when running from source: use `git pull`.
|
|
2006
|
+
|
|
2007
|
+
**A compiled binary installed by hand** replaces itself. It reads its
|
|
2008
|
+
platform package (`@vzn/vx-<os>-<arch>`) from the npm registry and
|
|
2009
|
+
checks npm's registry signature over `<name>@<version>:<integrity>`
|
|
2010
|
+
against npm's signing key, pinned in vx (key id
|
|
2011
|
+
`SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U`), then the
|
|
2012
|
+
tarball's SHA-512 against that signed integrity, and only then extracts
|
|
2013
|
+
`package/vx`, stages it in a fresh directory beside the binary and
|
|
2014
|
+
renames it over. The host that serves the tarball cannot make that
|
|
2015
|
+
signature, so a swapped tarball, or a document with a swapped integrity,
|
|
2016
|
+
is refused: `the npm registry signature for … is not valid — refusing
|
|
2017
|
+
to install; nothing replaced`, or `the download did not match the
|
|
2018
|
+
integrity npm signed … nothing replaced`. A release signed with a key
|
|
2019
|
+
id vx does not pin says so: `… is signed with an npm key this vx does
|
|
2020
|
+
not know (…) — if npm rotated its signing key, this vx's installer
|
|
2021
|
+
needs updating`. A release with no signature or no integrity is refused
|
|
2022
|
+
before the download. Every refusal comes before anything is written.
|
|
2023
|
+
There is no fallback to GitHub's release digest: when the registry is
|
|
2024
|
+
unreachable, the upgrade is refused.
|
|
2025
|
+
|
|
2026
|
+
A host it cannot reach (no route, a proxy that is down) is one line,
|
|
2027
|
+
`could not reach registry.npmjs.org to read the release (…) — check the
|
|
2028
|
+
network or the proxy and re-run`, never a stack; so is a document cut
|
|
2029
|
+
after the headers or not JSON (a captive portal's page served with a
|
|
2030
|
+
200), and a tarball cut the same way. A binary whose directory this user
|
|
2031
|
+
cannot write (a root-owned `/usr/local/bin`) is refused before the
|
|
1883
2032
|
download: `cannot write to <dir> (EACCES), where this vx lives — nothing
|
|
1884
|
-
downloaded; re-run as a user who can (sudo vx upgrade), …`. The new
|
|
1885
|
-
keeps the old one's mode (and, as root, its owner), and must
|
|
1886
|
-
`--version` before the upgrade reports it installed: one that
|
|
1887
|
-
start on this machine
|
|
1888
|
-
for the previous vx, and the command exits 1 saying so
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2033
|
+
downloaded; re-run as a user who can (sudo vx upgrade), …`. The new
|
|
2034
|
+
binary keeps the old one's mode (and, as root, its owner), and must
|
|
2035
|
+
answer `--version` before the upgrade reports it installed: one that
|
|
2036
|
+
does not start on this machine, or does not answer within 10 s, is
|
|
2037
|
+
swapped back for the previous vx, and the command exits 1 saying so
|
|
2038
|
+
(item 1097). On the version it already is it says `already at
|
|
2039
|
+
<version>` and downloads nothing.
|
|
2040
|
+
|
|
2041
|
+
**A vx a package manager installed** (the npm package's launcher runs
|
|
2042
|
+
`…/node_modules/@vzn/vx-<os>-<arch>/vx`) is upgraded by that package
|
|
2043
|
+
manager, never by a rename over its file, which the next install would
|
|
2044
|
+
undo. The path names the install:
|
|
2045
|
+
|
|
2046
|
+
| Install | Detected by | Runs |
|
|
2047
|
+
| ---------------------- | ------------------------------------- | ----------------------------- |
|
|
2048
|
+
| npm global | `<prefix>/lib/node_modules` | `npm install -g @vzn/vx@<v>` |
|
|
2049
|
+
| pnpm global | `…/pnpm/global/…` | `pnpm add -g @vzn/vx@<v>` |
|
|
2050
|
+
| bun global | `~/.bun/install/global` | `bun add -g @vzn/vx@<v>` |
|
|
2051
|
+
| yarn global | `…/yarn/global/…` | `yarn global add @vzn/vx@<v>` |
|
|
2052
|
+
| a project's dependency | the package.json that lists `@vzn/vx` | its manager's add, same field |
|
|
2053
|
+
|
|
2054
|
+
A project's package.json is the tree's root, or for a hoisted workspace
|
|
2055
|
+
the nearest one above the current directory that lists `@vzn/vx`. Its
|
|
2056
|
+
manager is its lockfile (`pnpm-lock.yaml`, `bun.lock`, `yarn.lock`,
|
|
2057
|
+
`package-lock.json`), else `packageManager`, else npm; the field it is
|
|
2058
|
+
listed in is kept (`--save-dev`, `--dev`, …), and pnpm at a workspace
|
|
2059
|
+
root adds `--workspace-root`. The lockfile is updated by that manager.
|
|
2060
|
+
Run inside a project that installs `@vzn/vx`, a global vx hands
|
|
2061
|
+
`vx upgrade` to the project's own (§ Which vx runs), so the project's
|
|
2062
|
+
dependency is what moves.
|
|
2063
|
+
vx prints what it runs (`vx upgrade: 0.0.1 → 0.0.2: running pnpm add
|
|
2064
|
+
@vzn/vx@0.0.2 --save-dev (in /repo)`), runs it there, and exits with an
|
|
2065
|
+
error if it fails. vx never installs Node or a package manager: one not
|
|
2066
|
+
on `PATH` is refused with the command to run. A vx run from an npx or
|
|
2067
|
+
bunx cache has nothing installed to upgrade, and says to ask for
|
|
2068
|
+
`@vzn/vx@latest` instead.
|
|
2069
|
+
|
|
2070
|
+
Exit codes: `0` installed, or already at that version; `1` anything
|
|
2071
|
+
refused or swapped back: from source, a cache install, a host it cannot
|
|
2072
|
+
reach, a directory it cannot write, a signature or integrity that does
|
|
2073
|
+
not verify, a binary that does not start, a package manager that failed
|
|
2074
|
+
or is missing.
|
|
1898
2075
|
|
|
1899
2076
|
## `vx init`
|
|
1900
2077
|
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
`
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
`
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
and
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
named
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
`
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
package.json scripts are `init`'s. A workspace with no scripts at
|
|
1948
|
-
all still gets the workspace file, a printed example config, and the
|
|
1949
|
-
next command to run. A root
|
|
1950
|
-
`package.json` with no `workspaces` field is single-project mode, and
|
|
1951
|
-
when `packages/*/package.json` files sit below it unreached, both
|
|
1952
|
-
`init` and a run that finds no config say so instead ("package.json
|
|
1953
|
-
declares no workspaces … Add "workspaces": ["packages/*"] to
|
|
1954
|
-
package.json and re-run") rather than "no scripts" or "run vx init". A
|
|
1955
|
-
workspace whose globs match no `package.json` gets a note naming them
|
|
1956
|
-
(M-42); a nameless member, or two sharing a name, is matched and
|
|
1957
|
-
named on its own line instead (M-46). When such skipped scripts are all there
|
|
1958
|
-
is, the report opens `no package.json scripts became tasks.` rather than
|
|
1959
|
-
saying there are none (M-59). When `pnpm-workspace.yaml` lists the
|
|
1960
|
-
members and the root `package.json`'s `workspaces` lists other globs,
|
|
1961
|
-
the note names the unread list (M-51): pnpm reads only the yaml, while
|
|
1962
|
-
bun, npm and yarn read `package.json`. With
|
|
1963
|
-
no `package.json` here or above, `init` says to create one (`bun init` or
|
|
1964
|
-
`npm init -y`) first. Every
|
|
1965
|
-
generated config is typed for the editor through
|
|
1966
|
-
`import type { ProjectConfig } from '@vzn/vx/config'` and `satisfies
|
|
1967
|
-
ProjectConfig` — a type-only import Bun erases, so the file loads in a
|
|
1968
|
-
workspace that runs the `vx` binary without the package installed. The
|
|
1969
|
-
workspace file takes the same form (`satisfies WorkspaceConfig`). A
|
|
1970
|
-
runtime `import { defineWorkspace } from '@vzn/vx'` costs nothing extra
|
|
1971
|
-
since 2026-09-10: the running `vx` serves its own core to that
|
|
1972
|
-
specifier (bin.ts registers the alias), so a config or a plugin package
|
|
1973
|
-
never loads a second copy, and a workspace that runs the binary needs
|
|
1974
|
-
no `@vzn/vx` installed for it — the type-only form is still what the
|
|
1975
|
-
scaffold writes, because it types the same with no import to resolve
|
|
1976
|
-
in an editor without the package. A workspace that imports a PLUGIN
|
|
1977
|
-
package at runtime without having installed it is told so, and to
|
|
1978
|
-
install its dependencies. `--mjs` writes the same objects as `vx.config.mjs` and
|
|
1979
|
-
`vx.workspace.mjs`, with no type import and no `satisfies`: a package whose
|
|
1980
|
-
own `tsconfig` includes every `.ts` under it compiles a `vx.config.ts`
|
|
1981
|
-
into its dist (TanStack/query's `tsc --build`, 2026-09-11), and an
|
|
1982
|
-
`.mjs` is outside that include. `@vzn/vx-migrate` takes the same flag.
|
|
1983
|
-
A file it writes that `.gitignore` drops (vueuse ignores
|
|
1984
|
-
`packages/**/*.mjs`) gets a `note:` naming it and the `!vx.config.mjs`
|
|
1985
|
-
line that keeps it, since a commit would leave it behind unnoticed.
|
|
1986
|
-
|
|
1987
|
-
Each script becomes a task with its command verbatim. `build` gets
|
|
1988
|
-
`dependsOn: ['^build']` (a `build` that only delegates, `pnpm run
|
|
1989
|
-
compile`, is a group, and the edge goes on the script that does the
|
|
1990
|
-
work) and **no cache block** — under a
|
|
1991
|
-
`TODO(vx-migrate)` showing the block to add with the package's real
|
|
1992
|
-
inputs and outputs, and the same block commented as the task's first
|
|
1993
|
-
line: set the real globs and delete the `// `. A task without a cache block always runs; a block
|
|
1994
|
-
with EMPTY outputs is not "uncached" but a no-output task that hits on
|
|
1995
|
-
unchanged inputs and skips the build with nothing to restore (a deleted
|
|
1996
|
-
`dist` stays deleted under a green `up-to-date` run — what `init`
|
|
1997
|
-
generated until 2026-09-04), and a guessed `dist/**` would restore the
|
|
1998
|
-
wrong tree for every package that writes elsewhere. The block the TODO
|
|
1999
|
-
shows names `dist/**`, or the default output of the framework the command
|
|
2000
|
-
runs: `.next/**` minus `!.next/cache/**` for `next build`, `.output/**`
|
|
2001
|
-
for Nuxt, `build/**` for Remix, React Router, Create React App and
|
|
2002
|
-
Docusaurus, `public/**` for Gatsby, `storybook-static/**` for Storybook;
|
|
2003
|
-
for any other command, the directory it names with `--outDir` / `--out-dir`
|
|
2004
|
-
/ `-d`, else the ones it cleans first (`del-cli distribution`, `rimraf lib
|
|
2005
|
-
types`), unless it makes one again with `mkdir` (D-90).
|
|
2006
|
-
A package in a cycle of builds (nuxt's `@nuxt/nitro-server` devDepends
|
|
2007
|
-
on `nuxt`, which depends on it; pnpm sorts it away) gets, instead of
|
|
2008
|
-
`^build`, an edge to each build outside its cycle that `^build` would
|
|
2009
|
-
reach, and a TODO to order the cycle: `^build` there refuses the run.
|
|
2010
|
-
`test` / `typecheck` wait for
|
|
2011
|
-
`build` when the package has one (`lint` reads sources and gets no
|
|
2012
|
-
edge); `dev` / `start` / `serve` / `watch` /
|
|
2013
|
-
`preview` become persistent tasks, with a TODO to add `readyWhen` when
|
|
2014
|
-
another task depends on one, and so does a watcher: a `watch` segment in the script's name (`build:watch`),
|
|
2015
|
-
a `--watch` flag, `tsc -w` / `rollup -w`, or nodemon (D-40), and a server:
|
|
2016
|
-
`serve <dir>`, `http-server`, bare `vite`, a tool's `dev` / `serve` /
|
|
2017
|
-
`start` / `preview` verb (`next start`, `netlify dev`), or a script that
|
|
2018
|
-
runs such a script of its package by name (`cross-env X=1 pnpm start`) or
|
|
2019
|
-
through a runner (`run-p web api`, `concurrently "npm:web" "npm:api"`, D-113),
|
|
2020
|
-
outside quotes and not sent to the background with `&` (D-91), read past
|
|
2021
|
-
a launcher's `--package <name>` / `-p <name>` (`pnpm dlx --package
|
|
2022
|
-
netlify-cli netlify dev`, D-112). A
|
|
2023
|
-
script whose name no task may carry (`lint#fix`, `^up`; the schema's
|
|
2024
|
-
rule, item 1000) is left out with a TODO rather than written into a
|
|
2025
|
-
config every later command refuses, and a `__proto__` script is written
|
|
2026
|
-
as a computed key, since a literal `__proto__:` sets the prototype. An
|
|
2027
|
-
existing `vx.workspace.*` in any extension the loader reads (`.mts`
|
|
2028
|
-
included) is kept, and `--force` REPLACES a package's config of another
|
|
2029
|
-
extension (`replaced:` in the report) rather than writing a second one
|
|
2030
|
-
the loader would choose between by its order (item 1033). Without
|
|
2031
|
-
`--force`, `init` never overwrites: a package that already has a vx
|
|
2032
|
-
config keeps it, untouched, the rest get theirs, and the report lists
|
|
2033
|
-
the kept ones under `kept`, so a half-adopted workspace adopts the rest
|
|
2034
|
-
(M-52). `@vzn/vx-migrate` still refuses an existing config.
|
|
2035
|
-
|
|
2036
|
-
A missing `vx.workspace.*` is not an error. A run where no package has
|
|
2037
|
-
a config fails before any task, exit 1:
|
|
2038
|
-
``vx run: no projects declare task(s): build. No package declares a vx.config — run `vx init` to write one per package from its package.json scripts.``
|
|
2039
|
-
|
|
2040
|
-
Two npm conventions are mapped rather than copied, because copying them
|
|
2041
|
-
loses behaviour. `pre<x>` / `post<x>` hooks, which npm runs around `x`
|
|
2042
|
-
without being named, are folded into `x`'s command in that order, each
|
|
2043
|
-
in its own subshell, so the chain stops at the first that fails whatever
|
|
2044
|
-
a part holds (a `;`, an `exit`), and forwarded `--` args reach `x` alone,
|
|
2045
|
-
as npm hands them to the script and not its hooks (item 905), appended
|
|
2046
|
-
as text, so no part sees them as `$1` (X-60). The
|
|
2047
|
-
command is a small shell function, `vx_script`, around the three parts;
|
|
2048
|
-
it carries a TODO saying so; a `pre<x>` with no `x` stays a task of its own, and
|
|
2049
|
-
npm's lifecycle hooks (`prepack`, `prepublishOnly`, …) are never tasks.
|
|
2050
|
-
A member with no script to run (none, or only lifecycle hooks: a
|
|
2051
|
-
types-only or config package) gets no `vx.config.ts` and is named in
|
|
2052
|
-
the report; it is still a project, and a task declared in its own
|
|
2053
|
-
`vx.config.ts` runs. A package with no `build` script whose `prepack`, `prepublishOnly`,
|
|
2054
|
-
`prepublish` or `prepare` runs a builder (`bob build`, `tsc`, `tsup`, …)
|
|
2055
|
-
is named in the report, which says to add a `build` script running it
|
|
2056
|
-
(react-navigation's twelve packages, whose root `build` is `lerna run
|
|
2057
|
-
prepack`, mapped with no build at all).
|
|
2058
|
-
Where the package's manager runs no such hooks every `pre<x>` and
|
|
2059
|
-
`post<x>` is a task of its own: Yarn 2+ (the nearest `packageManager:
|
|
2060
|
-
yarn@2+`, or a Berry `yarn.lock`, D-31; a `packageManager` naming none of npm,
|
|
2061
|
-
pnpm, yarn or bun defers to the lockfile, D-96), npm under `ignore-scripts=true`
|
|
2062
|
-
in the `.npmrc` beside its lockfile, pnpm under
|
|
2063
|
-
`enable-pre-post-scripts=false` there or `enablePrePostScripts: false` in
|
|
2064
|
-
`pnpm-workspace.yaml` (D-33). Bun and Yarn 1 run them whatever those say.
|
|
2065
|
-
Under Yarn 2+ a segment's `run <script>`, Yarn's shell builtin, is written
|
|
2066
|
-
`yarn run <script>`: vx's shell has no `run` (D-92).
|
|
2067
|
-
A Yarn 2+ repo on Plug'n'Play (no `nodeLinker: node-modules` or `pnpm` in
|
|
2068
|
-
`.yarnrc.yml`) keeps its bins in `.pnp.cjs`, where vx's PATH finds none;
|
|
2069
|
-
the report says so and names `nodeLinker: node-modules` or `yarn exec
|
|
2070
|
-
'<command>'` (D-108).
|
|
2071
|
-
A script reading `$npm_package_version`, `$npm_package_name` or
|
|
2072
|
-
`$npm_lifecycle_event`, which every manager sets and vx does not, gets
|
|
2073
|
-
them under `exec.env.define`, the first two read from an imported
|
|
2074
|
-
`package.json` so a version bump reaches them; any other `$npm_*` it
|
|
2075
|
-
reads gets a TODO (D-34). Among several packages, a workspace root
|
|
2076
|
-
script that runs the members (`pnpm -r build`, `--filter`, `-C`, pnpm's `pn` alias
|
|
2077
|
-
included (D-97), Yarn's
|
|
2078
|
-
`--cwd` and `yarn workspace <name>`, npm's `--prefix`, npm's
|
|
2079
|
-
and Yarn's workspace flags, a `cd` into or above a member, turbo, nx, lerna, `vp run`, vx itself) is not mapped
|
|
2080
|
-
(a flag counts on the package manager, or after `node <bin> run`, and not on the program it
|
|
2081
|
-
runs: berry's `yarn node -r ./setup.ts` is node's `--require`, D-81; bun's
|
|
2082
|
-
`cd test && …` enters no member, D-83),
|
|
2083
|
-
nor is one that runs such a script by name (vite's `ci-docs`: `pnpm build &&
|
|
2084
|
-
pnpm docs-build`; through `run-s` / `run-p` / `npm-run-all` or `concurrently
|
|
2085
|
-
"npm:x"` too, lexical's `ci-check`, D-95), and neither is one whose name a member's task carries, so `--all` never runs
|
|
2086
|
-
a check twice (D-45). The rest check the whole repo (`lint: oxlint .`,
|
|
2087
|
-
`test: vitest`) and become the root's own tasks in a root vx.config, when
|
|
2088
|
-
the root has a `"name"` (vx skips a nameless root's config) and no config
|
|
2089
|
-
of its own; one already there, hand-written or from an earlier
|
|
2090
|
-
`vx init`, stays as written, `--force` included, and is listed under
|
|
2091
|
-
`kept`. The report names each script left out and why
|
|
2092
|
-
(a `pre` / `post` hook goes with its script, D-85), its examples of running
|
|
2093
|
-
the members spelled by the repo's manager (`--workspaces` under npm, `yarn
|
|
2094
|
-
workspaces foreach` under Yarn 2+), and says which;
|
|
2095
|
-
with nothing mapped it names the root whenever it has a script, a member
|
|
2096
|
-
or not (pnpm's root is not), and why: its scripts run the workspace, or
|
|
2097
|
-
share a member's task name (listed), or both; it tells a root with no `"name"` to add one
|
|
2098
|
-
first (vuejs/core), naming the scripts that would then map (react, D-87), and a root whose
|
|
2099
|
-
`"name"` a member also carries to rename it, since a root config would make the
|
|
2100
|
-
workspace refuse every run for the duplicate (insomnia, D-129). A single-package repo's root is its project and maps.
|
|
2101
|
-
A lone root beside a `lerna.json` (Lerna-classic: packages listed there, not
|
|
2102
|
-
in `workspaces`) gets a note naming the `workspaces` globs to add, Lerna's
|
|
2103
|
-
`packages/*` default when it lists none (D-111).
|
|
2104
|
-
A member whose package.json has no `name` is no project; one with a
|
|
2105
|
-
script is named in the report, to be given a name (remix's
|
|
2106
|
-
`packages/component/bench`, D-106).
|
|
2107
|
-
A member's script that runs another member's work (`pnpm -C ../pinia
|
|
2108
|
-
run build`, `yarn workspace <name>`, `-r` / `--filter`, a `cd` or `-C`
|
|
2109
|
-
/ `--cwd` / `--prefix` into another member) keeps its command and gets a
|
|
2110
|
-
TODO naming that part: the graph runs that member's task once, so name it
|
|
2111
|
-
under `dependsOn` and drop it from the command (pinia). A directory inside
|
|
2112
|
-
the member or the root is no other member.
|
|
2113
|
-
A script that is nothing but `npm run <other>` (`pnpm <other>`, `yarn
|
|
2114
|
-
<other>`, `bun run <other>`, `npm test`, `npm start`) becomes a **group**
|
|
2115
|
-
over `<other>` — `dependsOn` and no command — so the graph runs and
|
|
2116
|
-
caches the target instead of a package-manager subprocess it cannot
|
|
2117
|
-
see. When `build` is such a group, the task it reaches that runs a
|
|
2118
|
-
command takes `^build` and the cache TODO, since a group has nothing
|
|
2119
|
-
to cache; it and the groups on the way drop the wait on `build` a
|
|
2120
|
-
`typecheck` or `test` would carry, since they are the build (D-16). Bare, a package manager's own command is not a script: `bun test`
|
|
2121
|
-
is Bun's test runner, `bun build` its bundler, `pnpm install` and `yarn
|
|
2122
|
-
add` the managers' verbs, so each stays a command (item 908), and so do
|
|
2123
|
-
the names each manager reserves or hands on: `pnpm docs` and `pnpm
|
|
2124
|
-
version` (npm's), `bun deploy`, `yarn check` (D-32); `pnpm test`, `yarn
|
|
2125
|
-
test` and `bun lint` do run the script. Arguments, flags or a `&&`
|
|
2126
|
-
chain make it a real command again and
|
|
2127
|
-
it is left verbatim, and so is one whose target becomes no task (a
|
|
2128
|
-
lifecycle script, or a hook folded into another script): a group over
|
|
2129
|
-
it would name a task nothing defines (D-12). A verbatim chain whose
|
|
2130
|
-
parts run this package's own scripts (`check: pnpm run build && pnpm run
|
|
2131
|
-
lint`) gets a TODO naming those parts: each ran again inside the command,
|
|
2132
|
-
beside its own task, so `vx run check` built twice (M-41). So does a
|
|
2133
|
-
part that runs them through `run-s`, `run-p` or `npm-run-all`
|
|
2134
|
-
(`build: run-p build:*`), unless the task is persistent (M-48). Its
|
|
2135
|
-
order may matter, so it is not made a group.
|
|
2136
|
-
|
|
2137
|
-
The report lists each TODO once per reason: tasks that share one are
|
|
2138
|
-
named together (the first five, then a count; the files carry each),
|
|
2139
|
-
and when no task caches it says uncommenting a TODO task's cache line
|
|
2140
|
-
makes the second run a hit. Its `next:` line is a command the user can type: the
|
|
2141
|
-
runner that started vx (`npx`, `pnpm`, `yarn`, `bunx`, read from
|
|
2142
|
-
`npm_config_user_agent`) with the installed `vx` bin, else the
|
|
2143
|
-
`@vzn/vx` package (`npx` under Yarn 1, which has no `dlx`); with no
|
|
2144
|
-
runner, a bare `vx`. Outside a git work tree it starts with
|
|
2145
|
-
`git init`, and with no script mapped with declaring a task, since the
|
|
2146
|
-
run would refuse without either. It runs `build`, else the first task that
|
|
2147
|
-
neither serves (`persistent`) nor changes the repo (`clean`, `release`,
|
|
2148
|
-
`publish`, `deploy`, `version`, `format`, `fix`, …), else the first task
|
|
2149
|
-
(react-navigation was told `vx run clean --all`).
|
|
2078
|
+
`vx init` reads no `package.json` (M, 2026-10-14). When the directory it
|
|
2079
|
+
runs in, or one above it up to the git top-level, holds a
|
|
2080
|
+
`package.json`, `pnpm-workspace.yaml`, `turbo.json`, `turbo.jsonc` or
|
|
2081
|
+
`nx.json`, it runs `@vzn/vx-migrate` there, so `vx init` is the one
|
|
2082
|
+
command (owner, 2026-10-08): the installed one (the nearest
|
|
2083
|
+
`node_modules/@vzn/vx-migrate` up from the working directory), else
|
|
2084
|
+
this vx's version through `bun x` (`BUN_BE_BUN=1` makes the compiled
|
|
2085
|
+
binary that runtime). `--dry`, `--format`, `--force`, `--mjs`,
|
|
2086
|
+
`--native` and `--keep` pass through, and the exit is vx-migrate's.
|
|
2087
|
+
vx-migrate maps a Turbo, Nx or Vite Task repo, else the `package.json`
|
|
2088
|
+
scripts, one `vx.config.ts` per package; writes the workspace file with
|
|
2089
|
+
the plugins the repo calls for (the lockfile's, a remote cache the repo
|
|
2090
|
+
shows); installs them; and ends with `vx lock`. Its README holds the
|
|
2091
|
+
rules; [`bunx @vzn/vx-migrate`](#bunx-vznvx-migrate) below sums them up.
|
|
2092
|
+
|
|
2093
|
+
With none of those files, it writes a workspace whose one project is
|
|
2094
|
+
the root, at the nearest workspace root up from the working directory,
|
|
2095
|
+
else there:
|
|
2096
|
+
|
|
2097
|
+
```ts
|
|
2098
|
+
// .vx/workspace.ts
|
|
2099
|
+
import { defineWorkspace } from '@vzn/vx'
|
|
2100
|
+
|
|
2101
|
+
export default defineWorkspace({ plugins: [] })
|
|
2102
|
+
```
|
|
2103
|
+
|
|
2104
|
+
```ts
|
|
2105
|
+
// vx.config.ts
|
|
2106
|
+
import { defineProject } from '@vzn/vx'
|
|
2107
|
+
|
|
2108
|
+
export default defineProject({ tasks: {} })
|
|
2109
|
+
```
|
|
2110
|
+
|
|
2111
|
+
plus `.vx/.gitignore` and `.vx/.gitattributes`. It then runs `vx lock`'s
|
|
2112
|
+
step, and `next:` says to declare a task and run `vx lock && vx run
|
|
2113
|
+
<task>`, preceded by `git init` outside a git work tree. The `@vzn/vx`
|
|
2114
|
+
import needs no install: the running vx serves its own core to that
|
|
2115
|
+
specifier (bin.ts registers the alias). A file already there is kept
|
|
2116
|
+
and named under `kept`; `--force` replaces it, a file of another
|
|
2117
|
+
extension included (`replaced`). `--mjs` writes `.vx/workspace.mjs` and
|
|
2118
|
+
`vx.config.mjs`. `--dry` prints the files and writes nothing;
|
|
2119
|
+
`--dry --format json` prints the plan as one JSON document
|
|
2120
|
+
(`schemas/init.json`): `files` (each path and its whole text), `kept`,
|
|
2121
|
+
`replaced`, `todos`, `notes` and `next`. `--native` and `--keep` answer
|
|
2122
|
+
a Turbo or Nx repo's question, so they are refused here. `--format json`
|
|
2123
|
+
without `--dry`, or with `--plugin`, is refused (`VX_E_USAGE`).
|
|
2150
2124
|
|
|
2151
2125
|
`vx init --plugin <seam>` writes a plugin instead: `plugins/<seam>.ts`,
|
|
2152
2126
|
a small runnable plugin for that seam (`executor`, `cache`,
|
|
2153
2127
|
`telemetry`, `schedule`, `admit`, `commands`, `project`, `graph`,
|
|
2154
2128
|
`key`), and `plugins/<seam>.test.ts`, which drives it through `run()`
|
|
2155
2129
|
(`bun test`, with `@vzn/vx` installed). It prints the line that
|
|
2156
|
-
declares it in the workspace file a run reads (
|
|
2130
|
+
declares it in the workspace file a run reads (`.vx/workspace.ts` when
|
|
2157
2131
|
there is none). The templates are TypeScript, so `--mjs` with
|
|
2158
2132
|
`--plugin` is refused. An existing file is refused without
|
|
2159
2133
|
`--force`; `--dry` names the files and writes nothing. The two files are
|
|
@@ -2161,25 +2135,35 @@ there is none). The templates are TypeScript, so `--mjs` with
|
|
|
2161
2135
|
core (`src/cli/plugin-templates.ts`) and held equal by
|
|
2162
2136
|
`tests/plugin-templates.unsafe.test.ts` (H-21).
|
|
2163
2137
|
|
|
2164
|
-
Exit codes: `0` the files written (or, with `--dry`, printed); `1`
|
|
2165
|
-
|
|
2166
|
-
|
|
2138
|
+
Exit codes: `0` the files written (or, with `--dry`, printed); `1` a
|
|
2139
|
+
refused flag, a parse error (`--mjs` with `--plugin` among them), or a
|
|
2140
|
+
plugin file already there without `--force`. Handed to
|
|
2141
|
+
`@vzn/vx-migrate`, the exit is its own.
|
|
2167
2142
|
|
|
2168
|
-
## `vx
|
|
2143
|
+
## `bunx @vzn/vx-migrate`
|
|
2169
2144
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2145
|
+
Mapping a repo's own config to vx is `@vzn/vx-migrate`, its own
|
|
2146
|
+
package, run before a workspace file exists (it writes one); `vx init`
|
|
2147
|
+
runs it in any repo with a `package.json` —
|
|
2172
2148
|
|
|
2173
2149
|
```
|
|
2174
|
-
bunx @vzn/vx-migrate # turbo.json, an Nx graph
|
|
2150
|
+
bunx @vzn/vx-migrate # turbo.json, an Nx graph, vite-plus run.tasks, else package.json scripts → vx.config.ts
|
|
2175
2151
|
bunx @vzn/vx-migrate --dry # print the generated files instead of writing
|
|
2176
2152
|
bunx @vzn/vx-migrate --force # overwrite existing vx.config.* / vx-preset.ts
|
|
2177
|
-
bunx @vzn/vx-migrate --from nx # disambiguate when
|
|
2178
|
-
```
|
|
2179
|
-
|
|
2180
|
-
—
|
|
2181
|
-
|
|
2182
|
-
|
|
2153
|
+
bunx @vzn/vx-migrate --from nx # disambiguate when several sources are checked in (turbo, nx, vite-task, scripts)
|
|
2154
|
+
```
|
|
2155
|
+
|
|
2156
|
+
— In a Turbo or Nx repo it asks a terminal native (a `vx.config.ts`
|
|
2157
|
+
per package, the default) or keep (a workspace file declaring `turbo()`
|
|
2158
|
+
or `nx()`, which read the repo's config live); `--native` or `--keep`
|
|
2159
|
+
answer it, and without a terminal it is native. Both declare
|
|
2160
|
+
`turboCache()` / `nxCache()` when the repo shows a remote cache
|
|
2161
|
+
(turbo.json's `remoteCache`, a `turbo link`ed `.turbo/config.json`, or
|
|
2162
|
+
a CI file setting `TURBO_TOKEN` / `NX_SELF_HOSTED_REMOTE_CACHE_SERVER`),
|
|
2163
|
+
and name Nx Cloud, whose wire vx does not speak. Elsewhere each
|
|
2164
|
+
package's `package.json` scripts become its tasks, and the workspace
|
|
2165
|
+
file declares the `@vzn/vx-lockfile` plugin for the root's lockfile.
|
|
2166
|
+
What the package writes goes through core's migration seam
|
|
2183
2167
|
(`applyMigration`, exported from `@vzn/vx`), which renders, guards
|
|
2184
2168
|
against overwriting, writes and reports. The mapping rules live in the
|
|
2185
2169
|
package's README.
|
|
@@ -2194,7 +2178,7 @@ none is dropped in silence. An Nx flag's camelCase spelling (`--nxBail`,
|
|
|
2194
2178
|
kebab-case row. A `[=<bool>]` row takes `=true` (the flag bare) and `=false` (left out), as Turbo's and Nx's parsers do. `vx run-many` and `vx affected` name the
|
|
2195
2179
|
`vx run` that does the same, and the other verbs a hand types from
|
|
2196
2180
|
either tool name what does it here: `graph` (`--graph`), `ls`
|
|
2197
|
-
(`vx show`), `query` (`--dry=json`), `reset` (`vx cache
|
|
2181
|
+
(`vx show`), `query` (`--dry=json`), `reset` (`vx cache clean`), and
|
|
2198
2182
|
`daemon`, `login`, `logout`, `link` and `unlink`, which vx has no use
|
|
2199
2183
|
for. Nx's `project:target` (`vx run web:build`,
|
|
2200
2184
|
when `web` declares `build`) is answered with `vx run web#build`: from
|
|
@@ -2220,8 +2204,8 @@ copy to the source.
|
|
|
2220
2204
|
| turbo | `--summarize[=<bool>]` | same | `--summarize[=<path>]` |
|
|
2221
2205
|
| turbo | `--output-logs=new-only` | refuse | use `--output-logs=full` (a hit replays its log) or `errors-only` |
|
|
2222
2206
|
| turbo | `--output-logs <v>` | same | `--output-logs full\|errors-only\|hash-only\|none` |
|
|
2223
|
-
| turbo | `--no-cache` | same | `--no-cache` reads nothing either; Turbo's (reads, no writes) is `--cache local:r,remote:r`
|
|
2224
|
-
| turbo | `--cache <v>` |
|
|
2207
|
+
| turbo | `--no-cache` | same | `--no-cache` reads nothing either; Turbo's (reads, no writes) is `--cache-policy local:r,remote:r` |
|
|
2208
|
+
| turbo | `--cache <v>` | alias | `--cache-policy`, the same spec (`local:rw,remote:r`) |
|
|
2225
2209
|
| turbo | `--cache-dir <v>` | same | `--cache-dir <path>` |
|
|
2226
2210
|
| turbo | `--profile` | same | `--profile[=<path>]` (Chrome trace) |
|
|
2227
2211
|
| turbo | `--only` | alias | `--exclude-dependencies` |
|
|
@@ -2232,13 +2216,13 @@ copy to the source.
|
|
|
2232
2216
|
| turbo | `--no-update-notifier` | refuse | vx prints no update notice: drop it |
|
|
2233
2217
|
| turbo | `--skip-infer` | refuse | vx runs the binary it is: drop it |
|
|
2234
2218
|
| turbo | `--root-turbo-json <v>` | refuse | `turbo()` reads the `turbo.json` at the workspace root: move it there |
|
|
2235
|
-
| turbo | `--experimental-otel-*` | refuse | telemetry is a plugin: `otel()` from @vzn/vx-otel in vx
|
|
2219
|
+
| turbo | `--experimental-otel-*` | refuse | telemetry is a plugin: `otel()` from @vzn/vx-otel in .vx/workspace.ts |
|
|
2236
2220
|
| nx | `--parallel <n>` | alias | `--concurrency <n>` (`--parallel=false` is 1) |
|
|
2237
2221
|
| turbo | `--parallel` | refuse | vx always honours `dependsOn`; `--concurrency <n>` sets how many run at once |
|
|
2238
2222
|
| turbo | `--scope <v>` | refuse | use `--filter <pkg>` |
|
|
2239
2223
|
| turbo | `--since <v>` | refuse | use `--filter '[<ref>]'` or `--affected=<ref>` |
|
|
2240
|
-
| turbo | `--remote-only[=<bool>]` | refuse | use `--cache local:,remote:rw`
|
|
2241
|
-
| turbo | `--remote-cache-read-only[=<bool>]` | refuse | use `--cache local:rw,remote:r`
|
|
2224
|
+
| turbo | `--remote-only[=<bool>]` | refuse | use `--cache-policy local:,remote:rw` |
|
|
2225
|
+
| turbo | `--remote-cache-read-only[=<bool>]` | refuse | use `--cache-policy local:rw,remote:r` |
|
|
2242
2226
|
| turbo | `--anon-profile` | refuse | use `--profile[=<path>]`; vx has no redacting variant, so read it before sharing it |
|
|
2243
2227
|
| turbo | `--cache-workers <v>` | refuse | vx sizes its own cache I/O: drop it |
|
|
2244
2228
|
| turbo | `--cwd <v>` | refuse | run vx from that directory: `cd <dir> && vx run …` |
|
|
@@ -2251,7 +2235,7 @@ copy to the source.
|
|
|
2251
2235
|
| turbo | `--preflight` | refuse | set `turboCache({ preflight: true })` or `TURBO_PREFLIGHT=1` |
|
|
2252
2236
|
| turbo | `--remote-cache-timeout <v>` | refuse | set `turboCache({ timeoutMs })` or `TURBO_REMOTE_CACHE_TIMEOUT` |
|
|
2253
2237
|
| turbo | `--single-package` | refuse | a repo with no workspaces is one project already: drop it |
|
|
2254
|
-
| turbo | `--token <v>`, `--team <v>`, `--api <v>` | refuse | a remote cache is a plugin: `turboCache()` from @vzn/vx-migrate in vx
|
|
2238
|
+
| turbo | `--token <v>`, `--team <v>`, `--api <v>` | refuse | a remote cache is a plugin: `turboCache()` from @vzn/vx-migrate in .vx/workspace.ts reads TURBO_TOKEN / TURBO_TEAM / TURBO_API |
|
|
2255
2239
|
| turbo | `--no-daemon`, `--daemon` | refuse | vx has no daemon: drop it |
|
|
2256
2240
|
| turbo | `--ui <v>`, `--log-order <v>`, `--log-prefix <v>` | refuse | vx frames each task’s output: `--output-logs <mode>` sets how much |
|
|
2257
2241
|
| nx | `-t <v>`, `--targets <v>`, `--target <v>` | alias | the task names, positional: `vx run build test` |
|
|
@@ -2268,13 +2252,13 @@ copy to the source.
|
|
|
2268
2252
|
| nx | `--uncommitted`, `--untracked` | refuse | use `--affected=HEAD` (the working tree against the last commit) |
|
|
2269
2253
|
| nx | `--max-parallel <v>` | alias | `--concurrency <n>` |
|
|
2270
2254
|
| nx | `--exclude-task-dependencies[=<bool>]` | alias | `--exclude-dependencies` |
|
|
2271
|
-
| nx | `--skip-remote-cache[=<bool>]` | alias | `--cache local:rw,remote:`
|
|
2255
|
+
| nx | `--skip-remote-cache[=<bool>]` | alias | `--cache-policy local:rw,remote:` |
|
|
2272
2256
|
| nx | `--verbose` | refuse | use `--verbosity <n>` (1 adds the summary table) |
|
|
2273
2257
|
| nx | `--files <v>` | refuse | vx asks git what changed: `--affected=<base>` |
|
|
2274
2258
|
| nx | `--batch` | refuse | vx runs one command per task: drop it |
|
|
2275
2259
|
| nx | `--dte`, `--use-agents` | refuse | vx distributes nothing: drop it |
|
|
2276
2260
|
| nx | `--nx-ignore-cycles` | refuse | vx refuses a task cycle by name: break it |
|
|
2277
|
-
| nx | `--runner <v>` | refuse | a remote cache is a plugin: `nxCache()` from @vzn/vx-migrate in vx
|
|
2261
|
+
| nx | `--runner <v>` | refuse | a remote cache is a plugin: `nxCache()` from @vzn/vx-migrate in .vx/workspace.ts |
|
|
2278
2262
|
| nx | `--skip-sync` | refuse | vx never runs sync generators: drop it |
|
|
2279
2263
|
| nx | `--tui`, `--no-tui`, `--tui-auto-exit` | refuse | vx frames each task’s output: `--output-logs <mode>` sets how much |
|
|
2280
2264
|
| nx | `--no-cloud` | refuse | vx has no cloud: drop it |
|
|
@@ -2288,8 +2272,9 @@ copy to the source.
|
|
|
2288
2272
|
|
|
2289
2273
|
## Machine-readable output
|
|
2290
2274
|
|
|
2291
|
-
`vx show`, `vx info`, `vx why`, `vx last` and `vx cache
|
|
2292
|
-
`--format json`. Each prints one JSON document
|
|
2275
|
+
`vx show`, `vx info`, `vx why`, `vx last` and `vx cache clean` take
|
|
2276
|
+
`--format json`. Each prints one JSON document on one line, as
|
|
2277
|
+
`vx run --format json` does, whose shape is a
|
|
2293
2278
|
checked-in JSON Schema (draft 2020-12) shipped with the package:
|
|
2294
2279
|
`schemas/show.json`, `schemas/info.json`, `schemas/why.json`,
|
|
2295
2280
|
`schemas/last.json`, `schemas/cache.json`
|
|
@@ -2321,15 +2306,15 @@ prose on stderr do not change. Branch on the code; the message may change.
|
|
|
2321
2306
|
|
|
2322
2307
|
#### `VX_E_USAGE`
|
|
2323
2308
|
|
|
2324
|
-
A flag, argument or
|
|
2325
|
-
it, and a close spelling the verb does take when there is one;
|
|
2309
|
+
A flag, argument, selection or `VX_*` environment value the verb does
|
|
2310
|
+
not take. The message names it, and a close spelling the verb does take when there is one;
|
|
2326
2311
|
`vx <verb> --help` lists every flag.
|
|
2327
2312
|
|
|
2328
2313
|
#### `VX_E_UNKNOWN_COMMAND`
|
|
2329
2314
|
|
|
2330
2315
|
No verb of that name here, core's or a declared plugin's. `vx --help`
|
|
2331
2316
|
lists core's verbs. A plugin's verb exists once its plugin is in
|
|
2332
|
-
|
|
2317
|
+
`.vx/workspace.ts` `plugins`; a task runs as `vx run <task>`.
|
|
2333
2318
|
|
|
2334
2319
|
#### `VX_E_UNKNOWN_TASK`
|
|
2335
2320
|
|
|
@@ -2355,20 +2340,24 @@ the task once (`vx run <task>`); history is this checkout's own.
|
|
|
2355
2340
|
|
|
2356
2341
|
#### `VX_E_CONFIG`
|
|
2357
2342
|
|
|
2358
|
-
A `vx.config.*` or
|
|
2343
|
+
A `vx.config.*` or `.vx/workspace.*` that does not load or is refused. The
|
|
2359
2344
|
message names the file and the field; fix that field
|
|
2360
2345
|
(`vx docs <field>` prints its reference). `vx info --format json` lists
|
|
2361
|
-
every config error at once.
|
|
2346
|
+
every config error at once. A project config's refusal is `vx lock`'s,
|
|
2347
|
+
not a run's: a run never evaluates one.
|
|
2362
2348
|
|
|
2363
2349
|
#### `VX_E_CYCLE`
|
|
2364
2350
|
|
|
2365
2351
|
The task graph has a cycle. The message names the loop; drop one of its
|
|
2366
2352
|
`dependsOn` edges.
|
|
2353
|
+
`vx lock` refuses it, not a run: a run trusts the graph the lock holds.
|
|
2367
2354
|
|
|
2368
2355
|
#### `VX_E_NO_WORKSPACE`
|
|
2369
2356
|
|
|
2370
|
-
No workspace
|
|
2371
|
-
|
|
2357
|
+
No `.vx/workspace.*` in the working directory or a parent, up to the git
|
|
2358
|
+
top-level (a file above the checkout never counts). The file marks the
|
|
2359
|
+
workspace root, so run `vx init` to create one. Outside git, one another
|
|
2360
|
+
user owns does not count (`security.md`); the message names it.
|
|
2372
2361
|
|
|
2373
2362
|
#### `VX_E_CACHE_VERSION`
|
|
2374
2363
|
|
|
@@ -2385,7 +2374,7 @@ message is that plugin's or check's: do what it says.
|
|
|
2385
2374
|
#### `VX_E_FS`
|
|
2386
2375
|
|
|
2387
2376
|
The file system refused a write: permissions, a full disk. The message
|
|
2388
|
-
names the path. Free space (`vx cache
|
|
2377
|
+
names the path. Free space (`vx cache clean --max-size <size>` trims the
|
|
2389
2378
|
cache) or grant the write, then re-run.
|
|
2390
2379
|
|
|
2391
2380
|
#### `VX_E_FDS`
|
|
@@ -2414,12 +2403,22 @@ A task's `dependsOn` names a task or project that is not declared, or uses
|
|
|
2414
2403
|
a form vx does not take (a bare wildcard, a negation, a pattern in
|
|
2415
2404
|
`pkg#task`). The message names the task and the entry: declare the
|
|
2416
2405
|
target, fix its spelling, or drop the entry.
|
|
2406
|
+
`vx lock` refuses it, not a run: a run trusts the graph the lock holds.
|
|
2407
|
+
|
|
2408
|
+
A `^task` with no edge source is refused too: ``<id>: ^build needs
|
|
2409
|
+
project edges; add pnpm()/bun()/npm()/yarn() or a plugin that returns
|
|
2410
|
+
`deps` ``. Core reads no edge from a manifest, so with no plugin
|
|
2411
|
+
returning `deps` every `^` would resolve to nothing and fold no
|
|
2412
|
+
upstream into the key. Declare the lockfile plugin in
|
|
2413
|
+
`.vx/workspace.ts` (or a plugin of your own that returns `deps`); a run
|
|
2414
|
+
refuses it as well when the plugin left the workspace after the lock.
|
|
2417
2415
|
|
|
2418
2416
|
#### `VX_E_OUTPUT_OVERLAP`
|
|
2419
2417
|
|
|
2420
2418
|
Two tasks declare overlapping outputs with no `dependsOn` edge between
|
|
2421
2419
|
them, so whichever runs second deletes the other's files. Give each task
|
|
2422
2420
|
its own output path, or make one depend on the other.
|
|
2421
|
+
`vx lock` refuses it, not a run: a run trusts the graph the lock holds.
|
|
2423
2422
|
|
|
2424
2423
|
#### `VX_E_INPUT_IS_OUTPUT`
|
|
2425
2424
|
|
|
@@ -2428,6 +2427,13 @@ a run, so it would delete its sources) or another task's (its key must
|
|
|
2428
2427
|
not read them; `dependsOn` already cascades). Exclude the outputs from
|
|
2429
2428
|
the inputs with a `!` glob, as the message shows, or declare no outputs
|
|
2430
2429
|
for a task that rewrites files in place.
|
|
2430
|
+
`vx lock` refuses it, not a run: a run trusts the graph the lock holds.
|
|
2431
|
+
|
|
2432
|
+
#### `VX_E_DEPENDENCY_RULE`
|
|
2433
|
+
|
|
2434
|
+
`vx lock` found a project depending on one a `run.rules.dependencies`
|
|
2435
|
+
rule denies or does not allow. A header counts the broken edges and
|
|
2436
|
+
each indented line under it names both projects and the rule. Drop the dependency, or change the project's tags or the rule.
|
|
2431
2437
|
|
|
2432
2438
|
#### `VX_E_INTERNAL`
|
|
2433
2439
|
|
|
@@ -2447,14 +2453,13 @@ product verb to it under a notice.
|
|
|
2447
2453
|
|
|
2448
2454
|
## `vx show`
|
|
2449
2455
|
|
|
2450
|
-
Introspect the workspace's **
|
|
2451
|
-
|
|
2452
|
-
plugin `config` and `project` stages
|
|
2453
|
-
gives tasks to (`turbo()` or `nx()` from
|
|
2454
|
-
temporary start toward native config) shows them
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
it directly if you want the frozen view).
|
|
2456
|
+
Introspect the workspace's **resolved configs** — what a run would see
|
|
2457
|
+
right now. Configs load through the same path a run uses: from
|
|
2458
|
+
`.vx/projects.lock.json`, plugin `config` and `project` stages applied,
|
|
2459
|
+
so a package a plugin gives tasks to (`turbo()` or `nx()` from
|
|
2460
|
+
`@vzn/vx-migrate`, a temporary start toward native config) shows them.
|
|
2461
|
+
`vx show` evaluates no config: an edit shows after `vx lock`, and with
|
|
2462
|
+
no lock it refuses as a run does (`no lock: run \`vx lock\``).
|
|
2458
2463
|
|
|
2459
2464
|
```
|
|
2460
2465
|
vx show # list every project
|
|
@@ -2549,7 +2554,7 @@ share `TMPDIR`: a `nix develop` shell sets its own.
|
|
|
2549
2554
|
Every verb: a path vx must write that this user cannot (`EACCES`,
|
|
2550
2555
|
`EPERM`, `EROFS` — a read-only checkout, another user's files) or that
|
|
2551
2556
|
the disk has no room for (`ENOSPC`, `EDQUOT`) exits 1 with one line
|
|
2552
|
-
naming the path, `vx: EACCES: permission denied, open '
|
|
2557
|
+
naming the path, `vx: EACCES: permission denied, open '…/.vx/projects.lock.json'
|
|
2553
2558
|
— a path vx must write is not writable by this user` (or `— the disk
|
|
2554
2559
|
that path is on is full`), never a stack. Inside a run the task's line
|
|
2555
2560
|
says the same. A process out of file descriptors (`EMFILE`, `ENFILE`;
|
|
@@ -2560,11 +2565,10 @@ is out of file descriptors; raise the limit (ulimit -n 4096) and re-run`.
|
|
|
2560
2565
|
|
|
2561
2566
|
Workspace doctor — one screen of facts for bug reports and sanity
|
|
2562
2567
|
checks. The task count and the sandbox row's declared count come from
|
|
2563
|
-
the same load a run uses, plugin stages included
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
row per config with the message `vx run` stops on):
|
|
2568
|
+
the same load a run uses: the lock, plugin stages included. `vx info`
|
|
2569
|
+
evaluates no config; with no lock, or one a run would refuse, both
|
|
2570
|
+
counts are zero and `projects lock` says `no` — a broken config is
|
|
2571
|
+
`vx lock`'s to report:
|
|
2568
2572
|
|
|
2569
2573
|
```
|
|
2570
2574
|
$ vx info
|
|
@@ -2577,21 +2581,18 @@ projects: 12 (34 tasks)
|
|
|
2577
2581
|
plugins: 2 — @vzn/vx-reapi (executor, cache); @vzn/vx-otel (telemetry)
|
|
2578
2582
|
workers: 2 — cgroup CPU quota 2 of 8 cores
|
|
2579
2583
|
memory: 13 GB usable — cgroup limit; the machine has 16 GB
|
|
2580
|
-
cache dir: /work/repo/.vx/cache
|
|
2581
|
-
cache store: /home/me/.vx/3f2a9c1e7b4d5a60/cache
|
|
2582
|
-
cache versions: keys vx-cache-
|
|
2584
|
+
cache dir: /work/repo/.vx/cache (8.2 MB, this workspace's run history)
|
|
2585
|
+
cache store: /home/me/.vx/3f2a9c1e7b4d5a60/cache (1.3 GB, shared by this repo's checkouts)
|
|
2586
|
+
cache versions: keys vx-cache-v43 · index schema v36
|
|
2583
2587
|
cache entries: 42 (1.3 GB)
|
|
2584
|
-
orphans: 3 artifacts (12 MB) the index does not know — `vx cache
|
|
2588
|
+
orphans: 3 artifacts (12 MB) the index does not know — `vx cache clean` reaps them
|
|
2585
2589
|
task runs (24h): 7 (5 cache hits)
|
|
2586
2590
|
flaky tasks: 1 — web#test (3 of 11 runs failed on unchanged inputs)
|
|
2587
2591
|
sandbox: available (9 tasks declare exec.sandbox)
|
|
2588
|
-
|
|
2592
|
+
projects lock: yes
|
|
2589
2593
|
```
|
|
2590
2594
|
|
|
2591
|
-
- `git` shows `(not found)` when the binary is missing
|
|
2592
|
-
project config contributes zero tasks instead of failing the
|
|
2593
|
-
printout, and the `config errors` row (present only then) names it
|
|
2594
|
-
with the loader's message — the same line `vx run` would stop on.
|
|
2595
|
+
- `git` shows `(not found)` when the binary is missing.
|
|
2595
2596
|
- `git status cache`: vx runs ONE `git status` per run to find dirty
|
|
2596
2597
|
and untracked files, and on a large tree that walk is the warm run's
|
|
2597
2598
|
critical path. git's `core.fsmonitor` (a daemon that watches the
|
|
@@ -2611,13 +2612,13 @@ exec.sandbox and will fail` says it first: root inside a container
|
|
|
2611
2612
|
unreported`: the sandbox still enforces, but a task that tolerates a
|
|
2612
2613
|
denied read passes and caches with no word of it. The `--format json` fact is
|
|
2613
2614
|
`sandbox.untraced`, the reason or `null`.
|
|
2614
|
-
- `plugins` names every plugin
|
|
2615
|
+
- `plugins` names every plugin `.vx/workspace.*` declares and the seams
|
|
2615
2616
|
each fills, in pipeline order (`config`, `discover`, `project`, `graph`, `key`,
|
|
2616
2617
|
`fingerprint`, `schedule`, `admit`, `executor`, `cache`, `telemetry`,
|
|
2617
|
-
`setup`, `commands`), or `none`. It reads the declarations: a plugin
|
|
2618
|
+
`output`, `setup`, `commands`), or `none`. It reads the declarations: a plugin
|
|
2618
2619
|
that declines a task at run time still lists its seam here.
|
|
2619
2620
|
- `workers` is the count a run defaults to and where it comes from:
|
|
2620
|
-
|
|
2621
|
+
`.vx/workspace.ts`'s `run.concurrency` when set, else the cores this
|
|
2621
2622
|
process may use — the CPU count, capped by the cgroup CPU quota a
|
|
2622
2623
|
container runs under (`8 — the CPU count`, `2 — cgroup CPU quota 2 of
|
|
2623
2624
|
8 cores`). `memory` is what a memory-packing policy budgets
|
|
@@ -2638,15 +2639,22 @@ unreported`: the sandbox still enforces, but a task that tolerates a
|
|
|
2638
2639
|
the hits are a share of it: three `vx run` of two tasks are six. An
|
|
2639
2640
|
invocation is what `vx last` calls a run; `vx last --list` counts
|
|
2640
2641
|
those.
|
|
2641
|
-
- `cache
|
|
2642
|
-
store
|
|
2643
|
-
|
|
2644
|
-
|
|
2642
|
+
- `cache dir` holds the index and this workspace's run history; its size
|
|
2643
|
+
is on disk. `cache store` is where the entries and their artifacts
|
|
2644
|
+
live: the store shared by this repo's checkouts (`docs/caching.md`),
|
|
2645
|
+
with its size on disk, or `none: the cache dir holds the entries` when
|
|
2646
|
+
the workspace names its cache dir. Before a first run it is the store a
|
|
2647
|
+
run would open. A bare `vx cache clean` frees both sizes.
|
|
2645
2648
|
- `cache entries` counts the entries and their size; with a
|
|
2646
2649
|
`cacheRetention.maxSize` it reads against it (`812 (3.0 GB of 10 GB,
|
|
2647
2650
|
30%)`), and a `cache retention` row says what each run's end evicts.
|
|
2648
2651
|
Paths under the home directory print as `~/…`; the JSON keeps them
|
|
2649
2652
|
absolute.
|
|
2653
|
+
- `cache policy` is the policy a `vx run` takes when no flag names one,
|
|
2654
|
+
and where it comes from (`VX_CACHE_POLICY`, `.vx/workspace`'s `cache.policy`,
|
|
2655
|
+
or the default, every layer on, remote writes off off CI); `cache scope`
|
|
2656
|
+
is the same for the scope (`VX_CACHE_SCOPE`, `cache.scope`, which
|
|
2657
|
+
`github()` may set, or none: the default keys).
|
|
2650
2658
|
- `cache versions` are the two constants a bug report needs and the
|
|
2651
2659
|
reset notice names: the key prefix (`CACHE_VERSION`; a bump orphans
|
|
2652
2660
|
every entry) and the index schema (`SCHEMA_VERSION`; a mismatch drops
|
|
@@ -2655,19 +2663,23 @@ unreported`: the sandbox still enforces, but a task that tolerates a
|
|
|
2655
2663
|
save temps the index has no row for, older than an hour (what a
|
|
2656
2664
|
`SCHEMA_VERSION` reset leaves behind; a fresh one is a save in
|
|
2657
2665
|
flight). A task that asks for one's key again indexes it and hits;
|
|
2658
|
-
the rest only `vx cache
|
|
2666
|
+
the rest only `vx cache clean` reclaims, so the doctor says so.
|
|
2659
2667
|
- `--format json` prints the same facts as one typed object, for a
|
|
2660
|
-
script or a bug-report template: `vx`, `
|
|
2668
|
+
script or a bug-report template: `vx`, `vxPath` (the file that ran),
|
|
2669
|
+
`handedOffFrom` (the vx that handed the call to the project's own, else
|
|
2670
|
+
null), `bun`, `bunSupported` (false
|
|
2661
2671
|
below Bun 1.4.0), `git` (null when not
|
|
2662
2672
|
found), `gitStatusCache` (`{ fsmonitor, untrackedCache }`, null when
|
|
2663
2673
|
git could not answer), `workspaceRoot`, `projects`, `tasks`,
|
|
2664
|
-
`
|
|
2665
|
-
empty when all did), `plugins` (`[{ name, seams }]`), `workers` (`{ count, source, cores,
|
|
2674
|
+
`plugins` (`[{ name, seams }]`), `workers` (`{ count, source, cores,
|
|
2666
2675
|
cpuQuota }`, the source one of `workspace` / `cgroup` / `cores`,
|
|
2667
2676
|
`cpuQuota` in cores or null), `memory` (`{ usableBytes, totalBytes,
|
|
2668
|
-
cgroupLimitBytes }`, the limit null when none binds), `cacheDir`, `
|
|
2669
|
-
(
|
|
2670
|
-
|
|
2677
|
+
cgroupLimitBytes }`, the limit null when none binds), `cacheDir`, `cacheDirBytes`
|
|
2678
|
+
(on disk: the index and run history), `cacheStore` (null when the cache
|
|
2679
|
+
dir holds the entries), `cacheStoreBytes` (on disk, null with no store), `cacheVersion`,
|
|
2680
|
+
`schemaVersion`, `cacheEntries`, `cacheBytes`, `settings` (every
|
|
2681
|
+
setting by path, `{ value, source }`, `source` one of `env` /
|
|
2682
|
+
`workspace` / `default`, `value` null for the default), `cacheRetention`
|
|
2671
2683
|
(`{ olderThan, maxSize, maxBytes }`, each only when set, null when none is declared),
|
|
2672
2684
|
`orphans` (`{ artifacts, bytes }`, always present), `runs24h`, `hits24h` (task
|
|
2673
2685
|
runs, as the row), `restored24h` (of those hits, the ones that restored
|
|
@@ -2678,10 +2690,10 @@ declared, untraced }`, `declared` the count of tasks with `exec.sandbox`,
|
|
|
2678
2690
|
pretty rows render this object;
|
|
2679
2691
|
there is no second source.
|
|
2680
2692
|
- `--cache-dir <path>` reads the cache a run with the same flag uses, as
|
|
2681
|
-
`vx why`, `vx last` and `vx cache
|
|
2693
|
+
`vx why`, `vx last` and `vx cache clean` do.
|
|
2682
2694
|
|
|
2683
|
-
Exit codes: `0` the facts printed,
|
|
2684
|
-
|
|
2695
|
+
Exit codes: `0` the facts printed, with no lock included; `1` no
|
|
2696
|
+
workspace here or above, or a parse error.
|
|
2685
2697
|
|
|
2686
2698
|
## `vx why`
|
|
2687
2699
|
|
|
@@ -2719,7 +2731,7 @@ cache (nothing re-ran), it re-executed on the same key, or it recorded
|
|
|
2719
2731
|
no cache outcome at all, in which case vx says so instead of guessing.
|
|
2720
2732
|
A re-execution names its cause when the index shows one: the previous
|
|
2721
2733
|
run on the key failed and saved nothing, the run did not read the cache
|
|
2722
|
-
(`--force`, or a `--cache` without read), no entry for the key was
|
|
2734
|
+
(`--force`, or a `--cache-policy` without read), no entry for the key was
|
|
2723
2735
|
there when it ran (pruned or evicted), the previous run was not saved
|
|
2724
2736
|
because it ran beside a failed task (a task run past a failed dependency
|
|
2725
2737
|
under `--continue` is never cached; said ahead of a prune, and as
|
|
@@ -2765,6 +2777,18 @@ which names the `vx why` to run next; a `config` line names the
|
|
|
2765
2777
|
`vx show` that prints the task's config now. `--format json` carries
|
|
2766
2778
|
the walk as `roots` (`chain`, `entries`).
|
|
2767
2779
|
|
|
2780
|
+
A moved input file whose last change fell inside a recorded task's run
|
|
2781
|
+
window names that task: a task that writes a file another key reads (a
|
|
2782
|
+
build rewriting a translation file in another package) re-keys that key
|
|
2783
|
+
on every run, so the miss is right and only moving the write ends it.
|
|
2784
|
+
`--format json` carries these as `writers` (`taskId`, `file`,
|
|
2785
|
+
`writers`); an edit made outside a run names none.
|
|
2786
|
+
|
|
2787
|
+
```
|
|
2788
|
+
changed during a run:
|
|
2789
|
+
packages/lib/src/gen.txt while app#build ran
|
|
2790
|
+
```
|
|
2791
|
+
|
|
2768
2792
|
A hit's line is `cache-hit · key …` (or `cache-hit-remote`): the status
|
|
2769
2793
|
names the hit and its tier, so only an executed run carries the word.
|
|
2770
2794
|
A skipped task never ran and derived no key: its line is `skipped · no key`.
|
|
@@ -2772,11 +2796,10 @@ A row's kind is what the key folded: `file` (an input file, by blob
|
|
|
2772
2796
|
id), `env` (a declared variable, by digest), `runtime` and
|
|
2773
2797
|
`ws-runtime` (a declared command's output, `inputs.runtime` and
|
|
2774
2798
|
`inputs.workspaceRuntime`), `forward` (the argv forwarded after `--`),
|
|
2775
|
-
`
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
lockfile
|
|
2779
|
-
fingerprint's), `plugin` (a `key` plugin's material, by name) and
|
|
2799
|
+
`config` (the evaluated task config), `upstream` (a dependency's input
|
|
2800
|
+
key — a lockfile change moves it too, so that row rides with the
|
|
2801
|
+
lockfile plugin's), `plugin` (a `key` plugin's material, by name; a
|
|
2802
|
+
lockfile plugin's dependency closure) and
|
|
2780
2803
|
`format` (the key format vx itself folds: it moves only with an
|
|
2781
2804
|
upgrade, and re-keys every task once, X-142).
|
|
2782
2805
|
The component-level rows come from the `entry_inputs` input
|
|
@@ -2789,39 +2812,13 @@ task with no `cache` block derives a key too — it is what dependents
|
|
|
2789
2812
|
fold — but saves no entry, so for it the verb can only report the key
|
|
2790
2813
|
change and says so.
|
|
2791
2814
|
`--format json` emits one machine-readable object (`{ taskId, runId,
|
|
2792
|
-
why, diff, roots }`); when the task's recorded runs predate run ids, `{ taskId,
|
|
2815
|
+
why, diff, roots, writers }`); when the task's recorded runs predate run ids, `{ taskId,
|
|
2793
2816
|
why, diff, explanation }`, `why` and `diff` null and `explanation` the
|
|
2794
2817
|
latest cache entry's key.
|
|
2795
2818
|
|
|
2796
2819
|
Exit codes: `0` the task's run explained; `1` no recorded run of the
|
|
2797
2820
|
task, a `--run` with no row for it, or a parse error.
|
|
2798
2821
|
|
|
2799
|
-
## `vx prune`
|
|
2800
|
-
|
|
2801
|
-
```
|
|
2802
|
-
vx prune <project...> [--out-dir <dir>] [--docker] [--production]
|
|
2803
|
-
```
|
|
2804
|
-
|
|
2805
|
-
A plugin verb of `@vzn/vx-lockfile`: declaring `pnpm()`, `bun()`,
|
|
2806
|
-
`npm()` or `yarn()` adds it. It copies the named projects, their
|
|
2807
|
-
transitive workspace dependencies, the root manifests (workspace lists
|
|
2808
|
-
rewritten to the subset), the vx configs and each lockfile pruned to what
|
|
2809
|
-
the subset installs into `--out-dir` (default `out/`), so a Docker build
|
|
2810
|
-
installs it with a frozen lockfile; `--docker` splits `json/` (the
|
|
2811
|
-
install layer) from `full/`; `--production` leaves out a workspace
|
|
2812
|
-
package only a dev dependency reaches and strikes it from the kept
|
|
2813
|
-
manifests and lockfiles. `turbo prune`. Reference:
|
|
2814
|
-
`packages/vx-lockfile/README.md` § `vx prune`. Without the plugin, typing
|
|
2815
|
-
`vx prune` says where it comes from and exits 1. History: core verb
|
|
2816
|
-
2026-08-25, `@vzn/vx-prune` 2026-09-10, removed 2026-09-11 (it copied the
|
|
2817
|
-
lockfile unpruned), back in `@vzn/vx-lockfile` 2026-10-03.
|
|
2818
|
-
|
|
2819
|
-
## `vx stats`
|
|
2820
|
-
|
|
2821
|
-
Removed (H-19). It was an alias of `vx info`, which absorbed it, and
|
|
2822
|
-
warned on every use. Typing `vx stats` points at `vx info` and exits 1 —
|
|
2823
|
-
unless a declared plugin claims the verb through the `commands` seam.
|
|
2824
|
-
|
|
2825
2822
|
## `vx last`
|
|
2826
2823
|
|
|
2827
2824
|
Replay a recorded run's summary from the local history — no
|
|
@@ -2831,7 +2828,7 @@ surface. A run a Ctrl-C (or SIGTERM, SIGHUP) stopped records nothing,
|
|
|
2831
2828
|
so `vx last` still shows the run before it.
|
|
2832
2829
|
|
|
2833
2830
|
```
|
|
2834
|
-
vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--format pretty|json] [--cache-dir <path>]
|
|
2831
|
+
vx last [RUNID] [--list[=N]] [--failed] [--log <task>] [--stats [<task>] [--branch <name>]] [--format pretty|json] [--cache-dir <path>]
|
|
2835
2832
|
```
|
|
2836
2833
|
|
|
2837
2834
|
Bare `vx last` replays the most recent run: a header (verdict, command,
|
|
@@ -2873,7 +2870,12 @@ passed, so a replayed failure that is already fixed says so. A
|
|
|
2873
2870
|
sandboxed task's row adds `sandboxFix`, the `exec.sandbox.allow`
|
|
2874
2871
|
entries (`read`, `write`, `network`, `systemInfo`) that would have
|
|
2875
2872
|
granted its denials, the same grants the terminal prints as `fix:`;
|
|
2876
|
-
`withheld` lists the credential paths it denied and left out
|
|
2873
|
+
`withheld` lists the credential paths it denied and left out (a `.env` file, or a store under home the sandbox denies, `security.md`), and, for
|
|
2874
|
+
a cached task, `inputs` the reads of its project no `cache.inputs.files`
|
|
2875
|
+
glob matches (the terminal's `and, if the output depends on them, …`
|
|
2876
|
+
line). A task
|
|
2877
|
+
failed by `inputStability.severity: 'error'` adds `inputMoved`, the
|
|
2878
|
+
input that changed during the run. They live in `<cacheDir>/failures/<runId>.json`, the
|
|
2877
2879
|
newest 50 failed runs; past those, or when the write was refused, a
|
|
2878
2880
|
failed row reads `output: ''` and `locations: []`. `--log <task>`
|
|
2879
2881
|
prints one task's output instead of the summary: in the latest run that
|
|
@@ -2884,7 +2886,23 @@ or in the run that saved it. An uncached task that passed, or a skip,
|
|
|
2884
2886
|
kept none: stdout is empty and stderr says so, exit 0. With
|
|
2885
2887
|
`--format json` it is `{ runId, taskId, status, source, output }`,
|
|
2886
2888
|
`source` `'failure'`, `'cache'` or `null`. A task no run recorded is
|
|
2887
|
-
refused, exit 1; `--log` does not combine with `--list` or `--failed`.
|
|
2889
|
+
refused, exit 1; `--log` does not combine with `--list` or `--failed`.
|
|
2890
|
+
`vx last app#build --log` reads the same as `--log app#build`: a `--log`
|
|
2891
|
+
with no value takes the positional as its task.
|
|
2892
|
+
`--stats [<task>]` prints per-task numbers over the last 50 runs, the
|
|
2893
|
+
window the scheduler and `--dry` learn from: runs, p50 (the forecast a
|
|
2894
|
+
run shows: the median of the task's last five timings on the branch,
|
|
2895
|
+
else on any branch), p99 of the executed successes, pass and hit rate, failure mode (`stable`,
|
|
2896
|
+
`flaky-recoverable`, `flaky-fatal`), the largest peak RSS and CPU
|
|
2897
|
+
parallelism (a hit counts what its producing run used), one table row
|
|
2898
|
+
per task. A task id narrows to that task, a task name to it in every
|
|
2899
|
+
project; one the window has no run of is refused, exit 1. The branch is
|
|
2900
|
+
`--branch`, else `VX_RUN_BRANCH`, else git's, then the CI provider's, as
|
|
2901
|
+
a run's. With `--format json` it is `{ window, branch, stats }`, each row `{ taskId, runs,
|
|
2902
|
+
p50DurationMs, p99DurationMs, successRate, hitRate, failureMode,
|
|
2903
|
+
maxPeakRssBytes, maxCpuParallelism }`, `null` where nothing was
|
|
2904
|
+
measured. `--stats` does not combine with `--list`, `--failed` or
|
|
2905
|
+
`--log`; `--branch` combines only with `--stats`. `--list --format json`
|
|
2888
2906
|
an array of the same `invocation` objects, newest first. An unknown run id fails
|
|
2889
2907
|
loud and points at `--list`. Before any run, every form says so
|
|
2890
2908
|
(`vx last: no recorded runs yet — run something first`, exit 1; `--list`
|
|
@@ -2898,14 +2916,14 @@ line of its own without them, X-140). The
|
|
|
2898
2916
|
replayed `$ vx run …` or `$ vx watch …` line and that one keep each
|
|
2899
2917
|
argument shell-quoted (`-- 'a b'`), so either pastes back as it ran (X-49).
|
|
2900
2918
|
|
|
2901
|
-
`vx why`, `vx last`, `vx info` and `vx cache
|
|
2919
|
+
`vx why`, `vx last`, `vx info` and `vx cache clean` all read the cache
|
|
2902
2920
|
a run wrote, so each takes `--cache-dir <path>` with `vx run`'s rules
|
|
2903
2921
|
(cwd-relative, absolute used as-is): a run that wrote its history
|
|
2904
2922
|
elsewhere is replayed, explained, reported on and pruned there. Without
|
|
2905
|
-
the flag they open the workspace's cache (`defineWorkspace({
|
|
2923
|
+
the flag they open the workspace's cache (`defineWorkspace({ cache: { dir } })`
|
|
2906
2924
|
or `.vx/cache`). A `--cache-dir` that is not there is refused by name
|
|
2907
2925
|
(`--cache-dir .vx/cahce: no such directory`). In a workspace that never
|
|
2908
|
-
ran, `vx why`, `vx last`, `vx info`, `vx show` and `vx cache
|
|
2926
|
+
ran, `vx why`, `vx last`, `vx info`, `vx show` and `vx cache clean`
|
|
2909
2927
|
(dry or not) read it as empty and create nothing (item 900, E-7, E-38).
|
|
2910
2928
|
|
|
2911
2929
|
Exit codes: `0` the run (or the list) printed, `--list` with no runs
|
|
@@ -2947,10 +2965,10 @@ of each verb, read from the same help text `vx <verb> --help` prints:
|
|
|
2947
2965
|
the verb's own Usage line, and for `run` and `watch` the run option
|
|
2948
2966
|
lines, less the ones `watch` refuses. So a flag cannot be documented
|
|
2949
2967
|
and not completed, nor completed and then refused: a flag another
|
|
2950
|
-
verb's line names in passing (`
|
|
2968
|
+
verb's line names in passing (`--check` in a line about `vx lock`) is
|
|
2951
2969
|
not one. After a flag with a fixed value set the script offers that
|
|
2952
2970
|
set: `--format` pretty or json, `--output-logs` full, errors-only,
|
|
2953
|
-
hash-only or none, `--download` all, toplevel or none — each value one
|
|
2971
|
+
hash-only or none, `--cache-download` all, toplevel or none — each value one
|
|
2954
2972
|
the flag's parser takes (`tests/completions.test.ts` holds it). A plugin verb completes `--help` only. Task and project
|
|
2955
2973
|
names are not completed (they are the workspace's, and a completion
|
|
2956
2974
|
that evaluates configs on every Tab is the wrong price). The zsh script
|
|
@@ -2965,7 +2983,7 @@ Exit codes: `0` the script printed; `1` an unknown shell or flag.
|
|
|
2965
2983
|
|
|
2966
2984
|
## Plugin commands
|
|
2967
2985
|
|
|
2968
|
-
A plugin declared in
|
|
2986
|
+
A plugin declared in `.vx/workspace.ts` can add verbs:
|
|
2969
2987
|
|
|
2970
2988
|
```ts
|
|
2971
2989
|
import { definePlugin, type VxPlugin } from '@vzn/vx'
|
|
@@ -2988,18 +3006,15 @@ export function mcp(): VxPlugin {
|
|
|
2988
3006
|
The plugin's name is its package name, which `definePlugin` reads from
|
|
2989
3007
|
`import.meta`; a plain object is refused when the workspace loads.
|
|
2990
3008
|
|
|
2991
|
-
(`@vzn/vx-
|
|
2992
|
-
|
|
2993
|
-
over the resolved task catalog, one the workspace doctor's facts.
|
|
2994
|
-
`@vzn/vx-schedule-history` adds `vx history`: what it learned per task
|
|
2995
|
-
and the reservation its `admit` hook packs.) The dispatcher tries core's verbs
|
|
3009
|
+
(`@vzn/vx-schedule-history` ships exactly this: `vx history`, what it
|
|
3010
|
+
learned per task and the reservation its `admit` hook packs.) The dispatcher tries core's verbs
|
|
2996
3011
|
first and consults plugins only for a word core does not know, loading
|
|
2997
3012
|
the workspace config from the cwd to find them (outside a workspace the
|
|
2998
3013
|
verb is simply unknown). A plugin verb that names a core verb, or one
|
|
2999
3014
|
plugins of two packages both declare, is refused when the workspace
|
|
3000
3015
|
loads — such a verb could never run, or would hide the other plugin's.
|
|
3001
|
-
Plugins of one package are one owner (
|
|
3002
|
-
|
|
3016
|
+
Plugins of one package are one owner (two of its plugins may carry the
|
|
3017
|
+
same verb): the first declared runs. A plugin verb's
|
|
3003
3018
|
return value is the exit code, an integer 0–255 (anything else — nothing,
|
|
3004
3019
|
a fraction, 256, which the OS would keep as 0 — fails naming the plugin
|
|
3005
3020
|
and the verb), and a thrown `UserError` prints as
|
|
@@ -3011,44 +3026,62 @@ An unknown verb is answered with what this workspace knows: the "did you
|
|
|
3011
3026
|
mean" set is core's verbs AND the ones its plugins declare (the lookup
|
|
3012
3027
|
that just failed already loaded them, so it costs nothing), and when
|
|
3013
3028
|
nothing is close enough to guess, a second line says where a verb can
|
|
3014
|
-
come from — the verbs declared here, or that
|
|
3029
|
+
come from — the verbs declared here, or that `.vx/workspace.ts` is what
|
|
3015
3030
|
would declare one, or that there is no workspace here at all. Core still
|
|
3016
3031
|
names no package: it lists what the workspace itself declares.
|
|
3017
3032
|
|
|
3018
3033
|
## Environment variables vx reads
|
|
3019
3034
|
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
is
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
|
3032
|
-
|
|
|
3033
|
-
| `
|
|
3034
|
-
| `
|
|
3035
|
-
| `
|
|
3036
|
-
| `
|
|
3037
|
-
| `
|
|
3038
|
-
| `
|
|
3039
|
-
|
|
|
3040
|
-
|
|
|
3041
|
-
| `
|
|
3042
|
-
| `
|
|
3043
|
-
| `
|
|
3044
|
-
| `
|
|
3045
|
-
| `
|
|
3046
|
-
| `
|
|
3047
|
-
| `
|
|
3048
|
-
| `
|
|
3049
|
-
| `
|
|
3050
|
-
| `
|
|
3051
|
-
| `
|
|
3035
|
+
Every setting has a variable derived from its path (`run.killGrace` →
|
|
3036
|
+
`VX_RUN_KILL_GRACE`), beating `.vx/workspace` and beaten by the flag; the
|
|
3037
|
+
table in `docs/schema.md` § Settings gives each value's form and
|
|
3038
|
+
default. Each is checked once before any verb runs: an empty one is
|
|
3039
|
+
unset, a value its kind does not take is refused naming the form, and a
|
|
3040
|
+
variable vx does not read is ignored. A
|
|
3041
|
+
plugin's own (the REAPI endpoints, the GitHub token) are in its README;
|
|
3042
|
+
what a TASK sees is `exec.env` in `docs/schema.md`.
|
|
3043
|
+
|
|
3044
|
+
<!-- variables table: generated by tests/settings.test.ts -->
|
|
3045
|
+
|
|
3046
|
+
| Variable | Setting | What it does |
|
|
3047
|
+
| ----------------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3048
|
+
| `VX_RUN_CONCURRENCY` | [`run.concurrency`](schema.md#settings) | Tasks at once: a positive integer, or a share of the cores (`'50%'`). |
|
|
3049
|
+
| `VX_RUN_SCHEDULE` | [`run.schedule`](schema.md#settings) | Which ready task starts first when there are more than workers. |
|
|
3050
|
+
| `VX_RUN_AFFECTED_BASE` | [`run.affectedBase`](schema.md#settings) | The git ref a bare `--affected` compares with. |
|
|
3051
|
+
| `VX_RUN_BRANCH` | [`run.branch`](schema.md#settings) | The branch a run records in its history and forecasts from. Flag and env only: a committed branch would be wrong on every other branch. |
|
|
3052
|
+
| `VX_RUN_TIMEOUT` | [`run.timeout`](schema.md#settings) | A task's time limit, for tasks without their own `exec.timeout` (`'30m'`). |
|
|
3053
|
+
| `VX_RUN_RETRIES` | [`run.retries`](schema.md#settings) | Re-runs of a failed task, for tasks without their own `exec.retries`. |
|
|
3054
|
+
| `VX_RUN_CONTINUE` | [`run.continue`](schema.md#settings) | What a failed task takes down: `never` stops the run, `deps-ok` skips its dependents, `always` runs them too. |
|
|
3055
|
+
| `VX_RUN_OUTPUT_LOGS` | [`run.outputLogs`](schema.md#settings) | Which task output the run prints. Chosen before the workspace loads, so flag and env only. |
|
|
3056
|
+
| `VX_RUN_KILL_GRACE` | [`run.killGrace`](schema.md#settings) | How long a task that ignores SIGTERM gets before SIGKILL. |
|
|
3057
|
+
| `VX_RUN_TEARDOWN_TIMEOUT` | [`run.teardownTimeout`](schema.md#settings) | The bound on each plugin's end-of-run flush and teardown. |
|
|
3058
|
+
| `VX_RUN_READY_NOTICE` | [`run.readyNotice`](schema.md#settings) | How long a persistent task may take to match `readyWhen` before vx says it waits. |
|
|
3059
|
+
| `VX_RUN_RULES_EXCLUSIVE_OUTPUTS` | [`run.rules.exclusiveOutputs`](schema.md#settings) | Refuse two tasks whose outputs overlap even when an edge orders them. |
|
|
3060
|
+
| `VX_RUN_RULES_UPFRONT_KEYS` | [`run.rules.upfrontKeys`](schema.md#settings) | Refuse a task whose inputs can match another task's outputs. |
|
|
3061
|
+
| `VX_CACHE_DIR` | [`cache.dir`](schema.md#settings) | Where the cache lives, relative to the workspace root; named, it holds the whole cache. |
|
|
3062
|
+
| `VX_CACHE_RETENTION_MAX_SIZE` | [`cache.retention.maxSize`](schema.md#settings) | Evict least-recently-used entries at each run's end until the cache is under this (`'10GB'`). |
|
|
3063
|
+
| `VX_CACHE_RETENTION_OLDER_THAN` | [`cache.retention.olderThan`](schema.md#settings) | Evict entries unused for this long at each run's end (`'30d'`). |
|
|
3064
|
+
| `VX_CACHE_POLICY` | [`cache.policy`](schema.md#settings) | Which layers a run reads and writes (`'local:rw,remote:r'`); `--no-cache` and `--force` apply on top. |
|
|
3065
|
+
| `VX_CACHE_SCOPE` | [`cache.scope`](schema.md#settings) | Where cache writes land (`'pr-123'`): reads try the default key, then the scope's. |
|
|
3066
|
+
| `VX_CACHE_DOWNLOAD` | [`cache.download`](schema.md#settings) | Which remote hits land on disk: every one, only the requested tasks, or none. |
|
|
3067
|
+
| `VX_CACHE_INPUT_STABILITY_STRATEGY` | [`cache.inputStability.strategy`](schema.md#settings) | What a write to an input during the command means for the save, for tasks without their own. |
|
|
3068
|
+
| `VX_CACHE_INPUT_STABILITY_SEVERITY` | [`cache.inputStability.severity`](schema.md#settings) | Whether a skipped save warns or fails the task, for tasks without their own. |
|
|
3069
|
+
| `VX_CACHE_OUTPUTS_CLEAN` | [`cache.outputs.clean`](schema.md#settings) | Remove a task's declared outputs before it runs on a miss, for tasks without their own. |
|
|
3070
|
+
| `VX_LOCK_REVIEW_FILES` | [`lock.reviewFiles`](schema.md#settings) | `vx lock` writes each project's resolved config, pretty, to `vx.lock.json` beside its `vx.config`, for review; vx never reads it. |
|
|
3071
|
+
| `VX_WATCH_POLL` | [`watch.poll`](schema.md#settings) | Poll for changes from the start instead of the OS watcher. |
|
|
3072
|
+
| `VX_CONFIG_WORKER_TIMEOUT` | [`config.workerTimeout`](schema.md#settings) | How long one config evaluation may take. |
|
|
3073
|
+
| `VX_DEBUG_TIMING` | [`debug.timing`](schema.md#settings) | Print the run's stage timings to stderr. |
|
|
3074
|
+
| `VX_DEBUG_DUMPABLE` | [`debug.dumpable`](schema.md#settings) | Linux: keep vx dumpable, so strace or a debugger can read its memory and `/proc/<pid>/fd`, and an unsandboxed task its env. |
|
|
3075
|
+
| `VX_HANDED_OFF` | — | Set by a global vx that hands the call to the project's own (§ Which vx runs): the path of the vx that handed off, so the receiver does not hand off again. |
|
|
3076
|
+
| `VX_RUN_WORKSPACE`, `VX_RUN_TASK` | — | Set by vx on every task (below); a `vx run` a task starts reads them back. |
|
|
3077
|
+
|
|
3078
|
+
<!-- /variables table -->
|
|
3079
|
+
|
|
3080
|
+
vx sets two variables on every task's environment: `VX_RUN_WORKSPACE`
|
|
3081
|
+
(the workspace root) and `VX_RUN_TASK` (`project#task`). A `vx run` a
|
|
3082
|
+
task starts reads them back: one in the same workspace is refused, since
|
|
3083
|
+
a nested run is invisible to the outer graph and a loop back to its own
|
|
3084
|
+
task forks without bound (`docs/schema.md` § `env`).
|
|
3052
3085
|
|
|
3053
3086
|
Colors are the two conventions in § Output format › Colors (`NO_COLOR`,
|
|
3054
3087
|
`FORCE_COLOR`). A truthy `CI` picks the CI output flow and
|
|
@@ -3118,7 +3151,9 @@ marked with a cyan `▸` after `┌─`/`└─`, and its close reads `running`
|
|
|
3118
3151
|
died on its own before the run stopped it.
|
|
3119
3152
|
|
|
3120
3153
|
There is **no top-of-run banner** — the run context lives in the
|
|
3121
|
-
footer.
|
|
3154
|
+
footer. The task log opens with one blank line before its first row,
|
|
3155
|
+
on a terminal or not; a note vx prints before it is not part of it.
|
|
3156
|
+
A broad run looks like:
|
|
3122
3157
|
|
|
3123
3158
|
```
|
|
3124
3159
|
⇢ 4ms success local @vzn/vx#format-check
|
|
@@ -3126,30 +3161,29 @@ footer. A broad run looks like:
|
|
|
3126
3161
|
|
|
3127
3162
|
─ vx 0.0.0 ───────────────────────────────────────────────────
|
|
3128
3163
|
projects ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱
|
|
3129
|
-
1
|
|
3164
|
+
1 affected · 3 total
|
|
3130
3165
|
tasks ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
|
|
3131
3166
|
2 success · 2 total
|
|
3132
3167
|
cache ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
|
|
3133
3168
|
1 miss · 1 local
|
|
3134
3169
|
|
|
3135
3170
|
info 8 workers · local cache
|
|
3136
|
-
time 5.34s ·
|
|
3137
|
-
result 2 tasks · 1 cached (50%) · 900ms saved · 5.34s
|
|
3171
|
+
time 5.34s · 900ms saved
|
|
3138
3172
|
```
|
|
3139
3173
|
|
|
3140
3174
|
The three bars are meters: `projects` is what the run covered against
|
|
3141
3175
|
the workspace, `tasks` is failed / success / skipped, `cache` is miss /
|
|
3142
3176
|
no-cache / up-to-date / local / remote (a skipped task rides it too, so
|
|
3143
|
-
the two
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
is counted apart (`1
|
|
3152
|
-
`--dry` labels it. A test renders this run and
|
|
3177
|
+
the two bars fill alike; its count is on the tasks legend only). The
|
|
3178
|
+
`time` row is the wall time, then what the cache saved (the hits' stored
|
|
3179
|
+
exec times, never the restore), then the spread over executed tasks
|
|
3180
|
+
only — a hit's restore time never enters it. The footer never says a
|
|
3181
|
+
fact twice: a spread value already on the row is left out, so one
|
|
3182
|
+
executed task shows no spread, and there is no closing `result` row
|
|
3183
|
+
repeating the tasks, cache and time rows. A task with no `cache` block,
|
|
3184
|
+
or any task of a run whose `--cache-policy` reads and writes nothing
|
|
3185
|
+
(`--no-cache`), could never hit, so it is counted apart (`1 no-cache`
|
|
3186
|
+
for `vx run dev`), as `--dry` labels it. A test renders this run and
|
|
3153
3187
|
checks it against this page, byte for byte.
|
|
3154
3188
|
|
|
3155
3189
|
Group tasks emit no framed block by design (they aren't real tasks);
|
|
@@ -3160,8 +3194,8 @@ What a run has to say about one task rides that task's row; what it
|
|
|
3160
3194
|
says about the run (a server that died, outputs left remote, a file
|
|
3161
3195
|
`--summarize` / `--profile` wrote, a history, upload, plugin or sandbox
|
|
3162
3196
|
warning) prints above the footer: it prints once the run's closing work
|
|
3163
|
-
is done, the `
|
|
3164
|
-
(`
|
|
3197
|
+
is done, the `VX_DEBUG_TIMING=1` table included. Cache housekeeping
|
|
3198
|
+
(`cache.retention`) prints nothing. Only a kept server's own output
|
|
3165
3199
|
follows it.
|
|
3166
3200
|
|
|
3167
3201
|
**Skipped rows.** A red run lists every task that never started as a
|
|
@@ -3188,9 +3222,9 @@ what it reached first (`not run`). Neither reached the cache:
|
|
|
3188
3222
|
◌ not run web#test
|
|
3189
3223
|
```
|
|
3190
3224
|
|
|
3191
|
-
The tasks legend
|
|
3192
|
-
|
|
3193
|
-
|
|
3225
|
+
The tasks legend counts them in the total, each state its own segment
|
|
3226
|
+
of the bar: `2 success · 1 aborted · 2 not run · 5 total`. The total is
|
|
3227
|
+
always last on its line. A framed task (focused or `full`) that
|
|
3194
3228
|
printed while it stopped shows its frame instead of a row.
|
|
3195
3229
|
|
|
3196
3230
|
**Flaky rows.** A task the run just proved nondeterministic says so
|
|
@@ -3204,7 +3238,7 @@ run history alone, no service:
|
|
|
3204
3238
|
|
|
3205
3239
|
A task is flaky when its exact cache key FAILED AFTER IT HAD PASSED
|
|
3206
3240
|
(this run counted; a cache hit is a pass, it replayed one), or when it
|
|
3207
|
-
needed a retry (`exec.retries` / `--
|
|
3241
|
+
needed a retry (`exec.retries` / `--retries`) this run. A failure on a
|
|
3208
3242
|
key that never passed is a break and carries no note — a changed input
|
|
3209
3243
|
that fails is what a red run usually means — and the first pass after
|
|
3210
3244
|
only failures is a recovery (a missing tool installed), not a flake.
|
|
@@ -3250,7 +3284,7 @@ Only a run's own output (`vx run`, `vx watch`) is painted, and the
|
|
|
3250
3284
|
help on a terminal: headings bold, `vx <verb>` and each flag or example
|
|
3251
3285
|
in cyan; `NO_COLOR` or `FORCE_COLOR=0` turns it off
|
|
3252
3286
|
(`tests/help-colors.test.ts`). Every other verb (`show`, `info`, `why`,
|
|
3253
|
-
`last`, a `--dry` plan, `--graph`, `cache
|
|
3287
|
+
`last`, a `--dry` plan, `--graph`, `cache clean`), and the help off a
|
|
3254
3288
|
terminal, prints plain text whatever `FORCE_COLOR` says, so a script
|
|
3255
3289
|
that forces colour for a run's log still parses them
|
|
3256
3290
|
(`tests/cli-colors-e2e.test.ts`).
|
|
@@ -3258,8 +3292,9 @@ that forces colour for a run's log still parses them
|
|
|
3258
3292
|
## Remote cache (plugin-driven)
|
|
3259
3293
|
|
|
3260
3294
|
Core ships **no remote-cache wire client** — the remote cache is a
|
|
3261
|
-
plugin concern. A `cache` plugin
|
|
3262
|
-
|
|
3295
|
+
plugin concern. A `cache` plugin returns a wire client (a
|
|
3296
|
+
`RemoteCacheLayer`) and core layers it over the local cache;
|
|
3297
|
+
`@vzn/vx-reapi` provides one for any Bazel REAPI server
|
|
3263
3298
|
(ActionCache + CAS).
|
|
3264
3299
|
|
|
3265
3300
|
Reads try local first, then remote (hydrating local on remote hit),
|
|
@@ -3273,7 +3308,7 @@ implement core's `RemoteCacheLayer` interface in a plugin's `cache`
|
|
|
3273
3308
|
capability — the recipe lives in the plugins guide. Embedders
|
|
3274
3309
|
holding a wire client can inject it per-run via
|
|
3275
3310
|
`RunOptions.remoteCache` (explicit injection wins over the plugin
|
|
3276
|
-
consult).
|
|
3311
|
+
consult).
|
|
3277
3312
|
|
|
3278
3313
|
## Run analytics
|
|
3279
3314
|
|