@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.
- package/LICENSE +1 -1
- package/README.md +15 -23
- package/package.json +5 -5
- package/schemas/info.json +0 -11
- package/schemas/init.json +5 -0
- package/skills/vx/SKILL.md +1 -1
- package/src/cache/archive.ts +89 -2
- package/src/cache/cache.ts +396 -176
- package/src/cache/file-hashes.ts +2 -14
- package/src/cache/fold-memo.ts +7 -3
- package/src/cache/index.ts +4 -2
- package/src/cache/key-fold.ts +4 -1
- package/src/cache/layer.ts +14 -10
- package/src/cache/layered-cache.ts +157 -41
- package/src/cache/plugin-cache.ts +40 -44
- package/src/cache/remote-lane.ts +59 -25
- package/src/cache/scoped-cache.ts +3 -80
- package/src/cache/tar-stream.ts +12 -7
- package/src/cli/format.ts +1 -2
- package/src/cli/help.ts +10 -4
- package/src/cli/info.ts +2 -19
- package/src/cli/init.ts +121 -20
- package/src/cli/plan-format.ts +6 -2
- package/src/cli/plugin-commands.ts +4 -5
- package/src/cli/plugin-templates.ts +1 -1
- package/src/cli/run.ts +3 -1
- package/src/cli/upgrade.ts +39 -9
- package/src/config.ts +13 -10
- package/src/exec/sandbox-paths.ts +2 -2
- package/src/graph/scheduler.ts +3 -1
- package/src/graph/task-graph.ts +15 -11
- package/src/index.ts +2 -2
- package/src/orchestrator/affected-tasks.ts +0 -18
- package/src/orchestrator/doctor.ts +0 -17
- package/src/orchestrator/execute-task.ts +1 -1
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/options.ts +6 -8
- package/src/orchestrator/plan.ts +7 -1
- package/src/orchestrator/plugin-host.ts +6 -5
- package/src/orchestrator/plugin.ts +12 -5
- package/src/orchestrator/prepare.ts +16 -46
- package/src/orchestrator/projects.ts +8 -1
- package/src/orchestrator/remote-prefetch.ts +11 -4
- package/src/orchestrator/run-context.ts +48 -5
- package/src/orchestrator/run-settings.ts +3 -0
- package/src/orchestrator/run.ts +10 -26
- package/src/orchestrator/task-hash.ts +1 -3
- package/src/report/logger.ts +0 -4
- package/src/report/summary.ts +14 -13
- package/src/settings.ts +38 -15
- package/src/util/index.ts +1 -0
- package/src/util/paths.ts +42 -33
- package/src/util/size.ts +4 -5
- package/src/workspace/affected.ts +0 -39
- package/src/workspace/config-schema.ts +0 -36
- package/src/workspace/index.ts +0 -1
package/LICENSE
CHANGED
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
|
|
38
|
+
In a git repo:
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
170
|
-
| ---------------------------------------------------------- |
|
|
171
|
-
| Nothing changed | **
|
|
172
|
-
| Nothing changed, outputs restored | **1.
|
|
173
|
-
| Cold build | **
|
|
174
|
-
| Cold build: CPU the runner burns |
|
|
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-
|
|
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
|
-
##
|
|
190
|
+
## Issues
|
|
192
191
|
|
|
193
|
-
|
|
194
|
-
|
|
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.
|
|
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.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
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
|
}
|
package/skills/vx/SKILL.md
CHANGED
|
@@ -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`.
|
package/src/cache/archive.ts
CHANGED
|
@@ -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
|
|
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
|
|
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(
|