@vzn/vx 0.0.640 → 0.0.641

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/docs/caching.md CHANGED
@@ -399,12 +399,26 @@ On a hit:
399
399
  clock (4 ms at HZ=250), so a same-size rewrite inside the tick of
400
400
  vx's own write, in place or as a new file that took the same inode,
401
401
  with its mtime forged back, still matches.
402
- 3. Otherwise the task's declared outputs are wiped from the project
403
- dir (`cleanOutputs`) — see
404
- [§ Strict output ownership](#strict-output-ownership) — and the
405
- `outputs/<rel>` (+ `workspace-outputs/<rel>`) entries are extracted
406
- from the artifact into place: its bytes inline in the index, read in
407
- one read transaction with the entry's rows, or `<cacheDir>/<hash>.tar.zst`.
402
+ 3. Otherwise the `outputs/<rel>` (+ `workspace-outputs/<rel>`) entries
403
+ are extracted from the artifact (its bytes inline in the index, read
404
+ in one read transaction with the entry's rows, or
405
+ `<cacheDir>/<hash>.tar.zst`) **in place**: every entry is staged in
406
+ the workspace's `.vx/tmp/` (never an output folder), and only once the
407
+ whole archive has staged is each renamed over the file it replaces. A
408
+ file that already holds the entry's bytes (compared byte for byte, up
409
+ to 4 MiB) is left in place, only its mode and mtime set. A
410
+ target on another filesystem goes through a temp beside it; a file
411
+ takes the group its directory gives new files (setgid on Linux, any
412
+ directory on macOS), ACLs are not copied. The next run removes
413
+ the staging a killed run left. A process reading the output tree during the
414
+ restore never finds a declared output missing or half-written, and a
415
+ restore that fails first (a full disk, a damaged entry) leaves the
416
+ outputs as they were. Then the declared outputs the entry does not
417
+ hold are removed — see
418
+ [§ Strict output ownership](#strict-output-ownership). Only a found
419
+ output that blocks a rename (a file where the entry needs a directory,
420
+ or the reverse) goes before. Turbo also restores in place; Nx deletes
421
+ and copies each file, and a reader meets a missing or short file.
408
422
  4. Captured stdout is replayed to the live terminal via the logger —
409
423
  the framed block looks like a fresh run. (stderr is not cached —
410
424
  only successful runs are cached and their stderr is near-always
@@ -483,10 +497,11 @@ the glob matches (a tracked output, a directory a `!` rule takes back);
483
497
  anything git cannot answer keeps the key late. Undeclared writes, an
484
498
  `outputs.workspaceFiles` producer upstream, a rewriter the key does not
485
499
  fold, and runtime probes after a writer still leave a key preliminary.
486
- The
487
- short-circuit never runs under a `LayeredCache` (remote prefetch owns
488
- those runs — an up-front `get` there would put remote GETs on the
489
- critical path), never fires with local reads off or on a local cache
500
+ With a
501
+ remote declared the short-circuit probes the local store alone, so a
502
+ local hit still restores ahead of its dependencies and no remote GET
503
+ sits on the critical path (remote prefetch asks the remote for the
504
+ rest). It never fires with local reads off or on a local cache
490
505
  that holds no artifact (`Cache.holdsNothing`: every probe would miss,
491
506
  and a cold 1,090-package run started its first task ~90 ms sooner
492
507
  without it), and never throws —
@@ -822,7 +837,7 @@ the run (`tests/runner.test.ts`) — and the save resolves the output
822
837
  globs at once. A descendant the command detached (`setsid … &`, a
823
838
  daemon) that writes after that is not in the artifact: the entry saves
824
839
  what was there, often nothing (with the `cache.outputs matched no
825
- files` line), and the next hit's clean-before-restore removes what the
840
+ files` line), and the next hit's restore removes what the
826
841
  descendant wrote and restores the saved entry in its place (upstream
827
842
  survey, turborepo#12786). vx does not wait for it and cannot see it: a
828
843
  process that left the task's session is outside the process group vx
@@ -849,14 +864,15 @@ Declared `cache.outputs.files` are wiped in two distinct places:
849
864
  - **Before exec on a cache miss.** A leftover `dist/old.js` from a
850
865
  prior build can't survive into a fresh build that doesn't rewrite
851
866
  it.
852
- - **Before restore on a cache hit.** The post-restore tree is the
853
- cached snapshot byte-for-byte. Hand-edits to output files don't
854
- persist through a cache replay. When the hit's own check walked the
855
- output globs to find the tree stale, the clean deletes from that walk's
856
- list instead of walking again: nothing is awaited between them (X-161).
857
-
858
- Both branches use the same `cleanOutputs` helper (`src/cache/inputs.ts`)
859
- with the same boundary rules, and vx's own paths stay off the wipe
867
+ - **After restore on a cache hit.** The post-restore tree is the
868
+ cached snapshot byte-for-byte: the restore renames the entry's files
869
+ over the ones it finds, then removes what the entry does not hold.
870
+ Hand-edits to output files don't persist through a cache replay. When
871
+ the hit's own check walked the output globs to find the tree stale,
872
+ the removal uses that walk's list instead of walking again (X-161).
873
+
874
+ Both use the same helpers (`cleanOutputs`, `cleanOutputPaths` in
875
+ `src/cache/inputs.ts`) with the same boundary rules, and vx's own paths stay off the wipe
860
876
  whatever the glob: `.git`, `.vx` and each `vx.lock.json` (`OUTPUT_NEVER`;
861
877
  `.vx` holds the workspace config and the lock, which a clean deleted and a
862
878
  hit restored stale while `.vx/cache` alone was listed). `node_modules` is
@@ -889,7 +905,7 @@ Skipped when:
889
905
  `.tsbuildinfo`). You opt out of the clean-tree guarantee: the save
890
906
  stores every file the outputs match after the run, so a leftover from
891
907
  an older build is cached and restored everywhere. A retry cleans as
892
- usual; a hit still cleans before its restore.
908
+ usual; a hit's restore still removes what its entry does not hold.
893
909
 
894
910
  A declared output the process cannot remove (a `dist/` another user
895
911
  wrote, a read-only checkout) fails the task with `cannot remove declared
@@ -1333,8 +1349,9 @@ pax `path`/`size`, GNU long names, header checksums, truncation; an
1333
1349
  extended header (read whole) past 1 MiB or a pax `size` that is not a
1334
1350
  whole number is refused as corrupt, since a 1 GiB pax header costs 2 GiB
1335
1351
  of memory from a ~32 KB artifact — and
1336
- every regular entry is written beside its target as a short `.vx-tmp-*` sibling (never a suffix on the target's name, which pushed a legal 242–255-byte name past NAME_MAX) and
1337
- renamed into place only after the whole archive has ended cleanly and
1352
+ every regular entry is written to the workspace's `.vx/tmp/<pid>-0/`
1353
+ (vx's own, ignored by git, never an output folder) and renamed over its
1354
+ target only after the whole archive has ended cleanly and
1338
1355
  the index's recorded outputs are all present. vx itself holds one
1339
1356
  chunk of the tar at a time (measured 2026-09-03, incompressible
1340
1357
  artifacts, fresh process: 150 MiB peaked at +644 MiB through
@@ -1413,7 +1430,14 @@ them, `CacheGetContext.outputs`), and never when it holds one path as a
1413
1430
  file and a directory at once. Either used to reach the tree, an input
1414
1431
  overwritten or a `.git/hooks` file planted under a green
1415
1432
  `cache-hit-remote`, or a restore that failed every later run from the
1416
- local copy (item 942); now the read is a miss and the task runs. On the save side a
1433
+ local copy (item 942); now the read is a miss and the task runs. Every
1434
+ restore asks the same of the local copy, whatever route it reached the
1435
+ store by: an entry outside the declared outputs, one a `!` entry takes
1436
+ back, one under `.git`, `.vx` or a `vx.lock.json`, or one under a
1437
+ `node_modules` no output glob names fails the whole restore before
1438
+ anything is written; the entry is dropped and the task runs. Remote
1439
+ execution (`@vzn/vx-reapi`) writes a remote action's outputs through the
1440
+ same gate (`restorableOutput`). On the save side a
1417
1441
  **symlinked output** is captured as its target's bytes and comes back
1418
1442
  as a regular file (a hit that finds the task's own link still current
1419
1443
  leaves it); a link to a directory, or a dangling one, has no bytes to
package/docs/cli.md CHANGED
@@ -37,7 +37,7 @@ vx show [PROJECT[#TASK] | TASK] [--filter <pattern>] [--affected[=<ref>]] [--for
37
37
  vx info [--format pretty|json] [--cache-dir <path>]
38
38
  vx why (TASK | PKG#TASK) [--run RUNID] [--format pretty|json] [--cache-dir <path>]
39
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
+ vx upgrade [TAG] [-g|--global]
41
41
  vx docs <query> [--limit N] [--format pretty|json]
42
42
  vx completions bash|zsh|fish
43
43
 
@@ -1243,8 +1243,7 @@ run...` precedes it.
1243
1243
  which reads what it likes, item 947, outside any project nested
1244
1244
  in it; a write the last cycle made is not, read off the later of
1245
1245
  its mtime and ctime, so an output a cache hit restores with its
1246
- saved mtime is the run's, and so is the restore's own `.vx-tmp-*`
1247
- file, `watch-restored-output.test.ts`) — or declared an
1246
+ saved mtime is the run's, `watch-restored-output.test.ts`) — or declared an
1248
1247
  output, or the loop re-runs on it; after three such cycles in a row
1249
1248
  watch names the path and the remedy, once, and keeps going. A dev
1250
1249
  server the last cycle left running counts as that cycle for as long
@@ -1305,8 +1304,8 @@ Always ignored (no re-trigger):
1305
1304
  tracked + untracked-not-ignored — so a cycle it started could change
1306
1305
  nothing. A tracked file that matches a pattern is not ignored, by
1307
1306
  git's own rule; outside a repository nothing is.
1308
- - Files ending in `~` (editor swap files), and a restore's own
1309
- `.vx-tmp-*` file.
1307
+ - Files ending in `~` (editor swap files), and `.vx/tmp/`, where
1308
+ restores stage.
1310
1309
  - The run's **resolved cache directory**, wherever it is. `.vx/cache/` covers the
1311
1310
  default, but `defineWorkspace({ cache: { dir } })` and `--cache-dir` can put it
1312
1311
  anywhere; watching it would let vx's own cache writes trigger the next
@@ -1906,6 +1905,12 @@ project that fails leaves the global as it was. The project's vx run
1906
1905
  directly (a package script, `node_modules/.bin/vx`) has no global behind it, so only
1907
1906
  the project moves.
1908
1907
 
1908
+ **`vx upgrade -g`** (`--global`) moves only the global vx, wherever it
1909
+ runs: it never hands the call to a project's vx, and it never touches a
1910
+ `package.json` or a plugin. Run as the project's own vx, it refuses:
1911
+ `vx upgrade -g: no global vx ran — <path> is the project's own. Install
1912
+ one (npm install -g @vzn/vx), or run \`vx upgrade\` to move the project's`.
1913
+
1909
1914
  ```
1910
1915
  › vx upgrade project 0.0.1 → 0.0.2
1911
1916
 
package/docs/execution.md CHANGED
@@ -174,12 +174,13 @@ terminal and a task succeeding or failing. Read it alongside
174
174
  │ overlap is the point); drained before cache.close(). At most
175
175
  │ one remote GET per key (shared in-flight map with the lazy
176
176
  │ read-through).
177
- │ • LOCAL SHORT-CIRCUIT (no remote layer, local reads on, ≥1
178
- │ task) — derive the same stable keys and probe them in ONE
179
- │ batched cache.getMany (a layer without getMany: cache.get once
180
- │ per task under a bounded pool) → a `preProbed` map (hits AND
181
- │ stable misses; execute reuses these probes) + a `restoreTier`
182
- │ set (confirmed hits). Awaited before scheduling; never throws
177
+ │ • LOCAL SHORT-CIRCUIT (local reads on, ≥1 task) — derive the
178
+ │ same stable keys and probe the LOCAL store in ONE batched
179
+ │ getMany (a layer without getMany: get once per task under a
180
+ │ bounded pool) → a `preProbed` map (hits AND stable misses;
181
+ │ with a remote declared, hits only: a local miss may be a
182
+ │ remote hit) + a `restoreTier` set (confirmed hits). Awaited
183
+ │ before scheduling; never asks a remote; never throws
183
184
  │ (degrades to the normal schedule).
184
185
  │
185
186
  ├─ Scheduling (src/graph/scheduler.ts:runGraph — two-tier)
package/docs/features.md CHANGED
@@ -121,7 +121,7 @@ config key or an environment variable is missing here.
121
121
  - **Interactive tasks** (`exec.interactive`) — a task that owns the terminal. [page](https://vznjs.github.io/vx/features/interactive-tasks/) · post: [When a task misbehaves](https://vznjs.github.io/vx/blog/tasks-that-misbehave/)
122
122
  - **Shell completions** (`vx completions`) — bash, zsh, fish. [page](https://vznjs.github.io/vx/features/completions/) · post: [The small things](https://vznjs.github.io/vx/blog/the-small-things/)
123
123
  - **Project-local vx** (`VX_HANDED_OFF`) — a global vx hands every call to the project's own `@vzn/vx`. [page](https://vznjs.github.io/vx/cli/#which-vx-runs)
124
- - **vx upgrade** (`vx upgrade`) — upgrade vx the way it was installed: a verified binary, or its package manager. Inside a project, the project's vx, its `@vzn/vx-*` plugins (one install, one release) and the global vx that ran it move together. [page](https://vznjs.github.io/vx/features/upgrade/) · post: [An upgrade you can trust](https://vznjs.github.io/vx/blog/upgrade-you-can-trust/)
124
+ - **vx upgrade** (`vx upgrade`) — upgrade vx the way it was installed: a verified binary, or its package manager. Inside a project, the project's vx, its `@vzn/vx-*` plugins (one install, one release) and the global vx that ran it move together; `vx upgrade -g` moves only the global. [page](https://vznjs.github.io/vx/features/upgrade/) · post: [An upgrade you can trust](https://vznjs.github.io/vx/blog/upgrade-you-can-trust/)
125
125
  - **Help and version** (`vx help`, `vx version`) — every verb's reference. [page](https://vznjs.github.io/vx/features/help-and-version/) · post: [An upgrade you can trust](https://vznjs.github.io/vx/blog/upgrade-you-can-trust/)
126
126
  - **Did-you-mean** — a mistyped flag or verb gets the nearest valid spelling. [page](https://vznjs.github.io/vx/features/did-you-mean/) · post: [The small things](https://vznjs.github.io/vx/blog/the-small-things/)
127
127
  - **Task typed as a verb** — `vx build app` answers with the exact `vx run` command that does it. [page](https://vznjs.github.io/vx/features/task-as-verb/) · post: [The small things](https://vznjs.github.io/vx/blog/the-small-things/)
package/docs/schema.md CHANGED
@@ -822,19 +822,25 @@ Always applied to every glob pass (regardless of what you wrote):
822
822
  `.cache/gen/**`), which is then walked on disk under that prefix only.
823
823
  Such a file is hashed from disk and never stored or restored; a
824
824
  missing one is not an input until it exists. A broad glob (`**`,
825
- `src/**` over a listed `src`) still skips ignored files, so
826
- `node_modules` is never walked, and an install stays out either way.
825
+ `src/**` over a listed `src`) still skips ignored files, so it never
826
+ walks `node_modules`.
827
827
  Its bytes differ per machine, so machines with different files miss
828
828
  each other's shared entries. An ignored file never selects a task
829
829
  under `--affected` on its own, as in Nx (only what git sees does); `vx
830
830
  watch` re-runs it on an edit there.
831
831
  - **Always-ignored** — `.git/**`, `.vx/cache/**`,
832
- `.vx/projects.lock.json`, `vx.lock.json`, `*.bun-build`, and Bun's cross-compile extraction
833
- directory `.<16 hex>-<8 hex>.tmp/**`, and vx's own restore temp
834
- `.vx-tmp-*`, at any depth.
835
- - **Installs** — an untracked file under any `node_modules/` (ignored or
836
- not). A file git TRACKS there, such as a committed test fixture, is an
837
- input like any other.
832
+ `.vx/projects.lock.json`, `vx.lock.json`, and `.vx/tmp/**`, where vx's
833
+ restores stage, at any depth. Nothing else: a tool's temp files are the
834
+ task's to exclude. A broad glob on a `bun build --compile` task, or on
835
+ one that runs beside it, excludes Bun's intermediate and its
836
+ cross-compile extraction directory itself:
837
+ `['**/*', '!**/*.bun-build', '!**/.????????????????-????????.tmp/**']`.
838
+ - **Installs** — an untracked file under any `node_modules/` that git
839
+ does not ignore. An ignored one follows the gitignore filter above
840
+ like any other ignored path: named outright, it is an input
841
+ (`node_modules/.prisma/client/**`, a generated client); a broad glob
842
+ skips it because it is ignored. A file git TRACKS there, such as a committed test fixture, is an input
843
+ like any other.
838
844
  - **Declared `outputs.files`** are excluded — a task never invalidates
839
845
  itself via its own output. A path an output `!` entry takes back is
840
846
  no output, so it stays an input (A-44). Another task's outputs are
@@ -887,7 +893,7 @@ backslash separator (`src\**`, `src\*.ts`) or a drive (`C:\src\**`,
887
893
  backslash stays an escape.
888
894
 
889
895
  Still applied: the always-ignored set (`.git/**`, `.vx/cache/**`,
890
- `.vx/projects.lock.json`, `vx.lock.json`, `*.bun-build`, `.<16 hex>-<8 hex>.tmp/**`, `.vx-tmp-*`),
896
+ `.vx/projects.lock.json`, `vx.lock.json`, `.vx/tmp/**`),
891
897
  untracked files under `node_modules/`, and the task's own declared
892
898
  outputs, `outputs.workspaceFiles` and the `outputs.files` its globs reach
893
899
  in its own project (a task never invalidates itself).
@@ -1666,8 +1672,9 @@ The loader rejects:
1666
1672
 
1667
1673
  Loaded from `.vx/workspace.{ts,mts,js,mjs,cts,cjs}`: the directory holding
1668
1674
  `.vx` is the workspace root. `.vx` also holds the lock (`.vx/projects.lock.json`)
1669
- and `.vx/cache`, the machine-local state (the cache, the workspace id),
1670
- which is the only part git ignores: `.vx/.gitignore` says `/cache`. Every field falls back to its
1675
+ `.vx/cache`, the machine-local state (the cache, the workspace id), and
1676
+ `.vx/tmp`, where a restore stages files; git ignores only those two:
1677
+ `.vx/.gitignore` says `/cache` and `/tmp`. Every field falls back to its
1671
1678
  built-in default.
1672
1679
 
1673
1680
  ```ts
@@ -1827,7 +1834,7 @@ interface CacheSettings {
1827
1834
  workspace (`'..'`, `'/'`) is refused before anything is written, since its `*`
1828
1835
  `.gitignore` would hide the sources from git and the cache keys. A
1829
1836
  directory other than the default `.vx/cache` gets that `*` `.gitignore`;
1830
- the default is ignored by `.vx/.gitignore` (`/cache`) instead, written
1837
+ the default is ignored by `.vx/.gitignore` (`/cache`, `/tmp`) instead, written
1831
1838
  when missing and never edited.
1832
1839
  - **`cache.retention`** — the `vx cache clean` policy, applied at the
1833
1840
  end of every run: entries unused for
package/docs/security.md CHANGED
@@ -45,9 +45,10 @@ vulnerability), never in a public issue.
45
45
  choosing is trusted as a writer, as it is in every action cache. Use a
46
46
  store only your builds write to, or `turboCache()`'s signature key,
47
47
  which refuses an artifact a keyholder did not sign. A job you do not
48
- trust (a fork's pull request) reads at most: `--cache-policy=local:rw,remote:r`,
49
- or the tools' own `TURBO_REMOTE_CACHE_READ_ONLY`, `TURBO_CACHE` and
50
- `NX_SKIP_REMOTE_CACHE`, which the cache plugins honour (L-34).
48
+ trust (a fork's pull request) reads at most: `--cache-policy=local:rw,remote:r`
49
+ or `cache.policy`. Core's policy alone decides; a cache plugin reads no
50
+ tool's own switch (`TURBO_CACHE`, `NX_SKIP_REMOTE_CACHE`) and cannot
51
+ narrow it.
51
52
 
52
53
  ## Cache poisoning
53
54
 
package/launcher.sh CHANGED
@@ -35,8 +35,15 @@ scope=${scope##*/}
35
35
  # directory up to the git top-level (handoff.ts workspaceFile, the same
36
36
  # rule). One hop (VX_HANDED_OFF, which also names this launcher), never to
37
37
  # this package itself (`-ef`: same inode, so a pnpm symlink counts as
38
- # itself). Shell tests and builtins only, no fork per level.
39
- if [ -z "${VX_HANDED_OFF-}" ]; then
38
+ # itself). Shell tests and builtins only, no fork per level. `vx upgrade -g`
39
+ # moves only this global vx, so it never hands off (owner, 2026-10-16).
40
+ stay=
41
+ if [ "${1-}" = upgrade ]; then
42
+ for a in "$@"; do
43
+ case $a in -g | --global) stay=1 ;; esac
44
+ done
45
+ fi
46
+ if [ -z "${VX_HANDED_OFF-}" ] && [ -z "$stay" ]; then
40
47
  d=$PWD
41
48
  while :; do
42
49
  root=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.640",
3
+ "version": "0.0.641",
4
4
  "description": "Task orchestration you own: correct, sandboxed, extensible, with a content-addressed build cache.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.640",
25
- "@vzn/vx-linux-arm64": "0.0.640",
26
- "@vzn/vx-darwin-x64": "0.0.640",
27
- "@vzn/vx-darwin-arm64": "0.0.640"
24
+ "@vzn/vx-linux-x64": "0.0.641",
25
+ "@vzn/vx-linux-arm64": "0.0.641",
26
+ "@vzn/vx-darwin-x64": "0.0.641",
27
+ "@vzn/vx-darwin-arm64": "0.0.641"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/bin.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env -S bun --no-env-file --no-install
2
2
  // Wrapped in an explicit async main so `bun build --compile` accepts
3
3
  // the file. The compile target doesn't allow top-level await.
4
- import { handOff, localVx, vxPath } from './handoff.js'
4
+ import { handOff, localVx, upgradesOnlyItself, vxPath } from './handoff.js'
5
5
 
6
6
  let settled = false
7
7
 
@@ -10,11 +10,13 @@ async function main(): Promise<void> {
10
10
  try {
11
11
  // Inside the try: `process.cwd()` throws EMFILE when out of descriptors,
12
12
  // and that is a one-line UserError below, not a stack.
13
- const local = await localVx(
14
- process.cwd(),
15
- process.execPath,
16
- Bun.main.startsWith('/$bunfs') || (process.argv[1] ?? '').startsWith('/$bunfs'),
17
- )
13
+ const local = upgradesOnlyItself(argv)
14
+ ? null
15
+ : await localVx(
16
+ process.cwd(),
17
+ process.execPath,
18
+ Bun.main.startsWith('/$bunfs') || (process.argv[1] ?? '').startsWith('/$bunfs'),
19
+ )
18
20
  if (local !== null) await handOff(local, argv)
19
21
  // `vx --version` needs one leaf module; the dispatcher and the util
20
22
  // barrel it pulled in were 20 ms of its 28 (2026-10-01).