@vzn/vx 0.0.129 → 0.0.131

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.129",
3
+ "version": "0.0.131",
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.129",
21
- "@vzn/vx-linux-arm64": "0.0.129",
22
- "@vzn/vx-darwin-x64": "0.0.129",
23
- "@vzn/vx-darwin-arm64": "0.0.129"
20
+ "@vzn/vx-linux-x64": "0.0.131",
21
+ "@vzn/vx-linux-arm64": "0.0.131",
22
+ "@vzn/vx-darwin-x64": "0.0.131",
23
+ "@vzn/vx-darwin-arm64": "0.0.131"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/exec/index.ts CHANGED
@@ -28,7 +28,7 @@ export {
28
28
  thrownReason,
29
29
  } from './sandbox-runtime.js'
30
30
  export { type DeniedCall } from './sandbox-violations.js'
31
- export { isMountableLiteral, MOUNT_WILDCARDS, toRealPath } from './sandbox-paths.js'
31
+ export { atOrUnder, isMountableLiteral, MOUNT_WILDCARDS, toRealPath } from './sandbox-paths.js'
32
32
  export { bindableWrites, punchWalls } from './sandbox-binds.js'
33
33
  export { isLocalExecutor, localExecutor } from './local-executor.js'
34
34
  export { holdGroups, killTree, untilGroupsGone } from './kill-tree.js'
@@ -52,10 +52,18 @@ export function absolutize(p: string, cwd?: string): string {
52
52
  return path.resolve(cwd ?? process.cwd(), p)
53
53
  }
54
54
 
55
+ /**
56
+ * `p` is `dir` or below it, by path: a sibling sharing `dir`'s name prefix
57
+ * is not, and `/` already ends in the separator.
58
+ */
59
+ export function atOrUnder(p: string, dir: string): boolean {
60
+ return p === dir || p.startsWith(dir.endsWith(path.sep) ? dir : dir + path.sep)
61
+ }
62
+
55
63
  export function isUnderAny(abs: string, allow: Set<string>): boolean {
56
64
  if (allow.has(abs)) return true
57
65
  for (const a of allow) {
58
- if (abs === a || abs.startsWith(a + path.sep)) return true
66
+ if (atOrUnder(abs, a)) return true
59
67
  }
60
68
  return false
61
69
  }
@@ -64,6 +64,7 @@ import {
64
64
  } from '../util/index.js'
65
65
  import { bindableWrites, buildCustomConfig } from './sandbox-binds.js'
66
66
  import {
67
+ atOrUnder,
67
68
  isMountableLiteral,
68
69
  localBindingOn,
69
70
  MOUNT_WILDCARDS,
@@ -719,16 +720,14 @@ function canonicalBaselines(
719
720
  function assertWriteStaysHome(grant: string, real: string, projectDir: string): void {
720
721
  if (grant.startsWith('~') || path.isAbsolute(grant)) return
721
722
  const lexical = path.resolve(projectDir, grant)
722
- const inside = (p: string, dir: string): boolean =>
723
- p === dir || p.startsWith(dir.endsWith(path.sep) ? dir : dir + path.sep)
724
- if (!inside(lexical, projectDir)) return
723
+ if (!atOrUnder(lexical, projectDir)) return
725
724
  const home = toRealPath(projectDir)
726
725
  // `toRealPath` stops at a DANGLING link and keeps its own path, which is
727
726
  // inside; the link's target is where a write through it would land.
728
727
  const target = throughLinks(real)
729
- if (inside(real, home) && inside(target, home)) return
728
+ if (atOrUnder(real, home) && atOrUnder(target, home)) return
730
729
  throw new UserError(
731
- `exec.sandbox.allow.write: "${grant}" resolves through a symlink to ${inside(real, home) ? target : real}, outside the ` +
730
+ `exec.sandbox.allow.write: "${grant}" resolves through a symlink to ${atOrUnder(real, home) ? target : real}, outside the ` +
732
731
  `project — a write grant binds the path it names in the project, and vx does not follow a ` +
733
732
  `link out of it. Remove the link, or grant the target by its own path.`,
734
733
  )
@@ -1557,8 +1556,6 @@ async function runSandboxedOnce(
1557
1556
  */
1558
1557
  function readableUnder(dir: string, granted: readonly string[]): boolean {
1559
1558
  const target = toRealPath(dir)
1560
- const under = (a: string, b: string): boolean =>
1561
- a === b || a.startsWith(b.endsWith(path.sep) ? b : b + path.sep)
1562
1559
  return granted.some((p) => {
1563
1560
  const g = toRealPath(p)
1564
1561
  // On Linux a grant INSIDE the cwd makes the cwd listable too: bwrap
@@ -1566,7 +1563,7 @@ function readableUnder(dir: string, granted: readonly string[]): boolean {
1566
1563
  // children around the walls (`wallOff`), so every failing root task
1567
1564
  // was told its cwd was unreadable while it listed it (B-20). Seatbelt
1568
1565
  // grants no parent, so on macOS the cwd itself must be covered.
1569
- return under(target, g) || (process.platform === 'linux' && under(g, target))
1566
+ return atOrUnder(target, g) || (process.platform === 'linux' && atOrUnder(g, target))
1570
1567
  })
1571
1568
  }
1572
1569
 
@@ -1736,9 +1733,7 @@ export function wallsGlobsReach(grants: readonly string[], walls: readonly strin
1736
1733
  const heads = grants
1737
1734
  .filter((g) => !isMountableLiteral(g))
1738
1735
  .map((g) => path.dirname(g.slice(0, g.search(MOUNT_WILDCARDS))))
1739
- return walls.filter((w) =>
1740
- heads.some((h) => w === h || w.startsWith(h === path.sep ? h : h + path.sep)),
1741
- )
1736
+ return walls.filter((w) => heads.some((h) => atOrUnder(w, h)))
1742
1737
  }
1743
1738
 
1744
1739
  /**
@@ -1760,7 +1755,7 @@ export function darwinWallRules(
1760
1755
  for (const w of walls) {
1761
1756
  const at = `(subpath "${sbplResolvedPath(w, 'wall')}")`
1762
1757
  const kept = literals
1763
- .filter((l) => isMountableLiteral(l) && (l === w || l.startsWith(w + path.sep)))
1758
+ .filter((l) => isMountableLiteral(l) && atOrUnder(l, w))
1764
1759
  .map((l) => `(require-not (subpath "${sbplResolvedPath(l, 'grant')}"))`)
1765
1760
  rules.push(
1766
1761
  kept.length === 0
@@ -1827,12 +1822,12 @@ function expandGrants(
1827
1822
  for (const hit of scanOrNothing(pattern, base)) {
1828
1823
  const abs = path.join(base, hit)
1829
1824
  const real = toRealPath(abs)
1830
- if (real !== home && !real.startsWith(home + path.sep)) continue
1825
+ if (!atOrUnder(real, home)) continue
1831
1826
  // A hit that IS a wall, or lies inside one, was matched, not named:
1832
1827
  // `read: ['*']` in a root project bound `.git` and `.vx`, and
1833
1828
  // `packages/*` a nested project its key excludes (B-1). A grant that
1834
1829
  // names a wall literally never reaches this loop and stays.
1835
- if (walls.some((w) => real === w || real.startsWith(w + path.sep))) continue
1830
+ if (walls.some((w) => atOrUnder(real, w))) continue
1836
1831
  out.push(abs)
1837
1832
  hits++
1838
1833
  }
@@ -5,7 +5,7 @@
5
5
  // no grant can avoid, minus what the task chose to ignore.
6
6
 
7
7
  import path from 'node:path'
8
- import { absolutize, isUnderAny, localBindingOn, toRealPath } from './sandbox-paths.js'
8
+ import { absolutize, atOrUnder, isUnderAny, localBindingOn, toRealPath } from './sandbox-paths.js'
9
9
  import type {
10
10
  ResolvedSandboxConfig,
11
11
  SandboxedRunArgs,
@@ -119,7 +119,7 @@ export async function parseStraceViolations(
119
119
  const abs = toRealPath(absolutize(rawPath, baselines.cwd))
120
120
  // Only report paths under the workspace-root deny anchor — system
121
121
  // libs / /proc / /sys / etc. probes are not interesting violations.
122
- if (!denyAnchors.some((root) => abs === root || abs.startsWith(root + path.sep))) continue
122
+ if (!denyAnchors.some((root) => atOrUnder(abs, root))) continue
123
123
  // Skip paths the user explicitly allowed (and their descendants).
124
124
  if (isUnderAny(abs, allowAbs)) continue
125
125
  const key = `${syscall}|${abs}`
@@ -18,6 +18,7 @@ import {
18
18
  toRealPath,
19
19
  type ExecuteRequest,
20
20
  isMountableLiteral,
21
+ atOrUnder,
21
22
  type SandboxViolation,
22
23
  } from '../exec/index.js'
23
24
  import type { TaskNode } from '../graph/index.js'
@@ -299,7 +300,11 @@ async function linkedDeps(
299
300
  const full = path.join(dir, e.name)
300
301
  if (e.isSymbolicLink()) {
301
302
  const target = await realpath(full).catch(() => undefined)
302
- if (target !== undefined && !dirs.some((d) => within(target, d)) && !within(self, target)) {
303
+ if (
304
+ target !== undefined &&
305
+ !dirs.some((d) => atOrUnder(target, d)) &&
306
+ !atOrUnder(self, target)
307
+ ) {
303
308
  found.push([target, full, scope + e.name])
304
309
  }
305
310
  } else if (scope === '' && e.isDirectory() && e.name.startsWith('@')) {
@@ -321,7 +326,7 @@ async function linkedDeps(
321
326
  const granted: string[] = []
322
327
  const withheld: WithheldLink[] = []
323
328
  for (const [target, { link, name }] of targets) {
324
- if (!within(target, root) || keyedDirs.has(target)) granted.push(target)
329
+ if (!atOrUnder(target, root) || keyedDirs.has(target)) granted.push(target)
325
330
  else
326
331
  withheld.push({
327
332
  dir: target,
@@ -355,7 +360,7 @@ export function reachedWithheld(
355
360
  violations: readonly SandboxViolation[],
356
361
  ): WithheldLink[] {
357
362
  return withheld.filter((w) =>
358
- violations.some((v) => v.path !== undefined && within(v.path, w.dir)),
363
+ violations.some((v) => v.path !== undefined && atOrUnder(v.path, w.dir)),
359
364
  )
360
365
  }
361
366
 
@@ -364,15 +369,6 @@ function posixRel(from: string, p: string): string {
364
369
  return path.relative(from, p).split(path.sep).join('/')
365
370
  }
366
371
 
367
- /**
368
- * `p` is `dir` or below it — by path, so a sibling sharing `dir`'s name
369
- * prefix is not. `/` already ends in the separator, and a link to it
370
- * holds every project.
371
- */
372
- function within(p: string, dir: string): boolean {
373
- return p === dir || p.startsWith(dir.endsWith(path.sep) ? dir : dir + path.sep)
374
- }
375
-
376
372
  /**
377
373
  * Ensure each declared write path exists on the host as either an empty
378
374
  * file (a literal path) or a directory (a glob's static prefix, or a
@@ -451,8 +447,8 @@ function wallOff(
451
447
  for (const bind of bindableWrites(config.allowWrite)) {
452
448
  // A bind outside the workspace is the user's own path (`/tmp/x`,
453
449
  // `~/.cache/y`), spelled there on purpose; only one inside is judged.
454
- if (bind !== home && !bind.startsWith(home + path.sep)) continue
455
- const wall = walls.find((w) => w === bind || w.startsWith(bind + path.sep))
450
+ if (!atOrUnder(bind, home)) continue
451
+ const wall = walls.find((w) => atOrUnder(w, bind))
456
452
  if (wall === undefined) continue
457
453
  throw new UserError(
458
454
  `exec.sandbox.allow.write: the grant binding ${bind} would make ${wall} writable — another ` +
@@ -558,8 +554,8 @@ export function commandWriteReach(node: TaskNode, workspaceRoot: string): WriteR
558
554
  let reach: 'none' | 'project' = 'none'
559
555
  for (const grant of exec.sandbox.allow?.write ?? []) {
560
556
  const abs = path.resolve(node.projectDir, expandHome(grantPrefix(grant)))
561
- if (within(abs, node.projectDir)) reach = 'project'
562
- else if (within(abs, workspaceRoot) || within(workspaceRoot, abs)) return 'workspace'
557
+ if (atOrUnder(abs, node.projectDir)) reach = 'project'
558
+ else if (atOrUnder(abs, workspaceRoot) || atOrUnder(workspaceRoot, abs)) return 'workspace'
563
559
  }
564
560
  return reach
565
561
  }
@@ -579,7 +575,7 @@ export function mayWriteFingerprint(node: TaskNode, workspaceRoot: string): bool
579
575
  for (const grant of exec.sandbox.allow?.write ?? []) {
580
576
  const abs = path.resolve(node.projectDir, expandHome(grantPrefix(grant)))
581
577
  for (const f of WORKSPACE_FINGERPRINT_FILES) {
582
- if (within(path.join(workspaceRoot, f), abs)) return true
578
+ if (atOrUnder(path.join(workspaceRoot, f), abs)) return true
583
579
  }
584
580
  }
585
581
  return false
@@ -18,7 +18,13 @@ import { UserError } from '../util/index.js'
18
18
  */
19
19
  export function keyUpstream(node: TaskNode, upstream: TaskOutcome[]): TaskOutcome[] {
20
20
  const order = node.orderOnly
21
- const keyed = order === undefined ? upstream : upstream.filter((o) => !order.includes(o.node.id))
21
+ // A restore-tier hit dispatches before its dependencies settle, and the
22
+ // scheduler hands it their outcomes so far: a hole stays a hole, as it
23
+ // does with no order-only edge (reading `.node` of one threw).
24
+ const keyed =
25
+ order === undefined
26
+ ? upstream
27
+ : upstream.filter((o) => o === undefined || !order.includes(o.node.id))
22
28
  return node.excludedUpstream === undefined ? keyed : [...keyed, ...node.excludedUpstream]
23
29
  }
24
30
 
@@ -722,6 +722,26 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
722
722
  },
723
723
  ],
724
724
  [EXEC_FIELDS, { cmd: '`command`', script: '`command`' }],
725
+ // Turbo's turbo.json and Nx's nx.json keys, written into vx.workspace
726
+ // (D-38).
727
+ [
728
+ WORKSPACE_FIELDS,
729
+ {
730
+ pipeline:
731
+ "each package's `vx.config` `tasks` (`bunx @vzn/vx-migrate` writes them from turbo.json)",
732
+ tasks:
733
+ "each package's `vx.config` `tasks` (`bunx @vzn/vx-migrate` writes them from turbo.json)",
734
+ targetDefaults:
735
+ "each package's `vx.config` `tasks`, sharing defaults through an imported module",
736
+ namedInputs: 'a module each `vx.config` imports, spread into `cache.inputs.files`',
737
+ globalEnv: '`cache.inputs.env` on the tasks it keys',
738
+ globalDependencies: '`cache.inputs.workspaceFiles` on the tasks it keys',
739
+ remoteCache:
740
+ 'a cache plugin in `plugins` (`turboCache()` or `nxCache()` from `@vzn/vx-migrate`)',
741
+ parallel: '`concurrency`',
742
+ cacheDirectory: '`cacheDir`',
743
+ },
744
+ ],
725
745
  ])
726
746
 
727
747
  function assertKnownFields(value: object, allowed: ReadonlySet<string>, where: string): void {