@vzn/vx 0.0.643 → 0.0.645

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.
Files changed (56) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +15 -23
  3. package/package.json +5 -5
  4. package/schemas/info.json +0 -11
  5. package/schemas/init.json +5 -0
  6. package/skills/vx/SKILL.md +1 -1
  7. package/src/cache/archive.ts +89 -2
  8. package/src/cache/cache.ts +396 -176
  9. package/src/cache/file-hashes.ts +2 -14
  10. package/src/cache/fold-memo.ts +7 -3
  11. package/src/cache/index.ts +4 -2
  12. package/src/cache/key-fold.ts +4 -1
  13. package/src/cache/layer.ts +14 -10
  14. package/src/cache/layered-cache.ts +157 -41
  15. package/src/cache/plugin-cache.ts +40 -44
  16. package/src/cache/remote-lane.ts +59 -25
  17. package/src/cache/scoped-cache.ts +3 -80
  18. package/src/cache/tar-stream.ts +12 -7
  19. package/src/cli/format.ts +1 -2
  20. package/src/cli/help.ts +10 -4
  21. package/src/cli/info.ts +2 -19
  22. package/src/cli/init.ts +121 -20
  23. package/src/cli/plan-format.ts +6 -2
  24. package/src/cli/plugin-commands.ts +4 -5
  25. package/src/cli/plugin-templates.ts +1 -1
  26. package/src/cli/run.ts +3 -1
  27. package/src/cli/upgrade.ts +39 -9
  28. package/src/config.ts +13 -10
  29. package/src/exec/sandbox-paths.ts +2 -2
  30. package/src/graph/scheduler.ts +3 -1
  31. package/src/graph/task-graph.ts +15 -11
  32. package/src/index.ts +2 -2
  33. package/src/orchestrator/affected-tasks.ts +0 -18
  34. package/src/orchestrator/doctor.ts +0 -17
  35. package/src/orchestrator/execute-task.ts +1 -1
  36. package/src/orchestrator/index.ts +1 -0
  37. package/src/orchestrator/options.ts +6 -8
  38. package/src/orchestrator/plan.ts +7 -1
  39. package/src/orchestrator/plugin-host.ts +6 -5
  40. package/src/orchestrator/plugin.ts +12 -5
  41. package/src/orchestrator/prepare.ts +16 -46
  42. package/src/orchestrator/projects.ts +8 -1
  43. package/src/orchestrator/remote-prefetch.ts +11 -4
  44. package/src/orchestrator/run-context.ts +48 -5
  45. package/src/orchestrator/run-settings.ts +3 -0
  46. package/src/orchestrator/run.ts +10 -26
  47. package/src/orchestrator/task-hash.ts +1 -3
  48. package/src/report/logger.ts +0 -4
  49. package/src/report/summary.ts +14 -13
  50. package/src/settings.ts +38 -15
  51. package/src/util/index.ts +1 -0
  52. package/src/util/paths.ts +42 -33
  53. package/src/util/size.ts +4 -5
  54. package/src/workspace/affected.ts +0 -39
  55. package/src/workspace/config-schema.ts +0 -36
  56. package/src/workspace/index.ts +0 -1
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 vx contributors
3
+ Copyright (c) 2026 VZN
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  <p align="center">
14
14
  <a href="https://www.npmjs.com/package/@vzn/vx"><img src="https://img.shields.io/npm/v/@vzn/vx?color=7cb518" alt="npm"></a>
15
- <a href="https://github.com/vznjs/vx/actions/workflows/ci.yml"><img src="https://github.com/vznjs/vx/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
15
+ <a href="https://github.com/vznjs/vx/actions/workflows/ci.yml?query=branch%3Amain+event%3Apush"><img src="https://github.com/vznjs/vx/actions/workflows/ci.yml/badge.svg?branch=main&event=push" alt="CI"></a>
16
16
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT license"></a>
17
17
  <a href="https://vznjs.github.io/vx/"><img src="https://img.shields.io/badge/docs-vznjs.github.io%2Fvx-c6f84e" alt="Documentation"></a>
18
18
  </p>
@@ -35,13 +35,12 @@ Windows: run vx inside WSL2.
35
35
 
36
36
  ## Quick start
37
37
 
38
- In a git repo with a `package.json` workspace:
38
+ In a git repo:
39
39
 
40
40
  ```sh
41
- npm install -D @vzn/vx # the scope matters: npm's `vx` is an unrelated package
42
- npx @vzn/vx init # beside turbo.json or nx.json: migrates them; else a vx.config.ts per package
43
- npx @vzn/vx run build --all # builds in dependency order and stores the results
44
- npx @vzn/vx run build --all # nothing changed: every task up-to-date
41
+ npx @vzn/vx init # installs vx globally and in the repo; beside turbo.json or nx.json: migrates them; else a root vx.config.ts
42
+ vx run build --all # builds in dependency order and stores the results
43
+ vx run build --all # nothing changed: every task up-to-date
45
44
  ```
46
45
 
47
46
  Bun workspaces: `bun add -d @vzn/vx` and `bunx @vzn/vx` (npm refuses to run in
@@ -166,17 +165,17 @@ Every gap, and when another tool is the better pick:
166
165
 
167
166
  <!-- bench:start — generated by packages/vx-bench/update-site.ts from results.json; do not hand-edit -->
168
167
 
169
- | Time added over the ideal run, 1,601 projects, 9,603 tasks | vx | Turborepo | Nx | Vite Task |
170
- | ---------------------------------------------------------- | ----------- | --------- | ---------- | --------- |
171
- | Nothing changed | **856 ms** | 855 ms | 23.22 s | 11.24 s |
172
- | Nothing changed, outputs restored | **1.45 s** | 1.50 s | 21.64 s | 11.21 s |
173
- | Cold build | **7.73 s** | 10.32 s | 3 min 23 s | 9.44 s |
174
- | Cold build: CPU the runner burns | **36.42 s** | 1 min 3 s | 5 min 35 s | 25.64 s |
168
+ | Time added over the ideal run, 1,601 projects, 9,603 tasks | vx | Turborepo | Nx | Vite Task |
169
+ | ---------------------------------------------------------- | ---------- | --------- | ---------- | --------- |
170
+ | Nothing changed | **757 ms** | 761 ms | 22.63 s | 12.26 s |
171
+ | Nothing changed, outputs restored | **1.27 s** | 1.76 s | 22.16 s | 11.66 s |
172
+ | Cold build | **6.13 s** | 8.87 s | 3 min 20 s | 7.94 s |
173
+ | Cold build: CPU the runner burns | 30.06 s | 1 min 8 s | 5 min 45 s | 23.91 s |
175
174
 
176
175
  Time each tool adds over the ideal run; lower is better.
177
176
 
178
177
  Benchmark workload: a synthetic monorepo of 1,601 projects and 9,603 tasks in 30 dependency levels, five core libraries a quarter of the projects use; build 300 ms, test and typecheck 150 ms, lint 75 ms, publish 30 ms; real repos with uneven task times will differ.
179
- Run 2026-10-12 on linux x64, 4 cores: vx at commit 126d60a93, Turborepo 2.11.7, Nx 23.3.0, Vite Task (vite-plus) 1.1.0.
178
+ Run 2026-10-20 on linux x64, 4 cores: vx at commit a88652b0b, Turborepo 2.11.9, Nx 23.3.0, Vite Task (vite-plus) 1.3.0.
180
179
 
181
180
  Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
182
181
 
@@ -188,17 +187,10 @@ Same graph, commands and concurrency: [how it is measured](https://vznjs.github.
188
187
  vx. The schema may still change; 1.0's promise:
189
188
  [versioning-1.0.md](packages/vx/docs/design/versioning-1.0.md).
190
189
 
191
- ## Contributing
190
+ ## Issues
192
191
 
193
- ```sh
194
- git clone https://github.com/vznjs/vx && cd vx
195
- bun install
196
- bun packages/vx/src/bin.ts run ci --all # lint, test, docs build: every package
197
- ```
198
-
199
- Needs Bun ≥ 1.4. Every dev task is a vx task; no `package.json`
200
- scripts. Start with [CONTRIBUTING.md](CONTRIBUTING.md) and
201
- [STATUS.md](packages/vx/docs/STATUS.md).
192
+ vx is maintained by VZN and takes issues, not pull requests: report a
193
+ bug or ask for a feature in [issues](https://github.com/vznjs/vx/issues).
202
194
 
203
195
  ## License
204
196
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.643",
3
+ "version": "0.0.645",
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.643",
25
- "@vzn/vx-linux-arm64": "0.0.643",
26
- "@vzn/vx-darwin-x64": "0.0.643",
27
- "@vzn/vx-darwin-arm64": "0.0.643"
24
+ "@vzn/vx-linux-x64": "0.0.645",
25
+ "@vzn/vx-linux-arm64": "0.0.645",
26
+ "@vzn/vx-darwin-x64": "0.0.645",
27
+ "@vzn/vx-darwin-arm64": "0.0.645"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/schemas/info.json CHANGED
@@ -26,7 +26,6 @@
26
26
  "cacheEntries",
27
27
  "cacheBytes",
28
28
  "settings",
29
- "cacheRetention",
30
29
  "orphans",
31
30
  "runs24h",
32
31
  "hits24h",
@@ -105,16 +104,6 @@
105
104
  }
106
105
  }
107
106
  },
108
- "cacheRetention": {
109
- "type": ["object", "null"],
110
- "description": "cache.retention from its variables, else the workspace, maxSize also in bytes; null when none is declared",
111
- "additionalProperties": false,
112
- "properties": {
113
- "olderThan": { "type": "string" },
114
- "maxSize": { "type": "string" },
115
- "maxBytes": { "type": "integer" }
116
- }
117
- },
118
107
  "orphans": {
119
108
  "type": "object",
120
109
  "additionalProperties": false,
package/schemas/init.json CHANGED
@@ -52,6 +52,11 @@
52
52
  "next": {
53
53
  "type": ["string", "null"],
54
54
  "description": "the command to type after the real run; null where the plan names none"
55
+ },
56
+ "installs": {
57
+ "type": "array",
58
+ "items": { "type": "string" },
59
+ "description": "core's plan only: the install commands the real run runs (global, then the root devDependency)"
55
60
  }
56
61
  }
57
62
  }
@@ -110,4 +110,4 @@ vx info --format json # vx and Bun versions, projects, config errors, plugins,
110
110
  has installed, copy it from there instead:
111
111
  `cp node_modules/@vzn/vx/skills/vx/SKILL.md .claude/skills/vx/`.
112
112
  `vx upgrade` inside a workspace moves the project's `@vzn/vx` and the
113
- global vx that ran it to the same version.
113
+ global vx that ran it to the same version, then runs `vx lock`.
@@ -70,6 +70,7 @@ import {
70
70
  stat,
71
71
  symlink,
72
72
  unlink,
73
+ utimes,
73
74
  writeFile,
74
75
  } from 'node:fs/promises'
75
76
  import path from 'node:path'
@@ -471,6 +472,84 @@ function streamOf(gen: AsyncGenerator<Uint8Array>): ReadableStream<Uint8Array> {
471
472
  })
472
473
  }
473
474
 
475
+ /**
476
+ * The tree form (`cache.store.decompress`, v47): an artifact's members
477
+ * as plain files under one directory, `outputs/<rel>` and
478
+ * `workspace-outputs/<rel>` as named in the tar, beside its sidecar.
479
+ * stdout lives in the index. A restore copies (or clones) each file out,
480
+ * so a hit decodes nothing; an upload packs the tree back into the
481
+ * artifact a remote expects.
482
+ *
483
+ * Write `plan`'s outputs into the empty `dir` as the tree form, each copied
484
+ * by the kernel (`copy_file_range`, or a clone where the filesystem has one)
485
+ * and given its sidecar mtime, and return what a scan of the plan's tar
486
+ * would: the save indexes from it without packing a tar.
487
+ */
488
+ export async function writeTree(plan: ArtifactPlan, dir: string): Promise<ArtifactScan> {
489
+ const fold = new ScanFold()
490
+ const meta = plan.inputs.find((i) => i.name === META_ENTRY)!.body as Uint8Array
491
+ const metaText = utf8.decode(meta)
492
+ fold.sidecar(metaText)
493
+ const times = (JSON.parse(metaText) as MetaFile).files
494
+ for (const i of plan.inputs) {
495
+ if (i.name === META_ENTRY) continue
496
+ assertSafeName(i.name)
497
+ if (i.name === STDOUT_ENTRY) fold.stdout = utf8.decode(i.body as Uint8Array)
498
+ fold.entry(i.name, i.size, times[i.name]?.[1] ?? 0)
499
+ }
500
+ // A stdout-only entry writes no file to make the directory.
501
+ await mkdir(dir, { recursive: true })
502
+ await Promise.all(
503
+ plan.inputs.map(async (i) => {
504
+ if (!(i.body instanceof Blob)) return
505
+ const to = path.join(dir, i.name)
506
+ await Bun.write(to, i.body)
507
+ const at = new Date(times[i.name]![1])
508
+ await utimes(to, at, at)
509
+ }),
510
+ )
511
+ await writeFile(path.join(dir, META_ENTRY), meta)
512
+ return fold.result()
513
+ }
514
+
515
+ /**
516
+ * Write a scanned artifact's sidecar into `dir`, whose outputs an extract
517
+ * just wrote: an ingested remote hit kept as a tree.
518
+ */
519
+ export async function sealTree(dir: string, scan: ArtifactScan): Promise<void> {
520
+ // An artifact with no outputs extracts nothing, so nothing made `dir`.
521
+ await mkdir(dir, { recursive: true })
522
+ const meta: MetaFile = { version: 1, files: {} }
523
+ if (scan.key !== undefined) meta.key = scan.key
524
+ for (const e of scan.entries) {
525
+ if (e.name !== STDOUT_ENTRY) meta.files[e.name] = [e.mode, e.mtimeMs]
526
+ }
527
+ if (scan.dirs.length > 0) meta.dirs = scan.dirs
528
+ if (scan.links.length > 0) meta.links = Object.fromEntries(scan.links)
529
+ if (scan.exec !== undefined) meta.exec = scan.exec
530
+ await writeFile(path.join(dir, META_ENTRY), JSON.stringify(meta))
531
+ }
532
+
533
+ /** The plan that packs the tree in `dir` back into the artifact it holds. */
534
+ export function planOfTree(dir: string, stdout: string): ArtifactPlan {
535
+ const metaBytes = readFileSync(path.join(dir, META_ENTRY))
536
+ const meta = JSON.parse(utf8.decode(metaBytes)) as MetaFile
537
+ const out = new TextEncoder().encode(stdout)
538
+ const inputs: TarInput[] = [{ name: STDOUT_ENTRY, size: out.byteLength, body: out }]
539
+ for (const [name, [mode, mtimeMs]] of Object.entries(meta.files)) {
540
+ const file = Bun.file(path.join(dir, name))
541
+ inputs.push({
542
+ name,
543
+ size: statSync(path.join(dir, name)).size,
544
+ mode,
545
+ mtime: Math.min(USTAR_MTIME_MAX, Math.max(0, Math.floor(mtimeMs / 1000))),
546
+ body: file,
547
+ })
548
+ }
549
+ inputs.push({ name: META_ENTRY, size: metaBytes.byteLength, body: new Uint8Array(metaBytes) })
550
+ return { inputs, size: tarSize([...inputs, sumInput('0'.repeat(SUM_SIZE))]) }
551
+ }
552
+
474
553
  /** What a scan of an artifact reads out of it. */
475
554
  interface ArtifactScan {
476
555
  entries: ArchiveEntry[]
@@ -664,6 +743,8 @@ export async function extractArtifactStream(
664
743
  accept?: (name: string) => boolean,
665
744
  /** The workspace root whose `.vx/tmp/` stages the entries; absent, each stages beside its target. */
666
745
  stageRoot?: string,
746
+ /** Handed what `scanArtifact` would read of the same bytes, once their sum has checked. */
747
+ onScan?: (scan: ArtifactScan) => void,
667
748
  ): Promise<Set<string>> {
668
749
  // No `mkdir -p` of the destination here: `stage` creates each entry's
669
750
  // directory chain, the destination included, and the cache restores
@@ -676,6 +757,7 @@ export async function extractArtifactStream(
676
757
  let meta: MetaFile['files'] = {}
677
758
  let dirs: string[] = []
678
759
  let links: Array<[string, string]> = []
760
+ const fold = onScan === undefined ? undefined : new ScanFold()
679
761
  try {
680
762
  const check = new SumCheck()
681
763
  for await (const e of tarEntries(tar)) {
@@ -683,7 +765,9 @@ export async function extractArtifactStream(
683
765
  const body = await check.body(e.name, e.body)
684
766
  if (body === null) continue
685
767
  if (e.name === META_ENTRY) {
686
- const parsed = JSON.parse(await textOf(body)) as MetaFile
768
+ const text = await textOf(body)
769
+ fold?.sidecar(text)
770
+ const parsed = JSON.parse(text) as MetaFile
687
771
  meta = parsed.files ?? {}
688
772
  dirs = dirsOf(parsed)
689
773
  links = linksOf(parsed)
@@ -697,9 +781,11 @@ export async function extractArtifactStream(
697
781
  `archive entry is not one of the task's declared outputs: ${e.name}`,
698
782
  )
699
783
  }
784
+ fold?.entry(e.name, e.size, e.mtimeMs)
700
785
  const dest = x.destFor(e.name)
701
786
  if (dest === null || dest.rel.length === 0) {
702
- await drain(body)
787
+ if (fold !== undefined && e.name === STDOUT_ENTRY) fold.stdout = await textOf(body)
788
+ else await drain(body)
703
789
  continue
704
790
  }
705
791
  const target = path.join(dest.base, dest.rel)
@@ -709,6 +795,7 @@ export async function extractArtifactStream(
709
795
  await x.stage(e.name, dest.base, target, e.size <= SMALL_ENTRY ? await bytesOf(body) : body)
710
796
  }
711
797
  check.done()
798
+ if (fold !== undefined) onScan!(fold.result())
712
799
  for (const name of dirs) {
713
800
  if (accept !== undefined && !accept(name)) {
714
801
  throw new ArchiveSecurityError(