@vzn/vx 0.0.650 → 0.0.652
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/README.md +7 -23
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/schemas/show.json +14 -20
- package/src/bin.ts +21 -31
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +221 -30
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +77 -14
- package/src/cache/layer.ts +17 -6
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +5 -3
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +56 -17
- package/src/cache/schema.ts +15 -2
- package/src/cli/cache.ts +28 -18
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +14 -25
- package/src/cli/index.ts +28 -9
- package/src/cli/info.ts +16 -8
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +246 -204
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +8 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +45 -52
- package/src/cli/task-verb.ts +22 -0
- package/src/cli/upgrade.ts +3 -1
- package/src/cli/watch-fs.ts +6 -1
- package/src/cli/watch-judge.ts +3 -2
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +94 -52
- package/src/cli/why.ts +123 -89
- package/src/cli/workspace-config.ts +2 -1
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +42 -2
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +58 -5
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/pipes.ts +152 -0
- package/src/exec/runner.ts +199 -182
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +317 -23
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/definition.ts +16 -3
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +17 -11
- package/src/graph/task-graph.ts +51 -24
- package/src/handoff.ts +10 -7
- package/src/history/failure-mode.ts +24 -1
- package/src/history/history.ts +5 -0
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +14 -6
- package/src/index.ts +7 -3
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +67 -20
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/live-usage.ts +16 -11
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +108 -29
- package/src/orchestrator/miss-save.ts +58 -11
- package/src/orchestrator/persistent-task.ts +5 -1
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +7 -3
- package/src/orchestrator/plugin-host.ts +67 -2
- package/src/orchestrator/plugin.ts +53 -2
- package/src/orchestrator/prepare.ts +16 -1
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +62 -13
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +88 -59
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +145 -41
- package/src/orchestrator/sandbox-request.ts +52 -9
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +21 -7
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +6 -14
- package/src/report/forecast.ts +10 -32
- package/src/report/framed-output.ts +283 -175
- package/src/report/index.ts +8 -3
- package/src/report/logger.ts +74 -39
- package/src/report/plain-output.ts +3 -1
- package/src/report/status-line.ts +49 -357
- package/src/report/summary.ts +105 -136
- package/src/settings.ts +8 -8
- package/src/ui/ansi.ts +48 -0
- package/src/ui/components.ts +506 -0
- package/src/ui/index.ts +79 -0
- package/src/ui/kit.ts +37 -0
- package/src/ui/live-region.ts +363 -0
- package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
- package/src/ui/terminal.ts +51 -0
- package/src/ui/theme.ts +224 -0
- package/src/ui/version-line.ts +28 -0
- package/src/util/config-frame.ts +191 -14
- package/src/util/edit-distance.ts +6 -1
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +21 -3
- package/src/util/secret-mask.ts +39 -3
- package/src/util/settle.ts +33 -1
- package/src/util/tail.ts +4 -1
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +70 -63
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +36 -1
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +16 -9
- package/src/workspace/workspace.ts +3 -0
- package/src/util/colors.ts +0 -133
- package/src/util/report.ts +0 -44
- package/src/util/ui.ts +0 -258
- /package/src/{report/path-links.ts → ui/links.ts} +0 -0
- /package/src/{report → ui}/terminal-signals.ts +0 -0
|
@@ -26,49 +26,26 @@
|
|
|
26
26
|
// `--ui=stream` mode.
|
|
27
27
|
|
|
28
28
|
import { withForwardArgs } from '../exec/index.js'
|
|
29
|
-
import { isGroupTask, type TaskNode, type TaskOutcome } from '../graph/index.js'
|
|
29
|
+
import { isCacheHit, isGroupTask, type TaskNode, type TaskOutcome } from '../graph/index.js'
|
|
30
|
+
import { formatElapsed, maskedCommand, printable } from '../util/index.js'
|
|
31
|
+
import * as ui from '../ui/index.js'
|
|
30
32
|
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
fileMemo,
|
|
34
|
+
hasEscape,
|
|
35
|
+
idParts,
|
|
36
|
+
lastRedraw,
|
|
37
|
+
leavesStyle,
|
|
38
|
+
linkPaths,
|
|
34
39
|
paint,
|
|
35
|
-
printable,
|
|
36
40
|
ruleWidth,
|
|
37
|
-
|
|
41
|
+
STYLE_END,
|
|
38
42
|
type ColorSupport,
|
|
39
|
-
|
|
43
|
+
type Style,
|
|
44
|
+
} from '../ui/index.js'
|
|
40
45
|
import { neverStarted } from './summary.js'
|
|
41
46
|
import { outcomeLabel, outcomeWord, ranNoCache, skippedReason } from './events.js'
|
|
42
|
-
import { fileMemo, linkPaths } from './path-links.js'
|
|
43
47
|
|
|
44
48
|
const NO_COLOR: ColorSupport = { enabled: false }
|
|
45
|
-
// Ends any colour a task's output left on, so vx's own lines below never
|
|
46
|
-
// take it (a task killed mid-write leaves one open).
|
|
47
|
-
export const RESET = '\x1b[0m'
|
|
48
|
-
|
|
49
|
-
/** Whether `text`'s last colour code sets a style rather than resetting it. */
|
|
50
|
-
export function leavesStyle(text: string): boolean {
|
|
51
|
-
const at = text.lastIndexOf('\x1b[')
|
|
52
|
-
if (at < 0) return false
|
|
53
|
-
const sgr = /^\[([\d;]*)m/.exec(text.slice(at + 1, at + 64))
|
|
54
|
-
// Not a colour code (a cursor move): look no further, as a reset costs nothing.
|
|
55
|
-
if (sgr === null) return true
|
|
56
|
-
return sgr[1] !== '' && sgr[1] !== '0'
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const ACCENT = '#06b6d4' // cyan-500 — bullets, remote-hit hint
|
|
60
|
-
// Identity hues (`identityColor`) are outside the status palette (green /
|
|
61
|
-
// red / yellow / cyan) so a task id can never read as an outcome. Each
|
|
62
|
-
// half of an id hashes its own name: same project, same hue; same target,
|
|
63
|
-
// same hue, in every run, list and region row.
|
|
64
|
-
|
|
65
|
-
const SUCCESS = '#22c55e' // green-500 — success / fresh
|
|
66
|
-
const WARN = '#eab308' // yellow-500 — skipped
|
|
67
|
-
const ERROR = '#ef4444' // red-500 — failed
|
|
68
|
-
const ABORTED = '#f97316' // orange-500 — killed by a shutdown signal
|
|
69
|
-
const LOCAL = '#38bdf8' // sky-400 — local cache hit
|
|
70
|
-
const REMOTE = '#2563eb' // blue-600 — remote cache hit
|
|
71
|
-
|
|
72
49
|
/** A piece of a task's output, as written: `err` for stderr. */
|
|
73
50
|
export interface OutputChunk {
|
|
74
51
|
text: string
|
|
@@ -111,11 +88,11 @@ export function formatTaskBlock(
|
|
|
111
88
|
const output = body.output ?? []
|
|
112
89
|
|
|
113
90
|
const idPainted = paintTaskId(node, colors, { bold: true })
|
|
114
|
-
const corner = (s: string) => paint('', s, colors
|
|
91
|
+
const corner = (s: string) => paint('muted', s, colors)
|
|
115
92
|
// A ready server's frame reads as the live one does: marked, and running.
|
|
116
93
|
const mark = persistentMark(node, colors)
|
|
117
94
|
const running = mark !== '' && outcome.status === 'success' && outcome.serverExit === undefined
|
|
118
|
-
const header = running ? paint(
|
|
95
|
+
const header = running ? paint('info', 'running', colors) : formatBlockHeader(outcome, colors)
|
|
119
96
|
const lines: string[] = [`${corner('┌─')} ${mark}${idPainted} ${corner('>')} ${header}`]
|
|
120
97
|
|
|
121
98
|
// The command section shows what actually ran — executed tasks
|
|
@@ -149,9 +126,7 @@ export function formatTaskBlock(
|
|
|
149
126
|
|
|
150
127
|
/** What a bounded capture dropped from its head, so a capped log never reads as whole. */
|
|
151
128
|
function droppedNote(lost: number, colors: ColorSupport): string {
|
|
152
|
-
return paint('', `… ${lost} earlier character${lost === 1 ? '' : 's'} dropped`, colors
|
|
153
|
-
dim: true,
|
|
154
|
-
})
|
|
129
|
+
return paint('muted', `… ${lost} earlier character${lost === 1 ? '' : 's'} dropped`, colors)
|
|
155
130
|
}
|
|
156
131
|
|
|
157
132
|
/**
|
|
@@ -163,7 +138,7 @@ function pushStreamSection(
|
|
|
163
138
|
lines: string[],
|
|
164
139
|
text: string,
|
|
165
140
|
title: string,
|
|
166
|
-
color:
|
|
141
|
+
color: Style,
|
|
167
142
|
lost: number,
|
|
168
143
|
colors: ColorSupport,
|
|
169
144
|
): void {
|
|
@@ -173,7 +148,7 @@ function pushStreamSection(
|
|
|
173
148
|
lines.push(droppedNote(lost, colors))
|
|
174
149
|
}
|
|
175
150
|
pushBodyLines(lines, text)
|
|
176
|
-
if (leavesStyle(text)) lines.push(`${lines.pop()}${
|
|
151
|
+
if (leavesStyle(text)) lines.push(`${lines.pop()}${STYLE_END}`)
|
|
177
152
|
lines.push('')
|
|
178
153
|
}
|
|
179
154
|
|
|
@@ -193,7 +168,7 @@ function pushOutputSection(
|
|
|
193
168
|
link?: (line: string) => string,
|
|
194
169
|
): void {
|
|
195
170
|
if (output.every((c) => c.text.trim().length === 0)) return
|
|
196
|
-
lines.push(sectionLine('OUTPUT', '', colors), '')
|
|
171
|
+
lines.push(sectionLine('OUTPUT', 'strong', colors), '')
|
|
197
172
|
if (lost > 0) {
|
|
198
173
|
lines.push(droppedNote(lost, colors))
|
|
199
174
|
}
|
|
@@ -216,10 +191,10 @@ function pushOutputSection(
|
|
|
216
191
|
for (const l of body) {
|
|
217
192
|
const text = link === undefined ? l.text : link(l.text)
|
|
218
193
|
if (!l.err || !colors.enabled) lines.push(text)
|
|
219
|
-
else lines.push(l.text
|
|
194
|
+
else lines.push(hasEscape(l.text) ? text : paint('error', text, colors))
|
|
220
195
|
}
|
|
221
|
-
const styled = output.findLast((c) => c.text
|
|
222
|
-
if (styled !== undefined && leavesStyle(styled.text)) lines.push(`${lines.pop()}${
|
|
196
|
+
const styled = output.findLast((c) => hasEscape(c.text))
|
|
197
|
+
if (styled !== undefined && leavesStyle(styled.text)) lines.push(`${lines.pop()}${STYLE_END}`)
|
|
223
198
|
lines.push('')
|
|
224
199
|
}
|
|
225
200
|
|
|
@@ -230,9 +205,9 @@ function pushOutputSection(
|
|
|
230
205
|
* the frame width, and sections get vertical margins so content stands
|
|
231
206
|
* clear of the furniture.
|
|
232
207
|
*/
|
|
233
|
-
function sectionLine(title: string, color:
|
|
234
|
-
const corner = (s: string) => paint('', s, colors
|
|
235
|
-
const label = paint(color, title, colors,
|
|
208
|
+
function sectionLine(title: string, color: Style, colors: ColorSupport): string {
|
|
209
|
+
const corner = (s: string) => paint('muted', s, colors)
|
|
210
|
+
const label = paint(color, title, colors, true)
|
|
236
211
|
// As wide as the footer's rule, so the frames and the footer line up.
|
|
237
212
|
const rule = corner('─'.repeat(Math.max(1, ruleWidth(colors) - 4 - title.length)))
|
|
238
213
|
return `${corner('├─')} ${label} ${rule}`
|
|
@@ -260,13 +235,7 @@ export function paintIdParts(
|
|
|
260
235
|
colors: ColorSupport,
|
|
261
236
|
opts: { bold?: boolean; sep?: '#' | ':' } = {},
|
|
262
237
|
): string {
|
|
263
|
-
|
|
264
|
-
const { sep = '#', ...style } = opts
|
|
265
|
-
return (
|
|
266
|
-
paint(identityColor(hueSource), projectText, colors, style) +
|
|
267
|
-
dim(sep) +
|
|
268
|
-
paint(identityColor(taskText), taskText, colors, style)
|
|
269
|
-
)
|
|
238
|
+
return idParts(hueSource, projectText, taskText, colors, opts)
|
|
270
239
|
}
|
|
271
240
|
|
|
272
241
|
function paintId(
|
|
@@ -294,19 +263,27 @@ const STATUS_COL = 7 // "success" / "running" / "skipped" / "aborted" / "not run
|
|
|
294
263
|
const CACHE_COL = 8 // "no-cache" / "remote" / "local" / "fresh" / "miss"
|
|
295
264
|
const BLOCKED_COL = 7 // `blocks`: how many tasks are downstream of the row's
|
|
296
265
|
const USUAL_COL = 7 // "~18s" live, "+6.0s" / "−1m 05s" after
|
|
297
|
-
const WAITED_COL = 7 // the time cell's width: "1.20s"
|
|
298
266
|
const CPU_COL = 5 // "180%", "1200%"
|
|
299
267
|
const MEM_COL = 6 // "412MB", "1.1GB", "1023MB"
|
|
300
268
|
const OUT_COL = 7 // `outputs`: "12MB", "900B"
|
|
301
269
|
const WORKER_COL = 12 // `local`, or a plugin's name for the machine, cut at 12
|
|
302
|
-
|
|
270
|
+
|
|
271
|
+
// A row's padding, built once: `' '.repeat` per cell was ~7 ms of an
|
|
272
|
+
// 8,002-hit warm run's rows, the largest single cost of writing them.
|
|
273
|
+
const SPACES = Array.from({ length: 33 }, (_, n) => ' '.repeat(n))
|
|
274
|
+
|
|
275
|
+
/** `n` spaces (none below 1). */
|
|
276
|
+
function pad(n: number): string {
|
|
277
|
+
return n <= 0 ? '' : (SPACES[n] ?? ' '.repeat(n))
|
|
278
|
+
}
|
|
303
279
|
|
|
304
280
|
/** Right-align the duration in a TIME_COL cell (pad on the left). */
|
|
305
281
|
function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
306
282
|
let raw = ms === null ? '' : formatElapsed(ms)
|
|
307
283
|
// `1000.00s` is wider than the cell; minutes fit it up to `99m 59s`.
|
|
308
284
|
if (ms !== null && raw.length > TIME_COL) raw = formatElapsed(ms, true)
|
|
309
|
-
|
|
285
|
+
// The time is the row's fact (value level); the cells around it are meta.
|
|
286
|
+
return pad(TIME_COL - raw.length) + paint('value', raw, colors)
|
|
310
287
|
}
|
|
311
288
|
|
|
312
289
|
/**
|
|
@@ -314,14 +291,9 @@ function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
|
314
291
|
* An empty color string renders the text dim (used for the `miss` cache
|
|
315
292
|
* word); empty text is a blank cell.
|
|
316
293
|
*/
|
|
317
|
-
function cell(text: string, color:
|
|
318
|
-
const painted =
|
|
319
|
-
|
|
320
|
-
? ''
|
|
321
|
-
: color === ''
|
|
322
|
-
? paint('', text, colors, { dim: true })
|
|
323
|
-
: paint(color, text, colors)
|
|
324
|
-
return painted + ' '.repeat(Math.max(0, width - text.length))
|
|
294
|
+
function cell(text: string, color: Style | '', width: number, colors: ColorSupport): string {
|
|
295
|
+
const painted = text === '' ? '' : paint(color === '' ? 'muted' : color, text, colors)
|
|
296
|
+
return painted + pad(width - text.length)
|
|
325
297
|
}
|
|
326
298
|
|
|
327
299
|
/** The cache-shape glyph (task-shape for non-success). */
|
|
@@ -343,34 +315,34 @@ function glyphShape(o: TaskOutcome): string {
|
|
|
343
315
|
}
|
|
344
316
|
|
|
345
317
|
/** Task-axis word + color (a cache hit is a success). */
|
|
346
|
-
function statusOf(o: TaskOutcome): { word: string; color:
|
|
318
|
+
function statusOf(o: TaskOutcome): { word: string; color: Style } {
|
|
347
319
|
switch (o.status) {
|
|
348
320
|
case 'failed':
|
|
349
|
-
return { word: 'failed', color:
|
|
321
|
+
return { word: 'failed', color: 'error' }
|
|
350
322
|
case 'skipped':
|
|
351
|
-
return { word: 'skipped', color:
|
|
323
|
+
return { word: 'skipped', color: 'warn' }
|
|
352
324
|
default:
|
|
353
|
-
return { word: 'success', color:
|
|
325
|
+
return { word: 'success', color: 'ok' }
|
|
354
326
|
}
|
|
355
327
|
}
|
|
356
328
|
|
|
357
329
|
/** Cache-axis word + color. Empty for states that never reached the cache. */
|
|
358
|
-
function cacheOf(o: TaskOutcome): { word: string; color:
|
|
330
|
+
function cacheOf(o: TaskOutcome): { word: string; color: Style } {
|
|
359
331
|
switch (o.status) {
|
|
360
332
|
case 'success':
|
|
361
333
|
case 'failed':
|
|
362
334
|
// A task no cache answered for never consulted one: not a miss.
|
|
363
|
-
return { word: ranNoCache(o) ? 'no-cache' : 'miss', color: '' } // dim
|
|
335
|
+
return { word: ranNoCache(o) ? 'no-cache' : 'miss', color: 'muted' } // dim
|
|
364
336
|
case 'cache-hit':
|
|
365
337
|
return o.restored === false
|
|
366
|
-
? { word: 'fresh', color:
|
|
367
|
-
: { word: 'local', color:
|
|
338
|
+
? { word: 'fresh', color: 'ok' }
|
|
339
|
+
: { word: 'local', color: 'local' }
|
|
368
340
|
case 'cache-hit-remote':
|
|
369
341
|
return o.restored === false
|
|
370
|
-
? { word: 'fresh', color:
|
|
371
|
-
: { word: 'remote', color:
|
|
342
|
+
? { word: 'fresh', color: 'ok' }
|
|
343
|
+
: { word: 'remote', color: 'remote' }
|
|
372
344
|
default:
|
|
373
|
-
return { word: '', color: '' }
|
|
345
|
+
return { word: '', color: 'muted' }
|
|
374
346
|
}
|
|
375
347
|
}
|
|
376
348
|
|
|
@@ -380,7 +352,7 @@ function taskGlyph(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
380
352
|
}
|
|
381
353
|
|
|
382
354
|
/**
|
|
383
|
-
* One reported task row on the list grid: `<glyph> <time> <usual>
|
|
355
|
+
* One reported task row on the list grid: `<glyph> <time> <usual>
|
|
384
356
|
* <status> <cache> <cpu> <mem> <blocks> <worker> <name>`. `glyph` is
|
|
385
357
|
* pre-painted; status/cache are raw text + color, padded to their
|
|
386
358
|
* columns; `paintedId` carries identity coloring. `ms = null` blanks the
|
|
@@ -390,9 +362,9 @@ export function formatTaskRow(
|
|
|
390
362
|
glyph: string,
|
|
391
363
|
ms: number | null,
|
|
392
364
|
status: string,
|
|
393
|
-
statusColor:
|
|
365
|
+
statusColor: Style | '',
|
|
394
366
|
cache: string,
|
|
395
|
-
cacheColor:
|
|
367
|
+
cacheColor: Style | '',
|
|
396
368
|
paintedId: string,
|
|
397
369
|
colors: ColorSupport = NO_COLOR,
|
|
398
370
|
cells: RowCells = {},
|
|
@@ -401,31 +373,40 @@ export function formatTaskRow(
|
|
|
401
373
|
// `aborting` runs one into the cache cell, which is blank beside it.
|
|
402
374
|
const ca = cell(cache, cacheColor, CACHE_COL - Math.max(0, status.length - STATUS_COL), colors)
|
|
403
375
|
const usual = cells.usual ?? { text: '', painted: '' }
|
|
404
|
-
const us = `${
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
const mem =
|
|
376
|
+
const us = `${pad(USUAL_COL - usual.text.length)}${usual.painted}`
|
|
377
|
+
const cpu =
|
|
378
|
+
cells.cpu === undefined ? '' : cells.cpu === null ? UNKNOWN : `${Math.round(cells.cpu * 100)}%`
|
|
379
|
+
const mem =
|
|
380
|
+
cells.memBytes === undefined
|
|
381
|
+
? ''
|
|
382
|
+
: cells.memBytes === null
|
|
383
|
+
? UNKNOWN
|
|
384
|
+
: formatSize(cells.memBytes)
|
|
408
385
|
const out = cells.outputBytes === undefined ? '' : formatSize(cells.outputBytes)
|
|
409
386
|
const blocks = cells.blocks ?? ''
|
|
410
387
|
const worker = workerName(cells.worker ?? '')
|
|
411
388
|
return (
|
|
412
|
-
` ${glyph} ${timeCell(ms, colors)} ${us} ${
|
|
389
|
+
` ${glyph} ${timeCell(ms, colors)} ${us} ${st} ${ca} ` +
|
|
413
390
|
`${right(cpu, CPU_COL, colors)} ${right(mem, MEM_COL, colors)} ${right(out, OUT_COL, colors)} ` +
|
|
414
|
-
`${
|
|
415
|
-
`${
|
|
391
|
+
`${pad(BLOCKED_COL - blocks.length)}${paint(blocks === '0' ? 'muted' : 'value', blocks, colors)} ` +
|
|
392
|
+
`${placeCell(worker, colors)}${pad(WORKER_COL - worker.length)} ${paintedId}`
|
|
416
393
|
)
|
|
417
394
|
}
|
|
418
395
|
|
|
419
|
-
/**
|
|
396
|
+
/**
|
|
397
|
+
* A cell vx cannot know: a peak no look saw, a task history never timed.
|
|
398
|
+
* Blank is a cell that does not apply (a hit ran nothing).
|
|
399
|
+
*/
|
|
400
|
+
const UNKNOWN = '\u2014'
|
|
401
|
+
|
|
402
|
+
/** A row's cells past time, status and cache; each blank when absent, `—` when null. */
|
|
420
403
|
export interface RowCells {
|
|
421
404
|
/** After the time, pre-painted: `~18s` live, `+6.0s` / `−1.4s` after. */
|
|
422
405
|
usual?: { text: string; painted: string }
|
|
423
|
-
/** How long the task waited ready for a slot. */
|
|
424
|
-
waitedMs?: number
|
|
425
406
|
/** Cores in use (live) or on average (after): 1.8 prints `180%`. */
|
|
426
|
-
cpu?: number
|
|
407
|
+
cpu?: number | null
|
|
427
408
|
/** Resident memory now (live) or at its peak (after). */
|
|
428
|
-
memBytes?: number
|
|
409
|
+
memBytes?: number | null
|
|
429
410
|
/** The bytes of the task's outputs: what its save stored, or its hit restored. */
|
|
430
411
|
outputBytes?: number
|
|
431
412
|
/** How many tasks are downstream. */
|
|
@@ -435,7 +416,7 @@ export interface RowCells {
|
|
|
435
416
|
}
|
|
436
417
|
|
|
437
418
|
function right(text: string, width: number, colors: ColorSupport): string {
|
|
438
|
-
return
|
|
419
|
+
return pad(width - text.length) + paint('muted', text, colors)
|
|
439
420
|
}
|
|
440
421
|
|
|
441
422
|
/** `900B`, `412MB`, `1.1GB`: one width for a size, whatever it is. */
|
|
@@ -449,19 +430,24 @@ export function formatSize(bytes: number): string {
|
|
|
449
430
|
return gb < 10 ? `${gb.toFixed(1)}GB` : `${Math.round(gb)}GB`
|
|
450
431
|
}
|
|
451
432
|
|
|
433
|
+
// A run names a handful of workers over thousands of rows: each painted
|
|
434
|
+
// once (escaping and painting per row was ~12 ms of a 9,603-task warm run).
|
|
435
|
+
const placeCells = new WeakMap<ColorSupport, Map<string, string>>()
|
|
436
|
+
|
|
437
|
+
function placeCell(worker: string, colors: ColorSupport): string {
|
|
438
|
+
if (worker === '') return ''
|
|
439
|
+
let byName = placeCells.get(colors)
|
|
440
|
+
if (byName === undefined) placeCells.set(colors, (byName = new Map()))
|
|
441
|
+
let painted = byName.get(worker)
|
|
442
|
+
if (painted === undefined) byName.set(worker, (painted = ui.inline(ui.place(worker), colors)))
|
|
443
|
+
return painted
|
|
444
|
+
}
|
|
445
|
+
|
|
452
446
|
/** A worker's name in its 12 columns: a longer one ends in `…`. */
|
|
453
447
|
function workerName(name: string): string {
|
|
454
448
|
return name.length > WORKER_COL ? `${name.slice(0, WORKER_COL - 1)}\u2026` : name
|
|
455
449
|
}
|
|
456
450
|
|
|
457
|
-
/**
|
|
458
|
-
* A wait worth a cell: under 50 ms the task started at once, the
|
|
459
|
-
* scheduler's own tick and a sibling's spawn, not a full pool.
|
|
460
|
-
*/
|
|
461
|
-
export function waited(ms: number | undefined): boolean {
|
|
462
|
-
return ms !== undefined && ms >= 50
|
|
463
|
-
}
|
|
464
|
-
|
|
465
451
|
/** A task's average cores over its run: CPU time over wall time. */
|
|
466
452
|
function coresOf(o: TaskOutcome): number | undefined {
|
|
467
453
|
return o.cpuMs === undefined || o.durationMs <= 0 ? undefined : o.cpuMs / o.durationMs
|
|
@@ -469,19 +455,27 @@ function coresOf(o: TaskOutcome): number | undefined {
|
|
|
469
455
|
|
|
470
456
|
/**
|
|
471
457
|
* The results list's `±usual` cell: how far an executed task ran from its
|
|
472
|
-
* usual time, `+6.0s` red or `−1.4s` green
|
|
473
|
-
*
|
|
458
|
+
* usual time, `+6.0s` red or `−1.4s` green past a fifth of the usual either
|
|
459
|
+
* way, dim within it; `—` (null) with no history for the task, blank
|
|
460
|
+
* (undefined) with no history to ask. A blank within a fifth read as no
|
|
461
|
+
* usual at all (2026-10-29).
|
|
474
462
|
*/
|
|
475
|
-
function
|
|
463
|
+
export function usualCell(
|
|
476
464
|
ms: number,
|
|
477
|
-
usualMs: number | undefined,
|
|
465
|
+
usualMs: number | null | undefined,
|
|
478
466
|
colors: ColorSupport = NO_COLOR,
|
|
479
467
|
): { text: string; painted: string } {
|
|
480
|
-
if (usualMs === undefined
|
|
481
|
-
|
|
468
|
+
if (usualMs === undefined) return { text: '', painted: '' }
|
|
469
|
+
if (usualMs === null || usualMs <= 0) return unknownUsual(colors)
|
|
482
470
|
const d = ms - usualMs
|
|
483
|
-
const text = `${d
|
|
484
|
-
return { text, painted: paint(
|
|
471
|
+
const text = `${d >= 0 ? '+' : '\u2212'}${formatElapsed(Math.abs(d), true)}`
|
|
472
|
+
if (Math.abs(d) <= usualMs * 0.2) return { text, painted: paint('muted', text, colors) }
|
|
473
|
+
return { text, painted: paint(d > 0 ? 'error' : 'ok', text, colors) }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** The `usual` cell of a task history has never timed. */
|
|
477
|
+
export function unknownUsual(colors: ColorSupport = NO_COLOR): { text: string; painted: string } {
|
|
478
|
+
return { text: UNKNOWN, painted: paint('muted', UNKNOWN, colors) }
|
|
485
479
|
}
|
|
486
480
|
|
|
487
481
|
/**
|
|
@@ -503,10 +497,137 @@ export function formatRunningHeader(colors: ColorSupport = NO_COLOR): string {
|
|
|
503
497
|
|
|
504
498
|
function header(usual: string, colors: ColorSupport): string {
|
|
505
499
|
const head =
|
|
506
|
-
` ${'time'.padStart(TIME_COL)} ${usual.padStart(USUAL_COL)}
|
|
500
|
+
` ${'time'.padStart(TIME_COL)} ${usual.padStart(USUAL_COL)} ` +
|
|
507
501
|
`${'status'.padEnd(STATUS_COL)} ${'cache'.padEnd(CACHE_COL)} ${'cpu'.padStart(CPU_COL)} ` +
|
|
508
502
|
`${'mem'.padStart(MEM_COL)} ${'outputs'.padStart(OUT_COL)} ${'blocks'.padStart(BLOCKED_COL)} ${'worker'.padEnd(WORKER_COL)} task`
|
|
509
|
-
return paint('', head, colors
|
|
503
|
+
return paint('label', head, colors)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* The task list on its grid, under its header: the run's results panel's
|
|
508
|
+
* body, and `vx last`'s, so a replay reads as the run did.
|
|
509
|
+
*/
|
|
510
|
+
export function taskList(rows: readonly ((colors: ColorSupport) => string)[]): ui.Block {
|
|
511
|
+
return { lines: (c) => [formatListHeader(c), ...rows.map((row) => row(c))] }
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** What history keeps of a task's row: `vx last` draws it again from this. */
|
|
515
|
+
export interface RecordedRow {
|
|
516
|
+
project: string
|
|
517
|
+
task: string
|
|
518
|
+
status: string
|
|
519
|
+
durationMs: number
|
|
520
|
+
restored: boolean | null
|
|
521
|
+
cached: boolean | null
|
|
522
|
+
cpuMs: number | null
|
|
523
|
+
peakRssBytes: number | null
|
|
524
|
+
placement: string | null
|
|
525
|
+
outputBytes: number | null
|
|
526
|
+
blocks: number | null
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* A recorded task's row, as the run printed it; `note` (dim) after its id.
|
|
531
|
+
* `usualMs` is the task's usual time now, from history (`null`: never timed).
|
|
532
|
+
*/
|
|
533
|
+
export function formatRecordedRow(
|
|
534
|
+
r: RecordedRow,
|
|
535
|
+
colors: ColorSupport = NO_COLOR,
|
|
536
|
+
note = '',
|
|
537
|
+
usualMs?: number | null,
|
|
538
|
+
): string {
|
|
539
|
+
// The row functions read a node's names and whether it declared a cache.
|
|
540
|
+
const node = {
|
|
541
|
+
id: `${r.project}#${r.task}`,
|
|
542
|
+
projectName: r.project,
|
|
543
|
+
taskName: r.task,
|
|
544
|
+
config: r.cached === false ? {} : { cache: true },
|
|
545
|
+
} as unknown as TaskNode
|
|
546
|
+
const o = {
|
|
547
|
+
node,
|
|
548
|
+
status: r.status,
|
|
549
|
+
durationMs: r.durationMs,
|
|
550
|
+
...(r.restored !== null ? { restored: r.restored } : {}),
|
|
551
|
+
...(r.cpuMs !== null ? { cpuMs: r.cpuMs } : {}),
|
|
552
|
+
...(r.peakRssBytes !== null ? { peakRssBytes: r.peakRssBytes } : {}),
|
|
553
|
+
...(r.placement !== null ? { where: r.placement } : {}),
|
|
554
|
+
...(r.outputBytes !== null ? { outputBytes: r.outputBytes } : {}),
|
|
555
|
+
} as TaskOutcome
|
|
556
|
+
const blocks = r.blocks ?? undefined
|
|
557
|
+
const row =
|
|
558
|
+
o.status === 'skipped'
|
|
559
|
+
? formatTaskSkippedLine(node, colors, undefined, blocks)
|
|
560
|
+
: o.status === 'aborted'
|
|
561
|
+
? formatTaskAbortedLine(node, o, colors, blocks)
|
|
562
|
+
: isCacheHit(o.status)
|
|
563
|
+
? formatTaskHitLine(node, o, colors, false, blocks)
|
|
564
|
+
: formatTaskExecutedLine(node, o, colors, false, blocks, usualMs)
|
|
565
|
+
return note === '' ? row : `${row} ${paint('muted', note, colors)}`
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* A running task's usual duration, fixed while the elapsed time counts up
|
|
570
|
+
* toward it (2026-10-14): `~5.2s`, `~42s`, `~2m 05s`. Fits the cache
|
|
571
|
+
* column below 100 minutes.
|
|
572
|
+
*/
|
|
573
|
+
export function formatExpected(ms: number): string {
|
|
574
|
+
// The tier by the rounded value: 999.6 ms printed `~1000ms` (item 1034's class).
|
|
575
|
+
if (Math.round(ms) < 1000) return `~${Math.round(ms)}ms`
|
|
576
|
+
if (ms < 9950) return `~${(ms / 1000).toFixed(1)}s`
|
|
577
|
+
const s = Math.round(ms / 1000)
|
|
578
|
+
if (s < 60) return `~${s}s`
|
|
579
|
+
return `~${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** A task `--dry` lists: what the run would do with it, on the running list's grid. */
|
|
583
|
+
export interface PlannedRow {
|
|
584
|
+
node: TaskNode
|
|
585
|
+
glyph: string
|
|
586
|
+
status: string
|
|
587
|
+
/** The glyph's and the status word's hue. */
|
|
588
|
+
tone: Style
|
|
589
|
+
cache: string
|
|
590
|
+
cacheTone: Style | ''
|
|
591
|
+
/** Its usual time, from history. */
|
|
592
|
+
usualMs?: number
|
|
593
|
+
/** What a remote hit would download. */
|
|
594
|
+
outputBytes?: number
|
|
595
|
+
/** How many tasks are downstream of it. */
|
|
596
|
+
blocks?: number
|
|
597
|
+
worker: string
|
|
598
|
+
/** Dim, after the id: its description, why it is affected. */
|
|
599
|
+
note: string
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** `--dry`'s list: the running list's header and grid, so a plan reads as the run would. */
|
|
603
|
+
export function planList(rows: readonly PlannedRow[]): ui.Block {
|
|
604
|
+
return {
|
|
605
|
+
lines: (c) => [
|
|
606
|
+
formatRunningHeader(c),
|
|
607
|
+
...rows.map((r) => {
|
|
608
|
+
const usual = r.usualMs === undefined ? undefined : formatExpected(r.usualMs)
|
|
609
|
+
const row = formatTaskRow(
|
|
610
|
+
paint(r.tone, r.glyph, c),
|
|
611
|
+
null,
|
|
612
|
+
r.status,
|
|
613
|
+
r.tone,
|
|
614
|
+
r.cache,
|
|
615
|
+
r.cacheTone,
|
|
616
|
+
paintTaskId(r.node, c),
|
|
617
|
+
c,
|
|
618
|
+
{
|
|
619
|
+
...(usual !== undefined
|
|
620
|
+
? { usual: { text: usual, painted: paint('muted', usual, c) } }
|
|
621
|
+
: {}),
|
|
622
|
+
...(r.outputBytes !== undefined ? { outputBytes: r.outputBytes } : {}),
|
|
623
|
+
...(r.blocks !== undefined ? { blocks: String(r.blocks) } : {}),
|
|
624
|
+
worker: r.worker,
|
|
625
|
+
},
|
|
626
|
+
)
|
|
627
|
+
return r.note === '' ? row : `${row} ${paint('muted', r.note, c)}`
|
|
628
|
+
}),
|
|
629
|
+
],
|
|
630
|
+
}
|
|
510
631
|
}
|
|
511
632
|
|
|
512
633
|
/** Compact one-liner for a cache hit with nothing to replay. */
|
|
@@ -532,8 +653,8 @@ export function formatTaskHashLine(
|
|
|
532
653
|
o: TaskOutcome,
|
|
533
654
|
colors: ColorSupport = NO_COLOR,
|
|
534
655
|
): string {
|
|
535
|
-
const color = o.status === 'aborted' ?
|
|
536
|
-
const key = o.hash === undefined ? '' : ` ${paint('', o.hash, colors
|
|
656
|
+
const color = o.status === 'aborted' ? 'aborted' : statusOf(o).color
|
|
657
|
+
const key = o.hash === undefined ? '' : ` ${paint('muted', o.hash, colors)}`
|
|
537
658
|
return `${paint(color, outcomeWord(o), colors)} ${paintTaskId(node, colors)}${key}`
|
|
538
659
|
}
|
|
539
660
|
|
|
@@ -544,21 +665,32 @@ export function formatTaskExecutedLine(
|
|
|
544
665
|
colors: ColorSupport = NO_COLOR,
|
|
545
666
|
onPath = false,
|
|
546
667
|
blocks?: number,
|
|
547
|
-
usualMs?: number,
|
|
668
|
+
usualMs?: number | null,
|
|
548
669
|
): string {
|
|
549
670
|
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath, {
|
|
550
671
|
...executedCells(o, blocks),
|
|
551
|
-
|
|
672
|
+
// A hit's time is a restore's, not the task's: nothing to compare.
|
|
673
|
+
...(o.status === 'success' || o.status === 'failed'
|
|
674
|
+
? { usual: usualCell(o.durationMs, usualMs, colors) }
|
|
675
|
+
: {}),
|
|
552
676
|
})
|
|
553
677
|
}
|
|
554
678
|
|
|
555
|
-
/**
|
|
679
|
+
/**
|
|
680
|
+
* An executed task's cells: where it ran, its average cores and peak
|
|
681
|
+
* memory; a task that ran with either unread shows `—`, a hit nothing.
|
|
682
|
+
*/
|
|
556
683
|
export function executedCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
557
684
|
const cores = coresOf(o)
|
|
685
|
+
const ran = o.status === 'success' || o.status === 'failed'
|
|
558
686
|
return {
|
|
559
687
|
...ranCells(o, blocks),
|
|
560
|
-
...(cores !== undefined ? { cpu: cores } : {}),
|
|
561
|
-
...(o.peakRssBytes !== undefined
|
|
688
|
+
...(cores !== undefined ? { cpu: cores } : ran ? { cpu: null } : {}),
|
|
689
|
+
...(o.peakRssBytes !== undefined
|
|
690
|
+
? { memBytes: o.peakRssBytes }
|
|
691
|
+
: ran
|
|
692
|
+
? { memBytes: null }
|
|
693
|
+
: {}),
|
|
562
694
|
}
|
|
563
695
|
}
|
|
564
696
|
|
|
@@ -568,7 +700,6 @@ function ranCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
|
568
700
|
// A skip reached no machine.
|
|
569
701
|
...(o.status === 'skipped' ? {} : { worker: o.where ?? 'local' }),
|
|
570
702
|
...(blocks !== undefined ? { blocks: String(blocks) } : {}),
|
|
571
|
-
...(waited(o.queuedMs) ? { waitedMs: o.queuedMs! } : {}),
|
|
572
703
|
...(o.outputBytes !== undefined ? { outputBytes: o.outputBytes } : {}),
|
|
573
704
|
}
|
|
574
705
|
}
|
|
@@ -603,7 +734,8 @@ function formatOutcomeRow(
|
|
|
603
734
|
|
|
604
735
|
/** `critical path`: the row's task held the run's end while others ran beside it. */
|
|
605
736
|
export function pathNote(colors: ColorSupport = NO_COLOR): string {
|
|
606
|
-
|
|
737
|
+
// A fact worth knowing, not a failure: never red.
|
|
738
|
+
return ` ${paint('info', 'critical path', colors)}`
|
|
607
739
|
}
|
|
608
740
|
|
|
609
741
|
/** Where an executor ran the task, when not here, right after its id: `@small#2` (2026-10-21). */
|
|
@@ -616,7 +748,7 @@ function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
|
|
|
616
748
|
* on the row, as a worker's name has no width to pad to (2026-10-21).
|
|
617
749
|
*/
|
|
618
750
|
function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
|
|
619
|
-
return `${paint('', ' \u00b7', colors
|
|
751
|
+
return `${paint('muted', ' \u00b7', colors)} ${ui.inline(ui.place(where), colors)}`
|
|
620
752
|
}
|
|
621
753
|
|
|
622
754
|
/**
|
|
@@ -634,7 +766,7 @@ export function flakyNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): stri
|
|
|
634
766
|
}
|
|
635
767
|
if ((o.attempts ?? 1) > 1) said.push(`${o.attempts} attempts`)
|
|
636
768
|
const text = said.length === 0 ? 'flaky' : `flaky - ${said.join(' \u00b7 ')}`
|
|
637
|
-
return ` ${paint('', text, colors
|
|
769
|
+
return ` ${paint('muted', text, colors)}`
|
|
638
770
|
}
|
|
639
771
|
|
|
640
772
|
/**
|
|
@@ -649,13 +781,13 @@ export function formatTaskSkippedLine(
|
|
|
649
781
|
blocks?: number,
|
|
650
782
|
): string {
|
|
651
783
|
const why = skippedReason(skip)
|
|
652
|
-
const suffix = why === '' ? '' : ` ${paint('', `• ${why}`, colors
|
|
784
|
+
const suffix = why === '' ? '' : ` ${paint('muted', `• ${why}`, colors)}`
|
|
653
785
|
return (
|
|
654
786
|
formatTaskRow(
|
|
655
|
-
paint(
|
|
787
|
+
paint('warn', '\u2298', colors),
|
|
656
788
|
null,
|
|
657
789
|
'skipped',
|
|
658
|
-
|
|
790
|
+
'warn',
|
|
659
791
|
'',
|
|
660
792
|
'',
|
|
661
793
|
paintTaskId(node, colors),
|
|
@@ -680,7 +812,7 @@ export function formatTaskAbortedLine(
|
|
|
680
812
|
const bl: RowCells = blocks === undefined ? {} : { blocks: String(blocks) }
|
|
681
813
|
if (neverStarted(o)) {
|
|
682
814
|
return formatTaskRow(
|
|
683
|
-
paint('', '\u25cc', colors
|
|
815
|
+
paint('muted', '\u25cc', colors),
|
|
684
816
|
null,
|
|
685
817
|
'not run',
|
|
686
818
|
'',
|
|
@@ -692,10 +824,10 @@ export function formatTaskAbortedLine(
|
|
|
692
824
|
)
|
|
693
825
|
}
|
|
694
826
|
return formatTaskRow(
|
|
695
|
-
paint(
|
|
827
|
+
paint('aborted', '\u2717', colors),
|
|
696
828
|
o.durationMs,
|
|
697
829
|
'aborted',
|
|
698
|
-
|
|
830
|
+
'aborted',
|
|
699
831
|
'',
|
|
700
832
|
'',
|
|
701
833
|
id,
|
|
@@ -716,7 +848,7 @@ export function formatFrameOpen(
|
|
|
716
848
|
colors: ColorSupport = NO_COLOR,
|
|
717
849
|
forwardArgs: readonly string[] = [],
|
|
718
850
|
): string {
|
|
719
|
-
const corner = (t: string) => paint('', t, colors
|
|
851
|
+
const corner = (t: string) => paint('muted', t, colors)
|
|
720
852
|
const cmd = shownCommand(node, forwardArgs)
|
|
721
853
|
const mark = persistentMark(node, colors)
|
|
722
854
|
return `${corner('┌─')} ${mark}${paintTaskId(node, colors, { bold: true })} ${corner('>')} $ ${cmd}`
|
|
@@ -748,7 +880,7 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
748
880
|
// glob matches and the user decides.
|
|
749
881
|
const fix = outcome.sandboxFix
|
|
750
882
|
const line = fix === undefined ? undefined : sandboxFixLine(fix)
|
|
751
|
-
const dim = (text: string): string => paint('', text, colors
|
|
883
|
+
const dim = (text: string): string => paint('muted', text, colors)
|
|
752
884
|
const note =
|
|
753
885
|
fix === undefined
|
|
754
886
|
? []
|
|
@@ -773,7 +905,7 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
773
905
|
// vx's own notes (`vx: …`) explain a failure; they are not denials.
|
|
774
906
|
const denials = body.filter((l) => !l.startsWith('vx: ')).length
|
|
775
907
|
return [
|
|
776
|
-
sectionLine(denials > 0 ? `SANDBOX VIOLATIONS (${denials})` : 'SANDBOX',
|
|
908
|
+
sectionLine(denials > 0 ? `SANDBOX VIOLATIONS (${denials})` : 'SANDBOX', 'error', colors),
|
|
777
909
|
'',
|
|
778
910
|
...body,
|
|
779
911
|
'',
|
|
@@ -786,7 +918,7 @@ export function formatFrameClose(
|
|
|
786
918
|
outcome: TaskOutcome,
|
|
787
919
|
colors: ColorSupport = NO_COLOR,
|
|
788
920
|
): string {
|
|
789
|
-
const corner = (t: string) => paint('', t, colors
|
|
921
|
+
const corner = (t: string) => paint('muted', t, colors)
|
|
790
922
|
const mark = persistentMark(node, colors)
|
|
791
923
|
// A persistent task's child keeps running past this "ready" outcome,
|
|
792
924
|
// so the close reads `running` (accent), not `success` — the ▸ marks
|
|
@@ -810,11 +942,11 @@ function shownCommand(node: TaskNode, forwardArgs: readonly string[]): string {
|
|
|
810
942
|
}
|
|
811
943
|
|
|
812
944
|
function persistentMark(node: TaskNode, colors: ColorSupport): string {
|
|
813
|
-
return node.config.exec?.persistent !== undefined ? `${paint(
|
|
945
|
+
return node.config.exec?.persistent !== undefined ? `${paint('info', '▸', colors)} ` : ''
|
|
814
946
|
}
|
|
815
947
|
|
|
816
948
|
function runningFooter(o: TaskOutcome, colors: ColorSupport): string {
|
|
817
|
-
return ` ${paint('', `(${formatElapsed(o.durationMs)})`, colors
|
|
949
|
+
return ` ${paint('muted', `(${formatElapsed(o.durationMs)})`, colors)} ${paint('info', 'running', colors)}`
|
|
818
950
|
}
|
|
819
951
|
|
|
820
952
|
/**
|
|
@@ -841,8 +973,8 @@ export function formatPersistentTailBlock(
|
|
|
841
973
|
const stderr = body.stderr ?? ''
|
|
842
974
|
if (stdout.trim().length === 0 && stderr.trim().length === 0) return ''
|
|
843
975
|
const lines: string[] = [formatFrameOpen(node, colors, forwardArgs)]
|
|
844
|
-
pushStreamSection(lines, stdout, 'STDOUT (since ready)',
|
|
845
|
-
pushStreamSection(lines, stderr, 'STDERR (since ready)',
|
|
976
|
+
pushStreamSection(lines, stdout, 'STDOUT (since ready)', 'ok', dropped.stdout ?? 0, colors)
|
|
977
|
+
pushStreamSection(lines, stderr, 'STDERR (since ready)', 'error', dropped.stderr ?? 0, colors)
|
|
846
978
|
lines.push(formatFrameClose(node, outcome, colors))
|
|
847
979
|
return lines.join('\n') + '\n'
|
|
848
980
|
}
|
|
@@ -853,40 +985,16 @@ export function formatPersistentTailBlock(
|
|
|
853
985
|
* logs read apart. `text` holds whole lines, its last newline cut off.
|
|
854
986
|
*/
|
|
855
987
|
export function formatKeptLines(node: TaskNode, text: string, colors: ColorSupport): string {
|
|
856
|
-
const prefix = `${paintTaskId(node, colors)} ${paint('', '│', colors
|
|
988
|
+
const prefix = `${paintTaskId(node, colors)} ${paint('muted', '│', colors)} `
|
|
857
989
|
return text
|
|
858
990
|
.split('\n')
|
|
859
991
|
.map((line) => {
|
|
860
992
|
const shown = lastRedraw(line)
|
|
861
|
-
return `${prefix}${shown}${leavesStyle(shown) ?
|
|
993
|
+
return `${prefix}${shown}${leavesStyle(shown) ? STYLE_END : ''}\n`
|
|
862
994
|
})
|
|
863
995
|
.join('')
|
|
864
996
|
}
|
|
865
997
|
|
|
866
|
-
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
867
|
-
const LINE_RESET = /\r|\x1b\[[12]K|\x1b\[[01]?G/g
|
|
868
|
-
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
869
|
-
const TRAILING_RESETS = /(?:\r|\x1b\[[12]K|\x1b\[[01]?G)+$/
|
|
870
|
-
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
871
|
-
const SGR = /\x1b\[[0-9;]*m/g
|
|
872
|
-
|
|
873
|
-
/**
|
|
874
|
-
* What a line shows once its redraws have played: the text after its last
|
|
875
|
-
* `\r`, line erase (`CSI 1K`/`2K`) or move to column 1 (`CSI G`), with the
|
|
876
|
-
* colours set before it. Written after an id prefix, a dev server's redraw
|
|
877
|
-
* (`\x1b[2K\x1b[1Gbuilt in 30ms`, a `10%\r100%` progress line) erased or
|
|
878
|
-
* overwrote the prefix on a terminal. A reset that ends the line, as in
|
|
879
|
-
* CRLF, redraws nothing.
|
|
880
|
-
*/
|
|
881
|
-
function lastRedraw(line: string): string {
|
|
882
|
-
if (!line.includes('\r') && !line.includes('\x1b')) return line
|
|
883
|
-
const text = line.replace(TRAILING_RESETS, '')
|
|
884
|
-
let cut = -1
|
|
885
|
-
for (const m of text.matchAll(LINE_RESET)) cut = m.index + m[0].length
|
|
886
|
-
if (cut === -1) return text
|
|
887
|
-
return (text.slice(0, cut).match(SGR) ?? []).join('') + text.slice(cut)
|
|
888
|
-
}
|
|
889
|
-
|
|
890
998
|
/**
|
|
891
999
|
* Between the requested task's frame and the summary footer, list the
|
|
892
1000
|
* persistent tasks (dev servers / watchers) the run is keeping alive in
|
|
@@ -900,28 +1008,28 @@ export function formatPersistentList(
|
|
|
900
1008
|
): string[] {
|
|
901
1009
|
return nodes.map(
|
|
902
1010
|
(n) =>
|
|
903
|
-
` ${paint(
|
|
1011
|
+
` ${paint('info', '▸', colors)} ${paintTaskId(n, colors)} ${paint('muted', 'running', colors)}`,
|
|
904
1012
|
)
|
|
905
1013
|
}
|
|
906
1014
|
|
|
907
1015
|
function formatBlockHeader(o: TaskOutcome, colors: ColorSupport): string {
|
|
908
1016
|
const shortHash = o.hash ? o.hash.slice(0, 8) : ''
|
|
909
|
-
const dim = (s: string) => paint('', s, colors
|
|
1017
|
+
const dim = (s: string) => paint('muted', s, colors)
|
|
910
1018
|
const word = outcomeLabel(o)
|
|
911
1019
|
switch (o.status) {
|
|
912
1020
|
case 'cache-hit':
|
|
913
|
-
return `${paint(
|
|
1021
|
+
return `${paint('ok', word, colors)} ${dim(`• ${shortHash}`)}`
|
|
914
1022
|
case 'cache-hit-remote':
|
|
915
1023
|
// A remote restore is the one hit painted accent: bytes came off the wire.
|
|
916
|
-
return `${paint(o.restored === false ?
|
|
1024
|
+
return `${paint(o.restored === false ? 'ok' : 'info', word, colors)} ${dim(`• ${shortHash}`)}`
|
|
917
1025
|
case 'failed':
|
|
918
1026
|
// The command lives in its own `├─ command` section; the header
|
|
919
1027
|
// carries the outcome like every other status.
|
|
920
|
-
return paint(
|
|
1028
|
+
return paint('error', word, colors, true)
|
|
921
1029
|
case 'skipped':
|
|
922
|
-
return paint(
|
|
1030
|
+
return paint('warn', word, colors)
|
|
923
1031
|
case 'aborted':
|
|
924
|
-
return paint(
|
|
1032
|
+
return paint('aborted', word, colors)
|
|
925
1033
|
default:
|
|
926
1034
|
return dim(word)
|
|
927
1035
|
}
|
|
@@ -935,7 +1043,7 @@ function formatBlockFooter(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
935
1043
|
// saved" on the strength of it. The stored time lives on
|
|
936
1044
|
// `TaskOutcome.storedDurationMs`.) Status differs by outcome — see
|
|
937
1045
|
// formatStatusTag.
|
|
938
|
-
const dur = paint('', `(${formatElapsed(o.durationMs)})`, colors
|
|
1046
|
+
const dur = paint('muted', `(${formatElapsed(o.durationMs)})`, colors)
|
|
939
1047
|
const tag = formatStatusTag(o, colors)
|
|
940
1048
|
return ` ${dur} ${tag}${flakyNote(o, colors)}`
|
|
941
1049
|
}
|
|
@@ -944,13 +1052,13 @@ function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
944
1052
|
const word = outcomeLabel(o)
|
|
945
1053
|
switch (o.status) {
|
|
946
1054
|
case 'failed':
|
|
947
|
-
return paint(
|
|
1055
|
+
return paint('error', word, colors, true)
|
|
948
1056
|
case 'skipped':
|
|
949
|
-
return paint(
|
|
1057
|
+
return paint('warn', word, colors)
|
|
950
1058
|
case 'aborted':
|
|
951
|
-
return paint(
|
|
1059
|
+
return paint('aborted', word, colors)
|
|
952
1060
|
default:
|
|
953
|
-
return paint('', word, colors
|
|
1061
|
+
return paint('muted', word, colors)
|
|
954
1062
|
}
|
|
955
1063
|
}
|
|
956
1064
|
|