@vzn/vx 0.0.498 → 0.0.499

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.498",
3
+ "version": "0.0.499",
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.498",
25
- "@vzn/vx-linux-arm64": "0.0.498",
26
- "@vzn/vx-darwin-x64": "0.0.498",
27
- "@vzn/vx-darwin-arm64": "0.0.498"
24
+ "@vzn/vx-linux-x64": "0.0.499",
25
+ "@vzn/vx-linux-arm64": "0.0.499",
26
+ "@vzn/vx-darwin-x64": "0.0.499",
27
+ "@vzn/vx-darwin-arm64": "0.0.499"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -28,7 +28,7 @@
28
28
  import { isGroupTask, type TaskNode, type TaskOutcome } from '../graph/index.js'
29
29
  import { maskedCommand } from '../util/index.js'
30
30
  import { paint, type ColorSupport } from './colors.js'
31
- import { formatDuration } from './summary.js'
31
+ import { formatDuration, neverStarted } from './summary.js'
32
32
  import { outcomeLabel, skippedReason } from './events.js'
33
33
 
34
34
  const NO_COLOR: ColorSupport = { enabled: false }
@@ -54,6 +54,7 @@ const TASK = '#f472b6' // pink-400 — task part of an id
54
54
  const SUCCESS = '#22c55e' // green-500 — success / fresh
55
55
  const WARN = '#eab308' // yellow-500 — skipped
56
56
  const ERROR = '#ef4444' // red-500 — failed
57
+ const ABORTED = '#f97316' // orange-500 — killed by a shutdown signal
57
58
  const LOCAL = '#38bdf8' // sky-400 — local cache hit
58
59
  const REMOTE = '#2563eb' // blue-600 — remote cache hit
59
60
 
@@ -210,13 +211,13 @@ function paintTaskId(node: TaskNode, colors: ColorSupport, opts: { bold?: boolea
210
211
 
211
212
  // ── Reported-line grid: glyph · time · status · cache · name ────────
212
213
  // glyph SHAPE encodes the cache axis (⏺ miss · ► fresh · ⇢ local · ⇣
213
- // remote; ◼ failed · ⊘ skipped), glyph COLOR encodes the
214
+ // remote; ◼ failed · ⊘ skipped · ✗ aborted · ◌ not run), glyph COLOR encodes the
214
215
  // task axis (green/red/yellow/cyan). The status and cache WORDS spell
215
216
  // the two axes out, each in its own color. Time is right-aligned in a
216
217
  // fixed cell so durations line up and a ticking elapsed never shifts
217
218
  // the row. All detail (exit code, output) lives in the framed block.
218
219
  export const TIME_COL = 7 // "0ms" … "999.99s"
219
- const STATUS_COL = 7 // "success" / "running" / "skipped"
220
+ const STATUS_COL = 7 // "success" / "running" / "skipped" / "aborted" / "not run"
220
221
  const CACHE_COL = 8 // "no-cache" / "remote" / "local" / "fresh" / "miss"
221
222
 
222
223
  /** Right-align the duration in a TIME_COL cell (pad on the left). */
@@ -381,6 +382,41 @@ export function formatTaskSkippedLine(
381
382
  )
382
383
  }
383
384
 
385
+ /**
386
+ * One row for a task a shutdown signal killed (`aborted`, its time so far)
387
+ * or one the stop reached before it ran (`not run`, no time). Neither
388
+ * reached the cache, so the cache cell is blank, as a skip's is.
389
+ */
390
+ export function formatTaskAbortedLine(
391
+ node: TaskNode,
392
+ o: TaskOutcome,
393
+ colors: ColorSupport = NO_COLOR,
394
+ ): string {
395
+ const id = paintTaskId(node, colors)
396
+ if (neverStarted(o)) {
397
+ return formatTaskRow(
398
+ paint('', '\u25cc', colors, { dim: true }),
399
+ null,
400
+ 'not run',
401
+ '',
402
+ '',
403
+ '',
404
+ id,
405
+ colors,
406
+ )
407
+ }
408
+ return formatTaskRow(
409
+ paint(ABORTED, '\u2717', colors),
410
+ o.durationMs,
411
+ 'aborted',
412
+ ABORTED,
413
+ '',
414
+ '',
415
+ id,
416
+ colors,
417
+ )
418
+ }
419
+
384
420
  /**
385
421
  * Live-frame brackets for focused mode: the requested task's output
386
422
  * streams raw between an opening line (id + command) and a closing
@@ -5,6 +5,7 @@ import {
5
5
  formatFrameOpen,
6
6
  formatKeptLines,
7
7
  formatPersistentTailBlock,
8
+ formatTaskAbortedLine,
8
9
  formatTaskBlock,
9
10
  formatTaskExecutedLine,
10
11
  formatTaskHitLine,
@@ -17,7 +18,7 @@ import {
17
18
  type StatusStream,
18
19
  type WorkerSlot,
19
20
  } from './status-line.js'
20
- import { formatDuration, formatSummarySection, type RunContext } from './summary.js'
21
+ import { formatDuration, formatSummarySection, neverStarted, type RunContext } from './summary.js'
21
22
  import { isGroupTask } from '../graph/index.js'
22
23
  import { appendTail, createTail, resetTail, tailText, type Tail } from '../util/index.js'
23
24
  import { isCacheHit } from './telemetry.js'
@@ -583,9 +584,9 @@ export function defaultLogger(
583
584
  // The pre-ready window closed either way: a failed task is over, and
584
585
  // a ready one starts a fresh post-ready tail below.
585
586
  persistentTails.delete(node.id)
586
- // An aborted task (child killed by a shutdown signal) reverts
587
- // to pending: free its worker slot, but never count or render it
588
- // — the run is tearing down and it has no honest outcome.
587
+ // An aborted task (killed by a shutdown signal, or reached by the stop
588
+ // before it ran) frees its worker slot but is never counted live: the
589
+ // run is tearing down and it has no honest outcome.
589
590
  if (outcome.status === 'aborted') {
590
591
  const si = slots.findIndex((s) => s !== null && s.id === node.id)
591
592
  if (si >= 0) slots[si] = slotQueue.shift() ?? null
@@ -594,24 +595,32 @@ export function defaultLogger(
594
595
  if (qi >= 0) slotQueue.splice(qi, 1)
595
596
  }
596
597
  refresh(true)
598
+ if (isGroupTask(node)) return
597
599
  // What it printed while it stopped (a trap's cleanup) still reaches
598
600
  // the terminal where a frame would have: a task that traps Ctrl-C
599
601
  // and exits 0 is aborted since item 962, and its frame was dropped
600
- // with the count.
602
+ // with the count. A task that never started opened no frame.
601
603
  const framed =
602
- view.mode === 'full' ||
603
- (view.mode === 'focused' && isPrimary(node)) ||
604
- (framesTerminal && holdsTerminal(node))
605
- if (framed && !isGroupTask(node)) {
606
- if (streamsLive(node)) {
607
- if (streamMidLine) {
608
- writer.write('\n')
609
- streamMidLine = false
610
- }
611
- emitFrameClose(formatFrameClose(node, outcome, colors))
612
- } else if (stdout.length > 0 || stderr.length > 0) {
613
- emitBlock(formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors, true))
604
+ !neverStarted(outcome) &&
605
+ (view.mode === 'full' ||
606
+ (view.mode === 'focused' && isPrimary(node)) ||
607
+ (framesTerminal && holdsTerminal(node)))
608
+ if (framed && streamsLive(node)) {
609
+ if (streamMidLine) {
610
+ writer.write('\n')
611
+ streamMidLine = false
614
612
  }
613
+ emitFrameClose(formatFrameClose(node, outcome, colors))
614
+ return
615
+ }
616
+ if (framed && (stdout.length > 0 || stderr.length > 0)) {
617
+ emitBlock(formatTaskBlock(node, outcome, { stdout, stderr, ...dropped }, colors, true))
618
+ return
619
+ }
620
+ // Otherwise a row in the task list, in every view that lists a
621
+ // failure: the run exits red, and the row names why.
622
+ if (view.mode !== 'none' && view.mode !== 'hash-only') {
623
+ emitLine(formatTaskAbortedLine(node, outcome, colors))
615
624
  }
616
625
  return
617
626
  }
@@ -90,12 +90,7 @@ import { assembleRunRecords } from './run-records.js'
90
90
  import { hasEnded, selectKeepAlive, shutdownPersistent } from './persistent.js'
91
91
  import { writeRunProfile, writeRunSummary } from './run-artifacts.js'
92
92
  import { createSaveLane } from './save-lane.js'
93
- import {
94
- formatAbortedSection,
95
- formatFlakySection,
96
- formatRunSummary,
97
- formatSkippedSection,
98
- } from './summary.js'
93
+ import { formatFlakySection, formatRunSummary, formatSkippedSection } from './summary.js'
99
94
  import { detectFlaky, type FlakyCandidate } from './failure-mode.js'
100
95
  import type { RunOptions, RunSummary } from './options.js'
101
96
 
@@ -1063,10 +1058,7 @@ async function runOnBus(
1063
1058
  for (const line of formatPersistentList(stillUp, colors)) log.status(line)
1064
1059
  }
1065
1060
  for (const line of formatRunSummary(list, totalMs, colors, runContext)) log.status(line)
1066
- // A task killed by a shutdown signal is in no bucket above, yet it makes
1067
- // `ok` false — name it, or the red exit is undiagnosable.
1068
- for (const line of formatAbortedSection(list)) log.status(line)
1069
- // And a dependency-only server that died before the end of the graph
1061
+ // A dependency-only server that died before the end of the graph
1070
1062
  // stopped it: the footer counts it failed, this says why.
1071
1063
  // A signal death is named as its exit code, as the keep-alive wait
1072
1064
  // names it: `code SIGTERM` read beside `code 143` for one event (item
@@ -19,6 +19,7 @@ const WARN = '#eab308'
19
19
  const LOCAL = '#38bdf8' // sky-400
20
20
  const REMOTE = '#2563eb' // blue-600
21
21
  const ERROR = '#ef4444'
22
+ const ABORTED = '#f97316' // orange-500: red's neighbour, not a failure
22
23
 
23
24
  const BAR_WIDTH = 50
24
25
  // Brand gradient for the rule: identity violet → pink (the project /
@@ -99,6 +100,10 @@ export interface SummaryStats {
99
100
  /** Ended OK — executed successes AND cache hits. */
100
101
  successful: number
101
102
  skipped: number
103
+ /** Killed by a shutdown signal (final summary only). Not in `total`. */
104
+ aborted?: number
105
+ /** Reached by the stop before they ran (final summary only). Not in `total`. */
106
+ notRun?: number
102
107
  total: number
103
108
  upToDate: number
104
109
  restoredLocal: number
@@ -143,6 +148,8 @@ export function formatSummarySection(
143
148
  const miss = stats.miss
144
149
  const noCache = stats.noCache ?? 0
145
150
  const left = stats.left ?? 0
151
+ const aborted = stats.aborted ?? 0
152
+ const notRun = stats.notRun ?? 0
146
153
  const remainder = { n: left, color: '', glyph: '\u25b1', dim: true }
147
154
 
148
155
  const dim = (txt: string) => paint('', txt, colors, { dim: true })
@@ -157,6 +164,12 @@ export function formatSummarySection(
157
164
  if (stats.successful > 0) taskParts.push(paint(SUCCESS, `${stats.successful} success`, colors))
158
165
  if (stats.skipped > 0) taskParts.push(paint(WARN, `${stats.skipped} skipped`, colors))
159
166
  if (stats.total > 0) taskParts.push(dim(`${stats.total} total`))
167
+ // Rows in the task list, but outside `total`: history and telemetry count
168
+ // the same tasks the total does, and an aborted task did no work.
169
+ const uncounted: string[] = []
170
+ if (aborted > 0) uncounted.push(paint(ABORTED, `${aborted} aborted`, colors))
171
+ if (notRun > 0) uncounted.push(dim(`${notRun} not run`))
172
+ if (uncounted.length > 0) taskParts.push(`${dim('not counted:')} ${uncounted.join(dim(', '))}`)
160
173
  const taskBar = segmentBar(
161
174
  [
162
175
  { n: stats.failed, color: ERROR },
@@ -201,6 +214,8 @@ export function formatSummarySection(
201
214
  if (taskParts.length > 0) lines.push(legend(taskParts))
202
215
  } else {
203
216
  lines.push(row('tasks', dim('0 tasks')))
217
+ // A stop before any task finished still names what it took down.
218
+ if (taskParts.length > 0) lines.push(legend(taskParts))
204
219
  }
205
220
 
206
221
  // Cache meter + numbers, fixed order: miss · up-to-date · local ·
@@ -324,11 +339,14 @@ export function formatRunSummary(
324
339
  .filter((o) => (o.status === 'success' || o.status === 'failed') && !isGroupTask(o.node))
325
340
  .map((o) => o.durationMs)
326
341
  const heldOutcomes = outcomes.filter((o) => o.admissionHeldMs !== undefined)
342
+ const notRun = outcomes.filter((o) => neverStarted(o) && !isGroupTask(o.node)).length
327
343
  return formatSummarySection(
328
344
  {
329
345
  failed: t.failed,
330
346
  successful: t.successful,
331
347
  skipped: t.skipped,
348
+ aborted: t.aborted - notRun,
349
+ notRun,
332
350
  total: t.total,
333
351
  upToDate: t.upToDate,
334
352
  restoredLocal: t.restoredLocal,
@@ -360,35 +378,12 @@ export function formatRunSummary(
360
378
  }
361
379
 
362
380
  /**
363
- * Post-summary section naming the tasks a shutdown signal killed, and those
364
- * the stop reached before they ran. An aborted task did no work, so it is in
365
- * no tally bucket and no history row — but the run still exits non-zero, and
366
- * without this the user reads a red exit over a fully green summary that
367
- * names nothing. Empty when nothing aborted.
381
+ * An `aborted` outcome the stop reached before dispatch. The scheduler gives
382
+ * it an invented exit 1; only a started task carries a wall-clock start, so
383
+ * that is what tells "never ran" from "killed" (item 1062).
368
384
  */
369
- export function formatAbortedSection(outcomes: readonly TaskOutcome[]): string[] {
370
- // The scheduler marks what the stop reached before dispatch `aborted` too,
371
- // with an invented exit 1: listed as killed, a task that never ran read as
372
- // one the signal cut short (item 1062). Only a started one carries a
373
- // wall-clock start. A group is no task here, as in the skipped section.
374
- const aborted = outcomes.filter((o) => o.status === 'aborted' && !isGroupTask(o.node))
375
- const killed = aborted.filter((o) => o.wallclockStartNs !== undefined)
376
- const unstarted = aborted.filter((o) => o.wallclockStartNs === undefined)
377
- const tasks = (n: number): string => `${n} task${n === 1 ? '' : 's'}`
378
- const lines: string[] = []
379
- if (killed.length > 0) {
380
- lines.push(
381
- '',
382
- ` Aborted: ${tasks(killed.length)} killed by a shutdown signal — not counted above`,
383
- )
384
- for (const o of killed) lines.push(` ✗ ${o.node.id} — exit ${o.exitCode}, nothing cached`)
385
- }
386
- if (unstarted.length > 0) {
387
- const they = unstarted.length === 1 ? 'it' : 'they'
388
- lines.push('', ` Not started: ${tasks(unstarted.length)} the run stopped before ${they} ran`)
389
- for (const o of unstarted) lines.push(` · ${o.node.id}`)
390
- }
391
- return lines
385
+ export function neverStarted(o: TaskOutcome): boolean {
386
+ return o.status === 'aborted' && o.wallclockStartNs === undefined
392
387
  }
393
388
 
394
389
  /**