@vzn/vx 0.0.21 → 0.0.23

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 (123) hide show
  1. package/README.md +21 -18
  2. package/package.json +6 -6
  3. package/src/bin.ts +52 -3
  4. package/src/cache/archive.ts +170 -21
  5. package/src/cache/cache.ts +667 -288
  6. package/src/cache/chained-cache.ts +14 -2
  7. package/src/cache/config-evals.ts +29 -6
  8. package/src/cache/file-hashes.ts +63 -35
  9. package/src/cache/git-inputs.ts +398 -69
  10. package/src/cache/index.ts +8 -1
  11. package/src/cache/inputs.ts +403 -102
  12. package/src/cache/key-fold.ts +232 -0
  13. package/src/cache/layer.ts +81 -11
  14. package/src/cache/layered-cache.ts +199 -32
  15. package/src/cache/output-index.ts +109 -10
  16. package/src/cache/run-history.ts +8 -3
  17. package/src/cache/zstd.ts +53 -30
  18. package/src/cli/cache.ts +51 -21
  19. package/src/cli/format.ts +4 -15
  20. package/src/cli/help.ts +13 -3
  21. package/src/cli/index.ts +35 -9
  22. package/src/cli/info.ts +49 -7
  23. package/src/cli/init.ts +17 -5
  24. package/src/cli/last.ts +96 -21
  25. package/src/cli/lock.ts +34 -17
  26. package/src/cli/plan-format.ts +4 -4
  27. package/src/cli/plugin-commands.ts +20 -5
  28. package/src/cli/run.ts +55 -12
  29. package/src/cli/select.ts +23 -13
  30. package/src/cli/show.ts +9 -4
  31. package/src/cli/upgrade.ts +134 -14
  32. package/src/cli/watch.ts +622 -102
  33. package/src/cli/why.ts +8 -16
  34. package/src/cli/workspace-config.ts +21 -9
  35. package/src/config.ts +46 -9
  36. package/src/exec/executor.ts +51 -5
  37. package/src/exec/index.ts +10 -0
  38. package/src/exec/kill-tree.ts +281 -0
  39. package/src/exec/local-executor.ts +1 -1
  40. package/src/exec/runner.ts +327 -82
  41. package/src/exec/sandbox-binds.ts +34 -5
  42. package/src/exec/sandbox-paths.ts +22 -2
  43. package/src/exec/sandbox-runtime.ts +627 -102
  44. package/src/exec/sandbox-violations.ts +36 -23
  45. package/src/graph/dependency-spec.ts +14 -14
  46. package/src/graph/index.ts +12 -2
  47. package/src/graph/priorities.ts +130 -0
  48. package/src/graph/scheduler.ts +182 -134
  49. package/src/graph/task-graph.ts +552 -188
  50. package/src/index.ts +16 -2
  51. package/src/orchestrator/admission.ts +41 -17
  52. package/src/orchestrator/colors.ts +8 -3
  53. package/src/orchestrator/deferred-outputs.ts +22 -7
  54. package/src/orchestrator/doctor.ts +122 -14
  55. package/src/orchestrator/events.ts +71 -2
  56. package/src/orchestrator/excluded-keys.ts +155 -0
  57. package/src/orchestrator/execute-task.ts +331 -37
  58. package/src/orchestrator/failure-recap.ts +121 -0
  59. package/src/orchestrator/fingerprint-watch.ts +89 -0
  60. package/src/orchestrator/framed-output.ts +81 -14
  61. package/src/orchestrator/history.ts +6 -7
  62. package/src/orchestrator/hit-restore.ts +82 -18
  63. package/src/orchestrator/index.ts +7 -1
  64. package/src/orchestrator/keyed-projects.ts +89 -0
  65. package/src/orchestrator/local-shortcircuit.ts +83 -23
  66. package/src/orchestrator/lockfile-claim.ts +152 -32
  67. package/src/orchestrator/logger.ts +116 -20
  68. package/src/orchestrator/metrics.ts +156 -60
  69. package/src/orchestrator/miss-save.ts +106 -43
  70. package/src/orchestrator/options.ts +30 -4
  71. package/src/orchestrator/persistent.ts +41 -21
  72. package/src/orchestrator/placement.ts +55 -25
  73. package/src/orchestrator/plan.ts +24 -11
  74. package/src/orchestrator/plugin-host.ts +17 -23
  75. package/src/orchestrator/plugin.ts +31 -8
  76. package/src/orchestrator/prepare.ts +99 -13
  77. package/src/orchestrator/projects.ts +19 -8
  78. package/src/orchestrator/remote-prefetch.ts +21 -1
  79. package/src/orchestrator/run-artifacts.ts +9 -0
  80. package/src/orchestrator/run-context.ts +19 -4
  81. package/src/orchestrator/run-lock.ts +349 -0
  82. package/src/orchestrator/run-records.ts +6 -23
  83. package/src/orchestrator/run-report.ts +4 -2
  84. package/src/orchestrator/run.ts +264 -57
  85. package/src/orchestrator/sandbox-request.ts +388 -62
  86. package/src/orchestrator/save-lane.ts +4 -7
  87. package/src/orchestrator/shell-verdict.ts +117 -0
  88. package/src/orchestrator/signals.ts +86 -30
  89. package/src/orchestrator/stable-keys.ts +202 -10
  90. package/src/orchestrator/status-line.ts +33 -1
  91. package/src/orchestrator/summary.ts +49 -2
  92. package/src/orchestrator/task-hash.ts +87 -7
  93. package/src/orchestrator/task-log-buffer.ts +7 -7
  94. package/src/orchestrator/telemetry-host.ts +16 -7
  95. package/src/orchestrator/telemetry.ts +69 -28
  96. package/src/orchestrator/upstream.ts +91 -45
  97. package/src/util/bun-version.ts +43 -0
  98. package/src/util/errors.ts +86 -0
  99. package/src/util/hash.ts +10 -1
  100. package/src/util/index.ts +32 -3
  101. package/src/util/paths.ts +147 -5
  102. package/src/util/procfs.ts +20 -0
  103. package/src/util/settle.ts +4 -2
  104. package/src/util/size.ts +33 -0
  105. package/src/util/task-id.ts +20 -0
  106. package/src/util/timing.ts +11 -1
  107. package/src/util/which.ts +48 -0
  108. package/src/workspace/affected.ts +272 -64
  109. package/src/workspace/config-cache.ts +262 -32
  110. package/src/workspace/config-eval.ts +49 -12
  111. package/src/workspace/config-imports.ts +126 -5
  112. package/src/workspace/config-schema.ts +237 -41
  113. package/src/workspace/filter.ts +33 -18
  114. package/src/workspace/fingerprint.ts +29 -11
  115. package/src/workspace/index.ts +7 -0
  116. package/src/workspace/json-data.ts +90 -0
  117. package/src/workspace/load-reads.ts +32 -0
  118. package/src/workspace/lockfile.ts +33 -11
  119. package/src/workspace/migrate-scripts.ts +60 -7
  120. package/src/workspace/migration.ts +40 -1
  121. package/src/workspace/package-graph.ts +154 -16
  122. package/src/workspace/project-loader.ts +227 -77
  123. package/src/workspace/workspace.ts +173 -23
@@ -1,6 +1,11 @@
1
1
  // `vx upgrade [tag]` — self-update the compiled binary in place.
2
- // Downloads the GitHub release asset for this os/arch, writes next to
3
- // the current executable, atomic rename over it.
2
+ // Asks the GitHub release API for the asset of this os/arch and the
3
+ // SHA-256 digest it publishes, downloads the asset, verifies the digest,
4
+ // writes next to the current executable, atomic rename over it. The
5
+ // digest is what makes a cut transfer (a proxy that drops a connection,
6
+ // a disk that fills) or a swapped asset a refusal instead of a binary
7
+ // that does not start: the rename is the last step, and a mismatch
8
+ // replaces nothing.
4
9
  // Named `upgrade` (not `update`) per CLI convention: bun upgrade,
5
10
  // deno upgrade — "update" is what package managers do to indexes.
6
11
 
@@ -35,6 +40,25 @@ function isCompiledBinary(): boolean {
35
40
  )
36
41
  }
37
42
 
43
+ /**
44
+ * The package directory that owns `execPath` when it sits under a
45
+ * `node_modules` — an npm install's platform binary, which the launcher
46
+ * runs as `…/node_modules/@vzn/vx-<os>-<arch>/vx`. That file is npm's:
47
+ * a rename over it upgrades this command until the next `npm install`
48
+ * (or a lockfile-pinned CI checkout) puts the version npm knows back,
49
+ * and `npm ls` never agrees with `vx --version` in between. Such an
50
+ * install updates through npm, and `vx upgrade` says so. Null for a
51
+ * binary installed by hand from a release.
52
+ */
53
+ export function npmOwnedBinary(execPath: string): string | null {
54
+ const parts = execPath.split(/[\\/]/)
55
+ const at = parts.lastIndexOf('node_modules')
56
+ if (at === -1) return null
57
+ const scoped = parts[at + 1]?.startsWith('@') === true
58
+ const pkg = parts.slice(at + 1, at + (scoped ? 3 : 2)).join('/')
59
+ return pkg.length > 0 ? pkg : 'node_modules'
60
+ }
61
+
38
62
  function assetName(): string {
39
63
  const os = process.platform === 'darwin' ? 'darwin' : process.platform
40
64
  const arch = process.arch === 'x64' || process.arch === 'arm64' ? process.arch : null
@@ -44,14 +68,102 @@ function assetName(): string {
44
68
  return `vx-${os}-${arch}`
45
69
  }
46
70
 
71
+ /** What the release API says about the asset this platform installs. */
72
+ export interface ReleaseAsset {
73
+ url: string
74
+ /** Lower-case hex SHA-256 of the asset's bytes. */
75
+ sha256: string
76
+ }
77
+
78
+ /**
79
+ * Pick this platform's asset out of a release document and read the
80
+ * digest the API publishes for it (`sha256:<hex>` on every asset a
81
+ * release built through `release.yml` carries). A release without the
82
+ * asset, or an asset without a digest, is refused here: an upgrade that
83
+ * cannot be verified is not attempted.
84
+ */
85
+ export function releaseAsset(release: unknown, name: string): ReleaseAsset {
86
+ const tag =
87
+ typeof release === 'object' &&
88
+ release !== null &&
89
+ typeof (release as { tag_name?: unknown }).tag_name === 'string'
90
+ ? (release as { tag_name: string }).tag_name
91
+ : '(unknown)'
92
+ const assets =
93
+ typeof release === 'object' &&
94
+ release !== null &&
95
+ Array.isArray((release as { assets?: unknown }).assets)
96
+ ? (release as { assets: unknown[] }).assets
97
+ : []
98
+ const asset = assets.find(
99
+ (a): a is { name: string; browser_download_url?: unknown; digest?: unknown } =>
100
+ typeof a === 'object' && a !== null && (a as { name?: unknown }).name === name,
101
+ )
102
+ if (asset === undefined) {
103
+ throw new UserError(`vx upgrade: release ${tag} has no asset ${name} for this platform`)
104
+ }
105
+ const url = asset.browser_download_url
106
+ if (typeof url !== 'string' || url.length === 0) {
107
+ throw new UserError(`vx upgrade: release ${tag} names no download for ${name}`)
108
+ }
109
+ const digest = asset.digest
110
+ const m = typeof digest === 'string' ? /^sha256:([0-9a-f]{64})$/i.exec(digest) : null
111
+ if (m === null) {
112
+ throw new UserError(
113
+ `vx upgrade: release ${tag} publishes no SHA-256 digest for ${name} — nothing to verify the download against, so it is not attempted`,
114
+ )
115
+ }
116
+ return { url, sha256: m[1]!.toLowerCase() }
117
+ }
118
+
119
+ /**
120
+ * `fetch` on a box that cannot reach the host rejects with Bun's own
121
+ * `TypeError` ("Unable to connect. Is the computer able to access the
122
+ * url?", "Was there a typo in the url or port?"), which the CLI printed
123
+ * as an internal error with a stack (a network namespace without a
124
+ * route, 2026-09-16). One line, the host and the reason.
125
+ */
126
+ async function fetchOrRefuse(url: string, init: RequestInit, what: string): Promise<Response> {
127
+ try {
128
+ return await fetch(url, init)
129
+ } catch (err) {
130
+ const message = err instanceof Error ? err.message : String(err)
131
+ throw new UserError(
132
+ `vx upgrade: could not reach ${new URL(url).host} to ${what} (${message}) — check the network or the proxy and re-run`,
133
+ )
134
+ }
135
+ }
136
+
137
+ /** The release document for `latest` or a tag, from the GitHub API. */
138
+ async function fetchRelease(tag: string | undefined): Promise<unknown> {
139
+ const url =
140
+ tag === undefined
141
+ ? `https://api.github.com/repos/${REPO}/releases/latest`
142
+ : `https://api.github.com/repos/${REPO}/releases/tags/${encodeURIComponent(tag)}`
143
+ const res = await fetchOrRefuse(
144
+ url,
145
+ {
146
+ redirect: 'follow',
147
+ headers: { Accept: 'application/vnd.github+json', 'User-Agent': `vx/${VERSION}` },
148
+ },
149
+ 'read the release',
150
+ )
151
+ if (!res.ok) {
152
+ throw new UserError(
153
+ `vx upgrade: could not read the release (${res.status}) — ${url}${res.status === 404 && tag !== undefined ? ` (is ${tag} a release tag?)` : ''}`,
154
+ )
155
+ }
156
+ return (await res.json()) as unknown
157
+ }
158
+
47
159
  /**
48
- * Download `url` and atomically replace `dest` with it. Exported for
49
- * tests (which point `url` at a local server and `dest` at a tmp
50
- * file); the CLI wires it to the GitHub release asset and
51
- * process.execPath.
160
+ * Download `url`, verify its SHA-256 against `sha256`, and atomically
161
+ * replace `dest` with it. Exported for tests (which stub `fetch` and
162
+ * point `dest` at a tmp file); the CLI wires it to the release asset
163
+ * and process.execPath.
52
164
  */
53
- export async function replaceBinary(dest: string, url: string): Promise<void> {
54
- const res = await fetch(url, { redirect: 'follow' })
165
+ export async function replaceBinary(dest: string, url: string, sha256: string): Promise<void> {
166
+ const res = await fetchOrRefuse(url, { redirect: 'follow' }, 'download the release asset')
55
167
  if (!res.ok) {
56
168
  throw new UserError(`vx upgrade: download failed (${res.status}) — ${url}`)
57
169
  }
@@ -59,6 +171,12 @@ export async function replaceBinary(dest: string, url: string): Promise<void> {
59
171
  if (bytes.byteLength === 0) {
60
172
  throw new UserError(`vx upgrade: empty download — ${url}`)
61
173
  }
174
+ const got = new Bun.CryptoHasher('sha256').update(bytes).digest('hex')
175
+ if (got !== sha256.toLowerCase()) {
176
+ throw new UserError(
177
+ `vx upgrade: the download did not match the release's SHA-256 (expected ${sha256}, got ${got}) — nothing replaced; try again, and if it repeats the asset is not the one the release published`,
178
+ )
179
+ }
62
180
  const tmp = `${dest}.upgrade-${process.pid}`
63
181
  try {
64
182
  await Bun.write(tmp, bytes)
@@ -88,14 +206,16 @@ export async function upgradeCmd(args: readonly string[]): Promise<number> {
88
206
  '(npm installs update with: npm install -g @vzn/vx@latest)',
89
207
  )
90
208
  }
91
- const asset = assetName()
92
- const url =
93
- tag === undefined
94
- ? `https://github.com/${REPO}/releases/latest/download/${asset}`
95
- : `https://github.com/${REPO}/releases/download/${tag}/${asset}`
96
209
  const dest = process.execPath
210
+ const owner = npmOwnedBinary(dest)
211
+ if (owner !== null) {
212
+ throw new UserError(
213
+ `vx upgrade: this vx was installed by npm (${dest} belongs to ${owner}) — npm owns that file, and the next npm install would put the version it knows back. Update with: npm install -g @vzn/vx@latest`,
214
+ )
215
+ }
216
+ const asset = releaseAsset(await fetchRelease(tag), assetName())
97
217
  process.stdout.write(`vx upgrade: ${VERSION} → ${tag ?? 'latest'} (${dest})\n`)
98
- await replaceBinary(dest, url)
218
+ await replaceBinary(dest, asset.url, asset.sha256)
99
219
  // Report the replaced binary's own version — the new build speaks
100
220
  // for itself rather than this process guessing.
101
221
  const proc = Bun.spawnSync({ cmd: [dest, '--version'], stdout: 'pipe', stderr: 'pipe' })