@vzn/vx 0.0.546 → 0.0.547

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.546",
3
+ "version": "0.0.547",
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.546",
25
- "@vzn/vx-linux-arm64": "0.0.546",
26
- "@vzn/vx-darwin-x64": "0.0.546",
27
- "@vzn/vx-darwin-arm64": "0.0.546"
24
+ "@vzn/vx-linux-x64": "0.0.547",
25
+ "@vzn/vx-linux-arm64": "0.0.547",
26
+ "@vzn/vx-darwin-x64": "0.0.547",
27
+ "@vzn/vx-darwin-arm64": "0.0.547"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -5,7 +5,7 @@
5
5
  import type { ProjectEntry } from '../workspace/index.js'
6
6
  import { loadWorkspace, unreachedHint, unreachedPackages } from '../workspace/index.js'
7
7
  import path from 'node:path'
8
- import { type CacheLayer, type CachePolicy, stopRuntimeProbes } from '../cache/index.js'
8
+ import { Cache, type CacheLayer, type CachePolicy, stopRuntimeProbes } from '../cache/index.js'
9
9
  import { VERSION } from '../version.js'
10
10
  import {
11
11
  resetSandbox,
@@ -1177,38 +1177,48 @@ async function runOnBus(
1177
1177
  // entry's save transaction (miss path only), so a warm all-cache-hit
1178
1178
  // run does no extra recording work. The telemetry mirror is built in
1179
1179
  // the same pass, only when a sink is active.
1180
- const records = assembleRunRecords({
1181
- outcomes: list,
1182
- runId,
1183
- startedAtMs: endedAtMsAtStart,
1184
- endedAtMs,
1185
- totalMs,
1186
- ok,
1187
- // The invocation is stored and `vx last` prints it: a secret passed
1188
- // after `--` (`-- --token=$NPM_TOKEN`) is masked here as the task's
1189
- // own output masks it, so cache.db holds no plaintext value.
1190
- command: maskInvocation(options.command ?? process.argv.slice(1).join(' ')),
1191
- requestedTasks: options.tasks,
1192
- cachePolicy: compactCachePolicy(policy),
1193
- concurrency,
1194
- flow: options.flow ?? null,
1195
- forwardArgs: options.forwardArgs,
1196
- tags: maskTags(options.tags ?? {}),
1197
- git: gitContext,
1198
- ci: ciContext,
1199
- host: hostContext,
1200
- withTelemetry: telemetry !== undefined,
1201
- })
1202
- try {
1203
- if (stoppedBy === undefined) cache.recordRunBundle(records)
1204
- } catch (err) {
1205
- // History is observability: a full cache disk at the very end must
1206
- // not turn a finished run's verdict into a stack and exit 1 (seen as
1207
- // an unprivileged user on a 2 MiB disk, 2026-09-16). The run said
1208
- // what happened; `vx last` and the flaky list will not know this one.
1209
- const message = err instanceof Error ? err.message : String(err)
1210
- log.status(`[vx] run history not recorded: ${message} — the verdict above stands`)
1180
+ const recordsOf = (final: TaskOutcome[], runOk: boolean) =>
1181
+ assembleRunRecords({
1182
+ outcomes: final,
1183
+ runId,
1184
+ startedAtMs: endedAtMsAtStart,
1185
+ endedAtMs,
1186
+ totalMs,
1187
+ ok: runOk,
1188
+ // The invocation is stored and `vx last` prints it: a secret passed
1189
+ // after `--` (`-- --token=$NPM_TOKEN`) is masked here as the task's
1190
+ // own output masks it, so cache.db holds no plaintext value.
1191
+ command: maskInvocation(options.command ?? process.argv.slice(1).join(' ')),
1192
+ requestedTasks: options.tasks,
1193
+ cachePolicy: compactCachePolicy(policy),
1194
+ concurrency,
1195
+ flow: options.flow ?? null,
1196
+ forwardArgs: options.forwardArgs,
1197
+ tags: maskTags(options.tags ?? {}),
1198
+ git: gitContext,
1199
+ ci: ciContext,
1200
+ host: hostContext,
1201
+ withTelemetry: telemetry !== undefined,
1202
+ })
1203
+ const records = recordsOf(list, ok)
1204
+ const recordHistory = (write: () => void): void => {
1205
+ try {
1206
+ write()
1207
+ } catch (err) {
1208
+ // History is observability: a full cache disk at the very end must
1209
+ // not turn a finished run's verdict into a stack and exit 1 (seen as
1210
+ // an unprivileged user on a 2 MiB disk, 2026-09-16). The run said
1211
+ // what happened; `vx last` and the flaky list will not know this one.
1212
+ const message = err instanceof Error ? err.message : String(err)
1213
+ log.status(`[vx] run history not recorded: ${message} — the verdict above stands`)
1214
+ }
1211
1215
  }
1216
+ // A server kept in the foreground can still fail the run after the
1217
+ // cache closes: its history is written once the wait has decided, or it
1218
+ // read `ok` over the exit 1 and fed the flaky list a pass (X-23).
1219
+ const historyAfterWait = keepAlive.children.length > 0 && !hold && stoppedBy === undefined
1220
+ if (stoppedBy === undefined && !historyAfterWait)
1221
+ recordHistory(() => cache.recordRunBundle(records))
1212
1222
  mark('record history')
1213
1223
  // Drain any still-in-flight background prefetches before closing the
1214
1224
  // cache handle — a prefetch ingesting into a closed SQLite DB would
@@ -1317,7 +1327,7 @@ async function runOnBus(
1317
1327
 
1318
1328
  // Edge case the summary already reported: the user requested a
1319
1329
  // persistent task (dev server / watcher). The run is "done" in every
1320
- // bookkeeping sense — summary printed, history recorded — but the
1330
+ // bookkeeping sense — summary printed, the cache closed — but the
1321
1331
  // server is still up and that's the point. Stay in the foreground
1322
1332
  // until ONE of them exits: Ctrl-C hits the whole process group (the
1323
1333
  // server dies; our SIGINT handler also exits 130), and a crash ends
@@ -1378,6 +1388,16 @@ async function runOnBus(
1378
1388
  if (!stopRun.signal.aborted && first.code !== 0) failServer(node.id, first.code)
1379
1389
  const final = [...outcomes.values()]
1380
1390
  await summarize(ok && first.code === 0, final)
1391
+ if (historyAfterWait) {
1392
+ recordHistory(() => {
1393
+ const local = Cache.inspect(prepared.cacheDir)
1394
+ try {
1395
+ local.recordRunBundle(recordsOf(final, ok && first.code === 0))
1396
+ } finally {
1397
+ local.close()
1398
+ }
1399
+ })
1400
+ }
1381
1401
  return { ok: ok && first.code === 0, outcomes: final }
1382
1402
  }
1383
1403
 
@@ -110,6 +110,15 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
110
110
  if (config.cacheDir !== undefined && typeof config.cacheDir !== 'string') {
111
111
  throw new UserError(`${configPath}: \`cacheDir\` must be a string`)
112
112
  }
113
+ // `' '` made a directory named three spaces at the root, hidden by the
114
+ // cache's own `.gitignore` (X-22).
115
+ if (
116
+ typeof config.cacheDir === 'string' &&
117
+ config.cacheDir !== '' &&
118
+ config.cacheDir.trim() === ''
119
+ ) {
120
+ throw new UserError(`${configPath}: \`cacheDir\` is only whitespace — name a directory`)
121
+ }
113
122
  if (config.timeout !== undefined) {
114
123
  if (
115
124
  typeof config.timeout !== 'number' ||
@@ -728,7 +737,9 @@ export function validateProjectConfig(config: ProjectConfig, configPath: string)
728
737
  if (list !== undefined) {
729
738
  if (
730
739
  !Array.isArray(list) ||
731
- list.some((s) => typeof s !== 'string' || s.length === 0 || s.includes('\0'))
740
+ // Whitespace alone ran as a no-op probe that folds nothing, as
741
+ // a blank `exec.command` did before item 1099 (X-22).
742
+ list.some((s) => typeof s !== 'string' || s.trim() === '' || s.includes('\0'))
732
743
  ) {
733
744
  throw new UserError(
734
745
  `${where}.cache.inputs.${field} must be an array of non-empty shell command strings with no NUL`,