@vzn/vx 0.0.642 → 0.0.644
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/package.json +5 -5
- package/schemas/info.json +0 -11
- package/skills/vx/SKILL.md +1 -1
- package/src/cache/archive.ts +153 -2
- package/src/cache/cache.ts +417 -183
- package/src/cache/file-hashes.ts +2 -14
- package/src/cache/fold-memo.ts +7 -3
- package/src/cache/index.ts +10 -2
- package/src/cache/key-fold.ts +4 -1
- package/src/cache/layer.ts +11 -4
- package/src/cache/layered-cache.ts +82 -8
- package/src/cache/plugin-cache.ts +51 -43
- package/src/cache/remote-lane.ts +18 -13
- package/src/cache/run-store.ts +4 -1
- package/src/cache/scoped-cache.ts +74 -239
- package/src/cache/tar-stream.ts +12 -7
- package/src/cli/format.ts +1 -2
- package/src/cli/help.ts +8 -4
- package/src/cli/info.ts +2 -19
- package/src/cli/plugin-commands.ts +11 -6
- package/src/cli/run.ts +7 -12
- package/src/cli/select.ts +7 -9
- package/src/cli/upgrade.ts +38 -8
- package/src/cli/workspace-config.ts +8 -2
- package/src/config.ts +22 -26
- 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 +5 -1
- package/src/orchestrator/affected-tasks.ts +23 -2
- package/src/orchestrator/doctor.ts +0 -17
- package/src/orchestrator/execute-task.ts +1 -1
- package/src/orchestrator/index.ts +4 -0
- package/src/orchestrator/options.ts +15 -1
- package/src/orchestrator/plugin-host.ts +11 -4
- package/src/orchestrator/plugin.ts +23 -2
- package/src/orchestrator/prepare.ts +81 -18
- package/src/orchestrator/projects.ts +26 -5
- package/src/orchestrator/remote-prefetch.ts +11 -4
- package/src/orchestrator/run-context.ts +85 -0
- package/src/orchestrator/run-settings.ts +3 -0
- package/src/orchestrator/run.ts +9 -26
- package/src/orchestrator/task-hash.ts +6 -0
- package/src/report/logger.ts +0 -4
- package/src/report/summary.ts +3 -11
- package/src/settings.ts +54 -25
- 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 +120 -63
- package/src/workspace/config-schema.ts +44 -52
- package/src/workspace/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.644",
|
|
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.644",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.644",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.644",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.644"
|
|
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/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'
|
|
@@ -162,6 +163,8 @@ class SumCheck {
|
|
|
162
163
|
add(bytes: Uint8Array): void {
|
|
163
164
|
this.sum.add(bytes)
|
|
164
165
|
}
|
|
166
|
+
/** The checked sum's text, once `verify` passed. */
|
|
167
|
+
checked = ''
|
|
165
168
|
verify(got: string): void {
|
|
166
169
|
if (got !== this.sum.hex()) {
|
|
167
170
|
throw new TarFormatError(
|
|
@@ -169,6 +172,7 @@ class SumCheck {
|
|
|
169
172
|
)
|
|
170
173
|
}
|
|
171
174
|
this.seen = true
|
|
175
|
+
this.checked = got
|
|
172
176
|
}
|
|
173
177
|
done(): void {
|
|
174
178
|
if (!this.seen) throw new TarFormatError('artifact carries no checksum')
|
|
@@ -471,6 +475,132 @@ function streamOf(gen: AsyncGenerator<Uint8Array>): ReadableStream<Uint8Array> {
|
|
|
471
475
|
})
|
|
472
476
|
}
|
|
473
477
|
|
|
478
|
+
/**
|
|
479
|
+
* The tree form (`cache.store.decompress`, v47): an artifact's members
|
|
480
|
+
* as plain files under one directory, `outputs/<rel>` and
|
|
481
|
+
* `workspace-outputs/<rel>` as named in the tar, beside its sidecar and a
|
|
482
|
+
* `.vx-sum` over them. stdout lives in the index. A restore copies (or
|
|
483
|
+
* clones) each file out, so a hit decodes nothing; an upload packs the
|
|
484
|
+
* tree back into the artifact a remote expects.
|
|
485
|
+
*/
|
|
486
|
+
const TREE_SUM = SUM_ENTRY
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Write `plan`'s outputs into the empty `dir` as the tree form, each copied
|
|
490
|
+
* by the kernel (`copy_file_range`, or a clone where the filesystem has one)
|
|
491
|
+
* and given its sidecar mtime, and return what a scan of the plan's tar
|
|
492
|
+
* would: the save indexes from it without packing a tar.
|
|
493
|
+
*/
|
|
494
|
+
export async function writeTree(plan: ArtifactPlan, dir: string): Promise<ArtifactScan> {
|
|
495
|
+
const fold = new ScanFold()
|
|
496
|
+
const meta = plan.inputs.find((i) => i.name === META_ENTRY)!.body as Uint8Array
|
|
497
|
+
const metaText = utf8.decode(meta)
|
|
498
|
+
fold.sidecar(metaText)
|
|
499
|
+
const times = (JSON.parse(metaText) as MetaFile).files
|
|
500
|
+
for (const i of plan.inputs) {
|
|
501
|
+
if (i.name === META_ENTRY) continue
|
|
502
|
+
assertSafeName(i.name)
|
|
503
|
+
if (i.name === STDOUT_ENTRY) fold.stdout = utf8.decode(i.body as Uint8Array)
|
|
504
|
+
fold.entry(i.name, i.size, times[i.name]?.[1] ?? 0)
|
|
505
|
+
}
|
|
506
|
+
// A stdout-only entry writes no file to make the directory.
|
|
507
|
+
await mkdir(dir, { recursive: true })
|
|
508
|
+
await Promise.all(
|
|
509
|
+
plan.inputs.map(async (i) => {
|
|
510
|
+
if (!(i.body instanceof Blob)) return
|
|
511
|
+
const to = path.join(dir, i.name)
|
|
512
|
+
await Bun.write(to, i.body)
|
|
513
|
+
const at = new Date(times[i.name]![1])
|
|
514
|
+
await utimes(to, at, at)
|
|
515
|
+
}),
|
|
516
|
+
)
|
|
517
|
+
await writeFile(path.join(dir, META_ENTRY), meta)
|
|
518
|
+
await writeFile(path.join(dir, TREE_SUM), await treeSum(dir, stdoutOf(plan)))
|
|
519
|
+
return fold.result()
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function stdoutOf(plan: ArtifactPlan): string {
|
|
523
|
+
return utf8.decode(plan.inputs.find((i) => i.name === STDOUT_ENTRY)!.body as Uint8Array)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Write a scanned artifact's sidecar and sum into `dir`, whose outputs an
|
|
528
|
+
* extract just wrote: an ingested remote hit kept as a tree.
|
|
529
|
+
*/
|
|
530
|
+
export async function sealTree(
|
|
531
|
+
dir: string,
|
|
532
|
+
scan: ArtifactScan,
|
|
533
|
+
/** The archive's own sidecar text and checked sum (`extractArtifactStream`'s `onScan`). */
|
|
534
|
+
raw?: { meta: string; sum: string },
|
|
535
|
+
): Promise<void> {
|
|
536
|
+
// An artifact with no outputs extracts nothing, so nothing made `dir`.
|
|
537
|
+
await mkdir(dir, { recursive: true })
|
|
538
|
+
const meta: MetaFile = { version: 1, files: {} }
|
|
539
|
+
if (scan.key !== undefined) meta.key = scan.key
|
|
540
|
+
for (const e of scan.entries) {
|
|
541
|
+
if (e.name !== STDOUT_ENTRY) meta.files[e.name] = [e.mode, e.mtimeMs]
|
|
542
|
+
}
|
|
543
|
+
if (scan.dirs.length > 0) meta.dirs = scan.dirs
|
|
544
|
+
if (scan.links.length > 0) meta.links = Object.fromEntries(scan.links)
|
|
545
|
+
if (scan.exec !== undefined) meta.exec = scan.exec
|
|
546
|
+
const text = JSON.stringify(meta)
|
|
547
|
+
await writeFile(path.join(dir, META_ENTRY), text)
|
|
548
|
+
// The tree packs back to the archive it came from when the sidecar is
|
|
549
|
+
// the same bytes and the entries came in its order (stdout first, the
|
|
550
|
+
// sidecar last): the sum the extract checked is then the tree's, and the
|
|
551
|
+
// tree is not read back to sum it.
|
|
552
|
+
const names = Object.keys(meta.files)
|
|
553
|
+
const same =
|
|
554
|
+
raw !== undefined &&
|
|
555
|
+
raw.meta === text &&
|
|
556
|
+
scan.entries[0]?.name === STDOUT_ENTRY &&
|
|
557
|
+
scan.entries.length === names.length + 1 &&
|
|
558
|
+
names.every((n, i) => scan.entries[i + 1]!.name === n)
|
|
559
|
+
await writeFile(path.join(dir, TREE_SUM), same ? raw.sum : await treeSum(dir, scan.stdout ?? ''))
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** The plan that packs the tree in `dir` back into the artifact it holds. */
|
|
563
|
+
export function planOfTree(dir: string, stdout: string): ArtifactPlan {
|
|
564
|
+
const metaBytes = readFileSync(path.join(dir, META_ENTRY))
|
|
565
|
+
const meta = JSON.parse(utf8.decode(metaBytes)) as MetaFile
|
|
566
|
+
const out = new TextEncoder().encode(stdout)
|
|
567
|
+
const inputs: TarInput[] = [{ name: STDOUT_ENTRY, size: out.byteLength, body: out }]
|
|
568
|
+
for (const [name, [mode, mtimeMs]] of Object.entries(meta.files)) {
|
|
569
|
+
const file = Bun.file(path.join(dir, name))
|
|
570
|
+
inputs.push({
|
|
571
|
+
name,
|
|
572
|
+
size: statSync(path.join(dir, name)).size,
|
|
573
|
+
mode,
|
|
574
|
+
mtime: Math.min(USTAR_MTIME_MAX, Math.max(0, Math.floor(mtimeMs / 1000))),
|
|
575
|
+
body: file,
|
|
576
|
+
})
|
|
577
|
+
}
|
|
578
|
+
inputs.push({ name: META_ENTRY, size: metaBytes.byteLength, body: new Uint8Array(metaBytes) })
|
|
579
|
+
return { inputs, size: tarSize([...inputs, sumInput('0'.repeat(SUM_SIZE))]) }
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** The sum over the tree's members, as `SUM_ENTRY` defines it for the tar it packs to. */
|
|
583
|
+
async function treeSum(dir: string, stdout: string): Promise<string> {
|
|
584
|
+
const sum = new EntrySum()
|
|
585
|
+
for (const i of planOfTree(dir, stdout).inputs) {
|
|
586
|
+
sum.entry(i.name)
|
|
587
|
+
if (i.body instanceof Blob && i.size > 1 << 20)
|
|
588
|
+
for await (const c of i.body.stream()) sum.add(c)
|
|
589
|
+
else if (i.body instanceof Blob) sum.add(await i.body.bytes())
|
|
590
|
+
else sum.add(i.body as Uint8Array)
|
|
591
|
+
}
|
|
592
|
+
return sum.hex()
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Whether the tree's bytes still sum to what was stored (`cache.store.verify: 'hash'`). */
|
|
596
|
+
export async function treeIntact(dir: string, stdout: string): Promise<boolean> {
|
|
597
|
+
try {
|
|
598
|
+
return (await treeSum(dir, stdout)) === readFileSync(path.join(dir, TREE_SUM), 'utf8')
|
|
599
|
+
} catch {
|
|
600
|
+
return false
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
474
604
|
/** What a scan of an artifact reads out of it. */
|
|
475
605
|
interface ArtifactScan {
|
|
476
606
|
entries: ArchiveEntry[]
|
|
@@ -664,6 +794,12 @@ export async function extractArtifactStream(
|
|
|
664
794
|
accept?: (name: string) => boolean,
|
|
665
795
|
/** The workspace root whose `.vx/tmp/` stages the entries; absent, each stages beside its target. */
|
|
666
796
|
stageRoot?: string,
|
|
797
|
+
/**
|
|
798
|
+
* Handed what `scanArtifact` would read of the same bytes, once their sum
|
|
799
|
+
* has checked, with the sidecar's text and the sum when the sidecar came
|
|
800
|
+
* last: what `sealTree` may reuse rather than read the tree back.
|
|
801
|
+
*/
|
|
802
|
+
onScan?: (scan: ArtifactScan, raw: { meta: string; sum: string } | undefined) => void,
|
|
667
803
|
): Promise<Set<string>> {
|
|
668
804
|
// No `mkdir -p` of the destination here: `stage` creates each entry's
|
|
669
805
|
// directory chain, the destination included, and the cache restores
|
|
@@ -676,14 +812,22 @@ export async function extractArtifactStream(
|
|
|
676
812
|
let meta: MetaFile['files'] = {}
|
|
677
813
|
let dirs: string[] = []
|
|
678
814
|
let links: Array<[string, string]> = []
|
|
815
|
+
const fold = onScan === undefined ? undefined : new ScanFold()
|
|
816
|
+
let metaText: string | undefined
|
|
817
|
+
let metaLast = false
|
|
679
818
|
try {
|
|
680
819
|
const check = new SumCheck()
|
|
681
820
|
for await (const e of tarEntries(tar)) {
|
|
682
821
|
if (e.type !== '0') continue
|
|
683
822
|
const body = await check.body(e.name, e.body)
|
|
684
823
|
if (body === null) continue
|
|
824
|
+
metaLast = false
|
|
685
825
|
if (e.name === META_ENTRY) {
|
|
686
|
-
const
|
|
826
|
+
const text = await textOf(body)
|
|
827
|
+
fold?.sidecar(text)
|
|
828
|
+
metaText = text
|
|
829
|
+
metaLast = true
|
|
830
|
+
const parsed = JSON.parse(text) as MetaFile
|
|
687
831
|
meta = parsed.files ?? {}
|
|
688
832
|
dirs = dirsOf(parsed)
|
|
689
833
|
links = linksOf(parsed)
|
|
@@ -697,9 +841,11 @@ export async function extractArtifactStream(
|
|
|
697
841
|
`archive entry is not one of the task's declared outputs: ${e.name}`,
|
|
698
842
|
)
|
|
699
843
|
}
|
|
844
|
+
fold?.entry(e.name, e.size, e.mtimeMs)
|
|
700
845
|
const dest = x.destFor(e.name)
|
|
701
846
|
if (dest === null || dest.rel.length === 0) {
|
|
702
|
-
await
|
|
847
|
+
if (fold !== undefined && e.name === STDOUT_ENTRY) fold.stdout = await textOf(body)
|
|
848
|
+
else await drain(body)
|
|
703
849
|
continue
|
|
704
850
|
}
|
|
705
851
|
const target = path.join(dest.base, dest.rel)
|
|
@@ -709,6 +855,11 @@ export async function extractArtifactStream(
|
|
|
709
855
|
await x.stage(e.name, dest.base, target, e.size <= SMALL_ENTRY ? await bytesOf(body) : body)
|
|
710
856
|
}
|
|
711
857
|
check.done()
|
|
858
|
+
if (fold !== undefined)
|
|
859
|
+
onScan!(
|
|
860
|
+
fold.result(),
|
|
861
|
+
metaLast && metaText !== undefined ? { meta: metaText, sum: check.checked } : undefined,
|
|
862
|
+
)
|
|
712
863
|
for (const name of dirs) {
|
|
713
864
|
if (accept !== undefined && !accept(name)) {
|
|
714
865
|
throw new ArchiveSecurityError(
|