@vzn/vx 0.0.428 → 0.0.430

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.428",
3
+ "version": "0.0.430",
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.428",
25
- "@vzn/vx-linux-arm64": "0.0.428",
26
- "@vzn/vx-darwin-x64": "0.0.428",
27
- "@vzn/vx-darwin-arm64": "0.0.428"
24
+ "@vzn/vx-linux-x64": "0.0.430",
25
+ "@vzn/vx-linux-arm64": "0.0.430",
26
+ "@vzn/vx-darwin-x64": "0.0.430",
27
+ "@vzn/vx-darwin-arm64": "0.0.430"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/init.ts CHANGED
@@ -374,6 +374,9 @@ function namelessNotes(root: string, dirs: readonly string[]): string[] {
374
374
  })
375
375
  .map((dir) => path.relative(root, dir).split(path.sep).join('/'))
376
376
  .sort()
377
+ // The root's own manifest is the empty relative path: a nameless
378
+ // single-package repo read `not mapped: — its package.json…` (M-54).
379
+ .map((rel) => (rel === '' ? 'the root' : rel))
377
380
  if (withScripts.length === 0) return []
378
381
  const shown =
379
382
  withScripts.slice(0, 3).join(', ') +
@@ -278,9 +278,15 @@ export function selectExecutor(
278
278
  for (const executor of executors) {
279
279
  if (task.pinnedLocal && executor.remote === true) continue
280
280
  if (executor.accepts === undefined) return executor
281
- let taken: boolean
281
+ let taken: unknown
282
282
  try {
283
283
  taken = executor.accepts(task)
284
+ // An `async accepts` answers a Promise: truthy, so the executor took
285
+ // every task, and a rejection went unheard (the shape of H-16's admit).
286
+ if (taken instanceof Promise) {
287
+ taken.catch(() => {})
288
+ throw new Error('returned a Promise; accepts is synchronous')
289
+ }
284
290
  } catch (err) {
285
291
  const m = err instanceof Error ? err.message : String(err)
286
292
  throw new UserError(`${label(executor)} failed in accepts for ${task.taskId}: ${m}`)
@@ -500,13 +500,20 @@ async function runOnBus(
500
500
  // it surfaced as a bare stack, or mid-run from the completion path
501
501
  // (item 1022).
502
502
  const tellDemand = (executor: TaskExecutor, remaining: ReadonlySet<string>): void => {
503
- try {
504
- executor.demand!(remaining)
505
- } catch (err) {
506
- demandOf.delete(executor)
503
+ const failed = (err: unknown): void => {
504
+ if (!demandOf.delete(executor)) return
507
505
  const m = err instanceof Error ? err.message : String(err)
508
506
  log.status(`[vx] ${executorLabel(executor)} failed in demand: ${m}; not asked again this run`)
509
507
  }
508
+ try {
509
+ // An `async demand` that rejects did so where no one listened: an
510
+ // unhandled rejection, a stack, the run killed. Its rejection is a
511
+ // throw like any other.
512
+ const ret: unknown = executor.demand!(remaining)
513
+ if (ret instanceof Promise) ret.catch(failed)
514
+ } catch (err) {
515
+ failed(err)
516
+ }
510
517
  }
511
518
  for (const [executor, remaining] of demandOf) tellDemand(executor, remaining)
512
519
  const narrowDemand =
@@ -388,9 +388,11 @@ export function createTelemetrySource(args: {
388
388
  // Never silently. The standing rule is that a never-fail path must still
389
389
  // WARN — telemetry that vanishes without a word is indistinguishable from
390
390
  // telemetry nobody configured.
391
- // Every hook call site skips a disabled sink first, so this runs at most
392
- // once per sink without a guard of its own (item 654).
391
+ // Every hook call site skips a disabled sink first (item 654), but an
392
+ // async hook's rejections land later, several records' worth of them
393
+ // after the first: those are said once too.
393
394
  const disable = (sink: TelemetrySink, hook: string, err: unknown): void => {
395
+ if (disabled.has(sink)) return
394
396
  disabled.add(sink)
395
397
  warn?.(
396
398
  `[vx] telemetry sink '${label(sink)}' threw in ${hook}; disabled for this run: ${err instanceof Error ? err.message : String(err)}`,
@@ -406,7 +408,10 @@ export function createTelemetrySource(args: {
406
408
  const kinds = sink.wants ?? DEFAULT_KINDS
407
409
  if (!kinds.includes(record.kind)) continue
408
410
  try {
409
- sink.onRecord(record)
411
+ // Typed `void`, yet an `async onRecord` rejects where no one
412
+ // listened: an unhandled rejection killed the run (C-101).
413
+ const ret: unknown = sink.onRecord(record)
414
+ if (ret instanceof Promise) ret.catch((err) => disable(sink, 'onRecord', err))
410
415
  } catch (err) {
411
416
  disable(sink, 'onRecord', err)
412
417
  }
@@ -483,7 +488,8 @@ export function createTelemetrySource(args: {
483
488
  for (const sink of sinks) {
484
489
  if (disabled.has(sink) || sink.onRunSummary === undefined) continue
485
490
  try {
486
- sink.onRunSummary(summary)
491
+ const ret: unknown = sink.onRunSummary(summary)
492
+ if (ret instanceof Promise) ret.catch((err) => disable(sink, 'onRunSummary', err))
487
493
  } catch (err) {
488
494
  disable(sink, 'onRunSummary', err)
489
495
  }
@@ -504,7 +504,10 @@ export async function loadProjectConfigs(
504
504
  ? []
505
505
  : [
506
506
  ...restoreBuiltins(builtins, overlapping),
507
- ...restoreEnv(env),
507
+ // Overlapping, the env is checked once at the round's end, as most
508
+ // built-ins are: reading all of it after every load was 15–25 µs a
509
+ // config (149 variables, compiled, 2026-10-03).
510
+ ...(overlapping ? [] : restoreEnv(env)),
508
511
  ...restoreCwd(cwd),
509
512
  // Alone in the round: nothing else reads the umask now. Beside
510
513
  // other loads the round reads it once they are done (see
@@ -591,7 +594,7 @@ export async function loadProjectConfigs(
591
594
  ...restoreUmask(umask),
592
595
  ]
593
596
  : []
594
- // Overlapping loads check most built-ins only here, so a change one
597
+ // Overlapping loads check most built-ins and the env only here, so a change one
595
598
  // config made may have broken another's load: it is refused first, and
596
599
  // nothing the round evaluated is stored.
597
600
  const changedInRound =