@vzn/vx 0.0.12 → 0.0.15
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 +106 -89
- package/launcher.mjs +36 -28
- package/package.json +5 -5
- package/src/cache/cas-backend.ts +2 -2
- package/src/cache/index.ts +1 -2
- package/src/cache/inputs.ts +6 -0
- package/src/cache/layered-cache.ts +22 -2
- package/src/cli/cache.ts +5 -10
- package/src/cli/help.ts +8 -2
- package/src/cli/info.ts +0 -3
- package/src/cli/run.ts +32 -10
- package/src/config.ts +40 -1
- package/src/graph/dependency-spec.ts +16 -0
- package/src/graph/index.ts +10 -1
- package/src/graph/scheduler.ts +230 -35
- package/src/graph/task-graph.ts +69 -17
- package/src/index.ts +29 -4
- package/src/orchestrator/execute-task.ts +66 -10
- package/src/orchestrator/index.ts +11 -0
- package/src/orchestrator/metrics.ts +351 -0
- package/src/orchestrator/options.ts +26 -5
- package/src/orchestrator/plugin-host.ts +5 -10
- package/src/orchestrator/prepare.ts +20 -11
- package/src/orchestrator/protocol.ts +16 -2
- package/src/orchestrator/resources.ts +59 -0
- package/src/orchestrator/run.ts +45 -0
- package/src/orchestrator/summary.ts +24 -0
- package/src/orchestrator/task-hash.ts +17 -2
- package/src/orchestrator/telemetry.ts +9 -1
- package/src/orchestrator/upstream.ts +34 -6
- package/src/orchestrator/verify.ts +34 -11
- package/src/util/index.ts +1 -0
- package/src/util/size.ts +16 -0
- package/src/workspace/project-loader.ts +43 -1
- package/src/cache/remote-cache.ts +0 -251
- package/src/orchestrator/remote-cache-setup.ts +0 -60
package/README.md
CHANGED
|
@@ -61,45 +61,48 @@ below is built into the binary; no external services required.
|
|
|
61
61
|
vx mcp # Model Context Protocol server (stdio)
|
|
62
62
|
# — Claude Code / Cursor / Continue.dev talk to vx as a typed tool
|
|
63
63
|
|
|
64
|
-
vx
|
|
65
|
-
vx
|
|
66
|
-
|
|
67
|
-
vx
|
|
68
|
-
# /v1/* JSON, SSE events, WS run protocol, CORS *
|
|
64
|
+
npm i -g @vzn/vx-cloud # the service CLI — also a standalone binary, no Bun
|
|
65
|
+
vx-cloud serve --ui --open # team backend + embedded dashboard (token-authenticated)
|
|
66
|
+
vx-cloud agent --url https://ci.acme.dev # join a distributed-execution pool (Nx-Agents-style DTE)
|
|
67
|
+
vx-cloud connect https://ci.acme.dev # one-URL analytics + remote cache for every `vx run`
|
|
69
68
|
```
|
|
70
69
|
|
|
71
70
|
### Open platform highlights
|
|
72
71
|
|
|
73
|
-
- **
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
- **
|
|
83
|
-
|
|
84
|
-
the
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
`
|
|
72
|
+
- **Provider-neutral core.** `vx` runs tasks and nothing else — the
|
|
73
|
+
dashboard, remote cache, distribution, and telemetry all arrive via
|
|
74
|
+
PLUGINS declared in `vx.workspace.ts`. `@vzn/vx-cloud` is the
|
|
75
|
+
first-party one; anyone can write another against the same seams
|
|
76
|
+
(backend / cache / telemetry).
|
|
77
|
+
- **MCP for AI agents, twice.** `vx mcp` (stdio, reads your real
|
|
78
|
+
`cache.db`: `getCacheStats`, `getRunHistory`, `explainCacheKey`,
|
|
79
|
+
`whyDidThisRerun`) and `POST /mcp` on any `vx-cloud serve` — local
|
|
80
|
+
or hosted, behind the bearer token.
|
|
81
|
+
- **Provable cache correctness.** `vx run --verify` re-runs each
|
|
82
|
+
cacheable task and byte-compares outputs; `--verify=inputs` runs it
|
|
83
|
+
sandboxed against the declared inputs and names any undeclared read.
|
|
84
|
+
The only runner that PROVES a cache entry safe instead of hoping.
|
|
85
|
+
- **Distributed CI (DTE).** `vx-cloud agent` joins a session-keyed
|
|
86
|
+
pool; a `vx run` fans tasks out across same-commit agents, outputs
|
|
87
|
+
propagate through the shared artifact store, and a standing pool
|
|
88
|
+
multiplexes concurrent runs with fair scheduling.
|
|
89
|
+
- **Remote cache, one URL.** `vx-cloud connect <url>` wires analytics
|
|
90
|
+
ingest AND a Turborepo-wire-compatible artifact store (HMAC signing,
|
|
91
|
+
trusted/untrusted tiers derived from the token). Third-party Turbo
|
|
92
|
+
cache servers work via two env vars.
|
|
93
|
+
- **OTel CI/CD spans.** Declare `otel()` from `@vzn/vx-otel` in
|
|
94
|
+
`vx.workspace.ts` — OTLP/HTTP traces + metrics with ZERO
|
|
95
|
+
OpenTelemetry SDK dependency; runs flow to Grafana / Honeycomb /
|
|
96
|
+
Datadog, including `--verify` hermeticity verdicts as span status.
|
|
97
|
+
- **Dashboard embedded in the `vx-cloud` binary.** Runs, flamegraphs,
|
|
98
|
+
live run cockpit with the task DAG, per-task logs + artifacts,
|
|
99
|
+
cache-key diffs ("why did this re-run?"), flaky-task detection.
|
|
100
|
+
Nothing to build or install; Docker image on ghcr for hosting.
|
|
101
|
+
- **Predictive scheduling.** Opt in with `predictive: true` — the
|
|
102
|
+
scheduler reads run history and starts the longest expected critical
|
|
103
|
+
path first.
|
|
104
|
+
|
|
105
|
+
Each surface lives behind a design doc under `docs/design/`.
|
|
103
106
|
|
|
104
107
|
## A cache that actually understands your build
|
|
105
108
|
|
|
@@ -130,15 +133,25 @@ with the invariant that keeps it valid —
|
|
|
130
133
|
|
|
131
134
|
## Built for trust
|
|
132
135
|
|
|
136
|
+
- **Provable cache correctness.** `vx run --verify` re-runs each
|
|
137
|
+
cacheable task and byte-compares outputs (determinism);
|
|
138
|
+
`--verify=inputs` sandboxes it against the declared inputs and fails
|
|
139
|
+
loud naming any undeclared read (completeness). No other runner can
|
|
140
|
+
prove a cache entry safe.
|
|
133
141
|
- **Signed artifacts.** HMAC signing on the remote-cache wire; with a
|
|
134
142
|
key configured, unsigned or tampered artifacts are rejected and the
|
|
135
143
|
task simply re-runs. A poisoned cache can't reach your machines.
|
|
144
|
+
- **Cache trust tiers.** The server derives trusted/untrusted from the
|
|
145
|
+
bearer token — a fork-PR artifact can never feed a trusted build.
|
|
136
146
|
- **Corruption can't go live.** Artifacts are validated before they
|
|
137
|
-
enter the store
|
|
147
|
+
enter the store (zstd-bomb and oversize downloads refused); bad
|
|
148
|
+
bytes degrade to a cache miss, never a crash.
|
|
138
149
|
- **Clean exits.** SIGINT/SIGTERM reap every child process — no
|
|
139
150
|
orphaned dev servers in CI.
|
|
140
151
|
- **Readiness you can bound.** Persistent tasks gate downstream work
|
|
141
|
-
on a `readyWhen` signal
|
|
152
|
+
on a `readyWhen` signal; `exec.timeout` bounds any task (with
|
|
153
|
+
`--timeout` / workspace defaults), `exec.retries` + `--retry` absorb
|
|
154
|
+
flakes — and retried-then-passed tasks are flagged flaky.
|
|
142
155
|
- **Kernel-level sandboxing**, opt-in per task, that fails the build
|
|
143
156
|
on violation instead of hiding it.
|
|
144
157
|
|
|
@@ -170,29 +183,33 @@ TypeScript config with real imports · task graph with `^task`
|
|
|
170
183
|
resolution that bridges packages without the task · multi-task runs
|
|
171
184
|
with one shared graph · pnpm-style filters and `--affected` ·
|
|
172
185
|
watch mode · `--dry` / `--graph` plans · persistent dev servers ·
|
|
173
|
-
remote caching via two env vars, wire-compatible with
|
|
174
|
-
artifact servers ·
|
|
175
|
-
|
|
186
|
+
remote caching via one URL (or two env vars, wire-compatible with
|
|
187
|
+
existing Turbo artifact servers) · retries, timeouts, `--continue`
|
|
188
|
+
modes · per-layer cache control (`--cache=local:r,remote:`) ·
|
|
189
|
+
`vx info`, `--summarize`, `--profile` Chrome traces, `--report` ·
|
|
190
|
+
`vx cache prune` with TTL and size caps · `vx migrate` from
|
|
191
|
+
turbo.json or an Nx graph.
|
|
176
192
|
|
|
177
193
|
## How it compares
|
|
178
194
|
|
|
179
|
-
| | vx
|
|
180
|
-
| ------------------------- |
|
|
181
|
-
| Fully cached, 100 pkgs¹ | **144 ms**
|
|
182
|
-
| Config | TypeScript, evaluated into the cache key
|
|
183
|
-
| Output ownership | **Strict** — wiped before exec AND restore
|
|
184
|
-
| Clean-tree hashing | **Zero reads** (git index OIDs)
|
|
185
|
-
| Daemon required for speed | **No**
|
|
186
|
-
| Artifact signing | **Hard-fail** on unsigned
|
|
187
|
-
| Per-task sandbox | **Yes** — kernel-level, opt-in
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
|
192
|
-
|
|
|
193
|
-
|
|
|
194
|
-
|
|
|
195
|
-
|
|
|
195
|
+
| | vx | Turborepo | Nx |
|
|
196
|
+
| ------------------------- | --------------------------------------------------------------------- | ------------------------------ | ------------------- |
|
|
197
|
+
| Fully cached, 100 pkgs¹ | **144 ms** | 279 ms | 583+ ms |
|
|
198
|
+
| Config | TypeScript, evaluated into the cache key | JSON (static) | JSON (static) |
|
|
199
|
+
| Output ownership | **Strict** — wiped before exec AND restore | Additive (stale files survive) | Additive |
|
|
200
|
+
| Clean-tree hashing | **Zero reads** (git index OIDs) | git OIDs | re-hash / daemon |
|
|
201
|
+
| Daemon required for speed | **No** | Optional | Yes |
|
|
202
|
+
| Artifact signing | **Hard-fail** on unsigned | Soft | No |
|
|
203
|
+
| Per-task sandbox | **Yes** — kernel-level, opt-in | No | No |
|
|
204
|
+
| Provable cache safety | **Yes** — `--verify` (determinism) + `--verify=inputs` (completeness) | No | No |
|
|
205
|
+
| MCP server for AI agents | **Yes** (`vx mcp` stdio + `POST /mcp` on the serve) | No | No |
|
|
206
|
+
| Distributed CI execution | **Yes** — OSS, self-hostable (`vx-cloud agent`, Nx-DTE-style) | No | Paid (Nx Cloud DTE) |
|
|
207
|
+
| Dashboard SPA | **Yes** — embedded in `vx-cloud serve --ui`: runs, logs, DAG, flaky | No | Paid |
|
|
208
|
+
| Self-hosted cloud | **Yes** — `vx-cloud` binary or the ghcr Docker image; one stack | Vercel-only | No (proprietary) |
|
|
209
|
+
| Plugin API | **Yes** — backend / cache / telemetry seams | No | Yes (TS-tied) |
|
|
210
|
+
| Predictive scheduling | **Yes** (opt-in: `predictive: true`) | No | No |
|
|
211
|
+
| OTel CI/CD spans | **Yes** — `otel()` plugin, zero OTel-SDK deps | No | Paid |
|
|
212
|
+
| Install | **Single binary** — npm or 1 curl line, no Node/Bun needed | npm + Node | npm + Node |
|
|
196
213
|
|
|
197
214
|
¹ Wall-clock, direct binaries, same machine and workspace — full
|
|
198
215
|
methodology and more scenarios in
|
|
@@ -251,22 +268,20 @@ Side-by-side feature matrix + every known gap: [`docs/comparison.md`](./docs/com
|
|
|
251
268
|
`bin.ts → cli/index.ts` dispatches subcommands.
|
|
252
269
|
`orchestrator/run.ts:run()` calls `prepareRun()` which discovers the
|
|
253
270
|
workspace, loads configs, builds the package + task graph, opens the
|
|
254
|
-
cache (local SQLite + optional remote layer),
|
|
255
|
-
`
|
|
256
|
-
|
|
257
|
-
|
|
271
|
+
cache (local SQLite + optional remote layer), and installs plugins
|
|
272
|
+
from `vx.workspace.ts`. The two-tier scheduler runs the graph in
|
|
273
|
+
topological order with bounded concurrency (confirmed cache hits
|
|
274
|
+
restore ahead of their deps); each task hits the cache (hash → get →
|
|
258
275
|
restore on hit; spawn → save on miss) or short-circuits as a group /
|
|
259
|
-
persistent. Every observation flows through one event bus —
|
|
260
|
-
terminal renderer
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
module has a docs page; every interface is a swappable seam.
|
|
276
|
+
persistent. Every observation flows through one event bus — the
|
|
277
|
+
terminal renderer subscribes directly, and PLUGINS receive the
|
|
278
|
+
versioned telemetry contract (`TelemetryRecord` / `RunSummaryRecord`)
|
|
279
|
+
— that's how `otel()` and `cloud()` export without core knowing them.
|
|
280
|
+
Core never imports a plugin; the arrow only points plugin → core.
|
|
281
|
+
Every module has a docs page; every interface is a swappable seam.
|
|
265
282
|
|
|
266
283
|
Read [`docs/architecture.md`](./docs/architecture.md) for the module
|
|
267
|
-
map
|
|
268
|
-
[`docs/design/`](./docs/design/) and
|
|
269
|
-
[`docs/progress/implementation-log-2026-06.md`](./docs/progress/implementation-log-2026-06.md).
|
|
284
|
+
map; the design record lives under [`docs/design/`](./docs/design/).
|
|
270
285
|
|
|
271
286
|
## Documentation
|
|
272
287
|
|
|
@@ -287,8 +302,8 @@ Full technical docs live under [`docs/`](./docs/):
|
|
|
287
302
|
- [`architecture-north-star-2026-06.md`](./docs/design/architecture-north-star-2026-06.md) — the unified vision
|
|
288
303
|
- [`architecture-review-2026-06.md`](./docs/design/architecture-review-2026-06.md) — review + applied checklist
|
|
289
304
|
- [`wire-protocol-2026-06.md`](./docs/design/wire-protocol-2026-06.md) — JSON-RPC 2.0 + OTel envelope (shipped)
|
|
290
|
-
- [`distributed-ci-2026-06.md`](./docs/design/distributed-ci-2026-06.md) — coordinator
|
|
291
|
-
- [`vx-cloud-2026-06.md`](./docs/design/vx-cloud-2026-06.md) — original CF cloud (superseded
|
|
305
|
+
- [`distributed-ci-2026-06.md`](./docs/design/distributed-ci-2026-06.md) — original coordinator/worker (superseded by `vx-cloud agent` DTE — `distributed-execution-2026-07.md`)
|
|
306
|
+
- [`vx-cloud-2026-06.md`](./docs/design/vx-cloud-2026-06.md) — original CF cloud (superseded by the standalone `vx-cloud` service)
|
|
292
307
|
- [`extension-protocol-2026-06.md`](./docs/design/extension-protocol-2026-06.md) — subscriber/inspector/driver/plugin (Phase 1 shipped)
|
|
293
308
|
- [`predictive-execution-2026-06.md`](./docs/design/predictive-execution-2026-06.md) — history-aware scheduling (Phase A-B shipped)
|
|
294
309
|
- [`docs/progress/implementation-log-2026-06.md`](./docs/progress/implementation-log-2026-06.md) — phase-by-phase narrative
|
|
@@ -296,25 +311,27 @@ Full technical docs live under [`docs/`](./docs/):
|
|
|
296
311
|
## Status
|
|
297
312
|
|
|
298
313
|
**Pre-alpha.** The schema is settling; we bump `CACHE_VERSION` rather
|
|
299
|
-
than maintain back-compat. **
|
|
300
|
-
every commit**; the project dogfoods itself (`
|
|
314
|
+
than maintain back-compat. **1,400+ tests (core + packages); CI green
|
|
315
|
+
on every commit**; the project dogfoods itself (`vx run ci`).
|
|
316
|
+
Published on npm: [`@vzn/vx`](https://www.npmjs.com/package/@vzn/vx)
|
|
317
|
+
and `@vzn/vx-cloud`, both as prebuilt standalone binaries.
|
|
301
318
|
|
|
302
319
|
Production readiness for the **core task runner**: the semantics are
|
|
303
|
-
solid. The rough
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
|
309
|
-
|
|
|
310
|
-
|
|
|
311
|
-
| `vx mcp`
|
|
312
|
-
| `vx serve` (
|
|
313
|
-
| `vx
|
|
314
|
-
| Plugin API
|
|
315
|
-
| Predictive scheduling
|
|
316
|
-
| Dashboard SPA (`
|
|
317
|
-
| OTel export (`@vzn/vx-otel`
|
|
320
|
+
solid. The main operational rough edge is Windows (unsupported).
|
|
321
|
+
|
|
322
|
+
Production readiness for the **platform layer**:
|
|
323
|
+
|
|
324
|
+
| Surface | Maturity | Notes |
|
|
325
|
+
| ---------------------------------------------- | -------------------------------- | ---------------------------------------------------------------- |
|
|
326
|
+
| Core task runner + caching | **production-ready** | dogfooded continuously; 1,150+ core tests, all green |
|
|
327
|
+
| `vx run --verify` (provable cache correctness) | **shippable** | determinism + input-completeness proofs; CI-gate recipe in docs |
|
|
328
|
+
| `vx mcp` + serve `POST /mcp` | **shippable** | live cache.db tools (stdio); serve-hosted MCP behind the bearer |
|
|
329
|
+
| `vx-cloud serve` (ingest + dashboard + /v8) | **shippable** | token-auth, multi-workspace, artifact store, ghcr image |
|
|
330
|
+
| `vx-cloud agent` distributed execution | **shippable for self-hosted CI** | session-keyed DTE, shared pools, fair multi-run scheduling |
|
|
331
|
+
| Plugin API (backend / cache / telemetry) | **shippable** | crash-isolated; `cloud()` + `otel()` are ordinary plugins |
|
|
332
|
+
| Predictive scheduling | **shippable as opt-in** | gated on `predictive: true` + observed data |
|
|
333
|
+
| Dashboard SPA (embedded in `vx-cloud`) | **iterating** | runs/logs/DAG/flaky live; data-first entity redesign in progress |
|
|
334
|
+
| OTel export (`@vzn/vx-otel`) | **shippable** | declare `otel()` in `vx.workspace.ts`; OTLP traces + metrics |
|
|
318
335
|
|
|
319
336
|
## Development
|
|
320
337
|
|
package/launcher.mjs
CHANGED
|
@@ -1,44 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// npm launcher for
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// npm launcher for a vx command — the entry a published package's `bin` points
|
|
3
|
+
// at. It execs the prebuilt standalone binary shipped as a per-platform
|
|
4
|
+
// optionalDependency, so end users get the command WITHOUT installing Bun. ONE
|
|
5
|
+
// launcher serves both `@vzn/vx` and `@vzn/vx-cloud`: everything is derived from
|
|
6
|
+
// this package's OWN name, read from the package.json sitting beside this file.
|
|
7
|
+
//
|
|
8
|
+
// @vzn/vx → platform pkg @vzn/vx-<key>, binary `vx`
|
|
9
|
+
// @vzn/vx-cloud → platform pkg @vzn/vx-cloud-<key>, binary `vx-cloud`
|
|
7
10
|
//
|
|
8
11
|
// Resolution order:
|
|
9
|
-
// 1. the matching
|
|
10
|
-
//
|
|
11
|
-
// 2. a source fallback: `bun
|
|
12
|
-
//
|
|
12
|
+
// 1. the matching <name>-<platform> optionalDependency's binary (the normal
|
|
13
|
+
// path — esbuild/turborepo/biome model, no install-time download);
|
|
14
|
+
// 2. a source fallback: `bun <sourceEntry>` (a source checkout, or a platform
|
|
15
|
+
// with no prebuilt binary if the user happens to have Bun).
|
|
13
16
|
// Anything else is a clear, actionable error.
|
|
14
17
|
|
|
15
18
|
import { spawnSync } from 'node:child_process'
|
|
16
19
|
import { existsSync } from 'node:fs'
|
|
17
20
|
import { createRequire } from 'node:module'
|
|
21
|
+
import { constants as osConstants } from 'node:os'
|
|
18
22
|
import { dirname, join } from 'node:path'
|
|
19
23
|
import { fileURLToPath } from 'node:url'
|
|
20
24
|
|
|
21
25
|
const require = createRequire(import.meta.url)
|
|
22
26
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
27
|
+
const pkg = require('./package.json')
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
// Derive the platform-package prefix + binary basename from this package's name
|
|
30
|
+
// so the same launcher works for @vzn/vx and @vzn/vx-cloud. `base` is the
|
|
31
|
+
// unscoped name (vx / vx-cloud) — the command AND the binary filename inside the
|
|
32
|
+
// platform package. `vxSourceEntry` (a package.json field) is the source-mode
|
|
33
|
+
// entry: `src/bin.ts` for vx, `src/cli/bin.ts` for vx-cloud.
|
|
34
|
+
const name = pkg.name
|
|
35
|
+
const base = name.replace(/^@[^/]+\//, '')
|
|
36
|
+
const sourceEntry = pkg.vxSourceEntry ?? 'src/bin.ts'
|
|
30
37
|
|
|
38
|
+
const SUPPORTED = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64']
|
|
31
39
|
const key = `${process.platform}-${process.arch}`
|
|
32
40
|
const args = process.argv.slice(2)
|
|
33
41
|
|
|
34
42
|
function platformBinary() {
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
if (!SUPPORTED.includes(key)) return undefined
|
|
44
|
+
const platformPkg = `${name}-${key}`
|
|
37
45
|
try {
|
|
38
46
|
// No `exports` restriction on the platform packages, so package.json
|
|
39
47
|
// resolves; the binary sits beside it. Works under npm hoisting + pnpm.
|
|
40
|
-
const manifest = require.resolve(`${
|
|
41
|
-
const bin = join(dirname(manifest),
|
|
48
|
+
const manifest = require.resolve(`${platformPkg}/package.json`)
|
|
49
|
+
const bin = join(dirname(manifest), base)
|
|
42
50
|
return existsSync(bin) ? bin : undefined
|
|
43
51
|
} catch {
|
|
44
52
|
return undefined
|
|
@@ -53,11 +61,11 @@ function hasBun() {
|
|
|
53
61
|
function run(cmd, cmdArgs) {
|
|
54
62
|
const res = spawnSync(cmd, cmdArgs, { stdio: 'inherit' })
|
|
55
63
|
if (res.error) {
|
|
56
|
-
process.stderr.write(
|
|
64
|
+
process.stderr.write(`${base}: failed to launch (${res.error.message})\n`)
|
|
57
65
|
process.exit(1)
|
|
58
66
|
}
|
|
59
67
|
// Mirror the child's exit; a signal death maps to the POSIX 128+signo code.
|
|
60
|
-
if (res.signal) process.exit(1)
|
|
68
|
+
if (res.signal) process.exit(128 + (osConstants.signals[res.signal] ?? 1))
|
|
61
69
|
process.exit(res.status ?? 0)
|
|
62
70
|
}
|
|
63
71
|
|
|
@@ -68,17 +76,17 @@ if (bin !== undefined) {
|
|
|
68
76
|
|
|
69
77
|
// No prebuilt binary for this platform — fall back to the shipped source if Bun
|
|
70
78
|
// is available (a source checkout, or an unsupported platform + Bun installed).
|
|
71
|
-
const
|
|
72
|
-
if (existsSync(
|
|
73
|
-
run('bun', [
|
|
79
|
+
const source = join(here, sourceEntry)
|
|
80
|
+
if (existsSync(source) && hasBun()) {
|
|
81
|
+
run('bun', [source, ...args])
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
const supported =
|
|
84
|
+
const supported = SUPPORTED.join(', ')
|
|
77
85
|
process.stderr.write(
|
|
78
|
-
|
|
86
|
+
`${base}: no prebuilt binary for ${key}.\n` +
|
|
79
87
|
` Supported platforms: ${supported}.\n` +
|
|
80
88
|
` If your platform should be supported, reinstall so npm fetches the\n` +
|
|
81
|
-
` matching
|
|
82
|
-
` run
|
|
89
|
+
` matching ${name}-${key} optionalDependency, or install Bun (>=1.3) to\n` +
|
|
90
|
+
` run ${base} from source.\n`,
|
|
83
91
|
)
|
|
84
92
|
process.exit(1)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"description": "An open, extensible monorepo task runner.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
"node": ">=18"
|
|
18
18
|
},
|
|
19
19
|
"optionalDependencies": {
|
|
20
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
21
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
22
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
23
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
20
|
+
"@vzn/vx-linux-x64": "0.0.15",
|
|
21
|
+
"@vzn/vx-linux-arm64": "0.0.15",
|
|
22
|
+
"@vzn/vx-darwin-x64": "0.0.15",
|
|
23
|
+
"@vzn/vx-darwin-arm64": "0.0.15"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@anthropic-ai/sandbox-runtime": "^0.0.56"
|
package/src/cache/cas-backend.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// CASBackend — pluggable content-addressed storage beneath the cache.
|
|
2
2
|
//
|
|
3
|
-
// Today's Cache + LayeredCache
|
|
4
|
-
//
|
|
3
|
+
// Today's Cache + LayeredCache both bundle "where do the bytes live"
|
|
4
|
+
// with "how do we look up entries metadata + key derivation."
|
|
5
5
|
// CASBackend separates the former so a future R2/S3 backend
|
|
6
6
|
// or REAPI CAS bridge can drop in without touching the
|
|
7
7
|
// orchestrator or the SQL entries index.
|
package/src/cache/index.ts
CHANGED
package/src/cache/inputs.ts
CHANGED
|
@@ -34,6 +34,12 @@ const ALWAYS_IGNORE = [
|
|
|
34
34
|
'**/.vx/**',
|
|
35
35
|
'**/*.tsbuildinfo',
|
|
36
36
|
'**/vx-lock.json',
|
|
37
|
+
// `bun build --compile` writes a transient `.<hash>-<n>.bun-build` intermediate
|
|
38
|
+
// in the cwd. It never rests on disk, so it can't be a real input — but a
|
|
39
|
+
// broad `inputs.files: ['**/*']` on a compile task would try to hash the temp
|
|
40
|
+
// file that a CONCURRENT compile is mid-write, racing to EACCES/ENOENT. Always
|
|
41
|
+
// exclude it (vx is Bun-native; compiling standalone binaries is a common task).
|
|
42
|
+
'**/*.bun-build',
|
|
37
43
|
]
|
|
38
44
|
|
|
39
45
|
const DEFAULT_FILE_GLOBS: readonly string[] = ['**/*']
|
|
@@ -32,7 +32,27 @@ import type {
|
|
|
32
32
|
SaveArgs,
|
|
33
33
|
} from './cache.js'
|
|
34
34
|
import { FULL_CACHE_POLICY } from './cache.js'
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What a remote cache layer must provide — THE plugin seam for remote
|
|
38
|
+
* caching. Core ships no wire client; a plugin's `cache` capability (or an
|
|
39
|
+
* embedder via `RunOptions.remoteCache`) supplies an implementation speaking
|
|
40
|
+
* whatever protocol it wants, and `LayeredCache` owns everything else:
|
|
41
|
+
* policy gating, in-flight dedup, remote provenance, and the never-fail
|
|
42
|
+
* contract (implementations THROW on failure; LayeredCache degrades every
|
|
43
|
+
* throw to a cache miss via `onRemoteError`). The artifact bytes are the
|
|
44
|
+
* local `<hash>.tar.zst` verbatim. See
|
|
45
|
+
* docs/design/native-cache-wire-2026-07.md.
|
|
46
|
+
*/
|
|
47
|
+
export interface RemoteCacheLayer {
|
|
48
|
+
/** Existence probe (drives the plan path's `--dry` remote prediction). */
|
|
49
|
+
has(hash: string): Promise<boolean>
|
|
50
|
+
/** Fetch an artifact's bytes; `null` = miss. `durationMs` is the
|
|
51
|
+
* producing task's duration when the wire carries it. */
|
|
52
|
+
get(hash: string): Promise<{ body: ArrayBuffer; durationMs: number | undefined } | null>
|
|
53
|
+
/** Store an artifact (fire-and-forget from LayeredCache's perspective). */
|
|
54
|
+
put(hash: string, body: ArrayBuffer | Uint8Array, meta: { durationMs: number }): Promise<void>
|
|
55
|
+
}
|
|
36
56
|
|
|
37
57
|
/**
|
|
38
58
|
* Cap on concurrent background PUTs. Keeps a burst of cache misses from
|
|
@@ -90,7 +110,7 @@ export class LayeredCache implements CacheLayer {
|
|
|
90
110
|
|
|
91
111
|
constructor(
|
|
92
112
|
private readonly local: Cache,
|
|
93
|
-
private readonly remote:
|
|
113
|
+
private readonly remote: RemoteCacheLayer,
|
|
94
114
|
private readonly options: LayeredCacheOptions = {},
|
|
95
115
|
) {
|
|
96
116
|
this.policy = options.policy ?? FULL_CACHE_POLICY
|
package/src/cli/cache.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { Cache } from '../cache/index.js'
|
|
2
|
+
import { parseSize } from '../util/index.js'
|
|
2
3
|
import { findWorkspaceRoot, loadWorkspaceConfig, resolveCacheDir } from '../workspace/index.js'
|
|
3
4
|
import { formatBytes } from './format.js'
|
|
4
5
|
|
|
6
|
+
// parseSize moved to `util` (the orchestrator's resource resolver needs it
|
|
7
|
+
// and can't import cli); re-exported here so existing callers are unchanged.
|
|
8
|
+
export { parseSize } from '../util/index.js'
|
|
9
|
+
|
|
5
10
|
export async function cacheCmd(args: readonly string[]): Promise<number> {
|
|
6
11
|
const [sub, ...rest] = args
|
|
7
12
|
switch (sub) {
|
|
@@ -89,13 +94,3 @@ export function parseDuration(input: string): number | null {
|
|
|
89
94
|
const mult = unit === 's' ? 1000 : unit === 'm' ? 60_000 : unit === 'h' ? 3_600_000 : 86_400_000
|
|
90
95
|
return n * mult
|
|
91
96
|
}
|
|
92
|
-
|
|
93
|
-
export function parseSize(input: string): number | null {
|
|
94
|
-
const m = input.match(/^(\d+)([KMGT])?B?$/i)
|
|
95
|
-
if (!m) return null
|
|
96
|
-
const n = Number(m[1])
|
|
97
|
-
const u = (m[2] ?? '').toUpperCase()
|
|
98
|
-
const mult =
|
|
99
|
-
u === '' ? 1 : u === 'K' ? 1024 : u === 'M' ? 1024 * 1024 : u === 'G' ? 1024 ** 3 : 1024 ** 4
|
|
100
|
-
return n * mult
|
|
101
|
-
}
|
package/src/cli/help.ts
CHANGED
|
@@ -44,10 +44,16 @@ export function printHelp(): void {
|
|
|
44
44
|
' --timeout <ms> Default per-task timeout for tasks without their own exec.timeout',
|
|
45
45
|
' (above VX_TASK_TIMEOUT env + workspace timeout). Kills + fails',
|
|
46
46
|
' a runaway task. Per-task exec.timeout always wins.',
|
|
47
|
+
' --memory <size> Memory budget (e.g. 8GB) that exec.resources.memory reservations',
|
|
48
|
+
' pack against. Default: total system RAM — pass this in',
|
|
49
|
+
' cgroup-limited containers (the default reads the HOST RAM).',
|
|
50
|
+
' Reservations are per-task config (exec.resources), not flags.',
|
|
47
51
|
' --verify[=<what>] Prove cache correctness. determinism (default): re-run each',
|
|
48
52
|
' executed cacheable task + content-compare outputs. inputs: sandbox',
|
|
49
53
|
' with the declared-input baseline + flag reads of undeclared inputs.',
|
|
50
|
-
'
|
|
54
|
+
' fingerprint: ship output-tree fingerprints for the cross-machine',
|
|
55
|
+
' diff on a connected serve (~1× exec — no re-run). all: everything.',
|
|
56
|
+
' An unsafe task fails the run with the exact paths.',
|
|
51
57
|
' ~2× exec (determinism); CI-shaped. inputs/all need the OS sandbox.',
|
|
52
58
|
' --verify-allow <pkg#task,…> Exempt known-nondeterministic tasks from failing --verify.',
|
|
53
59
|
' --frozen Load configs from vx-lock.json (CI; pair with vx lock --check).',
|
|
@@ -95,7 +101,7 @@ export function printHelp(): void {
|
|
|
95
101
|
' --format <fmt> pretty (default) | json.',
|
|
96
102
|
' vx info Workspace doctor: vx/bun/git versions, project +',
|
|
97
103
|
' task counts, cache dir/entries/size, recent runs,',
|
|
98
|
-
' lock
|
|
104
|
+
' lock status.',
|
|
99
105
|
' vx stats Deprecated: alias of vx info.',
|
|
100
106
|
' vx mcp Model Context Protocol server (stdio) — exposes inspector',
|
|
101
107
|
' RPCs as MCP tools for AI agents (Claude Code, Cursor, …).',
|
package/src/cli/info.ts
CHANGED
|
@@ -48,8 +48,6 @@ export async function infoCmd(args: readonly string[]): Promise<number> {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
const lockPresent = await Bun.file(lockfilePath(root)).exists()
|
|
51
|
-
const remoteConfigured =
|
|
52
|
-
Boolean(process.env.VX_REMOTE_CACHE_URL) && Boolean(process.env.VX_REMOTE_CACHE_TOKEN)
|
|
53
51
|
|
|
54
52
|
const rows: [string, string][] = [
|
|
55
53
|
['vx', VERSION],
|
|
@@ -61,7 +59,6 @@ export async function infoCmd(args: readonly string[]): Promise<number> {
|
|
|
61
59
|
['cache entries', `${stats.entryCount} (${formatBytes(stats.totalBytes)})`],
|
|
62
60
|
['runs (24h)', `${stats.runCountLast24h} (${stats.hitCountLast24h} cache hits)`],
|
|
63
61
|
['vx-lock.json', lockPresent ? 'yes' : 'no'],
|
|
64
|
-
['remote cache', remoteConfigured ? 'yes' : 'no'],
|
|
65
62
|
]
|
|
66
63
|
const labelW = Math.max(...rows.map(([label]) => label.length))
|
|
67
64
|
const lines = rows.map(([label, value]) => `${`${label}:`.padEnd(labelW + 1)} ${value}`)
|