@vzn/vx 0.0.650 → 0.0.652

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 (146) hide show
  1. package/README.md +7 -23
  2. package/package.json +5 -5
  3. package/schemas/last.json +15 -0
  4. package/schemas/show.json +14 -20
  5. package/src/bin.ts +21 -31
  6. package/src/cache/archive.ts +1 -1
  7. package/src/cache/cache.ts +221 -30
  8. package/src/cache/file-hashes.ts +3 -0
  9. package/src/cache/git-inputs.ts +188 -39
  10. package/src/cache/history-stream.ts +134 -0
  11. package/src/cache/history-writer.ts +76 -0
  12. package/src/cache/index.ts +3 -0
  13. package/src/cache/inputs.ts +77 -14
  14. package/src/cache/layer.ts +17 -6
  15. package/src/cache/layered-cache.ts +4 -0
  16. package/src/cache/linked-inputs.ts +3 -0
  17. package/src/cache/lockfile-memo.ts +5 -3
  18. package/src/cache/output-index.ts +5 -2
  19. package/src/cache/remote-lane.ts +2 -1
  20. package/src/cache/run-history.ts +56 -17
  21. package/src/cache/schema.ts +15 -2
  22. package/src/cli/cache.ts +28 -18
  23. package/src/cli/completions.ts +4 -2
  24. package/src/cli/docs.ts +10 -11
  25. package/src/cli/help.ts +14 -25
  26. package/src/cli/index.ts +28 -9
  27. package/src/cli/info.ts +16 -8
  28. package/src/cli/init.ts +23 -15
  29. package/src/cli/last.ts +246 -204
  30. package/src/cli/lock.ts +55 -5
  31. package/src/cli/plan-format.ts +100 -118
  32. package/src/cli/plugin-commands.ts +8 -2
  33. package/src/cli/plugin-templates.ts +36 -45
  34. package/src/cli/run.ts +46 -27
  35. package/src/cli/select.ts +43 -5
  36. package/src/cli/show.ts +45 -52
  37. package/src/cli/task-verb.ts +22 -0
  38. package/src/cli/upgrade.ts +3 -1
  39. package/src/cli/watch-fs.ts +6 -1
  40. package/src/cli/watch-judge.ts +3 -2
  41. package/src/cli/watch-set.ts +4 -4
  42. package/src/cli/watch.ts +94 -52
  43. package/src/cli/why.ts +123 -89
  44. package/src/cli/workspace-config.ts +2 -1
  45. package/src/config.ts +47 -30
  46. package/src/exec/executor.ts +42 -2
  47. package/src/exec/index.ts +1 -0
  48. package/src/exec/kill-tree.ts +58 -5
  49. package/src/exec/local-executor.ts +1 -1
  50. package/src/exec/pipes.ts +152 -0
  51. package/src/exec/runner.ts +199 -182
  52. package/src/exec/sandbox-binds.ts +1 -1
  53. package/src/exec/sandbox-paths.ts +3 -1
  54. package/src/exec/sandbox-runtime.ts +317 -23
  55. package/src/exec/spawn-off.ts +154 -0
  56. package/src/exec/task-proxy.ts +93 -0
  57. package/src/exec/warm-shell.ts +146 -0
  58. package/src/graph/critical-path.ts +40 -28
  59. package/src/graph/definition.ts +16 -3
  60. package/src/graph/priorities.ts +98 -44
  61. package/src/graph/scheduler.ts +17 -11
  62. package/src/graph/task-graph.ts +51 -24
  63. package/src/handoff.ts +10 -7
  64. package/src/history/failure-mode.ts +24 -1
  65. package/src/history/history.ts +5 -0
  66. package/src/history/index.ts +1 -0
  67. package/src/history/metrics.ts +14 -6
  68. package/src/index.ts +7 -3
  69. package/src/orchestrator/admission.ts +1 -1
  70. package/src/orchestrator/command-windows.ts +1 -1
  71. package/src/orchestrator/deferred-outputs.ts +1 -1
  72. package/src/orchestrator/doctor.ts +1 -1
  73. package/src/orchestrator/download-policy.ts +5 -7
  74. package/src/orchestrator/excluded-keys.ts +1 -1
  75. package/src/orchestrator/execute-task.ts +67 -20
  76. package/src/orchestrator/graph-memo.ts +50 -1
  77. package/src/orchestrator/graph-stage.ts +6 -0
  78. package/src/orchestrator/hit-restore.ts +5 -6
  79. package/src/orchestrator/index.ts +3 -0
  80. package/src/orchestrator/keyed-projects.ts +28 -0
  81. package/src/orchestrator/live-usage.ts +16 -11
  82. package/src/orchestrator/local-shortcircuit.ts +2 -2
  83. package/src/orchestrator/locked-digests.ts +63 -0
  84. package/src/orchestrator/lockfile-claim.ts +108 -29
  85. package/src/orchestrator/miss-save.ts +58 -11
  86. package/src/orchestrator/persistent-task.ts +5 -1
  87. package/src/orchestrator/placement.ts +1 -1
  88. package/src/orchestrator/plan.ts +7 -3
  89. package/src/orchestrator/plugin-host.ts +67 -2
  90. package/src/orchestrator/plugin.ts +53 -2
  91. package/src/orchestrator/prepare.ts +16 -1
  92. package/src/orchestrator/projects.ts +4 -1
  93. package/src/orchestrator/remote-prefetch.ts +1 -1
  94. package/src/orchestrator/run-context.ts +29 -10
  95. package/src/orchestrator/run-failures.ts +62 -13
  96. package/src/orchestrator/run-keys.ts +1 -1
  97. package/src/orchestrator/run-records.ts +88 -59
  98. package/src/orchestrator/run-report.ts +1 -1
  99. package/src/orchestrator/run.ts +145 -41
  100. package/src/orchestrator/sandbox-request.ts +52 -9
  101. package/src/orchestrator/shell-verdict.ts +9 -0
  102. package/src/orchestrator/signals.ts +8 -5
  103. package/src/orchestrator/stable-keys.ts +17 -18
  104. package/src/orchestrator/task-artifacts.ts +1 -1
  105. package/src/orchestrator/task-hash.ts +21 -7
  106. package/src/orchestrator/telemetry.ts +5 -1
  107. package/src/report/events.ts +6 -14
  108. package/src/report/forecast.ts +10 -32
  109. package/src/report/framed-output.ts +283 -175
  110. package/src/report/index.ts +8 -3
  111. package/src/report/logger.ts +74 -39
  112. package/src/report/plain-output.ts +3 -1
  113. package/src/report/status-line.ts +49 -357
  114. package/src/report/summary.ts +105 -136
  115. package/src/settings.ts +8 -8
  116. package/src/ui/ansi.ts +48 -0
  117. package/src/ui/components.ts +506 -0
  118. package/src/ui/index.ts +79 -0
  119. package/src/ui/kit.ts +37 -0
  120. package/src/ui/live-region.ts +363 -0
  121. package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
  122. package/src/ui/terminal.ts +51 -0
  123. package/src/ui/theme.ts +224 -0
  124. package/src/ui/version-line.ts +28 -0
  125. package/src/util/config-frame.ts +191 -14
  126. package/src/util/edit-distance.ts +6 -1
  127. package/src/util/index.ts +2 -15
  128. package/src/util/paths.ts +21 -3
  129. package/src/util/secret-mask.ts +39 -3
  130. package/src/util/settle.ts +33 -1
  131. package/src/util/tail.ts +4 -1
  132. package/src/util/timing.ts +4 -16
  133. package/src/workspace/affected.ts +89 -30
  134. package/src/workspace/bracket-literal.ts +2 -2
  135. package/src/workspace/config-schema.ts +70 -63
  136. package/src/workspace/dependencies-lock.ts +191 -0
  137. package/src/workspace/filter.ts +36 -1
  138. package/src/workspace/index.ts +9 -0
  139. package/src/workspace/lockfile.ts +159 -6
  140. package/src/workspace/migration.ts +16 -9
  141. package/src/workspace/workspace.ts +3 -0
  142. package/src/util/colors.ts +0 -133
  143. package/src/util/report.ts +0 -44
  144. package/src/util/ui.ts +0 -258
  145. /package/src/{report/path-links.ts → ui/links.ts} +0 -0
  146. /package/src/{report → ui}/terminal-signals.ts +0 -0
package/README.md CHANGED
@@ -47,9 +47,8 @@ export default defineProject({
47
47
  exec: { command: 'tsc -b' },
48
48
  dependsOn: ['^build'], // dependencies build first
49
49
  inputs: { files: ['src/**', 'tsconfig.json'] },
50
- cache: {
51
- outputs: { files: ['dist/**'] },
52
- },
50
+ outputs: { files: ['dist/**'] },
51
+ cache: true,
53
52
  },
54
53
  },
55
54
  })
@@ -142,9 +141,8 @@ not a way to run the repo. The benchmark below uses native vx config.
142
141
  | Plugin API | **Yes**: every pipeline stage | No | Yes (TS-tied) |
143
142
  | Install | **Single binary**; no Bun, Node optional | npm + Node | npm + Node |
144
143
 
145
- Every gap, and when another tool is the better pick:
146
- [comparison](packages/vx/docs/comparison.md) ·
147
- [vx, Turborepo, Nx, Bazel](https://vznjs.github.io/vx/compare/).
144
+ Every gap: [comparison](packages/vx/docs/comparison.md) ·
145
+ every tool on one suite of cases: [conformance](https://vznjs.github.io/vx/conformance/).
148
146
 
149
147
  ## Fast, without trading correctness
150
148
 
@@ -152,23 +150,9 @@ Every gap, and when another tool is the better pick:
152
150
  - **No source-file reads to hash:** on a clean tree, keys come from git's index; each project's `package.json` and the lockfile are still read.
153
151
  - **One pass:** one git walk per run; each stat and lookup happens once.
154
152
 
155
- <!-- bench:start — generated by private/vx-bench/update-site.ts from results.json; do not hand-edit -->
156
-
157
- | Time added over the ideal run, 1,601 projects, 9,603 tasks | vx | Turborepo | Nx | Vite Task |
158
- | ---------------------------------------------------------- | ---------- | ---------- | ---------- | --------- |
159
- | Nothing changed | **799 ms** | 927 ms | 24.15 s | 12.07 s |
160
- | Nothing changed, outputs restored | **1.28 s** | 2.01 s | 24.45 s | 11.86 s |
161
- | Cold build | **7.57 s** | 9.63 s | 3 min 6 s | 9.01 s |
162
- | Cold build: CPU the runner burns | 36.64 s | 1 min 12 s | 5 min 34 s | 25.32 s |
163
-
164
- Time each tool adds over the ideal run; lower is better.
165
-
166
- 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.
167
- Run 2026-10-25 on linux x64, 4 cores: vx at commit 9abdfb52c, Turborepo 2.11.10, Nx 24.0.1, Vite Task (vite-plus) 1.4.0.
168
-
169
- Same graph, commands and concurrency: [how it is measured](https://vznjs.github.io/vx/benchmarks/).
170
-
171
- <!-- bench:end -->
153
+ The head-to-head against Turborepo, Nx and Vite Task, each in its own
154
+ native config, is re-measured on every change to vx and published on the
155
+ site: [benchmarks](https://vznjs.github.io/vx/benchmarks/).
172
156
 
173
157
  ## Status
174
158
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.650",
3
+ "version": "0.0.652",
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.650",
25
- "@vzn/vx-linux-arm64": "0.0.650",
26
- "@vzn/vx-darwin-x64": "0.0.650",
27
- "@vzn/vx-darwin-arm64": "0.0.650"
24
+ "@vzn/vx-linux-x64": "0.0.652",
25
+ "@vzn/vx-linux-arm64": "0.0.652",
26
+ "@vzn/vx-darwin-x64": "0.0.652",
27
+ "@vzn/vx-darwin-arm64": "0.0.652"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/schemas/last.json CHANGED
@@ -242,7 +242,10 @@
242
242
  "peakRssBytes",
243
243
  "peakCores",
244
244
  "avgRssBytes",
245
+ "queuedMs",
245
246
  "placement",
247
+ "outputBytes",
248
+ "blocks",
246
249
  "wallclockStartNs",
247
250
  "wallclockEndNs",
248
251
  "blockedBy",
@@ -288,10 +291,22 @@
288
291
  "type": ["integer", "null"],
289
292
  "description": "mean resident memory over the samples; null when nothing sampled"
290
293
  },
294
+ "queuedMs": {
295
+ "type": ["integer", "null"],
296
+ "description": "milliseconds it waited, ready, for a slot; null when it never queued"
297
+ },
291
298
  "placement": {
292
299
  "type": ["string", "null"],
293
300
  "description": "where an executor ran it (small#1); null when here"
294
301
  },
302
+ "outputBytes": {
303
+ "type": ["integer", "null"],
304
+ "description": "bytes of its outputs: what its save stored, or its hit restored"
305
+ },
306
+ "blocks": {
307
+ "type": ["integer", "null"],
308
+ "description": "how many tasks were downstream of it"
309
+ },
295
310
  "wallclockStartNs": {
296
311
  "type": ["string", "null"],
297
312
  "description": "decimal nanoseconds from the run's start"
package/schemas/show.json CHANGED
@@ -109,25 +109,8 @@
109
109
  "env": { "$ref": "#/$defs/stringList" },
110
110
  "tasks": { "$ref": "#/$defs/stringList" },
111
111
  "runtime": { "$ref": "#/$defs/stringList" },
112
- "workspaceRuntime": { "$ref": "#/$defs/stringList" }
113
- }
114
- },
115
- "cache": {
116
- "type": "object",
117
- "additionalProperties": false,
118
- "required": ["outputs"],
119
- "properties": {
120
- "outputs": {
121
- "type": "object",
122
- "additionalProperties": false,
123
- "required": ["files"],
124
- "properties": {
125
- "files": { "$ref": "#/$defs/stringList" },
126
- "workspaceFiles": { "$ref": "#/$defs/stringList" },
127
- "clean": { "type": "boolean" }
128
- }
129
- },
130
- "inputStability": {
112
+ "workspaceRuntime": { "$ref": "#/$defs/stringList" },
113
+ "stability": {
131
114
  "type": "object",
132
115
  "additionalProperties": false,
133
116
  "properties": {
@@ -136,7 +119,18 @@
136
119
  }
137
120
  }
138
121
  }
139
- }
122
+ },
123
+ "outputs": {
124
+ "type": "object",
125
+ "additionalProperties": false,
126
+ "required": ["files"],
127
+ "properties": {
128
+ "files": { "$ref": "#/$defs/stringList" },
129
+ "workspaceFiles": { "$ref": "#/$defs/stringList" },
130
+ "clean": { "type": "boolean" }
131
+ }
132
+ },
133
+ "cache": { "type": "boolean" }
140
134
  }
141
135
  }
142
136
  }
package/src/bin.ts CHANGED
@@ -21,32 +21,14 @@ async function main(): Promise<void> {
21
21
  // `vx --version` needs one leaf module; the dispatcher and the util
22
22
  // barrel it pulled in were 20 ms of its 28 (2026-10-01).
23
23
  if (argv.length === 1 && (argv[0] === '--version' || argv[0] === 'version')) {
24
- // `Bun.stdout`, not `process.stdout`: the Node stream's first touch is
25
- // ~7 ms, half of this verb (2026-10-02). A reader gone (EPIPE) is no
26
- // failure of the line.
27
24
  // Handed off, it says which vx ran and which one handed it the call.
28
25
  const from = process.env['VX_HANDED_OFF'] || undefined
29
26
  const where = from === undefined ? '' : ` (${vxPath()}, handed off from ${from})`
30
- // Painted as every other line; piped, the same words a script reads.
31
- // Decided here, not by util's `detectColors`: the util barrel was the
32
- // 20 ms above, and `node:tty` 17 ms; fd 1 as a character device is the
33
- // TTY answer. A CI's log is not painted for this one line.
34
- const [{ VERSION }, { fstatSync }] = await Promise.all([
27
+ const [{ VERSION }, { printVersion }] = await Promise.all([
35
28
  import('./version.js'),
36
- import('node:fs'),
29
+ import('./ui/version-line.js'),
37
30
  ])
38
- const env = process.env
39
- const on = env['NO_COLOR']
40
- ? false
41
- : env['FORCE_COLOR'] !== undefined
42
- ? env['FORCE_COLOR'] !== '0' && env['FORCE_COLOR'] !== 'false'
43
- : env['TERM'] !== 'dumb' && fstatSync(1).isCharacterDevice()
44
- const sgr = (code: string, text: string): string =>
45
- on ? `\x1b[${code}m${text}\x1b[0m` : text
46
- await Bun.write(
47
- Bun.stdout,
48
- `${sgr('1', 'vx')} ${sgr('38;2;6;182;212', VERSION)}${where === '' ? '' : sgr('2', where)}\n`,
49
- ).catch(() => {})
31
+ await printVersion(VERSION, where)
50
32
  settled = true
51
33
  return
52
34
  }
@@ -64,8 +46,9 @@ async function main(): Promise<void> {
64
46
  // is set, and `exitOnceFlushed` exits after both streams have ended.
65
47
  process.exitCode = code
66
48
  } catch (err) {
49
+ const ui = await import('./ui/index.js')
50
+ const { refusalNotice } = ui
67
51
  const {
68
- detectColors,
69
52
  errorCode,
70
53
  errorDocument,
71
54
  fsRefusalHint,
@@ -74,17 +57,12 @@ async function main(): Promise<void> {
74
57
  isUserError,
75
58
  maskedLine,
76
59
  OUT_OF_FDS_HINT,
77
- refusalNotice,
78
- ui,
79
60
  wantsJson,
80
61
  } = await import('./util/index.js')
81
62
  // A plugin's failure quotes what it was told (a remote's reply, a
82
63
  // header), and a config's own throw is its author's text: a secret in
83
64
  // either reached stderr whole (L-11).
84
- const say = (line: string): void =>
85
- void process.stderr.write(
86
- `${ui.tagged('error', maskedLine(line), detectColors(process.stderr))}\n`,
87
- )
65
+ const say = (line: string): void => ui.say('error', maskedLine(line))
88
66
  // UserError (workspace not found, cycle, config invalid, ...) —
89
67
  // print the message only; the stack is noise the user can't act
90
68
  // on. Everything else gets the full stack so internal bugs are
@@ -100,7 +78,11 @@ async function main(): Promise<void> {
100
78
  named === null ? err.message : err.message.slice(named[0].length),
101
79
  err.parts,
102
80
  )
103
- process.stderr.write(maskedLine(ui.render([block], detectColors(process.stderr))))
81
+ ui.data('err', maskedLine(ui.render([block], ui.colorsOf('err'))))
82
+ } else if (cwdGone(err)) {
83
+ // Removed under the shell (a checkout of another branch, `rm -rf` in
84
+ // another): every verb starts from the cwd.
85
+ say('vx: the current directory no longer exists — cd into the workspace again and re-run\n')
104
86
  } else if (isOutOfFds(err)) {
105
87
  say(`vx: ${err.message} — ${OUT_OF_FDS_HINT}\n`)
106
88
  } else if (isFsRefusal(err)) {
@@ -114,7 +96,7 @@ async function main(): Promise<void> {
114
96
  // stdout got nothing to parse, and only prose on stderr to branch on.
115
97
  if (wantsJson(argv)) {
116
98
  const message = err instanceof Error ? err.message : String(err)
117
- process.stdout.write(errorDocument(errorCode(err), maskedLine(message)))
99
+ ui.data('out', errorDocument(errorCode(err), maskedLine(message)))
118
100
  }
119
101
  // Same reason as the success path above: a large stderr is truncated by
120
102
  // `process.exit` too, and an error message cut in half is the one a
@@ -174,13 +156,21 @@ function withCauses(err: unknown): string {
174
156
  process.on('beforeExit', () => {
175
157
  if (settled) return
176
158
  settled = true
177
- ;(require('./util/index.js') as typeof import('./util/index.js')).ui.say(
159
+ ;(require('./ui/index.js') as typeof import('./ui/index.js')).say(
178
160
  'error',
179
161
  'vx: the run stopped before it finished: something it awaited can never settle (a plugin hook?)',
180
162
  )
181
163
  process.exitCode = 1
182
164
  })
183
165
 
166
+ /** `process.cwd()`'s refusal of a directory removed under the process. */
167
+ function cwdGone(err: unknown): boolean {
168
+ return (
169
+ (err as NodeJS.ErrnoException | null)?.code === 'ENOENT' &&
170
+ /\buv_cwd\b/.test((err as Error).message)
171
+ )
172
+ }
173
+
184
174
  // A reader that leaves is not the run's failure. `vx run build | head -1`
185
175
  // closes the pipe after one line; every later write gets EPIPE, which Bun
186
176
  // raises as an `error` event on the stream, and an `error` nobody listens
@@ -350,7 +350,7 @@ export async function planArtifact(args: PackArgs): Promise<ArtifactPlan> {
350
350
  }
351
351
  if (!st.isFile()) {
352
352
  throw new UserError(
353
- `${shown} is not a regular file: vx stores regular files and symlinks only — emit a file there, or narrow cache.outputs.files to the files the task produces, or take it back with a '!' entry`,
353
+ `${shown} is not a regular file: vx stores regular files and symlinks only — emit a file there, or narrow outputs.files to the files the task produces, or take it back with a '!' entry`,
354
354
  )
355
355
  }
356
356
  const mode = st.mode & 0o777