@vzn/vx 0.0.400 → 0.0.402

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.400",
3
+ "version": "0.0.402",
4
4
  "description": "The fastest task runner and build cache for JavaScript monorepos.",
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.400",
25
- "@vzn/vx-linux-arm64": "0.0.400",
26
- "@vzn/vx-darwin-x64": "0.0.400",
27
- "@vzn/vx-darwin-arm64": "0.0.400"
24
+ "@vzn/vx-linux-x64": "0.0.402",
25
+ "@vzn/vx-linux-arm64": "0.0.402",
26
+ "@vzn/vx-darwin-x64": "0.0.402",
27
+ "@vzn/vx-darwin-arm64": "0.0.402"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -1891,7 +1891,8 @@ async function runSandboxedOnce(
1891
1891
  config: args.config,
1892
1892
  skip: [taskTmpRoot(), ...srtDefaultWritePaths()],
1893
1893
  })
1894
- if (outside.length > 0) violations.push(outsideWritesHint(outside, baselines.denyRead))
1894
+ if (outside.length > 0)
1895
+ violations.push(outsideWritesHint(outside, baselines.denyRead, args.reportWithin))
1895
1896
  }
1896
1897
 
1897
1898
  // The one denial macOS never logs. MEASURED 2026-09-05, same machine, two
@@ -1959,12 +1960,22 @@ async function runSandboxedOnce(
1959
1960
  }
1960
1961
 
1961
1962
  /** The hint for writes refused outside the project, a few paths named. */
1962
- function outsideWritesHint(paths: readonly string[], walled: readonly string[]): SandboxViolation {
1963
+ function outsideWritesHint(
1964
+ paths: readonly string[],
1965
+ walled: readonly string[],
1966
+ within: string,
1967
+ ): SandboxViolation {
1963
1968
  const shown = paths.slice(0, 5).join(', ')
1964
1969
  const more = paths.length > 5 ? ` and ${paths.length - 5} more` : ''
1965
1970
  const home = toRealPath(os.homedir())
1966
1971
  const dir = path.dirname(paths[0]!)
1967
- const spelled = atOrUnder(dir, home) ? `~${dir.slice(home.length)}` : dir
1972
+ // In the workspace, from the project, as a committed config spells it:
1973
+ // the absolute path held only on the machine that printed it.
1974
+ const spelled = walled.some((w) => atOrUnder(dir, toRealPath(w)))
1975
+ ? path.relative(toRealPath(within), dir) || '.'
1976
+ : atOrUnder(dir, home)
1977
+ ? `~${dir.slice(home.length)}`
1978
+ : dir
1968
1979
  const refused =
1969
1980
  `vx: the sandbox refused writes outside the project, which are not reported as ` +
1970
1981
  `violations: ${shown}${more}.`
@@ -1979,10 +1990,13 @@ function outsideWritesHint(paths: readonly string[], walled: readonly string[]):
1979
1990
  ? `${refused} The task has its own temp directory, empty at its start: write under ` +
1980
1991
  `$TMPDIR (os.tmpdir() in Node and Bun) instead of a fixed path.`
1981
1992
  : `${refused} If the task needs one, grant its directory, e.g. ` +
1982
- `\`allow: { write: ['${spelled}/'] }\`.`
1993
+ `\`allow: { write: [${jsString(`${spelled}/`)}] }\`.`
1983
1994
  return { timestamp: new Date(), hint: true, line }
1984
1995
  }
1985
1996
 
1997
+ /** A path as a JS string literal a config can take: a quote in it is escaped. */
1998
+ const jsString = (p: string): string => (p.includes("'") ? JSON.stringify(p) : `'${p}'`)
1999
+
1986
2000
  /** The host's shared temp directories, canonical: what a fixed temp path in a tool names. */
1987
2001
  function hostTempRoots(): string[] {
1988
2002
  return [...new Set(['/tmp', '/var/tmp', os.tmpdir()].map(toRealPath))]
@@ -1997,7 +2011,7 @@ function hiddenReadsHint(paths: readonly string[], within: string): SandboxViola
1997
2011
  line:
1998
2012
  `vx: the sandbox hid paths outside the project that exist on this machine, which are ` +
1999
2013
  `not reported as violations: ${shown}${more}. If the task reads one, grant it, e.g. ` +
2000
- `\`allow: { read: ['${path.relative(toRealPath(within), paths[0]!)}'] }\`.`,
2014
+ `\`allow: { read: [${jsString(path.relative(toRealPath(within), paths[0]!))}] }\`.`,
2001
2015
  }
2002
2016
  }
2003
2017
 
package/src/index.ts CHANGED
@@ -149,6 +149,7 @@ export type {
149
149
  VxPlugin,
150
150
  PluginHooks,
151
151
  PluginOrigin,
152
+ PluginOptionKinds,
152
153
  CacheContext,
153
154
  ExecutorContext,
154
155
  CommandContext,
@@ -87,6 +87,7 @@ export {
87
87
  type VxPlugin,
88
88
  definePlugin,
89
89
  refuseUnknownOptions,
90
+ type PluginOptionKinds,
90
91
  type PluginHooks,
91
92
  type PluginOrigin,
92
93
  } from './plugin.js'
@@ -483,29 +483,69 @@ export function definePlugin(origin: PluginOrigin, hooks: PluginHooks): VxPlugin
483
483
  return { ...hooks, name, [PLUGIN_PACKAGE]: name } as VxPlugin
484
484
  }
485
485
 
486
+ /** What `refuseUnknownOptions` holds an option's value to. */
487
+ type OptionKind<V> = [V] extends [string]
488
+ ? 'string'
489
+ : [V] extends [number]
490
+ ? 'number'
491
+ : [V] extends [boolean]
492
+ ? 'boolean'
493
+ : [V] extends [(...args: never[]) => unknown]
494
+ ? 'function'
495
+ : [V] extends [object]
496
+ ? 'object'
497
+ : 'any'
498
+
499
+ /**
500
+ * Every option a factory takes, each with the one kind its type allows
501
+ * (`'any'` for a union of kinds, such as `false | { … }`). Derived from
502
+ * the options interface, so the type checker refuses a missing option, an
503
+ * extra one or a wrong kind.
504
+ */
505
+ export type PluginOptionKinds<T> = {
506
+ readonly [K in keyof Required<T>]-?: OptionKind<Required<T>[K]>
507
+ }
508
+
486
509
  /**
487
- * Refuse an option a plugin factory does not take, as core refuses an
488
- * unknown config field. Bun strips a config's types, so a misspelt option
489
- * (`reapi({ endpont })`) reached the factory, which read it as unset and
490
- * quietly declined: the run went local with no word. `factory` names the
491
- * call in the message (`reapi()`), `known` is the options the factory reads.
510
+ * Refuse an option a plugin factory does not take, or a value of the wrong
511
+ * kind, as core refuses an unknown config field. Bun strips a config's
512
+ * types, so a misspelt option (`reapi({ endpont })`) reached the factory,
513
+ * which read it as unset and quietly declined, and a string where a number
514
+ * or a boolean belongs (`process.env.X`) was misread or threw a bare
515
+ * TypeError. `factory` names the call in the message (`reapi()`).
492
516
  */
493
- export function refuseUnknownOptions(
517
+ export function refuseUnknownOptions<T>(
494
518
  factory: string,
495
519
  options: unknown,
496
- known: readonly string[],
520
+ kinds: PluginOptionKinds<T>,
497
521
  ): void {
498
522
  if (options === undefined) return
499
523
  if (options === null || typeof options !== 'object' || Array.isArray(options)) {
500
524
  throw new UserError(`${factory}: options must be an object`)
501
525
  }
502
- for (const key of Object.keys(options)) {
503
- if (known.includes(key)) continue
504
- const near = nearest(key, known)
505
- throw new UserError(
506
- `${factory} has unknown option "${key}" (allowed: ${[...known].sort().join(', ')})` +
507
- (near === undefined ? '' : ` — did you mean ${near}?`),
508
- )
526
+ const known = kinds as Readonly<Record<string, string>>
527
+ const names = Object.keys(known)
528
+ for (const [key, value] of Object.entries(options)) {
529
+ const kind = known[key]
530
+ if (kind === undefined) {
531
+ const near = nearest(key, names)
532
+ throw new UserError(
533
+ `${factory} has unknown option "${key}" (allowed: ${[...names].sort().join(', ')})` +
534
+ (near === undefined ? '' : ` — did you mean ${near}?`),
535
+ )
536
+ }
537
+ if (value === undefined || kind === 'any') continue
538
+ const ok =
539
+ kind === 'object'
540
+ ? value !== null && typeof value === 'object' && !Array.isArray(value)
541
+ : typeof value === kind
542
+ if (!ok) {
543
+ const got =
544
+ typeof value === 'function' ? 'a function' : (JSON.stringify(value) ?? String(value))
545
+ throw new UserError(
546
+ `${factory} option "${key}" must be ${kind === 'object' ? 'an' : 'a'} ${kind}, got ${got}`,
547
+ )
548
+ }
509
549
  }
510
550
  }
511
551