@vzn/vx 0.0.98 → 0.0.100

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.98",
3
+ "version": "0.0.100",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
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.98",
21
- "@vzn/vx-linux-arm64": "0.0.98",
22
- "@vzn/vx-darwin-x64": "0.0.98",
23
- "@vzn/vx-darwin-arm64": "0.0.98"
20
+ "@vzn/vx-linux-x64": "0.0.100",
21
+ "@vzn/vx-linux-arm64": "0.0.100",
22
+ "@vzn/vx-darwin-x64": "0.0.100",
23
+ "@vzn/vx-darwin-arm64": "0.0.100"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/show.ts CHANGED
@@ -70,7 +70,12 @@ export async function showCmd(args: readonly string[]): Promise<number> {
70
70
  ? parsed.target
71
71
  : parsed.target.slice(0, hashAt)
72
72
  const taskName = hashAt === -1 ? undefined : parsed.target!.slice(hashAt + 1)
73
- if (taskName === '') throw new UserError(`missing task name after '#' in "${parsed.target}"`)
73
+ if (taskName === '') {
74
+ const list = projectName === '' ? 'vx show' : `vx show ${projectName}`
75
+ throw new UserError(
76
+ `vx show: missing task name after '#' in "${parsed.target}" (${list} lists the tasks)`,
77
+ )
78
+ }
74
79
  if (projectName !== undefined && (taskName !== undefined || byName.has(projectName))) {
75
80
  if (!byName.has(projectName)) {
76
81
  throw new UserError(
@@ -105,11 +105,15 @@ export function releaseAsset(release: unknown, name: string): ReleaseAsset {
105
105
  typeof a === 'object' && a !== null && (a as { name?: unknown }).name === name,
106
106
  )
107
107
  if (asset === undefined) {
108
- throw new UserError(`vx upgrade: release ${tag} has no asset ${name} for this platform`)
108
+ throw new UserError(
109
+ `vx upgrade: release ${tag} has no asset ${name} for this platform — nothing replaced; npm install -g @vzn/vx@latest installs it instead`,
110
+ )
109
111
  }
110
112
  const url = asset.browser_download_url
111
113
  if (typeof url !== 'string' || url.length === 0) {
112
- throw new UserError(`vx upgrade: release ${tag} names no download for ${name}`)
114
+ throw new UserError(
115
+ `vx upgrade: release ${tag} names no download for ${name} — nothing replaced; npm install -g @vzn/vx@latest installs it instead`,
116
+ )
113
117
  }
114
118
  const digest = asset.digest
115
119
  const m = typeof digest === 'string' ? /^sha256:([0-9a-f]{64})$/i.exec(digest) : null
@@ -217,13 +221,15 @@ export async function replaceBinary(
217
221
  ): Promise<void> {
218
222
  const res = await fetchOrRefuse(url, { redirect: 'follow' }, 'download the release asset')
219
223
  if (!res.ok) {
220
- throw new UserError(`vx upgrade: download failed (${res.status}) — ${url}`)
224
+ throw new UserError(
225
+ `vx upgrade: download failed (${res.status}) — ${url} — nothing replaced; re-run`,
226
+ )
221
227
  }
222
228
  const bytes = new Uint8Array(
223
229
  await readOrRefuse(() => res.arrayBuffer(), url, 'download the release asset'),
224
230
  )
225
231
  if (bytes.byteLength === 0) {
226
- throw new UserError(`vx upgrade: empty download — ${url}`)
232
+ throw new UserError(`vx upgrade: empty download — ${url} — nothing replaced; re-run`)
227
233
  }
228
234
  const got = new Bun.CryptoHasher('sha256').update(bytes).digest('hex')
229
235
  if (got !== sha256.toLowerCase()) {
@@ -340,7 +340,18 @@ function unlinkOnExit(file: string): void {
340
340
  * that names it outright still reaches it.
341
341
  */
342
342
  function taskTmpdir(tag: string): string {
343
- return path.join(sandboxTmpdir(), `vx-task-${process.pid}-${tag}`)
343
+ return path.join(taskTmpRoot(), `vx-task-${process.pid}-${tag}`)
344
+ }
345
+
346
+ /**
347
+ * The parent of every task's own temp directory, walled off from every
348
+ * sandboxed task, each granted its own directory inside: SRT binds its
349
+ * whole temp dir writable into every task, so a task listed and read what
350
+ * a concurrent one kept in its TMPDIR, or replaced its port bridge's
351
+ * socket (L-10).
352
+ */
353
+ function taskTmpRoot(): string {
354
+ return path.join(sandboxTmpdir(), 'vx-tasks')
344
355
  }
345
356
 
346
357
  const liveTaskTmpdirs = new Set<string>()
@@ -799,6 +810,11 @@ export async function wrapSandboxedCommand(
799
810
 
800
811
  const baselines = canonicalBaselines(args)
801
812
  const customConfig = buildCustomConfig(args, baselines)
813
+ customConfig!.filesystem!.denyRead!.push(toRealPath(taskTmpRoot()))
814
+ customConfig!.filesystem!.allowWrite!.push(toRealPath(tmp))
815
+ // Seatbelt re-allows a read inside a denied region only by name; on Linux
816
+ // a read grant over a write path would remount it read-only.
817
+ if (process.platform === 'darwin') customConfig!.filesystem!.allowRead!.push(toRealPath(tmp))
802
818
  // Linux: the ports a list grants are bridged out of the task's network
803
819
  // namespace. The task's side of each bridge is a socat in front of the
804
820
  // user command, so it goes INTO the sandboxed command; the host side is
@@ -972,7 +988,7 @@ export function bridgedPorts(c: Pick<ResolvedSandboxConfig, 'localBinding'>): nu
972
988
 
973
989
  /** Where a bridge's unix socket lives: the sandbox tmpdir, bound read-write on both sides. */
974
990
  export function portBridgeSocket(tag: string, port: number): string {
975
- return path.join(sandboxTmpdir(), `vx-port-${tag}-${port}.sock`)
991
+ return path.join(taskTmpdir(tag), `vx-port-${tag}-${port}.sock`)
976
992
  }
977
993
 
978
994
  /**
@@ -357,7 +357,13 @@ function renderConfigFile(
357
357
  if (p.importLines.length > 0) lines.push(...p.importLines)
358
358
  lines.push('', 'export default {', ' tasks: {')
359
359
  for (const t of p.tasks) {
360
- for (const todo of t.todos) lines.push(` // TODO(vx-migrate): ${todo}`)
360
+ // A reason can quote a manifest's own text (an Nx `env` key), and a
361
+ // line break there ended the comment: the rest was code in the file
362
+ // the next run evaluates (D-21). Each line stays inside the comment.
363
+ for (const todo of t.todos) {
364
+ const [first, ...rest] = todo.split(/\r\n|[\n\r\u2028\u2029]/)
365
+ lines.push(` // TODO(vx-migrate): ${first}`, ...rest.map((l) => ` // ${l}`))
366
+ }
361
367
  if (t.task === null) continue // skipped target — the TODO above explains
362
368
  // `__proto__: {…}` in a literal SETS the prototype, quoted or not, and
363
369
  // the run refused the config (item 1033); a computed key is a property.