@vzn/vx 0.0.16 → 0.0.18
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 +66 -89
- package/index.ts +10 -0
- package/launcher.mjs +10 -11
- package/package.json +19 -5
- package/plugins/local-cache/index.ts +2 -0
- package/plugins/local-executor/index.ts +2 -0
- package/plugins/schedule-history/index.ts +2 -0
- package/src/bin.ts +2 -2
- package/src/cache/archive.ts +557 -0
- package/src/cache/cache.ts +929 -250
- package/src/cache/cas-backend.ts +20 -2
- package/src/cache/chained-cache.ts +202 -0
- package/src/cache/digest.ts +0 -15
- package/src/cache/index.ts +13 -3
- package/src/cache/inputs.ts +445 -41
- package/src/cache/layered-cache.ts +88 -30
- package/src/cache/tar-stream.ts +358 -0
- package/src/cli/cache.ts +36 -10
- package/src/cli/help.ts +167 -128
- package/src/cli/index.ts +74 -43
- package/src/cli/info.ts +30 -0
- package/src/cli/last.ts +140 -0
- package/src/cli/migrate-emit.ts +21 -0
- package/src/cli/migrate-nx.ts +23 -0
- package/src/cli/migrate-scripts.ts +127 -0
- package/src/cli/migrate-turbo.ts +43 -6
- package/src/cli/migrate.ts +119 -33
- package/src/cli/plan-format.ts +58 -3
- package/src/cli/plugin-commands.ts +96 -0
- package/src/cli/prune.ts +291 -0
- package/src/cli/run.ts +272 -80
- package/src/cli/upgrade.ts +7 -6
- package/src/cli/watch.ts +259 -61
- package/src/cli/why.ts +193 -0
- package/src/config.ts +40 -23
- package/src/exec/env.ts +7 -1
- package/src/exec/executor.ts +239 -0
- package/src/exec/index.ts +19 -1
- package/src/exec/runner.ts +116 -29
- package/src/exec/sandbox-runtime.ts +194 -24
- package/src/graph/index.ts +3 -1
- package/src/graph/scheduler.ts +92 -18
- package/src/graph/task-graph.ts +220 -6
- package/src/index.ts +82 -84
- package/src/orchestrator/deferred-outputs.ts +144 -0
- package/src/orchestrator/download-policy.ts +171 -0
- package/src/orchestrator/events.ts +30 -0
- package/src/orchestrator/execute-task.ts +328 -94
- package/src/orchestrator/failure-mode.ts +61 -0
- package/src/orchestrator/framed-output.ts +87 -35
- package/src/orchestrator/history.ts +78 -31
- package/src/orchestrator/index.ts +43 -90
- package/src/orchestrator/local-shortcircuit.ts +22 -0
- package/src/orchestrator/logger.ts +196 -14
- package/src/orchestrator/metrics.ts +57 -1376
- package/src/orchestrator/missing-plugin.ts +9 -0
- package/src/orchestrator/options.ts +22 -4
- package/src/orchestrator/plan.ts +144 -1
- package/src/orchestrator/plugin-host.ts +199 -119
- package/src/orchestrator/plugin.ts +144 -57
- package/src/orchestrator/prepare.ts +220 -69
- package/src/orchestrator/remote-prefetch.ts +12 -8
- package/src/orchestrator/resources.ts +20 -34
- package/src/orchestrator/run-artifacts.ts +41 -16
- package/src/orchestrator/run-context.ts +167 -29
- package/src/orchestrator/run-report.ts +73 -46
- package/src/orchestrator/run.ts +590 -122
- package/src/orchestrator/stable-keys.ts +74 -33
- package/src/orchestrator/status-line.ts +47 -8
- package/src/orchestrator/summary.ts +22 -1
- package/src/orchestrator/tally.ts +78 -35
- package/src/orchestrator/task-hash.ts +105 -11
- package/src/orchestrator/task-log-buffer.ts +291 -0
- package/src/orchestrator/telemetry-host.ts +24 -6
- package/src/orchestrator/telemetry.ts +181 -21
- package/src/orchestrator/upstream.ts +72 -31
- package/src/orchestrator/verify.ts +12 -2
- package/src/plugins/local-cache/index.ts +11 -0
- package/src/plugins/local-executor/index.ts +45 -0
- package/src/plugins/schedule-history/index.ts +109 -0
- package/src/util/edit-distance.ts +21 -0
- package/src/util/errors.ts +15 -1
- package/src/util/index.ts +7 -2
- package/src/util/num.ts +37 -0
- package/src/util/paths.ts +43 -0
- package/src/util/settle.ts +48 -0
- package/src/util/size.ts +5 -1
- package/src/util/tail.ts +66 -0
- package/src/util/timing.ts +63 -0
- package/src/workspace/affected.ts +142 -15
- package/src/workspace/config-cache.ts +304 -0
- package/src/workspace/config-eval.ts +210 -0
- package/src/workspace/config-imports.ts +161 -0
- package/src/workspace/filter.ts +22 -4
- package/src/workspace/fingerprint.ts +16 -1
- package/src/workspace/index.ts +15 -3
- package/src/workspace/lockfile.ts +13 -2
- package/src/workspace/nested-dirs.ts +21 -7
- package/src/workspace/package-graph.ts +23 -20
- package/src/workspace/project-loader.ts +503 -59
- package/src/workspace/workspace.ts +213 -72
- package/src/cache/tar.ts +0 -343
- package/src/cli/backend.ts +0 -23
- package/src/cli/mcp-rpc.ts +0 -292
- package/src/cli/mcp.ts +0 -92
- package/src/orchestrator/devframe-surface.ts +0 -74
- package/src/orchestrator/predict.ts +0 -106
- package/src/orchestrator/protocol.ts +0 -156
- package/src/orchestrator/run-state.ts +0 -145
- package/src/orchestrator/wire-render.ts +0 -92
- package/src/orchestrator/wire.ts +0 -212
package/src/cache/cas-backend.ts
CHANGED
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
// reference impls so downstream work (R2, otel-bridge, distributed-ci)
|
|
19
19
|
// can rely on the type. Byte-identical behaviour today.
|
|
20
20
|
|
|
21
|
-
import { rm } from 'node:fs/promises'
|
|
21
|
+
import { mkdir, rename, rm } from 'node:fs/promises'
|
|
22
22
|
import path from 'node:path'
|
|
23
23
|
import type { Digest } from './digest.js'
|
|
24
24
|
|
|
25
|
+
/** Per-process counter for put() temp names; uniqueness only. */
|
|
26
|
+
let casTmpSeq = 0
|
|
27
|
+
|
|
25
28
|
export interface CASBackend {
|
|
26
29
|
/** Write bytes under `digest`. Idempotent — putting the same digest twice is a no-op. */
|
|
27
30
|
put(digest: Digest, bytes: Uint8Array): Promise<void>
|
|
@@ -78,7 +81,22 @@ export class FsCASBackend implements CASBackend {
|
|
|
78
81
|
`FsCASBackend.put: sizeBytes mismatch (digest=${digest.sizeBytes}, actual=${bytes.byteLength})`,
|
|
79
82
|
)
|
|
80
83
|
}
|
|
81
|
-
|
|
84
|
+
// Temp + rename, the same shape `Cache.writeArtifactAndIndex` and the
|
|
85
|
+
// archive extractor use: two writers of the SAME content-addressed
|
|
86
|
+
// blob are expected (that is what content-addressing invites), and a
|
|
87
|
+
// plain write lets a reader observe a half-written file under a name
|
|
88
|
+
// that promises complete bytes. rename(2) is atomic, so a concurrent
|
|
89
|
+
// reader sees either the old file or the whole new one.
|
|
90
|
+
const final = this.pathFor(digest)
|
|
91
|
+
const tmp = `${final}.tmp-${process.pid.toString(36)}-${(casTmpSeq++).toString(36)}`
|
|
92
|
+
await mkdir(this.rootDir, { recursive: true })
|
|
93
|
+
try {
|
|
94
|
+
await Bun.write(tmp, bytes)
|
|
95
|
+
await rename(tmp, final)
|
|
96
|
+
} catch (err) {
|
|
97
|
+
await rm(tmp, { force: true })
|
|
98
|
+
throw err
|
|
99
|
+
}
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
async get(digest: Digest): Promise<Uint8Array | null> {
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Several declared cache layers, consulted in declaration order. Lookup
|
|
2
|
+
// walks the layers until one answers; a save reaches every layer; the FIRST
|
|
3
|
+
// layer owns the run index (history, stats, prune) so a run is recorded
|
|
4
|
+
// once. Restore goes to the layer that produced the hit — remembered per
|
|
5
|
+
// hash — because an entry's artifact lives wherever it was found.
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
Cache,
|
|
9
|
+
CacheEntry,
|
|
10
|
+
CacheGetContext,
|
|
11
|
+
CacheKeyInput,
|
|
12
|
+
CacheLayer,
|
|
13
|
+
CacheStats,
|
|
14
|
+
CacheStatsOptions,
|
|
15
|
+
IngestMeta,
|
|
16
|
+
InvocationRecord,
|
|
17
|
+
OutputDirRow,
|
|
18
|
+
OutputFileRow,
|
|
19
|
+
PruneOptions,
|
|
20
|
+
PruneResult,
|
|
21
|
+
RunRecord,
|
|
22
|
+
} from './cache.js'
|
|
23
|
+
|
|
24
|
+
export class ChainedCache implements CacheLayer {
|
|
25
|
+
readonly hasRemote: boolean
|
|
26
|
+
private readonly hitLayer = new Map<string, CacheLayer>()
|
|
27
|
+
|
|
28
|
+
constructor(readonly layers: readonly CacheLayer[]) {
|
|
29
|
+
if (layers.length < 2) throw new Error('ChainedCache needs at least two layers')
|
|
30
|
+
this.hasRemote = layers.some((l) => l.hasRemote === true)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get local(): Cache | undefined {
|
|
34
|
+
return this.layers[0]!.local
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private owner(hash: string): CacheLayer {
|
|
38
|
+
return this.hitLayer.get(hash) ?? this.layers[0]!
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
key(input: CacheKeyInput): Promise<string> {
|
|
42
|
+
return this.layers[0]!.key(input)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async get(hash: string, ctx?: CacheGetContext): Promise<CacheEntry | null> {
|
|
46
|
+
for (const layer of this.layers) {
|
|
47
|
+
const entry = await layer.get(hash, ctx)
|
|
48
|
+
if (entry !== null) {
|
|
49
|
+
this.hitLayer.set(hash, layer)
|
|
50
|
+
return entry
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async has(hash: string): Promise<'local' | 'remote' | null> {
|
|
57
|
+
for (const layer of this.layers) {
|
|
58
|
+
const where = await layer.has(hash)
|
|
59
|
+
if (where !== null) {
|
|
60
|
+
this.hitLayer.set(hash, layer)
|
|
61
|
+
return where
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async prefetch(hash: string, ctx?: CacheGetContext): Promise<boolean> {
|
|
68
|
+
for (const layer of this.layers) {
|
|
69
|
+
if (await layer.prefetch(hash, ctx)) {
|
|
70
|
+
this.hitLayer.set(hash, layer)
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async remoteHasMany(hashes: readonly string[]): Promise<Set<string> | null> {
|
|
78
|
+
// The caller (remote-prefetch) treats a non-null answer as authoritative
|
|
79
|
+
// for the WHOLE chain: complement = absent, broadcast. That is sound only
|
|
80
|
+
// if EVERY remote layer answered — a partial union would poison a layer
|
|
81
|
+
// that cannot batch with another layer's negatives, and its later lazy
|
|
82
|
+
// get() would skip a real remote hit. So: each answering layer gets its
|
|
83
|
+
// OWN complement marked here (its own truth — this also spares it the
|
|
84
|
+
// per-hash GETs for hashes only a sibling holds), and the merged answer
|
|
85
|
+
// is returned only when no remote layer was left unanswered.
|
|
86
|
+
let out: Set<string> | null = null
|
|
87
|
+
let complete = true
|
|
88
|
+
for (const layer of this.layers) {
|
|
89
|
+
if (layer.hasRemote !== true) continue
|
|
90
|
+
const found = (await layer.remoteHasMany?.(hashes)) ?? null
|
|
91
|
+
if (found === null) {
|
|
92
|
+
complete = false
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
layer.markRemoteAbsent?.(hashes.filter((h) => !found.has(h)))
|
|
96
|
+
out ??= new Set()
|
|
97
|
+
for (const h of found) out.add(h)
|
|
98
|
+
}
|
|
99
|
+
return complete ? out : null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
markRemoteAbsent(hashes: Iterable<string>): void {
|
|
103
|
+
const list = [...hashes]
|
|
104
|
+
for (const layer of this.layers) layer.markRemoteAbsent?.(list)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async drainUploads(): Promise<void> {
|
|
108
|
+
await Promise.all(this.layers.map((l) => l.drainUploads?.()))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
loadOutputFilesBatch(hashes: readonly string[]): Map<string, OutputFileRow[]> {
|
|
112
|
+
const out = new Map<string, OutputFileRow[]>()
|
|
113
|
+
for (const layer of this.layers) {
|
|
114
|
+
for (const [h, rows] of layer.loadOutputFilesBatch(hashes)) {
|
|
115
|
+
if (!out.has(h)) out.set(h, rows)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return out
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
isOutputsCurrent(projectDir: string, expected: readonly OutputFileRow[]): Promise<boolean> {
|
|
122
|
+
return this.layers[0]!.isOutputsCurrent(projectDir, expected)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The directory short-circuit lives with the layer that owns the local
|
|
126
|
+
// rows — the first, like the file check.
|
|
127
|
+
recordOutputDirs(hash: string, projectDir: string, prefixes: readonly string[]): Promise<void> {
|
|
128
|
+
return this.layers[0]!.recordOutputDirs?.(hash, projectDir, prefixes) ?? Promise.resolve()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
loadOutputDirsBatch(hashes: readonly string[]): Map<string, OutputDirRow[]> {
|
|
132
|
+
return this.layers[0]!.loadOutputDirsBatch?.(hashes) ?? new Map()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
outputDirsCurrent(projectDir: string, rows: readonly OutputDirRow[]): Promise<boolean> {
|
|
136
|
+
return this.layers[0]!.outputDirsCurrent?.(projectDir, rows) ?? Promise.resolve(false)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
restoreOutputs(hash: string, projectDir: string, workspaceRoot?: string): Promise<void> {
|
|
140
|
+
return this.owner(hash).restoreOutputs(hash, projectDir, workspaceRoot)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async save(args: Parameters<CacheLayer['save']>[0]): Promise<void> {
|
|
144
|
+
// Layers wrapping the SAME local handle (two remote plugins over
|
|
145
|
+
// ctx.localCache) would each pack + write the identical artifact; the
|
|
146
|
+
// first write is the only one that matters, so later layers get
|
|
147
|
+
// `skipLocalWrite` and go straight to their remote upload (which reads
|
|
148
|
+
// the artifact the first layer just wrote — same handle, same path).
|
|
149
|
+
const seenLocals = new Set<NonNullable<CacheLayer['local']>>()
|
|
150
|
+
for (const layer of this.layers) {
|
|
151
|
+
const local = layer.local
|
|
152
|
+
const skip = local !== undefined && seenLocals.has(local)
|
|
153
|
+
await layer.save(skip ? { ...args, skipLocalWrite: true } : args)
|
|
154
|
+
if (local !== undefined) seenLocals.add(local)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
ingest(hash: string, compressed: Uint8Array, meta: IngestMeta): Promise<void> {
|
|
159
|
+
return this.layers[0]!.ingest(hash, compressed, meta)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
recordRun(run: RunRecord): void {
|
|
163
|
+
this.layers[0]!.recordRun(run)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
recordRuns(runs: readonly RunRecord[]): void {
|
|
167
|
+
this.layers[0]!.recordRuns(runs)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
recordRunBundle(bundle: { runs: readonly RunRecord[]; invocation: InvocationRecord }): void {
|
|
171
|
+
this.layers[0]!.recordRunBundle(bundle)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
stats(opts?: CacheStatsOptions): CacheStats {
|
|
175
|
+
return this.layers[0]!.stats(opts)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
hashFile(filePath: string): Promise<string> {
|
|
179
|
+
return this.layers[0]!.hashFile(filePath)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
outputsPath(hash: string): string {
|
|
183
|
+
return this.owner(hash).outputsPath(hash)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
prune(options: PruneOptions): Promise<PruneResult> {
|
|
187
|
+
return this.layers[0]!.prune(options)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
close(): void {
|
|
191
|
+
// Every layer closes even if an earlier one throws; the first error wins.
|
|
192
|
+
let failure: unknown
|
|
193
|
+
for (const layer of this.layers) {
|
|
194
|
+
try {
|
|
195
|
+
layer.close()
|
|
196
|
+
} catch (err) {
|
|
197
|
+
failure ??= err
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (failure !== undefined) throw failure
|
|
201
|
+
}
|
|
202
|
+
}
|
package/src/cache/digest.ts
CHANGED
|
@@ -30,18 +30,3 @@ export function makeDigest(hash: string, sizeBytes: number): Digest {
|
|
|
30
30
|
}
|
|
31
31
|
return { hash, sizeBytes }
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
export function digestEqual(a: Digest, b: Digest): boolean {
|
|
35
|
-
return a.hash === b.hash && a.sizeBytes === b.sizeBytes
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Stable wire format for logging / RPC: `hash/sizeBytes`. */
|
|
39
|
-
export function digestString(d: Digest): string {
|
|
40
|
-
return `${d.hash}/${d.sizeBytes}`
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function parseDigest(s: string): Digest {
|
|
44
|
-
const slash = s.indexOf('/')
|
|
45
|
-
if (slash === -1) throw new Error(`invalid digest string: ${JSON.stringify(s)}`)
|
|
46
|
-
return makeDigest(s.slice(0, slash), Number(s.slice(slash + 1)))
|
|
47
|
-
}
|
package/src/cache/index.ts
CHANGED
|
@@ -5,24 +5,34 @@
|
|
|
5
5
|
export {
|
|
6
6
|
Cache,
|
|
7
7
|
type CacheEntry,
|
|
8
|
+
type CacheKeyInput,
|
|
8
9
|
type CacheLayer,
|
|
9
10
|
type CachePolicy,
|
|
11
|
+
EXECUTED_RUNS_SQL,
|
|
10
12
|
FULL_CACHE_POLICY,
|
|
11
13
|
type InvocationRecord,
|
|
14
|
+
KEYED_RUNS_SQL,
|
|
12
15
|
parseCachePolicy,
|
|
13
16
|
type RunRecord,
|
|
14
17
|
type TaskInputRow,
|
|
15
18
|
WORKSPACE_OUTPUT_PREFIX,
|
|
19
|
+
type OutputDirRow,
|
|
20
|
+
OUTPUT_DIRS_CAP,
|
|
21
|
+
OUTPUT_DIRS_RACY_MS,
|
|
16
22
|
} from './cache.js'
|
|
17
|
-
export { type CASBackend, FsCASBackend
|
|
18
|
-
export { type Digest
|
|
23
|
+
export { type CASBackend, FsCASBackend } from './cas-backend.js'
|
|
24
|
+
export { type Digest } from './digest.js'
|
|
19
25
|
export {
|
|
20
26
|
cleanOutputs,
|
|
21
27
|
cleanWorkspaceOutputs,
|
|
22
28
|
GitFilesCache,
|
|
23
|
-
|
|
29
|
+
applyGitEnumeration,
|
|
30
|
+
gitPathspecs,
|
|
31
|
+
startGitEnumeration,
|
|
32
|
+
type GitEnumeration,
|
|
24
33
|
resolveInputs,
|
|
25
34
|
resolveOutputs,
|
|
26
35
|
resolveWorkspaceOutputs,
|
|
27
36
|
} from './inputs.js'
|
|
28
37
|
export { LayeredCache, type RemoteCacheLayer } from './layered-cache.js'
|
|
38
|
+
export { ChainedCache } from './chained-cache.js'
|