@vzn/vx 0.0.636 → 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 +39 -11
- package/docs/caching.md +213 -207
- package/docs/cli.md +699 -655
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +26 -25
- package/docs/schema.md +376 -198
- 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 +5 -3
- 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 -63
- 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 +103 -149
- 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 +169 -70
- 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 +24 -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 +158 -63
- 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 +42 -29
- 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 +113 -72
- 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 +121 -95
- 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 +209 -268
- 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 +20 -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,33 +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
|
-
| `--cache-
|
|
485
|
-
| `--
|
|
486
|
-
| `--
|
|
487
|
-
| `--
|
|
488
|
-
| `--
|
|
489
|
-
| `--
|
|
490
|
-
| `--
|
|
491
|
-
| `--
|
|
492
|
-
| `--
|
|
493
|
-
| `--
|
|
494
|
-
| `--
|
|
495
|
-
| `--
|
|
496
|
-
| `--
|
|
497
|
-
| `--
|
|
498
|
-
| `--
|
|
499
|
-
| `--
|
|
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. |
|
|
500
503
|
|
|
501
504
|
Mutual exclusion:
|
|
502
505
|
|
|
@@ -551,11 +554,13 @@ shell variable), never a path or task id. Use the `=` form for a literal
|
|
|
551
554
|
leading dash.
|
|
552
555
|
|
|
553
556
|
**Numeric flags take a plain decimal integer.** `--concurrency`,
|
|
554
|
-
`--
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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.
|
|
559
564
|
|
|
560
565
|
An empty `=` value means "no value" on four flags, which then act like
|
|
561
566
|
their bare forms: `--profile=` writes `profile.json`, `--summarize=`
|
|
@@ -563,26 +568,34 @@ writes `<cacheDir>/runs/<run_id>.json`, `--graph=` prints to stdout and
|
|
|
563
568
|
`--affected=` uses the default base. Every other flag refuses an empty
|
|
564
569
|
value: `--dry=`, `--report=`, `--continue=` and
|
|
565
570
|
`--exclude-dependencies=` as well as the ones with no bare form
|
|
566
|
-
(`--
|
|
571
|
+
(`--retries=`, `--timeout=`, `--cache-dir=`, `--filter=`, `--cache-policy=`).
|
|
567
572
|
|
|
568
|
-
#### Cache control: `--cache`, `--no-cache`, `--force`
|
|
573
|
+
#### Cache control: `--cache-policy`, `--no-cache`, `--force`
|
|
569
574
|
|
|
570
575
|
The cache has four independent axes — **localRead**, **localWrite**,
|
|
571
576
|
**remoteRead**, **remoteWrite** — and the three flags above resolve
|
|
572
577
|
them in this precedence order:
|
|
573
578
|
|
|
574
579
|
1. Start with every axis **on** (the default).
|
|
575
|
-
2. Apply each `--cache=<spec>` segment (the base), or, with no
|
|
576
|
-
`--cache`, the `
|
|
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`).
|
|
577
583
|
3. If `--no-cache` was passed, force **all four off**.
|
|
578
584
|
4. If `--force` was passed, force **both reads off** (writes stay
|
|
579
|
-
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.
|
|
580
590
|
|
|
581
591
|
A spec that names a remote axis (`remote:r`, `remote:w`, `remote:rw`)
|
|
582
592
|
in a workspace whose plugins supply no remote layer gets one status
|
|
583
593
|
line saying so — the axes are inert without a layer to serve them, and
|
|
584
594
|
a CI job that believes it is filling a shared cache should be told.
|
|
585
595
|
|
|
596
|
+
Whatever the axes, a lookup asks the local cache first, then each
|
|
597
|
+
remote in the order the plugins declare them.
|
|
598
|
+
|
|
586
599
|
So `--no-cache` always wins over `--force`. The common cases:
|
|
587
600
|
|
|
588
601
|
- `--no-cache` → nothing reads, nothing writes, and declared output
|
|
@@ -593,29 +606,30 @@ So `--no-cache` always wins over `--force`. The common cases:
|
|
|
593
606
|
before each task so the saved snapshot is clean. This is the "rebuild
|
|
594
607
|
and refresh the cache" flag.
|
|
595
608
|
|
|
596
|
-
`--cache=<spec>` is a comma-separated list of `<layer>:<flags>`
|
|
609
|
+
`--cache-policy=<spec>` is a comma-separated list of `<layer>:<flags>`
|
|
597
610
|
segments. `layer` is `local` or `remote`; `flags` is any subset of `r`
|
|
598
611
|
(read) and `w` (write), order-independent and possibly empty. A
|
|
599
612
|
**mentioned** layer is set EXACTLY to its flags; an **unmentioned**
|
|
600
|
-
layer keeps its current value. Both `--cache=<spec>` and the space form
|
|
601
|
-
`--cache <spec>` are accepted.
|
|
613
|
+
layer keeps its current value. Both `--cache-policy=<spec>` and the space form
|
|
614
|
+
`--cache-policy <spec>` are accepted.
|
|
602
615
|
|
|
603
|
-
| Spec
|
|
604
|
-
|
|
|
605
|
-
| `--cache=local:rw,remote:r` | remote read-only (won't upload); local full |
|
|
606
|
-
| `--cache=local:r` | local read-only; remote untouched (still full) |
|
|
607
|
-
| `--cache=remote:` | remote fully off; local untouched |
|
|
608
|
-
| `--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 |
|
|
609
622
|
|
|
610
623
|
`local:` means "don't serve hits out of the pre-existing local cache" —
|
|
611
624
|
a remote hit is still delivered (the artifact has to land on disk to be
|
|
612
625
|
extracted), it just never short-circuits the remote read.
|
|
613
626
|
|
|
614
627
|
Combine with `--force` for "re-execute and refresh only the remote":
|
|
615
|
-
`--cache=local: --force` (local off, reads off, remote write-only).
|
|
628
|
+
`--cache-policy=local: --force` (local off, reads off, remote write-only).
|
|
616
629
|
|
|
617
630
|
Invalid layers/flags are a parse error
|
|
618
|
-
(
|
|
631
|
+
(`--cache-policy: invalid layer 'disk'`, `--cache-policy: invalid flag 'x'`, …);
|
|
632
|
+
`VX_CACHE_POLICY` and `cache.policy` say their own name instead.
|
|
619
633
|
|
|
620
634
|
### Output
|
|
621
635
|
|
|
@@ -731,7 +745,7 @@ tracks the run live. Top to bottom:
|
|
|
731
745
|
persistent tasks it depends on, until it — or, with several, the
|
|
732
746
|
first of them — exits (one kept only as a dependency that exits 0,
|
|
733
747
|
a daemon that forked and returned, does not end it); the rest still up are then torn
|
|
734
|
-
down (SIGTERM, `
|
|
748
|
+
down (SIGTERM, `run.killGrace`, SIGKILL), one status line names
|
|
735
749
|
the task and its code (`vx: app#dev exited with code 1; stopping 1
|
|
736
750
|
other persistent task`), and a non-zero exit makes the run exit 1
|
|
737
751
|
and is what `vx last` records.
|
|
@@ -744,15 +758,18 @@ other persistent task`), and a non-zero exit makes the run exit 1
|
|
|
744
758
|
each one's id (`app#dev │ Local: http://localhost:5173`), except
|
|
745
759
|
under `--output-logs errors-only`, `hash-only` or `none`; a single
|
|
746
760
|
requested server in the focused flow streams raw from its frame.
|
|
747
|
-
3. **Worker rows** — one per worker slot (
|
|
761
|
+
3. **Worker rows** — one per worker slot that has been busy (at most
|
|
748
762
|
`min(concurrency, 10)`), no glyph and no spinner: the live ticking
|
|
749
763
|
elapsed time leads (` 568ms running <id>`). A task stays in
|
|
750
|
-
its row for its whole life;
|
|
751
|
-
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
|
|
752
766
|
says what waits on it: `critical path` (yellow) on the running task
|
|
753
767
|
the longest unfinished chain waits on, by the same p50s as the
|
|
754
768
|
forecast below and only while two or more run, and a dim
|
|
755
|
-
`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`).
|
|
756
773
|
4. **The live summary section** — the SAME meters the final footer
|
|
757
774
|
prints (`tasks` + `cache` bars with legends, `time`), filling in as
|
|
758
775
|
the run progresses, under a bare `vx` wordmark rule. A second in,
|
|
@@ -762,7 +779,10 @@ other persistent task`), and a non-zero exit makes the run exit 1
|
|
|
762
779
|
workers, a running task's spent time taken off its own. It says
|
|
763
780
|
nothing when history knows none of what is left, or once a running
|
|
764
781
|
task is 25% past its p50. A run that ends inside the second never
|
|
765
|
-
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.
|
|
766
786
|
|
|
767
787
|
In a terminal known to read them, the region also sends zero-width
|
|
768
788
|
escapes: tab or taskbar progress (OSC 9;4: Ghostty, iTerm2 3.6+,
|
|
@@ -783,7 +803,8 @@ text is the task's output unchanged; a successful task's frame is never
|
|
|
783
803
|
linked.
|
|
784
804
|
|
|
785
805
|
The region is redrawn in place (cursor-up + clear; not a TUI — no
|
|
786
|
-
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
|
|
787
808
|
focused flow it only lives while dependencies run; it disappears for
|
|
788
809
|
good the moment the requested task starts streaming.
|
|
789
810
|
|
|
@@ -850,7 +871,7 @@ down with it:
|
|
|
850
871
|
The mode is the local scheduler's: a task a plugin executor runs
|
|
851
872
|
elsewhere is one dispatch, failed or not, like any other.
|
|
852
873
|
|
|
853
|
-
### `--download <mode>`
|
|
874
|
+
### `--cache-download <mode>`
|
|
854
875
|
|
|
855
876
|
Where a **remotely-executed** task's outputs land. `all` (default)
|
|
856
877
|
downloads every task's outputs to this machine as it completes —
|
|
@@ -864,7 +885,7 @@ bytes"). A CI job that only wants the verdict moves no output bytes at
|
|
|
864
885
|
all.
|
|
865
886
|
|
|
866
887
|
Locally-executed tasks always write in place and ignore the flag, and
|
|
867
|
-
`exec.remote: 'only'` still means never — `--download` cannot override
|
|
888
|
+
`exec.remote: 'only'` still means never — `--cache-download` cannot override
|
|
868
889
|
it in either direction. **It never affects a cache key**: transfer
|
|
869
890
|
tuning cannot change what a command produces.
|
|
870
891
|
|
|
@@ -976,8 +997,9 @@ $ vx run build lint --affected=main --dry
|
|
|
976
997
|
```
|
|
977
998
|
|
|
978
999
|
`kind` is `input` (a changed `file` is a declared input), `project` (its
|
|
979
|
-
project changed and the task reads it whole: an uncached task, or
|
|
980
|
-
|
|
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),
|
|
981
1003
|
`package` (a `^` edge passes a changed `project` with no such task),
|
|
982
1004
|
`named` (`pkg#task` beside a bare task) or `selected` (another filter
|
|
983
1005
|
selected its project). `via` is the `dependsOn` chain, nearest first, to
|
|
@@ -1089,7 +1111,7 @@ outcome bucket or `total`, but they make the run red, so they are
|
|
|
1089
1111
|
listed separately and counted as `summary.aborted`.
|
|
1090
1112
|
|
|
1091
1113
|
**`noCache: true`** marks a task no cache answered for: it declares no
|
|
1092
|
-
`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`
|
|
1093
1115
|
read and wrote nothing (`--no-cache`), so a hit rate should leave it out
|
|
1094
1116
|
of the denominator. The key is present only when true; every other row is
|
|
1095
1117
|
unchanged. Its `hash` is still set: dependents fold it.
|
|
@@ -1192,14 +1214,14 @@ or a timeout's reason, `failed (timed out, exit 143)`, a persistent
|
|
|
1192
1214
|
task's `never ready: …`, or a sandboxed task's violation count, or
|
|
1193
1215
|
`skipped`, naming what blocked it: `skipped (blocked by lib#build)`);
|
|
1194
1216
|
`Cache` is its provenance (`miss` / `no-cache` for a task no cache
|
|
1195
|
-
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 —
|
|
1196
1218
|
which never consulted one / `local` / `remote` / `up-to-date` /
|
|
1197
1219
|
`—`). A stopped run (a Ctrl-C teardown) reads `## vx run — interrupted`,
|
|
1198
1220
|
and its tasks keep the terminal's words: one the signal killed is
|
|
1199
1221
|
`aborted` with its time so far, one the stop reached before it ran is
|
|
1200
|
-
`not run` with no time.
|
|
1201
|
-
|
|
1202
|
-
|
|
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
|
|
1203
1225
|
work, and the header's counts match the terminal summary and
|
|
1204
1226
|
`--summarize` exactly.
|
|
1205
1227
|
|
|
@@ -1321,7 +1343,7 @@ sandbox run as usual, and the failure's dependents follow `--continue`.
|
|
|
1321
1343
|
## `vx watch`
|
|
1322
1344
|
|
|
1323
1345
|
```
|
|
1324
|
-
vx watch [OPTIONS] TASK [-- forwarded-args...]
|
|
1346
|
+
vx watch [OPTIONS] [--poll] TASK [-- forwarded-args...]
|
|
1325
1347
|
```
|
|
1326
1348
|
|
|
1327
1349
|
Run the named task, then re-run it on every filesystem change in the
|
|
@@ -1347,11 +1369,11 @@ run...` precedes it.
|
|
|
1347
1369
|
`dependsOn` edges included — is watched recursively, less any project
|
|
1348
1370
|
nested inside it, with a config or not (a root project's key leaves a nested
|
|
1349
1371
|
project's files out, so an edit there is no cycle; X-42, X-57). The workspace root is
|
|
1350
|
-
watched (non-recursively) for lockfile
|
|
1351
|
-
|
|
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
|
|
1352
1374
|
shapes a run (plugins, `config` stage, concurrency) without being any
|
|
1353
1375
|
task's input; the cycle after it re-evaluates the file. A `.gitignore`
|
|
1354
|
-
there or in a
|
|
1376
|
+
there or in a project's parent directory is a cycle too: inputs are
|
|
1355
1377
|
gitignore-aware, so it changes what keys read (X-136). A file a
|
|
1356
1378
|
config imports by relative path from outside the watched projects (a
|
|
1357
1379
|
shared preset) is watched too, and its edit is a cycle that re-reads
|
|
@@ -1359,20 +1381,20 @@ run...` precedes it.
|
|
|
1359
1381
|
config's own edit does (WD-14). A file the workspace config imports is loaded once per
|
|
1360
1382
|
process, so its edit is named with the restart it needs rather than
|
|
1361
1383
|
run stale (item 949). An import whose file is gone stays on the list
|
|
1362
|
-
by the name it would have, so its return is a cycle (X-133). The
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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`,
|
|
1376
1398
|
`outputs.workspaceFiles`; a plugin's `project` stage counts, as in
|
|
1377
1399
|
a run) never trigger a re-run — a cycle that writes `dist/` is not
|
|
1378
1400
|
an edit, nor is the `dist` directory itself coming and going (a
|
|
@@ -1427,7 +1449,8 @@ run...` precedes it.
|
|
|
1427
1449
|
at the root, a `coverage/` or `.turbo/` tree, an editor's scratch
|
|
1428
1450
|
file are dropped before the trigger — so `vx watch … > build.log`
|
|
1429
1451
|
inside the repo settles instead of feeding itself a cycle per line
|
|
1430
|
-
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
|
|
1431
1454
|
printed only after every watcher has reported a probe file written
|
|
1432
1455
|
under it (`.vx-watch-probe`, re-written on a short backoff until its
|
|
1433
1456
|
event arrives, then removed): on macOS a directory watcher can return
|
|
@@ -1439,10 +1462,14 @@ run...` precedes it.
|
|
|
1439
1462
|
watcher whose probe is not heard within 2 s is replaced by a poller
|
|
1440
1463
|
that checks every 250 ms, with the notice `vx watch: <dir>: no OS
|
|
1441
1464
|
watch events within 2000 ms; polling every 250 ms instead`.
|
|
1442
|
-
3. **On change.**
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
|
1446
1473
|
orchestrator is invoked again with the same options, save one: the
|
|
1447
1474
|
`--affected` diff is read once, for the initial run. A later cycle
|
|
1448
1475
|
runs the requested task in every project of the scope fixed at start,
|
|
@@ -1457,15 +1484,15 @@ watch events within 2000 ms; polling every 250 ms instead`.
|
|
|
1457
1484
|
|
|
1458
1485
|
Always ignored (no re-trigger):
|
|
1459
1486
|
|
|
1460
|
-
- `node_modules/`, `.git/`, `.vx/` anywhere in the path.
|
|
1487
|
+
- `node_modules/`, `.git/`, `.vx/cache/` anywhere in the path.
|
|
1461
1488
|
- A path git ignores (`git check-ignore`, asked once per debounce
|
|
1462
1489
|
window, never per event): invisible to every cache key — inputs are
|
|
1463
1490
|
tracked + untracked-not-ignored — so a cycle it started could change
|
|
1464
1491
|
nothing. A tracked file that matches a pattern is not ignored, by
|
|
1465
1492
|
git's own rule; outside a repository nothing is.
|
|
1466
1493
|
- Files ending in `.tsbuildinfo` or `~` (editor swap files).
|
|
1467
|
-
- The run's **resolved cache directory**, wherever it is. `.vx/` covers the
|
|
1468
|
-
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
|
|
1469
1496
|
anywhere; watching it would let vx's own cache writes trigger the next
|
|
1470
1497
|
cycle, which writes again — a loop that never settles.
|
|
1471
1498
|
|
|
@@ -1494,19 +1521,20 @@ It samples each file's later clock of mtime and ctime, so a replacement
|
|
|
1494
1521
|
that carries the old file's mtime (`cp -p`, `rsync -a`, a `mv`) is an
|
|
1495
1522
|
edit to it as it is to the OS watcher.
|
|
1496
1523
|
|
|
1497
|
-
###
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
`pnpm-
|
|
1501
|
-
|
|
1502
|
-
Watch mode hears
|
|
1503
|
-
(non-recursively)
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
(`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).
|
|
1510
1538
|
|
|
1511
1539
|
### Constraints
|
|
1512
1540
|
|
|
@@ -1541,7 +1569,7 @@ re-runs itself and what it depends on.
|
|
|
1541
1569
|
- `0` — clean Ctrl+C / SIGTERM exit. A cycle in flight is torn down
|
|
1542
1570
|
first — its children, and the dev server held between cycles, get the
|
|
1543
1571
|
signal watch received (a Ctrl-C as SIGINT; a SIGTERM or SIGHUP as SIGTERM),
|
|
1544
|
-
`
|
|
1572
|
+
`run.killGrace` (2 s), then SIGKILL — and the process leaves only
|
|
1545
1573
|
once it has returned, so a CI cancellation never orphans a task.
|
|
1546
1574
|
Also `0`, with no watch, when `--affected` selects nothing: watch
|
|
1547
1575
|
prints `vx watch: nothing affected since <ref>` and exits as `vx run`
|
|
@@ -1560,32 +1588,34 @@ the watch loop — a failed cycle just prints the framed FAILED block
|
|
|
1560
1588
|
and waits for the next change. This matches `turbo watch` / `nx
|
|
1561
1589
|
watch`.
|
|
1562
1590
|
|
|
1563
|
-
## `vx cache
|
|
1591
|
+
## `vx cache clean`
|
|
1564
1592
|
|
|
1565
1593
|
Evict old or oversized cache entries. Operates on the entries and
|
|
1566
1594
|
`<hash>.tar.zst` artifacts of the store the workspace's index records
|
|
1567
1595
|
(`~/.vx/<id>/cache`, shared by every checkout of the repository), or of
|
|
1568
|
-
`<cacheDir>` itself when the workspace names one (`
|
|
1596
|
+
`<cacheDir>` itself when the workspace names one (`cache.dir`,
|
|
1569
1597
|
`VX_CACHE_DIR`, `--cache-dir`).
|
|
1570
1598
|
|
|
1571
|
-
`
|
|
1599
|
+
`clean` is the only `vx cache` subcommand: the statistics other runners
|
|
1572
1600
|
put under a `cache` verb — the directory, the entry count, the size —
|
|
1573
1601
|
are part of [`vx info`](#vx-info), and `vx cache stats`, `list`, `ls`,
|
|
1574
|
-
`
|
|
1575
|
-
`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".
|
|
1576
1604
|
|
|
1577
1605
|
```
|
|
1578
|
-
vx cache
|
|
1579
|
-
vx cache
|
|
1580
|
-
vx cache
|
|
1581
|
-
vx cache
|
|
1582
|
-
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.
|
|
1583
1613
|
```
|
|
1584
1614
|
|
|
1585
|
-
|
|
1586
|
-
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
|
|
1587
1617
|
total is still over the size cap. The same policy runs unattended at
|
|
1588
|
-
the end of every run when
|
|
1618
|
+
the end of every run when `.vx/workspace.ts` declares
|
|
1589
1619
|
`cacheRetention: { olderThan, maxSize }` (same spellings; see
|
|
1590
1620
|
[schema](./schema.md)).
|
|
1591
1621
|
|
|
@@ -1601,7 +1631,7 @@ vx version sharing the store may still use it. A temp goes once it is an
|
|
|
1601
1631
|
hour old; nothing younger than an hour is taken. Only the names vx writes are taken:
|
|
1602
1632
|
`<hash>` is the key's 16 lowercase hex digits and the temp suffix is
|
|
1603
1633
|
the one a save makes, so a `release.tar.zst` beside the index in a
|
|
1604
|
-
`
|
|
1634
|
+
`cache.dir` you share is never touched (item 968). The converse, an
|
|
1605
1635
|
index row whose artifact is gone (deleted by hand), is dropped by the
|
|
1606
1636
|
same prune, and its bytes count toward neither `--max-size` nor the
|
|
1607
1637
|
evicted total: they are on no disk, and counting them evicted real
|
|
@@ -1609,7 +1639,7 @@ entries to make room for them (item 975). A row used within the hour
|
|
|
1609
1639
|
is kept until a later prune, but its bytes are left out all the same
|
|
1610
1640
|
(item 1081). The sweep runs on every prune, under
|
|
1611
1641
|
either flag, and reports separately from the policy's evictions. A
|
|
1612
|
-
workspace's `
|
|
1642
|
+
workspace's `cache.retention` runs it too, at the end of a run, at most
|
|
1613
1643
|
once an hour even when nothing the index holds is due (the policy sums
|
|
1614
1644
|
index rows, so orphans alone never make it due).
|
|
1615
1645
|
|
|
@@ -1626,22 +1656,20 @@ write `10G`, or `10B` when bytes really are the unit.
|
|
|
1626
1656
|
|
|
1627
1657
|
**A zero bound is rejected.** `--max-size 0` and `--older-than 0d` would
|
|
1628
1658
|
evict every entry in the cache, which is far more often a
|
|
1629
|
-
computed-to-zero retention than an intent
|
|
1630
|
-
|
|
1631
|
-
full prune). Delete the cache store `vx info` names when that is really
|
|
1632
|
-
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.
|
|
1633
1661
|
|
|
1634
1662
|
```
|
|
1635
|
-
$ vx cache
|
|
1663
|
+
$ vx cache clean --older-than 30d
|
|
1636
1664
|
Pruned 42 entries (1.3 GB freed)
|
|
1637
1665
|
|
|
1638
|
-
$ vx cache
|
|
1666
|
+
$ vx cache clean --older-than 7d --max-size 500M
|
|
1639
1667
|
Pruned 18 entries (320 MB freed)
|
|
1640
1668
|
|
|
1641
|
-
$ vx cache
|
|
1669
|
+
$ vx cache clean --older-than 30d # after a SCHEMA_VERSION bump
|
|
1642
1670
|
Pruned 0 entries (0 B freed), reaped 42 orphaned artifacts (1.3 GB)
|
|
1643
1671
|
|
|
1644
|
-
$ vx cache
|
|
1672
|
+
$ vx cache clean --older-than 30d --dry-run
|
|
1645
1673
|
Would prune 42 entries (1.3 GB), would reap 3 orphaned artifacts (12 MB)
|
|
1646
1674
|
```
|
|
1647
1675
|
|
|
@@ -1670,49 +1698,128 @@ Exit codes:
|
|
|
1670
1698
|
nothing is created.
|
|
1671
1699
|
- `1` — parse error, missing policy, or workspace-discovery error.
|
|
1672
1700
|
|
|
1673
|
-
`vx cache
|
|
1674
|
-
`defineWorkspace({
|
|
1701
|
+
`vx cache clean` resolves the workspace root from cwd and honors a
|
|
1702
|
+
`defineWorkspace({ cache: { dir } })` override — it prunes the same
|
|
1675
1703
|
directory a run would use. `--cache-dir <path>` (cwd-relative, as on
|
|
1676
1704
|
`vx run`) names another one, so a run that wrote elsewhere can be
|
|
1677
1705
|
pruned there.
|
|
1678
1706
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
`vx
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
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.
|
|
1689
1758
|
|
|
1690
1759
|
## `vx lock`
|
|
1691
1760
|
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
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.
|
|
1701
1784
|
vx lock --check --format json # The audit as {upToDate, audited, notAudited, problems}.
|
|
1702
1785
|
```
|
|
1703
1786
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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
|
|
1707
1807
|
semantics: env reads in a config keep their lock-time values; a
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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 …`.
|
|
1716
1823
|
`--format json` prints the audit as one document
|
|
1717
1824
|
(`schemas/lock.json`, exit code unchanged; a missing lock is one of
|
|
1718
1825
|
its `problems`); it answers `--check` only.
|
|
@@ -1722,18 +1829,18 @@ Full design: `docs/design/config-lock-2026-06.md`. A lock written by a
|
|
|
1722
1829
|
A project with no `vx.config.*` — its tasks from a plugin's `project`
|
|
1723
1830
|
stage (`turbo()` from `@vzn/vx-migrate`), or none at all — has nothing
|
|
1724
1831
|
to freeze: the lock records nothing for it, `--check` does not audit
|
|
1725
|
-
it, and
|
|
1726
|
-
|
|
1727
|
-
vx
|
|
1728
|
-
|
|
1729
|
-
|
|
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.
|
|
1730
1837
|
|
|
1731
1838
|
The lock is committed, so `vx lock` refuses to write one that holds a
|
|
1732
1839
|
secret: a config that evaluated to the value of a secret-named variable
|
|
1733
1840
|
(or one a task lists in `exec.env.secret`), say
|
|
1734
1841
|
`` `--token ${process.env.API_TOKEN}` ``. It names each place
|
|
1735
1842
|
(`a: tasks.deploy.exec.command holds $API_TOKEN`) and writes nothing.
|
|
1736
|
-
Masking it instead would freeze a `***` that
|
|
1843
|
+
Masking it instead would freeze a `***` that every run runs. Let the
|
|
1737
1844
|
shell expand it: `$API_TOKEN` in the command, the name in
|
|
1738
1845
|
`exec.env.passThrough` (L-42).
|
|
1739
1846
|
|
|
@@ -1824,8 +1931,8 @@ One-time setup, per package, on npmjs.com → package → Settings →
|
|
|
1824
1931
|
Trusted Publisher → GitHub Actions: owner `vznjs`, repository `vx`,
|
|
1825
1932
|
workflow `npm.yml`, environment left blank. Do this for `@vzn/vx`,
|
|
1826
1933
|
`@vzn/vx-darwin-x64`, `@vzn/vx-darwin-arm64`, `@vzn/vx-linux-x64`,
|
|
1827
|
-
`@vzn/vx-linux-arm64` and the
|
|
1828
|
-
`@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`,
|
|
1829
1936
|
`@vzn/vx-reapi` and `@vzn/vx-schedule-history`.
|
|
1830
1937
|
|
|
1831
1938
|
Each platform package carries `LICENSE` and `THIRD_PARTY_NOTICES.txt`:
|
|
@@ -1854,308 +1961,173 @@ direct publishing (the `E401 token is invalid` that stopped v0.0.17).
|
|
|
1854
1961
|
Every `uses:` in both workflows is pinned to a commit SHA with the
|
|
1855
1962
|
version in a comment; bump the SHA and the comment together.
|
|
1856
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
|
+
|
|
1857
2000
|
## `vx upgrade`
|
|
1858
2001
|
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
`
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
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
|
|
1885
2032
|
download: `cannot write to <dir> (EACCES), where this vx lives — nothing
|
|
1886
|
-
downloaded; re-run as a user who can (sudo vx upgrade), …`. The new
|
|
1887
|
-
keeps the old one's mode (and, as root, its owner), and must
|
|
1888
|
-
`--version` before the upgrade reports it installed: one that
|
|
1889
|
-
start on this machine
|
|
1890
|
-
for the previous vx, and the command exits 1 saying so
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
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.
|
|
1900
2075
|
|
|
1901
2076
|
## `vx init`
|
|
1902
2077
|
|
|
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
|
-
|
|
1935
|
-
|
|
1936
|
-
and
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
named
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
`
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
package.json scripts are `init`'s. A workspace with no scripts at
|
|
1950
|
-
all still gets the workspace file, a printed example config, and the
|
|
1951
|
-
next command to run. A root
|
|
1952
|
-
`package.json` with no `workspaces` field is single-project mode, and
|
|
1953
|
-
when `packages/*/package.json` files sit below it unreached, both
|
|
1954
|
-
`init` and a run that finds no config say so instead ("package.json
|
|
1955
|
-
declares no workspaces … Add "workspaces": ["packages/*"] to
|
|
1956
|
-
package.json and re-run") rather than "no scripts" or "run vx init". A
|
|
1957
|
-
workspace whose globs match no `package.json` gets a note naming them
|
|
1958
|
-
(M-42); a nameless member, or two sharing a name, is matched and
|
|
1959
|
-
named on its own line instead (M-46). When such skipped scripts are all there
|
|
1960
|
-
is, the report opens `no package.json scripts became tasks.` rather than
|
|
1961
|
-
saying there are none (M-59). When `pnpm-workspace.yaml` lists the
|
|
1962
|
-
members and the root `package.json`'s `workspaces` lists other globs,
|
|
1963
|
-
the note names the unread list (M-51): pnpm reads only the yaml, while
|
|
1964
|
-
bun, npm and yarn read `package.json`. With
|
|
1965
|
-
no `package.json` here or above, `init` says to create one (`bun init` or
|
|
1966
|
-
`npm init -y`) first. Every
|
|
1967
|
-
generated config is typed for the editor through
|
|
1968
|
-
`import type { ProjectConfig } from '@vzn/vx/config'` and `satisfies
|
|
1969
|
-
ProjectConfig` — a type-only import Bun erases, so the file loads in a
|
|
1970
|
-
workspace that runs the `vx` binary without the package installed. The
|
|
1971
|
-
workspace file takes the same form (`satisfies WorkspaceConfig`). A
|
|
1972
|
-
runtime `import { defineWorkspace } from '@vzn/vx'` costs nothing extra
|
|
1973
|
-
since 2026-09-10: the running `vx` serves its own core to that
|
|
1974
|
-
specifier (bin.ts registers the alias), so a config or a plugin package
|
|
1975
|
-
never loads a second copy, and a workspace that runs the binary needs
|
|
1976
|
-
no `@vzn/vx` installed for it — the type-only form is still what the
|
|
1977
|
-
scaffold writes, because it types the same with no import to resolve
|
|
1978
|
-
in an editor without the package. A workspace that imports a PLUGIN
|
|
1979
|
-
package at runtime without having installed it is told so, and to
|
|
1980
|
-
install its dependencies. `--mjs` writes the same objects as `vx.config.mjs` and
|
|
1981
|
-
`vx.workspace.mjs`, with no type import and no `satisfies`: a package whose
|
|
1982
|
-
own `tsconfig` includes every `.ts` under it compiles a `vx.config.ts`
|
|
1983
|
-
into its dist (TanStack/query's `tsc --build`, 2026-09-11), and an
|
|
1984
|
-
`.mjs` is outside that include. `@vzn/vx-migrate` takes the same flag.
|
|
1985
|
-
A file it writes that `.gitignore` drops (vueuse ignores
|
|
1986
|
-
`packages/**/*.mjs`) gets a `note:` naming it and the `!vx.config.mjs`
|
|
1987
|
-
line that keeps it, since a commit would leave it behind unnoticed.
|
|
1988
|
-
|
|
1989
|
-
Each script becomes a task with its command verbatim. `build` gets
|
|
1990
|
-
`dependsOn: ['^build']` (a `build` that only delegates, `pnpm run
|
|
1991
|
-
compile`, is a group, and the edge goes on the script that does the
|
|
1992
|
-
work) and **no cache block** — under a
|
|
1993
|
-
`TODO(vx-migrate)` showing the block to add with the package's real
|
|
1994
|
-
inputs and outputs, and the same block commented as the task's first
|
|
1995
|
-
line: set the real globs and delete the `// `. A task without a cache block always runs; a block
|
|
1996
|
-
with EMPTY outputs is not "uncached" but a no-output task that hits on
|
|
1997
|
-
unchanged inputs and skips the build with nothing to restore (a deleted
|
|
1998
|
-
`dist` stays deleted under a green `up-to-date` run — what `init`
|
|
1999
|
-
generated until 2026-09-04), and a guessed `dist/**` would restore the
|
|
2000
|
-
wrong tree for every package that writes elsewhere. The block the TODO
|
|
2001
|
-
shows names `dist/**`, or the default output of the framework the command
|
|
2002
|
-
runs: `.next/**` minus `!.next/cache/**` for `next build`, `.output/**`
|
|
2003
|
-
for Nuxt, `build/**` for Remix, React Router, Create React App and
|
|
2004
|
-
Docusaurus, `public/**` for Gatsby, `storybook-static/**` for Storybook;
|
|
2005
|
-
for any other command, the directory it names with `--outDir` / `--out-dir`
|
|
2006
|
-
/ `-d`, else the ones it cleans first (`del-cli distribution`, `rimraf lib
|
|
2007
|
-
types`), unless it makes one again with `mkdir` (D-90).
|
|
2008
|
-
A package in a cycle of builds (nuxt's `@nuxt/nitro-server` devDepends
|
|
2009
|
-
on `nuxt`, which depends on it; pnpm sorts it away) gets, instead of
|
|
2010
|
-
`^build`, an edge to each build outside its cycle that `^build` would
|
|
2011
|
-
reach, and a TODO to order the cycle: `^build` there refuses the run.
|
|
2012
|
-
`test` / `typecheck` wait for
|
|
2013
|
-
`build` when the package has one (`lint` reads sources and gets no
|
|
2014
|
-
edge); `dev` / `start` / `serve` / `watch` /
|
|
2015
|
-
`preview` become persistent tasks, with a TODO to add `readyWhen` when
|
|
2016
|
-
another task depends on one, and so does a watcher: a `watch` segment in the script's name (`build:watch`),
|
|
2017
|
-
a `--watch` flag, `tsc -w` / `rollup -w`, or nodemon (D-40), and a server:
|
|
2018
|
-
`serve <dir>`, `http-server`, bare `vite`, a tool's `dev` / `serve` /
|
|
2019
|
-
`start` / `preview` verb (`next start`, `netlify dev`), or a script that
|
|
2020
|
-
runs such a script of its package by name (`cross-env X=1 pnpm start`) or
|
|
2021
|
-
through a runner (`run-p web api`, `concurrently "npm:web" "npm:api"`, D-113),
|
|
2022
|
-
outside quotes and not sent to the background with `&` (D-91), read past
|
|
2023
|
-
a launcher's `--package <name>` / `-p <name>` (`pnpm dlx --package
|
|
2024
|
-
netlify-cli netlify dev`, D-112). A
|
|
2025
|
-
script whose name no task may carry (`lint#fix`, `^up`; the schema's
|
|
2026
|
-
rule, item 1000) is left out with a TODO rather than written into a
|
|
2027
|
-
config every later command refuses, and a `__proto__` script is written
|
|
2028
|
-
as a computed key, since a literal `__proto__:` sets the prototype. An
|
|
2029
|
-
existing `vx.workspace.*` in any extension the loader reads (`.mts`
|
|
2030
|
-
included) is kept, and `--force` REPLACES a package's config of another
|
|
2031
|
-
extension (`replaced:` in the report) rather than writing a second one
|
|
2032
|
-
the loader would choose between by its order (item 1033). Without
|
|
2033
|
-
`--force`, `init` never overwrites: a package that already has a vx
|
|
2034
|
-
config keeps it, untouched, the rest get theirs, and the report lists
|
|
2035
|
-
the kept ones under `kept`, so a half-adopted workspace adopts the rest
|
|
2036
|
-
(M-52). `@vzn/vx-migrate` still refuses an existing config.
|
|
2037
|
-
|
|
2038
|
-
A missing `vx.workspace.*` is not an error. A run where no package has
|
|
2039
|
-
a config fails before any task, exit 1:
|
|
2040
|
-
``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.``
|
|
2041
|
-
|
|
2042
|
-
Two npm conventions are mapped rather than copied, because copying them
|
|
2043
|
-
loses behaviour. `pre<x>` / `post<x>` hooks, which npm runs around `x`
|
|
2044
|
-
without being named, are folded into `x`'s command in that order, each
|
|
2045
|
-
in its own subshell, so the chain stops at the first that fails whatever
|
|
2046
|
-
a part holds (a `;`, an `exit`), and forwarded `--` args reach `x` alone,
|
|
2047
|
-
as npm hands them to the script and not its hooks (item 905), appended
|
|
2048
|
-
as text, so no part sees them as `$1` (X-60). The
|
|
2049
|
-
command is a small shell function, `vx_script`, around the three parts;
|
|
2050
|
-
it carries a TODO saying so; a `pre<x>` with no `x` stays a task of its own, and
|
|
2051
|
-
npm's lifecycle hooks (`prepack`, `prepublishOnly`, …) are never tasks.
|
|
2052
|
-
A member with no script to run (none, or only lifecycle hooks: a
|
|
2053
|
-
types-only or config package) gets no `vx.config.ts` and is named in
|
|
2054
|
-
the report; it is still a project, and a task declared in its own
|
|
2055
|
-
`vx.config.ts` runs. A package with no `build` script whose `prepack`, `prepublishOnly`,
|
|
2056
|
-
`prepublish` or `prepare` runs a builder (`bob build`, `tsc`, `tsup`, …)
|
|
2057
|
-
is named in the report, which says to add a `build` script running it
|
|
2058
|
-
(react-navigation's twelve packages, whose root `build` is `lerna run
|
|
2059
|
-
prepack`, mapped with no build at all).
|
|
2060
|
-
Where the package's manager runs no such hooks every `pre<x>` and
|
|
2061
|
-
`post<x>` is a task of its own: Yarn 2+ (the nearest `packageManager:
|
|
2062
|
-
yarn@2+`, or a Berry `yarn.lock`, D-31; a `packageManager` naming none of npm,
|
|
2063
|
-
pnpm, yarn or bun defers to the lockfile, D-96), npm under `ignore-scripts=true`
|
|
2064
|
-
in the `.npmrc` beside its lockfile, pnpm under
|
|
2065
|
-
`enable-pre-post-scripts=false` there or `enablePrePostScripts: false` in
|
|
2066
|
-
`pnpm-workspace.yaml` (D-33). Bun and Yarn 1 run them whatever those say.
|
|
2067
|
-
Under Yarn 2+ a segment's `run <script>`, Yarn's shell builtin, is written
|
|
2068
|
-
`yarn run <script>`: vx's shell has no `run` (D-92).
|
|
2069
|
-
A Yarn 2+ repo on Plug'n'Play (no `nodeLinker: node-modules` or `pnpm` in
|
|
2070
|
-
`.yarnrc.yml`) keeps its bins in `.pnp.cjs`, where vx's PATH finds none;
|
|
2071
|
-
the report says so and names `nodeLinker: node-modules` or `yarn exec
|
|
2072
|
-
'<command>'` (D-108).
|
|
2073
|
-
A script reading `$npm_package_version`, `$npm_package_name` or
|
|
2074
|
-
`$npm_lifecycle_event`, which every manager sets and vx does not, gets
|
|
2075
|
-
them under `exec.env.define`, the first two read from an imported
|
|
2076
|
-
`package.json` so a version bump reaches them; any other `$npm_*` it
|
|
2077
|
-
reads gets a TODO (D-34). Among several packages, a workspace root
|
|
2078
|
-
script that runs the members (`pnpm -r build`, `--filter`, `-C`, pnpm's `pn` alias
|
|
2079
|
-
included (D-97), Yarn's
|
|
2080
|
-
`--cwd` and `yarn workspace <name>`, npm's `--prefix`, npm's
|
|
2081
|
-
and Yarn's workspace flags, a `cd` into or above a member, turbo, nx, lerna, `vp run`, vx itself) is not mapped
|
|
2082
|
-
(a flag counts on the package manager, or after `node <bin> run`, and not on the program it
|
|
2083
|
-
runs: berry's `yarn node -r ./setup.ts` is node's `--require`, D-81; bun's
|
|
2084
|
-
`cd test && …` enters no member, D-83),
|
|
2085
|
-
nor is one that runs such a script by name (vite's `ci-docs`: `pnpm build &&
|
|
2086
|
-
pnpm docs-build`; through `run-s` / `run-p` / `npm-run-all` or `concurrently
|
|
2087
|
-
"npm:x"` too, lexical's `ci-check`, D-95), and neither is one whose name a member's task carries, so `--all` never runs
|
|
2088
|
-
a check twice (D-45). The rest check the whole repo (`lint: oxlint .`,
|
|
2089
|
-
`test: vitest`) and become the root's own tasks in a root vx.config, when
|
|
2090
|
-
the root has a `"name"` (vx skips a nameless root's config) and no config
|
|
2091
|
-
of its own; one already there, hand-written or from an earlier
|
|
2092
|
-
`vx init`, stays as written, `--force` included, and is listed under
|
|
2093
|
-
`kept`. The report names each script left out and why
|
|
2094
|
-
(a `pre` / `post` hook goes with its script, D-85), its examples of running
|
|
2095
|
-
the members spelled by the repo's manager (`--workspaces` under npm, `yarn
|
|
2096
|
-
workspaces foreach` under Yarn 2+), and says which;
|
|
2097
|
-
with nothing mapped it names the root whenever it has a script, a member
|
|
2098
|
-
or not (pnpm's root is not), and why: its scripts run the workspace, or
|
|
2099
|
-
share a member's task name (listed), or both; it tells a root with no `"name"` to add one
|
|
2100
|
-
first (vuejs/core), naming the scripts that would then map (react, D-87), and a root whose
|
|
2101
|
-
`"name"` a member also carries to rename it, since a root config would make the
|
|
2102
|
-
workspace refuse every run for the duplicate (insomnia, D-129). A single-package repo's root is its project and maps.
|
|
2103
|
-
A lone root beside a `lerna.json` (Lerna-classic: packages listed there, not
|
|
2104
|
-
in `workspaces`) gets a note naming the `workspaces` globs to add, Lerna's
|
|
2105
|
-
`packages/*` default when it lists none (D-111).
|
|
2106
|
-
A member whose package.json has no `name` is no project; one with a
|
|
2107
|
-
script is named in the report, to be given a name (remix's
|
|
2108
|
-
`packages/component/bench`, D-106).
|
|
2109
|
-
A member's script that runs another member's work (`pnpm -C ../pinia
|
|
2110
|
-
run build`, `yarn workspace <name>`, `-r` / `--filter`, a `cd` or `-C`
|
|
2111
|
-
/ `--cwd` / `--prefix` into another member) keeps its command and gets a
|
|
2112
|
-
TODO naming that part: the graph runs that member's task once, so name it
|
|
2113
|
-
under `dependsOn` and drop it from the command (pinia). A directory inside
|
|
2114
|
-
the member or the root is no other member.
|
|
2115
|
-
A script that is nothing but `npm run <other>` (`pnpm <other>`, `yarn
|
|
2116
|
-
<other>`, `bun run <other>`, `npm test`, `npm start`) becomes a **group**
|
|
2117
|
-
over `<other>` — `dependsOn` and no command — so the graph runs and
|
|
2118
|
-
caches the target instead of a package-manager subprocess it cannot
|
|
2119
|
-
see. When `build` is such a group, the task it reaches that runs a
|
|
2120
|
-
command takes `^build` and the cache TODO, since a group has nothing
|
|
2121
|
-
to cache; it and the groups on the way drop the wait on `build` a
|
|
2122
|
-
`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`
|
|
2123
|
-
is Bun's test runner, `bun build` its bundler, `pnpm install` and `yarn
|
|
2124
|
-
add` the managers' verbs, so each stays a command (item 908), and so do
|
|
2125
|
-
the names each manager reserves or hands on: `pnpm docs` and `pnpm
|
|
2126
|
-
version` (npm's), `bun deploy`, `yarn check` (D-32); `pnpm test`, `yarn
|
|
2127
|
-
test` and `bun lint` do run the script. Arguments, flags or a `&&`
|
|
2128
|
-
chain make it a real command again and
|
|
2129
|
-
it is left verbatim, and so is one whose target becomes no task (a
|
|
2130
|
-
lifecycle script, or a hook folded into another script): a group over
|
|
2131
|
-
it would name a task nothing defines (D-12). A verbatim chain whose
|
|
2132
|
-
parts run this package's own scripts (`check: pnpm run build && pnpm run
|
|
2133
|
-
lint`) gets a TODO naming those parts: each ran again inside the command,
|
|
2134
|
-
beside its own task, so `vx run check` built twice (M-41). So does a
|
|
2135
|
-
part that runs them through `run-s`, `run-p` or `npm-run-all`
|
|
2136
|
-
(`build: run-p build:*`), unless the task is persistent (M-48). Its
|
|
2137
|
-
order may matter, so it is not made a group.
|
|
2138
|
-
|
|
2139
|
-
The report lists each TODO once per reason: tasks that share one are
|
|
2140
|
-
named together (the first five, then a count; the files carry each),
|
|
2141
|
-
and when no task caches it says uncommenting a TODO task's cache line
|
|
2142
|
-
makes the second run a hit. Its `next:` line is a command the user can type: the
|
|
2143
|
-
runner that started vx (`npx`, `pnpm`, `yarn`, `bunx`, read from
|
|
2144
|
-
`npm_config_user_agent`) with the installed `vx` bin, else the
|
|
2145
|
-
`@vzn/vx` package (`npx` under Yarn 1, which has no `dlx`); with no
|
|
2146
|
-
runner, a bare `vx`. Outside a git work tree it starts with
|
|
2147
|
-
`git init`, and with no script mapped with declaring a task, since the
|
|
2148
|
-
run would refuse without either. It runs `build`, else the first task that
|
|
2149
|
-
neither serves (`persistent`) nor changes the repo (`clean`, `release`,
|
|
2150
|
-
`publish`, `deploy`, `version`, `format`, `fix`, …), else the first task
|
|
2151
|
-
(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`).
|
|
2152
2124
|
|
|
2153
2125
|
`vx init --plugin <seam>` writes a plugin instead: `plugins/<seam>.ts`,
|
|
2154
2126
|
a small runnable plugin for that seam (`executor`, `cache`,
|
|
2155
2127
|
`telemetry`, `schedule`, `admit`, `commands`, `project`, `graph`,
|
|
2156
2128
|
`key`), and `plugins/<seam>.test.ts`, which drives it through `run()`
|
|
2157
2129
|
(`bun test`, with `@vzn/vx` installed). It prints the line that
|
|
2158
|
-
declares it in the workspace file a run reads (
|
|
2130
|
+
declares it in the workspace file a run reads (`.vx/workspace.ts` when
|
|
2159
2131
|
there is none). The templates are TypeScript, so `--mjs` with
|
|
2160
2132
|
`--plugin` is refused. An existing file is refused without
|
|
2161
2133
|
`--force`; `--dry` names the files and writes nothing. The two files are
|
|
@@ -2163,25 +2135,35 @@ there is none). The templates are TypeScript, so `--mjs` with
|
|
|
2163
2135
|
core (`src/cli/plugin-templates.ts`) and held equal by
|
|
2164
2136
|
`tests/plugin-templates.unsafe.test.ts` (H-21).
|
|
2165
2137
|
|
|
2166
|
-
Exit codes: `0` the files written (or, with `--dry`, printed); `1`
|
|
2167
|
-
|
|
2168
|
-
|
|
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.
|
|
2169
2142
|
|
|
2170
|
-
## `vx
|
|
2143
|
+
## `bunx @vzn/vx-migrate`
|
|
2171
2144
|
|
|
2172
|
-
|
|
2173
|
-
|
|
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` —
|
|
2174
2148
|
|
|
2175
2149
|
```
|
|
2176
|
-
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
|
|
2177
2151
|
bunx @vzn/vx-migrate --dry # print the generated files instead of writing
|
|
2178
2152
|
bunx @vzn/vx-migrate --force # overwrite existing vx.config.* / vx-preset.ts
|
|
2179
|
-
bunx @vzn/vx-migrate --from nx # disambiguate when
|
|
2180
|
-
```
|
|
2181
|
-
|
|
2182
|
-
—
|
|
2183
|
-
|
|
2184
|
-
|
|
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
|
|
2185
2167
|
(`applyMigration`, exported from `@vzn/vx`), which renders, guards
|
|
2186
2168
|
against overwriting, writes and reports. The mapping rules live in the
|
|
2187
2169
|
package's README.
|
|
@@ -2196,7 +2178,7 @@ none is dropped in silence. An Nx flag's camelCase spelling (`--nxBail`,
|
|
|
2196
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
|
|
2197
2179
|
`vx run` that does the same, and the other verbs a hand types from
|
|
2198
2180
|
either tool name what does it here: `graph` (`--graph`), `ls`
|
|
2199
|
-
(`vx show`), `query` (`--dry=json`), `reset` (`vx cache
|
|
2181
|
+
(`vx show`), `query` (`--dry=json`), `reset` (`vx cache clean`), and
|
|
2200
2182
|
`daemon`, `login`, `logout`, `link` and `unlink`, which vx has no use
|
|
2201
2183
|
for. Nx's `project:target` (`vx run web:build`,
|
|
2202
2184
|
when `web` declares `build`) is answered with `vx run web#build`: from
|
|
@@ -2222,8 +2204,8 @@ copy to the source.
|
|
|
2222
2204
|
| turbo | `--summarize[=<bool>]` | same | `--summarize[=<path>]` |
|
|
2223
2205
|
| turbo | `--output-logs=new-only` | refuse | use `--output-logs=full` (a hit replays its log) or `errors-only` |
|
|
2224
2206
|
| turbo | `--output-logs <v>` | same | `--output-logs full\|errors-only\|hash-only\|none` |
|
|
2225
|
-
| turbo | `--no-cache` | same | `--no-cache` reads nothing either; Turbo's (reads, no writes) is `--cache local:r,remote:r`
|
|
2226
|
-
| 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`) |
|
|
2227
2209
|
| turbo | `--cache-dir <v>` | same | `--cache-dir <path>` |
|
|
2228
2210
|
| turbo | `--profile` | same | `--profile[=<path>]` (Chrome trace) |
|
|
2229
2211
|
| turbo | `--only` | alias | `--exclude-dependencies` |
|
|
@@ -2234,13 +2216,13 @@ copy to the source.
|
|
|
2234
2216
|
| turbo | `--no-update-notifier` | refuse | vx prints no update notice: drop it |
|
|
2235
2217
|
| turbo | `--skip-infer` | refuse | vx runs the binary it is: drop it |
|
|
2236
2218
|
| turbo | `--root-turbo-json <v>` | refuse | `turbo()` reads the `turbo.json` at the workspace root: move it there |
|
|
2237
|
-
| 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 |
|
|
2238
2220
|
| nx | `--parallel <n>` | alias | `--concurrency <n>` (`--parallel=false` is 1) |
|
|
2239
2221
|
| turbo | `--parallel` | refuse | vx always honours `dependsOn`; `--concurrency <n>` sets how many run at once |
|
|
2240
2222
|
| turbo | `--scope <v>` | refuse | use `--filter <pkg>` |
|
|
2241
2223
|
| turbo | `--since <v>` | refuse | use `--filter '[<ref>]'` or `--affected=<ref>` |
|
|
2242
|
-
| turbo | `--remote-only[=<bool>]` | refuse | use `--cache local:,remote:rw`
|
|
2243
|
-
| 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` |
|
|
2244
2226
|
| turbo | `--anon-profile` | refuse | use `--profile[=<path>]`; vx has no redacting variant, so read it before sharing it |
|
|
2245
2227
|
| turbo | `--cache-workers <v>` | refuse | vx sizes its own cache I/O: drop it |
|
|
2246
2228
|
| turbo | `--cwd <v>` | refuse | run vx from that directory: `cd <dir> && vx run …` |
|
|
@@ -2253,7 +2235,7 @@ copy to the source.
|
|
|
2253
2235
|
| turbo | `--preflight` | refuse | set `turboCache({ preflight: true })` or `TURBO_PREFLIGHT=1` |
|
|
2254
2236
|
| turbo | `--remote-cache-timeout <v>` | refuse | set `turboCache({ timeoutMs })` or `TURBO_REMOTE_CACHE_TIMEOUT` |
|
|
2255
2237
|
| turbo | `--single-package` | refuse | a repo with no workspaces is one project already: drop it |
|
|
2256
|
-
| 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 |
|
|
2257
2239
|
| turbo | `--no-daemon`, `--daemon` | refuse | vx has no daemon: drop it |
|
|
2258
2240
|
| turbo | `--ui <v>`, `--log-order <v>`, `--log-prefix <v>` | refuse | vx frames each task’s output: `--output-logs <mode>` sets how much |
|
|
2259
2241
|
| nx | `-t <v>`, `--targets <v>`, `--target <v>` | alias | the task names, positional: `vx run build test` |
|
|
@@ -2270,13 +2252,13 @@ copy to the source.
|
|
|
2270
2252
|
| nx | `--uncommitted`, `--untracked` | refuse | use `--affected=HEAD` (the working tree against the last commit) |
|
|
2271
2253
|
| nx | `--max-parallel <v>` | alias | `--concurrency <n>` |
|
|
2272
2254
|
| nx | `--exclude-task-dependencies[=<bool>]` | alias | `--exclude-dependencies` |
|
|
2273
|
-
| nx | `--skip-remote-cache[=<bool>]` | alias | `--cache local:rw,remote:`
|
|
2255
|
+
| nx | `--skip-remote-cache[=<bool>]` | alias | `--cache-policy local:rw,remote:` |
|
|
2274
2256
|
| nx | `--verbose` | refuse | use `--verbosity <n>` (1 adds the summary table) |
|
|
2275
2257
|
| nx | `--files <v>` | refuse | vx asks git what changed: `--affected=<base>` |
|
|
2276
2258
|
| nx | `--batch` | refuse | vx runs one command per task: drop it |
|
|
2277
2259
|
| nx | `--dte`, `--use-agents` | refuse | vx distributes nothing: drop it |
|
|
2278
2260
|
| nx | `--nx-ignore-cycles` | refuse | vx refuses a task cycle by name: break it |
|
|
2279
|
-
| 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 |
|
|
2280
2262
|
| nx | `--skip-sync` | refuse | vx never runs sync generators: drop it |
|
|
2281
2263
|
| nx | `--tui`, `--no-tui`, `--tui-auto-exit` | refuse | vx frames each task’s output: `--output-logs <mode>` sets how much |
|
|
2282
2264
|
| nx | `--no-cloud` | refuse | vx has no cloud: drop it |
|
|
@@ -2290,8 +2272,9 @@ copy to the source.
|
|
|
2290
2272
|
|
|
2291
2273
|
## Machine-readable output
|
|
2292
2274
|
|
|
2293
|
-
`vx show`, `vx info`, `vx why`, `vx last` and `vx cache
|
|
2294
|
-
`--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
|
|
2295
2278
|
checked-in JSON Schema (draft 2020-12) shipped with the package:
|
|
2296
2279
|
`schemas/show.json`, `schemas/info.json`, `schemas/why.json`,
|
|
2297
2280
|
`schemas/last.json`, `schemas/cache.json`
|
|
@@ -2323,15 +2306,15 @@ prose on stderr do not change. Branch on the code; the message may change.
|
|
|
2323
2306
|
|
|
2324
2307
|
#### `VX_E_USAGE`
|
|
2325
2308
|
|
|
2326
|
-
A flag, argument or
|
|
2327
|
-
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;
|
|
2328
2311
|
`vx <verb> --help` lists every flag.
|
|
2329
2312
|
|
|
2330
2313
|
#### `VX_E_UNKNOWN_COMMAND`
|
|
2331
2314
|
|
|
2332
2315
|
No verb of that name here, core's or a declared plugin's. `vx --help`
|
|
2333
2316
|
lists core's verbs. A plugin's verb exists once its plugin is in
|
|
2334
|
-
|
|
2317
|
+
`.vx/workspace.ts` `plugins`; a task runs as `vx run <task>`.
|
|
2335
2318
|
|
|
2336
2319
|
#### `VX_E_UNKNOWN_TASK`
|
|
2337
2320
|
|
|
@@ -2357,20 +2340,24 @@ the task once (`vx run <task>`); history is this checkout's own.
|
|
|
2357
2340
|
|
|
2358
2341
|
#### `VX_E_CONFIG`
|
|
2359
2342
|
|
|
2360
|
-
A `vx.config.*` or
|
|
2343
|
+
A `vx.config.*` or `.vx/workspace.*` that does not load or is refused. The
|
|
2361
2344
|
message names the file and the field; fix that field
|
|
2362
2345
|
(`vx docs <field>` prints its reference). `vx info --format json` lists
|
|
2363
|
-
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.
|
|
2364
2348
|
|
|
2365
2349
|
#### `VX_E_CYCLE`
|
|
2366
2350
|
|
|
2367
2351
|
The task graph has a cycle. The message names the loop; drop one of its
|
|
2368
2352
|
`dependsOn` edges.
|
|
2353
|
+
`vx lock` refuses it, not a run: a run trusts the graph the lock holds.
|
|
2369
2354
|
|
|
2370
2355
|
#### `VX_E_NO_WORKSPACE`
|
|
2371
2356
|
|
|
2372
|
-
No workspace
|
|
2373
|
-
|
|
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.
|
|
2374
2361
|
|
|
2375
2362
|
#### `VX_E_CACHE_VERSION`
|
|
2376
2363
|
|
|
@@ -2387,7 +2374,7 @@ message is that plugin's or check's: do what it says.
|
|
|
2387
2374
|
#### `VX_E_FS`
|
|
2388
2375
|
|
|
2389
2376
|
The file system refused a write: permissions, a full disk. The message
|
|
2390
|
-
names the path. Free space (`vx cache
|
|
2377
|
+
names the path. Free space (`vx cache clean --max-size <size>` trims the
|
|
2391
2378
|
cache) or grant the write, then re-run.
|
|
2392
2379
|
|
|
2393
2380
|
#### `VX_E_FDS`
|
|
@@ -2416,12 +2403,22 @@ A task's `dependsOn` names a task or project that is not declared, or uses
|
|
|
2416
2403
|
a form vx does not take (a bare wildcard, a negation, a pattern in
|
|
2417
2404
|
`pkg#task`). The message names the task and the entry: declare the
|
|
2418
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.
|
|
2419
2415
|
|
|
2420
2416
|
#### `VX_E_OUTPUT_OVERLAP`
|
|
2421
2417
|
|
|
2422
2418
|
Two tasks declare overlapping outputs with no `dependsOn` edge between
|
|
2423
2419
|
them, so whichever runs second deletes the other's files. Give each task
|
|
2424
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.
|
|
2425
2422
|
|
|
2426
2423
|
#### `VX_E_INPUT_IS_OUTPUT`
|
|
2427
2424
|
|
|
@@ -2430,6 +2427,13 @@ a run, so it would delete its sources) or another task's (its key must
|
|
|
2430
2427
|
not read them; `dependsOn` already cascades). Exclude the outputs from
|
|
2431
2428
|
the inputs with a `!` glob, as the message shows, or declare no outputs
|
|
2432
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.
|
|
2433
2437
|
|
|
2434
2438
|
#### `VX_E_INTERNAL`
|
|
2435
2439
|
|
|
@@ -2449,14 +2453,13 @@ product verb to it under a notice.
|
|
|
2449
2453
|
|
|
2450
2454
|
## `vx show`
|
|
2451
2455
|
|
|
2452
|
-
Introspect the workspace's **
|
|
2453
|
-
|
|
2454
|
-
plugin `config` and `project` stages
|
|
2455
|
-
gives tasks to (`turbo()` or `nx()` from
|
|
2456
|
-
temporary start toward native config) shows them
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
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\``).
|
|
2460
2463
|
|
|
2461
2464
|
```
|
|
2462
2465
|
vx show # list every project
|
|
@@ -2551,7 +2554,7 @@ share `TMPDIR`: a `nix develop` shell sets its own.
|
|
|
2551
2554
|
Every verb: a path vx must write that this user cannot (`EACCES`,
|
|
2552
2555
|
`EPERM`, `EROFS` — a read-only checkout, another user's files) or that
|
|
2553
2556
|
the disk has no room for (`ENOSPC`, `EDQUOT`) exits 1 with one line
|
|
2554
|
-
naming the path, `vx: EACCES: permission denied, open '
|
|
2557
|
+
naming the path, `vx: EACCES: permission denied, open '…/.vx/projects.lock.json'
|
|
2555
2558
|
— a path vx must write is not writable by this user` (or `— the disk
|
|
2556
2559
|
that path is on is full`), never a stack. Inside a run the task's line
|
|
2557
2560
|
says the same. A process out of file descriptors (`EMFILE`, `ENFILE`;
|
|
@@ -2562,11 +2565,10 @@ is out of file descriptors; raise the limit (ulimit -n 4096) and re-run`.
|
|
|
2562
2565
|
|
|
2563
2566
|
Workspace doctor — one screen of facts for bug reports and sanity
|
|
2564
2567
|
checks. The task count and the sandbox row's declared count come from
|
|
2565
|
-
the same load a run uses, plugin stages included
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
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:
|
|
2570
2572
|
|
|
2571
2573
|
```
|
|
2572
2574
|
$ vx info
|
|
@@ -2579,21 +2581,18 @@ projects: 12 (34 tasks)
|
|
|
2579
2581
|
plugins: 2 — @vzn/vx-reapi (executor, cache); @vzn/vx-otel (telemetry)
|
|
2580
2582
|
workers: 2 — cgroup CPU quota 2 of 8 cores
|
|
2581
2583
|
memory: 13 GB usable — cgroup limit; the machine has 16 GB
|
|
2582
|
-
cache dir: /work/repo/.vx/cache
|
|
2583
|
-
cache store: /home/me/.vx/3f2a9c1e7b4d5a60/cache
|
|
2584
|
-
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
|
|
2585
2587
|
cache entries: 42 (1.3 GB)
|
|
2586
|
-
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
|
|
2587
2589
|
task runs (24h): 7 (5 cache hits)
|
|
2588
2590
|
flaky tasks: 1 — web#test (3 of 11 runs failed on unchanged inputs)
|
|
2589
2591
|
sandbox: available (9 tasks declare exec.sandbox)
|
|
2590
|
-
|
|
2592
|
+
projects lock: yes
|
|
2591
2593
|
```
|
|
2592
2594
|
|
|
2593
|
-
- `git` shows `(not found)` when the binary is missing
|
|
2594
|
-
project config contributes zero tasks instead of failing the
|
|
2595
|
-
printout, and the `config errors` row (present only then) names it
|
|
2596
|
-
with the loader's message — the same line `vx run` would stop on.
|
|
2595
|
+
- `git` shows `(not found)` when the binary is missing.
|
|
2597
2596
|
- `git status cache`: vx runs ONE `git status` per run to find dirty
|
|
2598
2597
|
and untracked files, and on a large tree that walk is the warm run's
|
|
2599
2598
|
critical path. git's `core.fsmonitor` (a daemon that watches the
|
|
@@ -2613,13 +2612,13 @@ exec.sandbox and will fail` says it first: root inside a container
|
|
|
2613
2612
|
unreported`: the sandbox still enforces, but a task that tolerates a
|
|
2614
2613
|
denied read passes and caches with no word of it. The `--format json` fact is
|
|
2615
2614
|
`sandbox.untraced`, the reason or `null`.
|
|
2616
|
-
- `plugins` names every plugin
|
|
2615
|
+
- `plugins` names every plugin `.vx/workspace.*` declares and the seams
|
|
2617
2616
|
each fills, in pipeline order (`config`, `discover`, `project`, `graph`, `key`,
|
|
2618
2617
|
`fingerprint`, `schedule`, `admit`, `executor`, `cache`, `telemetry`,
|
|
2619
|
-
`setup`, `commands`), or `none`. It reads the declarations: a plugin
|
|
2618
|
+
`output`, `setup`, `commands`), or `none`. It reads the declarations: a plugin
|
|
2620
2619
|
that declines a task at run time still lists its seam here.
|
|
2621
2620
|
- `workers` is the count a run defaults to and where it comes from:
|
|
2622
|
-
|
|
2621
|
+
`.vx/workspace.ts`'s `run.concurrency` when set, else the cores this
|
|
2623
2622
|
process may use — the CPU count, capped by the cgroup CPU quota a
|
|
2624
2623
|
container runs under (`8 — the CPU count`, `2 — cgroup CPU quota 2 of
|
|
2625
2624
|
8 cores`). `memory` is what a memory-packing policy budgets
|
|
@@ -2640,15 +2639,22 @@ unreported`: the sandbox still enforces, but a task that tolerates a
|
|
|
2640
2639
|
the hits are a share of it: three `vx run` of two tasks are six. An
|
|
2641
2640
|
invocation is what `vx last` calls a run; `vx last --list` counts
|
|
2642
2641
|
those.
|
|
2643
|
-
- `cache
|
|
2644
|
-
store
|
|
2645
|
-
|
|
2646
|
-
|
|
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.
|
|
2647
2648
|
- `cache entries` counts the entries and their size; with a
|
|
2648
2649
|
`cacheRetention.maxSize` it reads against it (`812 (3.0 GB of 10 GB,
|
|
2649
2650
|
30%)`), and a `cache retention` row says what each run's end evicts.
|
|
2650
2651
|
Paths under the home directory print as `~/…`; the JSON keeps them
|
|
2651
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).
|
|
2652
2658
|
- `cache versions` are the two constants a bug report needs and the
|
|
2653
2659
|
reset notice names: the key prefix (`CACHE_VERSION`; a bump orphans
|
|
2654
2660
|
every entry) and the index schema (`SCHEMA_VERSION`; a mismatch drops
|
|
@@ -2657,19 +2663,23 @@ unreported`: the sandbox still enforces, but a task that tolerates a
|
|
|
2657
2663
|
save temps the index has no row for, older than an hour (what a
|
|
2658
2664
|
`SCHEMA_VERSION` reset leaves behind; a fresh one is a save in
|
|
2659
2665
|
flight). A task that asks for one's key again indexes it and hits;
|
|
2660
|
-
the rest only `vx cache
|
|
2666
|
+
the rest only `vx cache clean` reclaims, so the doctor says so.
|
|
2661
2667
|
- `--format json` prints the same facts as one typed object, for a
|
|
2662
|
-
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
|
|
2663
2671
|
below Bun 1.4.0), `git` (null when not
|
|
2664
2672
|
found), `gitStatusCache` (`{ fsmonitor, untrackedCache }`, null when
|
|
2665
2673
|
git could not answer), `workspaceRoot`, `projects`, `tasks`,
|
|
2666
|
-
`
|
|
2667
|
-
empty when all did), `plugins` (`[{ name, seams }]`), `workers` (`{ count, source, cores,
|
|
2674
|
+
`plugins` (`[{ name, seams }]`), `workers` (`{ count, source, cores,
|
|
2668
2675
|
cpuQuota }`, the source one of `workspace` / `cgroup` / `cores`,
|
|
2669
2676
|
`cpuQuota` in cores or null), `memory` (`{ usableBytes, totalBytes,
|
|
2670
|
-
cgroupLimitBytes }`, the limit null when none binds), `cacheDir`, `
|
|
2671
|
-
(
|
|
2672
|
-
|
|
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`
|
|
2673
2683
|
(`{ olderThan, maxSize, maxBytes }`, each only when set, null when none is declared),
|
|
2674
2684
|
`orphans` (`{ artifacts, bytes }`, always present), `runs24h`, `hits24h` (task
|
|
2675
2685
|
runs, as the row), `restored24h` (of those hits, the ones that restored
|
|
@@ -2680,10 +2690,10 @@ declared, untraced }`, `declared` the count of tasks with `exec.sandbox`,
|
|
|
2680
2690
|
pretty rows render this object;
|
|
2681
2691
|
there is no second source.
|
|
2682
2692
|
- `--cache-dir <path>` reads the cache a run with the same flag uses, as
|
|
2683
|
-
`vx why`, `vx last` and `vx cache
|
|
2693
|
+
`vx why`, `vx last` and `vx cache clean` do.
|
|
2684
2694
|
|
|
2685
|
-
Exit codes: `0` the facts printed,
|
|
2686
|
-
|
|
2695
|
+
Exit codes: `0` the facts printed, with no lock included; `1` no
|
|
2696
|
+
workspace here or above, or a parse error.
|
|
2687
2697
|
|
|
2688
2698
|
## `vx why`
|
|
2689
2699
|
|
|
@@ -2721,7 +2731,7 @@ cache (nothing re-ran), it re-executed on the same key, or it recorded
|
|
|
2721
2731
|
no cache outcome at all, in which case vx says so instead of guessing.
|
|
2722
2732
|
A re-execution names its cause when the index shows one: the previous
|
|
2723
2733
|
run on the key failed and saved nothing, the run did not read the cache
|
|
2724
|
-
(`--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
|
|
2725
2735
|
there when it ran (pruned or evicted), the previous run was not saved
|
|
2726
2736
|
because it ran beside a failed task (a task run past a failed dependency
|
|
2727
2737
|
under `--continue` is never cached; said ahead of a prune, and as
|
|
@@ -2767,6 +2777,18 @@ which names the `vx why` to run next; a `config` line names the
|
|
|
2767
2777
|
`vx show` that prints the task's config now. `--format json` carries
|
|
2768
2778
|
the walk as `roots` (`chain`, `entries`).
|
|
2769
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
|
+
|
|
2770
2792
|
A hit's line is `cache-hit · key …` (or `cache-hit-remote`): the status
|
|
2771
2793
|
names the hit and its tier, so only an executed run carries the word.
|
|
2772
2794
|
A skipped task never ran and derived no key: its line is `skipped · no key`.
|
|
@@ -2774,11 +2796,10 @@ A row's kind is what the key folded: `file` (an input file, by blob
|
|
|
2774
2796
|
id), `env` (a declared variable, by digest), `runtime` and
|
|
2775
2797
|
`ws-runtime` (a declared command's output, `inputs.runtime` and
|
|
2776
2798
|
`inputs.workspaceRuntime`), `forward` (the argv forwarded after `--`),
|
|
2777
|
-
`
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
lockfile
|
|
2781
|
-
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
|
|
2782
2803
|
`format` (the key format vx itself folds: it moves only with an
|
|
2783
2804
|
upgrade, and re-keys every task once, X-142).
|
|
2784
2805
|
The component-level rows come from the `entry_inputs` input
|
|
@@ -2791,39 +2812,13 @@ task with no `cache` block derives a key too — it is what dependents
|
|
|
2791
2812
|
fold — but saves no entry, so for it the verb can only report the key
|
|
2792
2813
|
change and says so.
|
|
2793
2814
|
`--format json` emits one machine-readable object (`{ taskId, runId,
|
|
2794
|
-
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,
|
|
2795
2816
|
why, diff, explanation }`, `why` and `diff` null and `explanation` the
|
|
2796
2817
|
latest cache entry's key.
|
|
2797
2818
|
|
|
2798
2819
|
Exit codes: `0` the task's run explained; `1` no recorded run of the
|
|
2799
2820
|
task, a `--run` with no row for it, or a parse error.
|
|
2800
2821
|
|
|
2801
|
-
## `vx prune`
|
|
2802
|
-
|
|
2803
|
-
```
|
|
2804
|
-
vx prune <project...> [--out-dir <dir>] [--docker] [--production]
|
|
2805
|
-
```
|
|
2806
|
-
|
|
2807
|
-
A plugin verb of `@vzn/vx-lockfile`: declaring `pnpm()`, `bun()`,
|
|
2808
|
-
`npm()` or `yarn()` adds it. It copies the named projects, their
|
|
2809
|
-
transitive workspace dependencies, the root manifests (workspace lists
|
|
2810
|
-
rewritten to the subset), the vx configs and each lockfile pruned to what
|
|
2811
|
-
the subset installs into `--out-dir` (default `out/`), so a Docker build
|
|
2812
|
-
installs it with a frozen lockfile; `--docker` splits `json/` (the
|
|
2813
|
-
install layer) from `full/`; `--production` leaves out a workspace
|
|
2814
|
-
package only a dev dependency reaches and strikes it from the kept
|
|
2815
|
-
manifests and lockfiles. `turbo prune`. Reference:
|
|
2816
|
-
`packages/vx-lockfile/README.md` § `vx prune`. Without the plugin, typing
|
|
2817
|
-
`vx prune` says where it comes from and exits 1. History: core verb
|
|
2818
|
-
2026-08-25, `@vzn/vx-prune` 2026-09-10, removed 2026-09-11 (it copied the
|
|
2819
|
-
lockfile unpruned), back in `@vzn/vx-lockfile` 2026-10-03.
|
|
2820
|
-
|
|
2821
|
-
## `vx stats`
|
|
2822
|
-
|
|
2823
|
-
Removed (H-19). It was an alias of `vx info`, which absorbed it, and
|
|
2824
|
-
warned on every use. Typing `vx stats` points at `vx info` and exits 1 —
|
|
2825
|
-
unless a declared plugin claims the verb through the `commands` seam.
|
|
2826
|
-
|
|
2827
2822
|
## `vx last`
|
|
2828
2823
|
|
|
2829
2824
|
Replay a recorded run's summary from the local history — no
|
|
@@ -2833,7 +2828,7 @@ surface. A run a Ctrl-C (or SIGTERM, SIGHUP) stopped records nothing,
|
|
|
2833
2828
|
so `vx last` still shows the run before it.
|
|
2834
2829
|
|
|
2835
2830
|
```
|
|
2836
|
-
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>]
|
|
2837
2832
|
```
|
|
2838
2833
|
|
|
2839
2834
|
Bare `vx last` replays the most recent run: a header (verdict, command,
|
|
@@ -2875,7 +2870,12 @@ passed, so a replayed failure that is already fixed says so. A
|
|
|
2875
2870
|
sandboxed task's row adds `sandboxFix`, the `exec.sandbox.allow`
|
|
2876
2871
|
entries (`read`, `write`, `network`, `systemInfo`) that would have
|
|
2877
2872
|
granted its denials, the same grants the terminal prints as `fix:`;
|
|
2878
|
-
`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
|
|
2879
2879
|
newest 50 failed runs; past those, or when the write was refused, a
|
|
2880
2880
|
failed row reads `output: ''` and `locations: []`. `--log <task>`
|
|
2881
2881
|
prints one task's output instead of the summary: in the latest run that
|
|
@@ -2886,7 +2886,23 @@ or in the run that saved it. An uncached task that passed, or a skip,
|
|
|
2886
2886
|
kept none: stdout is empty and stderr says so, exit 0. With
|
|
2887
2887
|
`--format json` it is `{ runId, taskId, status, source, output }`,
|
|
2888
2888
|
`source` `'failure'`, `'cache'` or `null`. A task no run recorded is
|
|
2889
|
-
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`
|
|
2890
2906
|
an array of the same `invocation` objects, newest first. An unknown run id fails
|
|
2891
2907
|
loud and points at `--list`. Before any run, every form says so
|
|
2892
2908
|
(`vx last: no recorded runs yet — run something first`, exit 1; `--list`
|
|
@@ -2900,14 +2916,14 @@ line of its own without them, X-140). The
|
|
|
2900
2916
|
replayed `$ vx run …` or `$ vx watch …` line and that one keep each
|
|
2901
2917
|
argument shell-quoted (`-- 'a b'`), so either pastes back as it ran (X-49).
|
|
2902
2918
|
|
|
2903
|
-
`vx why`, `vx last`, `vx info` and `vx cache
|
|
2919
|
+
`vx why`, `vx last`, `vx info` and `vx cache clean` all read the cache
|
|
2904
2920
|
a run wrote, so each takes `--cache-dir <path>` with `vx run`'s rules
|
|
2905
2921
|
(cwd-relative, absolute used as-is): a run that wrote its history
|
|
2906
2922
|
elsewhere is replayed, explained, reported on and pruned there. Without
|
|
2907
|
-
the flag they open the workspace's cache (`defineWorkspace({
|
|
2923
|
+
the flag they open the workspace's cache (`defineWorkspace({ cache: { dir } })`
|
|
2908
2924
|
or `.vx/cache`). A `--cache-dir` that is not there is refused by name
|
|
2909
2925
|
(`--cache-dir .vx/cahce: no such directory`). In a workspace that never
|
|
2910
|
-
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`
|
|
2911
2927
|
(dry or not) read it as empty and create nothing (item 900, E-7, E-38).
|
|
2912
2928
|
|
|
2913
2929
|
Exit codes: `0` the run (or the list) printed, `--list` with no runs
|
|
@@ -2949,10 +2965,10 @@ of each verb, read from the same help text `vx <verb> --help` prints:
|
|
|
2949
2965
|
the verb's own Usage line, and for `run` and `watch` the run option
|
|
2950
2966
|
lines, less the ones `watch` refuses. So a flag cannot be documented
|
|
2951
2967
|
and not completed, nor completed and then refused: a flag another
|
|
2952
|
-
verb's line names in passing (`
|
|
2968
|
+
verb's line names in passing (`--check` in a line about `vx lock`) is
|
|
2953
2969
|
not one. After a flag with a fixed value set the script offers that
|
|
2954
2970
|
set: `--format` pretty or json, `--output-logs` full, errors-only,
|
|
2955
|
-
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
|
|
2956
2972
|
the flag's parser takes (`tests/completions.test.ts` holds it). A plugin verb completes `--help` only. Task and project
|
|
2957
2973
|
names are not completed (they are the workspace's, and a completion
|
|
2958
2974
|
that evaluates configs on every Tab is the wrong price). The zsh script
|
|
@@ -2967,7 +2983,7 @@ Exit codes: `0` the script printed; `1` an unknown shell or flag.
|
|
|
2967
2983
|
|
|
2968
2984
|
## Plugin commands
|
|
2969
2985
|
|
|
2970
|
-
A plugin declared in
|
|
2986
|
+
A plugin declared in `.vx/workspace.ts` can add verbs:
|
|
2971
2987
|
|
|
2972
2988
|
```ts
|
|
2973
2989
|
import { definePlugin, type VxPlugin } from '@vzn/vx'
|
|
@@ -2990,18 +3006,15 @@ export function mcp(): VxPlugin {
|
|
|
2990
3006
|
The plugin's name is its package name, which `definePlugin` reads from
|
|
2991
3007
|
`import.meta`; a plain object is refused when the workspace loads.
|
|
2992
3008
|
|
|
2993
|
-
(`@vzn/vx-
|
|
2994
|
-
|
|
2995
|
-
over the resolved task catalog, one the workspace doctor's facts.
|
|
2996
|
-
`@vzn/vx-schedule-history` adds `vx history`: what it learned per task
|
|
2997
|
-
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
|
|
2998
3011
|
first and consults plugins only for a word core does not know, loading
|
|
2999
3012
|
the workspace config from the cwd to find them (outside a workspace the
|
|
3000
3013
|
verb is simply unknown). A plugin verb that names a core verb, or one
|
|
3001
3014
|
plugins of two packages both declare, is refused when the workspace
|
|
3002
3015
|
loads — such a verb could never run, or would hide the other plugin's.
|
|
3003
|
-
Plugins of one package are one owner (
|
|
3004
|
-
|
|
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
|
|
3005
3018
|
return value is the exit code, an integer 0–255 (anything else — nothing,
|
|
3006
3019
|
a fraction, 256, which the OS would keep as 0 — fails naming the plugin
|
|
3007
3020
|
and the verb), and a thrown `UserError` prints as
|
|
@@ -3013,33 +3026,62 @@ An unknown verb is answered with what this workspace knows: the "did you
|
|
|
3013
3026
|
mean" set is core's verbs AND the ones its plugins declare (the lookup
|
|
3014
3027
|
that just failed already loaded them, so it costs nothing), and when
|
|
3015
3028
|
nothing is close enough to guess, a second line says where a verb can
|
|
3016
|
-
come from — the verbs declared here, or that
|
|
3029
|
+
come from — the verbs declared here, or that `.vx/workspace.ts` is what
|
|
3017
3030
|
would declare one, or that there is no workspace here at all. Core still
|
|
3018
3031
|
names no package: it lists what the workspace itself declares.
|
|
3019
3032
|
|
|
3020
3033
|
## Environment variables vx reads
|
|
3021
3034
|
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
is
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
|
3034
|
-
|
|
|
3035
|
-
| `
|
|
3036
|
-
| `
|
|
3037
|
-
| `
|
|
3038
|
-
| `
|
|
3039
|
-
| `
|
|
3040
|
-
| `
|
|
3041
|
-
| `
|
|
3042
|
-
| `
|
|
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`).
|
|
3043
3085
|
|
|
3044
3086
|
Colors are the two conventions in § Output format › Colors (`NO_COLOR`,
|
|
3045
3087
|
`FORCE_COLOR`). A truthy `CI` picks the CI output flow and
|
|
@@ -3109,7 +3151,9 @@ marked with a cyan `▸` after `┌─`/`└─`, and its close reads `running`
|
|
|
3109
3151
|
died on its own before the run stopped it.
|
|
3110
3152
|
|
|
3111
3153
|
There is **no top-of-run banner** — the run context lives in the
|
|
3112
|
-
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:
|
|
3113
3157
|
|
|
3114
3158
|
```
|
|
3115
3159
|
⇢ 4ms success local @vzn/vx#format-check
|
|
@@ -3117,30 +3161,29 @@ footer. A broad run looks like:
|
|
|
3117
3161
|
|
|
3118
3162
|
─ vx 0.0.0 ───────────────────────────────────────────────────
|
|
3119
3163
|
projects ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱▱
|
|
3120
|
-
1
|
|
3164
|
+
1 affected · 3 total
|
|
3121
3165
|
tasks ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
|
|
3122
3166
|
2 success · 2 total
|
|
3123
3167
|
cache ▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰
|
|
3124
3168
|
1 miss · 1 local
|
|
3125
3169
|
|
|
3126
3170
|
info 8 workers · local cache
|
|
3127
|
-
time 5.34s ·
|
|
3128
|
-
result 2 tasks · 1 cached (50%) · 900ms saved · 5.34s
|
|
3171
|
+
time 5.34s · 900ms saved
|
|
3129
3172
|
```
|
|
3130
3173
|
|
|
3131
3174
|
The three bars are meters: `projects` is what the run covered against
|
|
3132
3175
|
the workspace, `tasks` is failed / success / skipped, `cache` is miss /
|
|
3133
3176
|
no-cache / up-to-date / local / remote (a skipped task rides it too, so
|
|
3134
|
-
the two
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
is counted apart (`1
|
|
3143
|
-
`--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
|
|
3144
3187
|
checks it against this page, byte for byte.
|
|
3145
3188
|
|
|
3146
3189
|
Group tasks emit no framed block by design (they aren't real tasks);
|
|
@@ -3151,8 +3194,8 @@ What a run has to say about one task rides that task's row; what it
|
|
|
3151
3194
|
says about the run (a server that died, outputs left remote, a file
|
|
3152
3195
|
`--summarize` / `--profile` wrote, a history, upload, plugin or sandbox
|
|
3153
3196
|
warning) prints above the footer: it prints once the run's closing work
|
|
3154
|
-
is done, the `
|
|
3155
|
-
(`
|
|
3197
|
+
is done, the `VX_DEBUG_TIMING=1` table included. Cache housekeeping
|
|
3198
|
+
(`cache.retention`) prints nothing. Only a kept server's own output
|
|
3156
3199
|
follows it.
|
|
3157
3200
|
|
|
3158
3201
|
**Skipped rows.** A red run lists every task that never started as a
|
|
@@ -3179,9 +3222,9 @@ what it reached first (`not run`). Neither reached the cache:
|
|
|
3179
3222
|
◌ not run web#test
|
|
3180
3223
|
```
|
|
3181
3224
|
|
|
3182
|
-
The tasks legend
|
|
3183
|
-
|
|
3184
|
-
|
|
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
|
|
3185
3228
|
printed while it stopped shows its frame instead of a row.
|
|
3186
3229
|
|
|
3187
3230
|
**Flaky rows.** A task the run just proved nondeterministic says so
|
|
@@ -3195,7 +3238,7 @@ run history alone, no service:
|
|
|
3195
3238
|
|
|
3196
3239
|
A task is flaky when its exact cache key FAILED AFTER IT HAD PASSED
|
|
3197
3240
|
(this run counted; a cache hit is a pass, it replayed one), or when it
|
|
3198
|
-
needed a retry (`exec.retries` / `--
|
|
3241
|
+
needed a retry (`exec.retries` / `--retries`) this run. A failure on a
|
|
3199
3242
|
key that never passed is a break and carries no note — a changed input
|
|
3200
3243
|
that fails is what a red run usually means — and the first pass after
|
|
3201
3244
|
only failures is a recovery (a missing tool installed), not a flake.
|
|
@@ -3241,7 +3284,7 @@ Only a run's own output (`vx run`, `vx watch`) is painted, and the
|
|
|
3241
3284
|
help on a terminal: headings bold, `vx <verb>` and each flag or example
|
|
3242
3285
|
in cyan; `NO_COLOR` or `FORCE_COLOR=0` turns it off
|
|
3243
3286
|
(`tests/help-colors.test.ts`). Every other verb (`show`, `info`, `why`,
|
|
3244
|
-
`last`, a `--dry` plan, `--graph`, `cache
|
|
3287
|
+
`last`, a `--dry` plan, `--graph`, `cache clean`), and the help off a
|
|
3245
3288
|
terminal, prints plain text whatever `FORCE_COLOR` says, so a script
|
|
3246
3289
|
that forces colour for a run's log still parses them
|
|
3247
3290
|
(`tests/cli-colors-e2e.test.ts`).
|
|
@@ -3249,8 +3292,9 @@ that forces colour for a run's log still parses them
|
|
|
3249
3292
|
## Remote cache (plugin-driven)
|
|
3250
3293
|
|
|
3251
3294
|
Core ships **no remote-cache wire client** — the remote cache is a
|
|
3252
|
-
plugin concern. A `cache` plugin
|
|
3253
|
-
|
|
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
|
|
3254
3298
|
(ActionCache + CAS).
|
|
3255
3299
|
|
|
3256
3300
|
Reads try local first, then remote (hydrating local on remote hit),
|
|
@@ -3264,7 +3308,7 @@ implement core's `RemoteCacheLayer` interface in a plugin's `cache`
|
|
|
3264
3308
|
capability — the recipe lives in the plugins guide. Embedders
|
|
3265
3309
|
holding a wire client can inject it per-run via
|
|
3266
3310
|
`RunOptions.remoteCache` (explicit injection wins over the plugin
|
|
3267
|
-
consult).
|
|
3311
|
+
consult).
|
|
3268
3312
|
|
|
3269
3313
|
## Run analytics
|
|
3270
3314
|
|