@vzn/vx 0.0.159 → 0.0.161

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.159",
3
+ "version": "0.0.161",
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.159",
21
- "@vzn/vx-linux-arm64": "0.0.159",
22
- "@vzn/vx-darwin-x64": "0.0.159",
23
- "@vzn/vx-darwin-arm64": "0.0.159"
20
+ "@vzn/vx-linux-x64": "0.0.161",
21
+ "@vzn/vx-linux-arm64": "0.0.161",
22
+ "@vzn/vx-darwin-x64": "0.0.161",
23
+ "@vzn/vx-darwin-arm64": "0.0.161"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -46,6 +46,7 @@ export function localExecutor(): TaskExecutor {
46
46
  reportWithin: req.sandbox.reportWithin,
47
47
  reportLinked: req.sandbox.reportLinked,
48
48
  config: req.sandbox.config,
49
+ ...(req.signal !== undefined ? { signal: req.signal } : {}),
49
50
  })
50
51
  },
51
52
  }
@@ -619,6 +619,8 @@ export interface SandboxedRunArgs {
619
619
  onStderr?: (chunk: string) => void
620
620
  /** See `RunOptions.liveChildren` — same contract for sandboxed spawns. */
621
621
  liveChildren?: Set<ReturnType<typeof Bun.spawn>>
622
+ /** `ExecuteRequest.signal`: once aborted, the tracer retry spawns nothing. */
623
+ signal?: AbortSignal
622
624
  /** See `RunOptions.timeoutMs` — SIGTERM the child after this many ms. */
623
625
  timeoutMs?: number
624
626
  /** See `CaptureConfig` — which streams are retained on the result. */
@@ -1226,12 +1228,14 @@ const TRACER_RETRY_LINE =
1226
1228
  * strace runs detached (`-DD`), and a tracer that dies leaves the command
1227
1229
  * running untraced — but the trace then stops short, and a denial after it
1228
1230
  * goes unreported. So an attempt whose stderr carries strace's own word is
1229
- * run once more, whatever its exit: the sandbox kept its writes to what it
1231
+ * run once more, whatever its exit, unless the run is stopping: the sandbox kept its writes to what it
1230
1232
  * declared, so a second run redoes, not doubles, it.
1231
1233
  */
1232
1234
  export async function runSandboxed(args: SandboxedRunArgs): Promise<SandboxedRunResult> {
1233
1235
  const { tracerFailed, ...first } = await runSandboxedOnce(args)
1234
- if (!tracerFailed) return first
1236
+ // A stopping run has killed the children it holds; a retry would be one
1237
+ // spawned after that kill.
1238
+ if (!tracerFailed || args.signal?.aborted === true) return first
1235
1239
  args.onStderr?.(TRACER_RETRY_LINE)
1236
1240
  const { tracerFailed: _again, ...second } = await runSandboxedOnce(args)
1237
1241
  return {
@@ -15,9 +15,16 @@ import type { Database } from 'bun:sqlite'
15
15
  import { EXECUTED_RUNS_SQL } from '../cache/index.js'
16
16
  import { failureModeOf, mixedOutcomeKeysSql } from './failure-mode.js'
17
17
  import type { FailureMode } from './failure-mode.js'
18
+ import { isPassStatus, TASK_STATUSES } from './telemetry.js'
18
19
 
19
20
  const DEFAULT_RECENT = 50
20
21
 
22
+ // A hit is recorded as `cache-hit` / `cache-hit-remote` and replays a
23
+ // success; the list is derived so a new status cannot miss it.
24
+ const PASS_STATUSES = `(${TASK_STATUSES.filter(isPassStatus)
25
+ .map((s) => `'${s}'`)
26
+ .join(', ')})`
27
+
21
28
  /** Per (project#task) — last RECENT runs collapsed into a summary. */
22
29
  export interface TaskHistory {
23
30
  /** Total runs in the recent window. */
@@ -108,7 +115,7 @@ export class LocalHistoryProvider implements HistoryProvider {
108
115
  r.project,
109
116
  r.task,
110
117
  COUNT(*) AS total,
111
- SUM(CASE WHEN r.status = 'success' THEN 1 ELSE 0 END) AS successes,
118
+ SUM(CASE WHEN r.status IN ${PASS_STATUSES} THEN 1 ELSE 0 END) AS successes,
112
119
  SUM(CASE WHEN r.cache_hit = 1 THEN 1 ELSE 0 END) AS hits,
113
120
  SUM(CASE WHEN r.status = 'failed' THEN 1 ELSE 0 END) AS failures,
114
121
  SUM(CASE WHEN r.attempts > 1 THEN 1 ELSE 0 END) AS retried,
@@ -159,21 +166,21 @@ export class LocalHistoryProvider implements HistoryProvider {
159
166
  // nondeterminism); a green history never pays for it.
160
167
  const mixed = this.mixedOutcomeKeys(
161
168
  floor,
162
- rows.filter((r) => (r.failures || 0) > 0 && (r.retried || 0) === 0),
169
+ rows.filter((r) => r.failures > 0 && r.retried === 0),
163
170
  )
164
171
 
165
172
  for (const row of rows) {
166
173
  const key = `${row.project}#${row.task}`
167
- const total = row.total || 0
168
- const counts = { total, failures: row.failures || 0, retried: row.retried || 0 }
174
+ const total = row.total
175
+ const counts = { total, failures: row.failures, retried: row.retried }
169
176
  const durations = row.ds === null ? undefined : row.ds.split(',').map(Number)
170
177
  if (durations !== undefined) durations.sort((a, b) => a - b)
171
178
  out.set(key, {
172
179
  runs: total,
173
180
  p50DurationMs: durations ? pickPercentile(durations, 0.5) : undefined,
174
181
  p99DurationMs: durations ? pickPercentile(durations, 0.99) : undefined,
175
- successRate: total > 0 ? (row.successes || 0) / total : 0,
176
- hitRate: total > 0 ? (row.hits || 0) / total : 0,
182
+ successRate: row.successes / total,
183
+ hitRate: row.hits / total,
177
184
  failureMode: failureModeOf(counts, () => mixed.get(key) ?? 0),
178
185
  ...(row.rss !== null ? { maxPeakRssBytes: row.rss } : {}),
179
186
  ...(row.cpu !== null ? { maxCpuParallelism: row.cpu } : {}),
@@ -207,8 +214,8 @@ export class LocalHistoryProvider implements HistoryProvider {
207
214
  }
208
215
  }
209
216
 
217
+ // A group holds at least one row, so every SUM is a number and `sorted` is
218
+ // never empty; q < 1 keeps the index inside it.
210
219
  function pickPercentile(sorted: number[], q: number): number {
211
- if (sorted.length === 0) return 0
212
- const idx = Math.min(sorted.length - 1, Math.floor(q * sorted.length))
213
- return sorted[idx]!
220
+ return sorted[Math.floor(q * sorted.length)]!
214
221
  }
@@ -260,12 +260,37 @@ export function formatSummarySection(
260
260
  )
261
261
  }
262
262
  lines.push('', row('info', join(info)), row('time', `${formatDuration(totalMs)}${spread}`))
263
+ // The run in one line, last, where the eye lands: what a Turbo user
264
+ // reads first in its own summary (tasks, cached, time), in vx's words.
265
+ if (stats.total > 0) lines.push(row('result', resultLine(stats, hits, totalMs, colors)))
263
266
  } else {
264
267
  lines.push('', row('time', `${formatDuration(totalMs)}${spread}`))
265
268
  }
266
269
  return lines
267
270
  }
268
271
 
272
+ /**
273
+ * `42 tasks · 38 cached (90%) · 3.2s`, `2 tasks · all cached · 23ms`,
274
+ * with the failures first when there are any. Cached is every hit (up to
275
+ * date, restored locally or remotely) over every counted task.
276
+ */
277
+ function resultLine(
278
+ stats: SummaryStats,
279
+ hits: number,
280
+ totalMs: number,
281
+ colors: ColorSupport,
282
+ ): string {
283
+ const parts = [`${stats.total} task${stats.total === 1 ? '' : 's'}`]
284
+ if (stats.failed > 0) parts.push(paint(ERROR, `${stats.failed} failed`, colors, { bold: true }))
285
+ parts.push(
286
+ hits === stats.total
287
+ ? paint(SUCCESS, 'all cached', colors, { bold: true })
288
+ : `${hits} cached (${Math.floor((hits / stats.total) * 100)}%)`,
289
+ )
290
+ parts.push(formatDuration(totalMs))
291
+ return parts.join(` ${paint('', '\u00b7', colors, { dim: true })} `)
292
+ }
293
+
269
294
  // Local byte formatter — the orchestrator can't import cli/format.ts
270
295
  // (module boundary), and the footer only needs one compact form.
271
296
  export function formatRunSummary(
@@ -848,6 +848,14 @@ const FOREIGN_TOKENS: readonly (readonly [string, string])[] = [
848
848
  ]
849
849
 
850
850
  function assertNoForeignToken(glob: string, where: string): void {
851
+ // `^production`: Nx's named input of the dependencies (D-51). No file
852
+ // starts with `^` in practice, while a bare `default` can be a directory,
853
+ // so only the upstream form is refused; the bare one keeps the warning.
854
+ if ((glob.startsWith('!') ? glob.slice(1) : glob).startsWith('^')) {
855
+ throw new UserError(
856
+ `${where}: "${glob}" starts with ^, Nx's named input of the dependencies — vx keys a task on its dependencies through \`dependsOn\` (\`^build\`); list this project's files here`,
857
+ )
858
+ }
851
859
  for (const [token, meaning] of FOREIGN_TOKENS) {
852
860
  if (glob.includes(token)) {
853
861
  throw new UserError(`${where}: "${glob}" holds ${token}, ${meaning}`)