@vzn/vx 0.0.649 → 0.0.651
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 +5 -20
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/src/bin.ts +9 -31
- package/src/cache/cache.ts +225 -51
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/fold-memo.ts +8 -1
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/index.ts +12 -0
- package/src/cache/inputs.ts +3 -0
- package/src/cache/layer.ts +11 -0
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +120 -0
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-hits.ts +9 -1
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +11 -1
- package/src/cache/run-notes.ts +123 -0
- package/src/cache/schema.ts +54 -1
- package/src/cli/cache.ts +26 -16
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +12 -23
- package/src/cli/index.ts +4 -9
- package/src/cli/info.ts +15 -7
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +224 -223
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +10 -3
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +38 -42
- 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 +2 -1
- package/src/cli/watch.ts +83 -44
- package/src/cli/why.ts +126 -92
- package/src/cli/workspace-config.ts +2 -1
- package/src/exec/executor.ts +2 -1
- 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-runtime.ts +11 -2
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +12 -2
- package/src/graph/task-graph.ts +27 -1
- package/src/handoff.ts +10 -7
- package/src/history/history.ts +39 -2
- package/src/history/index.ts +3 -0
- package/src/history/metrics.ts +9 -1
- package/src/index.ts +12 -3
- package/src/orchestrator/doctor.ts +3 -3
- package/src/orchestrator/execute-task.ts +16 -2
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +3 -0
- package/src/orchestrator/index.ts +3 -1
- package/src/orchestrator/live-usage.ts +98 -0
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +176 -102
- package/src/orchestrator/miss-save.ts +31 -3
- package/src/orchestrator/persistent-task.ts +3 -1
- package/src/orchestrator/plan.ts +5 -1
- package/src/orchestrator/plugin-host.ts +61 -0
- package/src/orchestrator/plugin.ts +49 -4
- package/src/orchestrator/prepare.ts +18 -2
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +35 -114
- package/src/orchestrator/run-records.ts +10 -0
- package/src/orchestrator/run.ts +89 -26
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-hash.ts +17 -3
- package/src/report/forecast.ts +12 -33
- package/src/report/framed-output.ts +388 -161
- package/src/report/index.ts +8 -2
- package/src/report/logger.ts +100 -36
- package/src/report/status-line.ts +81 -365
- package/src/report/summary.ts +104 -131
- 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 +65 -10
- package/src/util/edit-distance.ts +6 -1
- package/src/util/hidden-prompt.ts +8 -2
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +20 -2
- package/src/util/secret-mask.ts +39 -3
- package/src/util/secrets.ts +3 -0
- package/src/util/settle.ts +31 -0
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/config-schema.ts +4 -2
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +28 -0
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +4 -3
- package/src/workspace/workspace.ts +3 -0
- package/src/orchestrator/run-steps.ts +0 -93
- package/src/util/colors.ts +0 -130
- 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(
|
|
@@ -292,16 +261,20 @@ function paintTaskId(node: TaskNode, colors: ColorSupport, opts: { bold?: boolea
|
|
|
292
261
|
export const TIME_COL = 7 // "0ms" … "999.99s", then "16m 40s" … "99m 59s"
|
|
293
262
|
const STATUS_COL = 7 // "success" / "running" / "skipped" / "aborted" / "not run"
|
|
294
263
|
const CACHE_COL = 8 // "no-cache" / "remote" / "local" / "fresh" / "miss"
|
|
295
|
-
const BLOCKED_COL = 7 //
|
|
296
|
-
|
|
297
|
-
const
|
|
264
|
+
const BLOCKED_COL = 7 // `blocks`: how many tasks are downstream of the row's
|
|
265
|
+
const USUAL_COL = 7 // "~18s" live, "+6.0s" / "−1m 05s" after
|
|
266
|
+
const CPU_COL = 5 // "180%", "1200%"
|
|
267
|
+
const MEM_COL = 6 // "412MB", "1.1GB", "1023MB"
|
|
268
|
+
const OUT_COL = 7 // `outputs`: "12MB", "900B"
|
|
269
|
+
const WORKER_COL = 12 // `local`, or a plugin's name for the machine, cut at 12
|
|
298
270
|
|
|
299
271
|
/** Right-align the duration in a TIME_COL cell (pad on the left). */
|
|
300
272
|
function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
301
273
|
let raw = ms === null ? '' : formatElapsed(ms)
|
|
302
274
|
// `1000.00s` is wider than the cell; minutes fit it up to `99m 59s`.
|
|
303
275
|
if (ms !== null && raw.length > TIME_COL) raw = formatElapsed(ms, true)
|
|
304
|
-
|
|
276
|
+
// The time is the row's fact (value level); the cells around it are meta.
|
|
277
|
+
return ' '.repeat(Math.max(0, TIME_COL - raw.length)) + paint('value', raw, colors)
|
|
305
278
|
}
|
|
306
279
|
|
|
307
280
|
/**
|
|
@@ -309,13 +282,8 @@ function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
|
309
282
|
* An empty color string renders the text dim (used for the `miss` cache
|
|
310
283
|
* word); empty text is a blank cell.
|
|
311
284
|
*/
|
|
312
|
-
function cell(text: string, color:
|
|
313
|
-
const painted =
|
|
314
|
-
text === ''
|
|
315
|
-
? ''
|
|
316
|
-
: color === ''
|
|
317
|
-
? paint('', text, colors, { dim: true })
|
|
318
|
-
: paint(color, text, colors)
|
|
285
|
+
function cell(text: string, color: Style | '', width: number, colors: ColorSupport): string {
|
|
286
|
+
const painted = text === '' ? '' : paint(color === '' ? 'muted' : color, text, colors)
|
|
319
287
|
return painted + ' '.repeat(Math.max(0, width - text.length))
|
|
320
288
|
}
|
|
321
289
|
|
|
@@ -338,34 +306,34 @@ function glyphShape(o: TaskOutcome): string {
|
|
|
338
306
|
}
|
|
339
307
|
|
|
340
308
|
/** Task-axis word + color (a cache hit is a success). */
|
|
341
|
-
function statusOf(o: TaskOutcome): { word: string; color:
|
|
309
|
+
function statusOf(o: TaskOutcome): { word: string; color: Style } {
|
|
342
310
|
switch (o.status) {
|
|
343
311
|
case 'failed':
|
|
344
|
-
return { word: 'failed', color:
|
|
312
|
+
return { word: 'failed', color: 'error' }
|
|
345
313
|
case 'skipped':
|
|
346
|
-
return { word: 'skipped', color:
|
|
314
|
+
return { word: 'skipped', color: 'warn' }
|
|
347
315
|
default:
|
|
348
|
-
return { word: 'success', color:
|
|
316
|
+
return { word: 'success', color: 'ok' }
|
|
349
317
|
}
|
|
350
318
|
}
|
|
351
319
|
|
|
352
320
|
/** Cache-axis word + color. Empty for states that never reached the cache. */
|
|
353
|
-
function cacheOf(o: TaskOutcome): { word: string; color:
|
|
321
|
+
function cacheOf(o: TaskOutcome): { word: string; color: Style } {
|
|
354
322
|
switch (o.status) {
|
|
355
323
|
case 'success':
|
|
356
324
|
case 'failed':
|
|
357
325
|
// A task no cache answered for never consulted one: not a miss.
|
|
358
|
-
return { word: ranNoCache(o) ? 'no-cache' : 'miss', color: '' } // dim
|
|
326
|
+
return { word: ranNoCache(o) ? 'no-cache' : 'miss', color: 'muted' } // dim
|
|
359
327
|
case 'cache-hit':
|
|
360
328
|
return o.restored === false
|
|
361
|
-
? { word: 'fresh', color:
|
|
362
|
-
: { word: 'local', color:
|
|
329
|
+
? { word: 'fresh', color: 'ok' }
|
|
330
|
+
: { word: 'local', color: 'local' }
|
|
363
331
|
case 'cache-hit-remote':
|
|
364
332
|
return o.restored === false
|
|
365
|
-
? { word: 'fresh', color:
|
|
366
|
-
: { word: 'remote', color:
|
|
333
|
+
? { word: 'fresh', color: 'ok' }
|
|
334
|
+
: { word: 'remote', color: 'remote' }
|
|
367
335
|
default:
|
|
368
|
-
return { word: '', color: '' }
|
|
336
|
+
return { word: '', color: 'muted' }
|
|
369
337
|
}
|
|
370
338
|
}
|
|
371
339
|
|
|
@@ -375,46 +343,282 @@ function taskGlyph(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
375
343
|
}
|
|
376
344
|
|
|
377
345
|
/**
|
|
378
|
-
* One reported task row: `<glyph> <time> <
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
346
|
+
* One reported task row on the list grid: `<glyph> <time> <usual>
|
|
347
|
+
* <status> <cache> <cpu> <mem> <blocks> <worker> <name>`. `glyph` is
|
|
348
|
+
* pre-painted; status/cache are raw text + color, padded to their
|
|
349
|
+
* columns; `paintedId` carries identity coloring. `ms = null` blanks the
|
|
350
|
+
* time (a skip never ran); `cache = ''` and an absent cell blank theirs.
|
|
382
351
|
*/
|
|
383
352
|
export function formatTaskRow(
|
|
384
353
|
glyph: string,
|
|
385
354
|
ms: number | null,
|
|
386
355
|
status: string,
|
|
387
|
-
statusColor:
|
|
356
|
+
statusColor: Style | '',
|
|
388
357
|
cache: string,
|
|
389
|
-
cacheColor:
|
|
358
|
+
cacheColor: Style | '',
|
|
390
359
|
paintedId: string,
|
|
391
360
|
colors: ColorSupport = NO_COLOR,
|
|
392
|
-
|
|
393
|
-
blocked?: string,
|
|
361
|
+
cells: RowCells = {},
|
|
394
362
|
): string {
|
|
395
363
|
const st = cell(status, statusColor, STATUS_COL, colors)
|
|
396
364
|
// `aborting` runs one into the cache cell, which is blank beside it.
|
|
397
365
|
const ca = cell(cache, cacheColor, CACHE_COL - Math.max(0, status.length - STATUS_COL), colors)
|
|
398
|
-
const
|
|
399
|
-
|
|
366
|
+
const usual = cells.usual ?? { text: '', painted: '' }
|
|
367
|
+
const us = `${' '.repeat(Math.max(0, USUAL_COL - usual.text.length))}${usual.painted}`
|
|
368
|
+
const cpu =
|
|
369
|
+
cells.cpu === undefined ? '' : cells.cpu === null ? UNKNOWN : `${Math.round(cells.cpu * 100)}%`
|
|
370
|
+
const mem =
|
|
371
|
+
cells.memBytes === undefined
|
|
400
372
|
? ''
|
|
401
|
-
:
|
|
402
|
-
|
|
373
|
+
: cells.memBytes === null
|
|
374
|
+
? UNKNOWN
|
|
375
|
+
: formatSize(cells.memBytes)
|
|
376
|
+
const out = cells.outputBytes === undefined ? '' : formatSize(cells.outputBytes)
|
|
377
|
+
const blocks = cells.blocks ?? ''
|
|
378
|
+
const worker = workerName(cells.worker ?? '')
|
|
379
|
+
return (
|
|
380
|
+
` ${glyph} ${timeCell(ms, colors)} ${us} ${st} ${ca} ` +
|
|
381
|
+
`${right(cpu, CPU_COL, colors)} ${right(mem, MEM_COL, colors)} ${right(out, OUT_COL, colors)} ` +
|
|
382
|
+
`${' '.repeat(Math.max(0, BLOCKED_COL - blocks.length))}${paint(blocks === '0' ? 'muted' : 'value', blocks, colors)} ` +
|
|
383
|
+
`${placeCell(worker, colors)}${' '.repeat(WORKER_COL - worker.length)} ${paintedId}`
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* A cell vx cannot know: a peak no look saw, a task history never timed.
|
|
389
|
+
* Blank is a cell that does not apply (a hit ran nothing).
|
|
390
|
+
*/
|
|
391
|
+
const UNKNOWN = '\u2014'
|
|
392
|
+
|
|
393
|
+
/** A row's cells past time, status and cache; each blank when absent, `—` when null. */
|
|
394
|
+
export interface RowCells {
|
|
395
|
+
/** After the time, pre-painted: `~18s` live, `+6.0s` / `−1.4s` after. */
|
|
396
|
+
usual?: { text: string; painted: string }
|
|
397
|
+
/** Cores in use (live) or on average (after): 1.8 prints `180%`. */
|
|
398
|
+
cpu?: number | null
|
|
399
|
+
/** Resident memory now (live) or at its peak (after). */
|
|
400
|
+
memBytes?: number | null
|
|
401
|
+
/** The bytes of the task's outputs: what its save stored, or its hit restored. */
|
|
402
|
+
outputBytes?: number
|
|
403
|
+
/** How many tasks are downstream. */
|
|
404
|
+
blocks?: string
|
|
405
|
+
/** The machine it runs or ran on: `local`, or the executor's name for it. */
|
|
406
|
+
worker?: string
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function right(text: string, width: number, colors: ColorSupport): string {
|
|
410
|
+
return ' '.repeat(Math.max(0, width - text.length)) + paint('muted', text, colors)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** `900B`, `412MB`, `1.1GB`: one width for a size, whatever it is. */
|
|
414
|
+
export function formatSize(bytes: number): string {
|
|
415
|
+
if (bytes < 1024) return `${bytes}B`
|
|
416
|
+
const mb = bytes / 1024 / 1024
|
|
417
|
+
if (mb < 1) return `${Math.round(bytes / 1024)}KB`
|
|
418
|
+
if (mb < 9.95) return `${mb.toFixed(1)}MB`
|
|
419
|
+
if (mb < 1000) return `${Math.round(mb)}MB`
|
|
420
|
+
const gb = mb / 1024
|
|
421
|
+
return gb < 10 ? `${gb.toFixed(1)}GB` : `${Math.round(gb)}GB`
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// A run names a handful of workers over thousands of rows: each painted
|
|
425
|
+
// once (escaping and painting per row was ~12 ms of a 9,603-task warm run).
|
|
426
|
+
const placeCells = new WeakMap<ColorSupport, Map<string, string>>()
|
|
427
|
+
|
|
428
|
+
function placeCell(worker: string, colors: ColorSupport): string {
|
|
429
|
+
if (worker === '') return ''
|
|
430
|
+
let byName = placeCells.get(colors)
|
|
431
|
+
if (byName === undefined) placeCells.set(colors, (byName = new Map()))
|
|
432
|
+
let painted = byName.get(worker)
|
|
433
|
+
if (painted === undefined) byName.set(worker, (painted = ui.inline(ui.place(worker), colors)))
|
|
434
|
+
return painted
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** A worker's name in its 12 columns: a longer one ends in `…`. */
|
|
438
|
+
function workerName(name: string): string {
|
|
439
|
+
return name.length > WORKER_COL ? `${name.slice(0, WORKER_COL - 1)}\u2026` : name
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** A task's average cores over its run: CPU time over wall time. */
|
|
443
|
+
function coresOf(o: TaskOutcome): number | undefined {
|
|
444
|
+
return o.cpuMs === undefined || o.durationMs <= 0 ? undefined : o.cpuMs / o.durationMs
|
|
403
445
|
}
|
|
404
446
|
|
|
405
447
|
/**
|
|
406
|
-
* The
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
448
|
+
* The results list's `±usual` cell: how far an executed task ran from its
|
|
449
|
+
* usual time, `+6.0s` red or `−1.4s` green past a fifth of the usual either
|
|
450
|
+
* way, dim within it; `—` (null) with no history for the task, blank
|
|
451
|
+
* (undefined) with no history to ask. A blank within a fifth read as no
|
|
452
|
+
* usual at all (2026-10-29).
|
|
410
453
|
*/
|
|
411
|
-
export function
|
|
412
|
-
|
|
454
|
+
export function usualCell(
|
|
455
|
+
ms: number,
|
|
456
|
+
usualMs: number | null | undefined,
|
|
413
457
|
colors: ColorSupport = NO_COLOR,
|
|
414
|
-
|
|
458
|
+
): { text: string; painted: string } {
|
|
459
|
+
if (usualMs === undefined) return { text: '', painted: '' }
|
|
460
|
+
if (usualMs === null || usualMs <= 0) return unknownUsual(colors)
|
|
461
|
+
const d = ms - usualMs
|
|
462
|
+
const text = `${d >= 0 ? '+' : '\u2212'}${formatElapsed(Math.abs(d), true)}`
|
|
463
|
+
if (Math.abs(d) <= usualMs * 0.2) return { text, painted: paint('muted', text, colors) }
|
|
464
|
+
return { text, painted: paint(d > 0 ? 'error' : 'ok', text, colors) }
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** The `usual` cell of a task history has never timed. */
|
|
468
|
+
export function unknownUsual(colors: ColorSupport = NO_COLOR): { text: string; painted: string } {
|
|
469
|
+
return { text: UNKNOWN, painted: paint('muted', UNKNOWN, colors) }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* The header over a task list, on the row grid: dim and underlined across
|
|
474
|
+
* the whole line, gaps included, straight over its rows (bold in a hue, it
|
|
475
|
+
* blended into rows of the same hues).
|
|
476
|
+
*/
|
|
477
|
+
export function formatListHeader(colors: ColorSupport = NO_COLOR): string {
|
|
478
|
+
return header('\u00b1usual', colors)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* The live list's header: the usual duration where the results list has
|
|
483
|
+
* how far a task ran from it; the other columns are the results'.
|
|
484
|
+
*/
|
|
485
|
+
export function formatRunningHeader(colors: ColorSupport = NO_COLOR): string {
|
|
486
|
+
return header('usual', colors)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function header(usual: string, colors: ColorSupport): string {
|
|
490
|
+
const head =
|
|
491
|
+
` ${'time'.padStart(TIME_COL)} ${usual.padStart(USUAL_COL)} ` +
|
|
492
|
+
`${'status'.padEnd(STATUS_COL)} ${'cache'.padEnd(CACHE_COL)} ${'cpu'.padStart(CPU_COL)} ` +
|
|
493
|
+
`${'mem'.padStart(MEM_COL)} ${'outputs'.padStart(OUT_COL)} ${'blocks'.padStart(BLOCKED_COL)} ${'worker'.padEnd(WORKER_COL)} task`
|
|
494
|
+
return paint('label', head, colors)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* The task list on its grid, under its header: the run's results panel's
|
|
499
|
+
* body, and `vx last`'s, so a replay reads as the run did.
|
|
500
|
+
*/
|
|
501
|
+
export function taskList(rows: readonly ((colors: ColorSupport) => string)[]): ui.Block {
|
|
502
|
+
return { lines: (c) => [formatListHeader(c), ...rows.map((row) => row(c))] }
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** What history keeps of a task's row: `vx last` draws it again from this. */
|
|
506
|
+
export interface RecordedRow {
|
|
507
|
+
project: string
|
|
508
|
+
task: string
|
|
509
|
+
status: string
|
|
510
|
+
durationMs: number
|
|
511
|
+
restored: boolean | null
|
|
512
|
+
cached: boolean | null
|
|
513
|
+
cpuMs: number | null
|
|
514
|
+
peakRssBytes: number | null
|
|
515
|
+
placement: string | null
|
|
516
|
+
outputBytes: number | null
|
|
517
|
+
blocks: number | null
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* A recorded task's row, as the run printed it; `note` (dim) after its id.
|
|
522
|
+
* `usualMs` is the task's usual time now, from history (`null`: never timed).
|
|
523
|
+
*/
|
|
524
|
+
export function formatRecordedRow(
|
|
525
|
+
r: RecordedRow,
|
|
526
|
+
colors: ColorSupport = NO_COLOR,
|
|
527
|
+
note = '',
|
|
528
|
+
usualMs?: number | null,
|
|
415
529
|
): string {
|
|
416
|
-
|
|
417
|
-
|
|
530
|
+
// The row functions read a node's names and whether it declared a cache.
|
|
531
|
+
const node = {
|
|
532
|
+
id: `${r.project}#${r.task}`,
|
|
533
|
+
projectName: r.project,
|
|
534
|
+
taskName: r.task,
|
|
535
|
+
config: r.cached === false ? {} : { cache: {} },
|
|
536
|
+
} as unknown as TaskNode
|
|
537
|
+
const o = {
|
|
538
|
+
node,
|
|
539
|
+
status: r.status,
|
|
540
|
+
durationMs: r.durationMs,
|
|
541
|
+
...(r.restored !== null ? { restored: r.restored } : {}),
|
|
542
|
+
...(r.cpuMs !== null ? { cpuMs: r.cpuMs } : {}),
|
|
543
|
+
...(r.peakRssBytes !== null ? { peakRssBytes: r.peakRssBytes } : {}),
|
|
544
|
+
...(r.placement !== null ? { where: r.placement } : {}),
|
|
545
|
+
...(r.outputBytes !== null ? { outputBytes: r.outputBytes } : {}),
|
|
546
|
+
} as TaskOutcome
|
|
547
|
+
const blocks = r.blocks ?? undefined
|
|
548
|
+
const row =
|
|
549
|
+
o.status === 'skipped'
|
|
550
|
+
? formatTaskSkippedLine(node, colors, undefined, blocks)
|
|
551
|
+
: o.status === 'aborted'
|
|
552
|
+
? formatTaskAbortedLine(node, o, colors, blocks)
|
|
553
|
+
: isCacheHit(o.status)
|
|
554
|
+
? formatTaskHitLine(node, o, colors, false, blocks)
|
|
555
|
+
: formatTaskExecutedLine(node, o, colors, false, blocks, usualMs)
|
|
556
|
+
return note === '' ? row : `${row} ${paint('muted', note, colors)}`
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* A running task's usual duration, fixed while the elapsed time counts up
|
|
561
|
+
* toward it (2026-10-14): `~5.2s`, `~42s`, `~2m 05s`. Fits the cache
|
|
562
|
+
* column below 100 minutes.
|
|
563
|
+
*/
|
|
564
|
+
export function formatExpected(ms: number): string {
|
|
565
|
+
// The tier by the rounded value: 999.6 ms printed `~1000ms` (item 1034's class).
|
|
566
|
+
if (Math.round(ms) < 1000) return `~${Math.round(ms)}ms`
|
|
567
|
+
if (ms < 9950) return `~${(ms / 1000).toFixed(1)}s`
|
|
568
|
+
const s = Math.round(ms / 1000)
|
|
569
|
+
if (s < 60) return `~${s}s`
|
|
570
|
+
return `~${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** A task `--dry` lists: what the run would do with it, on the running list's grid. */
|
|
574
|
+
export interface PlannedRow {
|
|
575
|
+
node: TaskNode
|
|
576
|
+
glyph: string
|
|
577
|
+
status: string
|
|
578
|
+
/** The glyph's and the status word's hue. */
|
|
579
|
+
tone: Style
|
|
580
|
+
cache: string
|
|
581
|
+
cacheTone: Style | ''
|
|
582
|
+
/** Its usual time, from history. */
|
|
583
|
+
usualMs?: number
|
|
584
|
+
/** What a remote hit would download. */
|
|
585
|
+
outputBytes?: number
|
|
586
|
+
/** How many tasks are downstream of it. */
|
|
587
|
+
blocks?: number
|
|
588
|
+
worker: string
|
|
589
|
+
/** Dim, after the id: its description, why it is affected. */
|
|
590
|
+
note: string
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** `--dry`'s list: the running list's header and grid, so a plan reads as the run would. */
|
|
594
|
+
export function planList(rows: readonly PlannedRow[]): ui.Block {
|
|
595
|
+
return {
|
|
596
|
+
lines: (c) => [
|
|
597
|
+
formatRunningHeader(c),
|
|
598
|
+
...rows.map((r) => {
|
|
599
|
+
const usual = r.usualMs === undefined ? undefined : formatExpected(r.usualMs)
|
|
600
|
+
const row = formatTaskRow(
|
|
601
|
+
paint(r.tone, r.glyph, c),
|
|
602
|
+
null,
|
|
603
|
+
r.status,
|
|
604
|
+
r.tone,
|
|
605
|
+
r.cache,
|
|
606
|
+
r.cacheTone,
|
|
607
|
+
paintTaskId(r.node, c),
|
|
608
|
+
c,
|
|
609
|
+
{
|
|
610
|
+
...(usual !== undefined
|
|
611
|
+
? { usual: { text: usual, painted: paint('muted', usual, c) } }
|
|
612
|
+
: {}),
|
|
613
|
+
...(r.outputBytes !== undefined ? { outputBytes: r.outputBytes } : {}),
|
|
614
|
+
...(r.blocks !== undefined ? { blocks: String(r.blocks) } : {}),
|
|
615
|
+
worker: r.worker,
|
|
616
|
+
},
|
|
617
|
+
)
|
|
618
|
+
return r.note === '' ? row : `${row} ${paint('muted', r.note, c)}`
|
|
619
|
+
}),
|
|
620
|
+
],
|
|
621
|
+
}
|
|
418
622
|
}
|
|
419
623
|
|
|
420
624
|
/** Compact one-liner for a cache hit with nothing to replay. */
|
|
@@ -423,8 +627,11 @@ export function formatTaskHitLine(
|
|
|
423
627
|
o: TaskOutcome,
|
|
424
628
|
colors: ColorSupport = NO_COLOR,
|
|
425
629
|
onPath = false,
|
|
630
|
+
blocks?: number,
|
|
426
631
|
): string {
|
|
427
|
-
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath
|
|
632
|
+
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath, {
|
|
633
|
+
...ranCells(o, blocks),
|
|
634
|
+
})
|
|
428
635
|
}
|
|
429
636
|
|
|
430
637
|
/**
|
|
@@ -437,8 +644,8 @@ export function formatTaskHashLine(
|
|
|
437
644
|
o: TaskOutcome,
|
|
438
645
|
colors: ColorSupport = NO_COLOR,
|
|
439
646
|
): string {
|
|
440
|
-
const color = o.status === 'aborted' ?
|
|
441
|
-
const key = o.hash === undefined ? '' : ` ${paint('', o.hash, colors
|
|
647
|
+
const color = o.status === 'aborted' ? 'aborted' : statusOf(o).color
|
|
648
|
+
const key = o.hash === undefined ? '' : ` ${paint('muted', o.hash, colors)}`
|
|
442
649
|
return `${paint(color, outcomeWord(o), colors)} ${paintTaskId(node, colors)}${key}`
|
|
443
650
|
}
|
|
444
651
|
|
|
@@ -448,8 +655,44 @@ export function formatTaskExecutedLine(
|
|
|
448
655
|
o: TaskOutcome,
|
|
449
656
|
colors: ColorSupport = NO_COLOR,
|
|
450
657
|
onPath = false,
|
|
658
|
+
blocks?: number,
|
|
659
|
+
usualMs?: number | null,
|
|
451
660
|
): string {
|
|
452
|
-
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath
|
|
661
|
+
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath, {
|
|
662
|
+
...executedCells(o, blocks),
|
|
663
|
+
// A hit's time is a restore's, not the task's: nothing to compare.
|
|
664
|
+
...(o.status === 'success' || o.status === 'failed'
|
|
665
|
+
? { usual: usualCell(o.durationMs, usualMs, colors) }
|
|
666
|
+
: {}),
|
|
667
|
+
})
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* An executed task's cells: where it ran, its average cores and peak
|
|
672
|
+
* memory; a task that ran with either unread shows `—`, a hit nothing.
|
|
673
|
+
*/
|
|
674
|
+
export function executedCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
675
|
+
const cores = coresOf(o)
|
|
676
|
+
const ran = o.status === 'success' || o.status === 'failed'
|
|
677
|
+
return {
|
|
678
|
+
...ranCells(o, blocks),
|
|
679
|
+
...(cores !== undefined ? { cpu: cores } : ran ? { cpu: null } : {}),
|
|
680
|
+
...(o.peakRssBytes !== undefined
|
|
681
|
+
? { memBytes: o.peakRssBytes }
|
|
682
|
+
: ran
|
|
683
|
+
? { memBytes: null }
|
|
684
|
+
: {}),
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** The cells every row of a task that reached a machine carries. */
|
|
689
|
+
function ranCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
690
|
+
return {
|
|
691
|
+
// A skip reached no machine.
|
|
692
|
+
...(o.status === 'skipped' ? {} : { worker: o.where ?? 'local' }),
|
|
693
|
+
...(blocks !== undefined ? { blocks: String(blocks) } : {}),
|
|
694
|
+
...(o.outputBytes !== undefined ? { outputBytes: o.outputBytes } : {}),
|
|
695
|
+
}
|
|
453
696
|
}
|
|
454
697
|
|
|
455
698
|
/** Shared: render any outcome on the grid. */
|
|
@@ -459,6 +702,7 @@ function formatOutcomeRow(
|
|
|
459
702
|
ms: number | null,
|
|
460
703
|
colors: ColorSupport,
|
|
461
704
|
onPath: boolean,
|
|
705
|
+
cells: RowCells,
|
|
462
706
|
): string {
|
|
463
707
|
const st = statusOf(o)
|
|
464
708
|
const ca = cacheOf(o)
|
|
@@ -472,20 +716,21 @@ function formatOutcomeRow(
|
|
|
472
716
|
ca.color,
|
|
473
717
|
paintedId,
|
|
474
718
|
colors,
|
|
719
|
+
cells,
|
|
475
720
|
) +
|
|
476
721
|
(onPath ? pathNote(colors) : '') +
|
|
477
|
-
whereNote(o, colors) +
|
|
478
722
|
flakyNote(o, colors)
|
|
479
723
|
)
|
|
480
724
|
}
|
|
481
725
|
|
|
482
726
|
/** `critical path`: the row's task held the run's end while others ran beside it. */
|
|
483
727
|
export function pathNote(colors: ColorSupport = NO_COLOR): string {
|
|
484
|
-
|
|
728
|
+
// A fact worth knowing, not a failure: never red.
|
|
729
|
+
return ` ${paint('info', 'critical path', colors)}`
|
|
485
730
|
}
|
|
486
731
|
|
|
487
732
|
/** Where an executor ran the task, when not here, right after its id: `@small#2` (2026-10-21). */
|
|
488
|
-
|
|
733
|
+
function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
|
|
489
734
|
return o.where === undefined ? '' : placeNote(o.where, colors)
|
|
490
735
|
}
|
|
491
736
|
|
|
@@ -493,8 +738,8 @@ export function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): stri
|
|
|
493
738
|
* ` · <where>` after the id: the dot dim, the place in its own hue. Last
|
|
494
739
|
* on the row, as a worker's name has no width to pad to (2026-10-21).
|
|
495
740
|
*/
|
|
496
|
-
|
|
497
|
-
return `${paint('', ' \u00b7', colors
|
|
741
|
+
function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
|
|
742
|
+
return `${paint('muted', ' \u00b7', colors)} ${ui.inline(ui.place(where), colors)}`
|
|
498
743
|
}
|
|
499
744
|
|
|
500
745
|
/**
|
|
@@ -512,7 +757,7 @@ export function flakyNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): stri
|
|
|
512
757
|
}
|
|
513
758
|
if ((o.attempts ?? 1) > 1) said.push(`${o.attempts} attempts`)
|
|
514
759
|
const text = said.length === 0 ? 'flaky' : `flaky - ${said.join(' \u00b7 ')}`
|
|
515
|
-
return ` ${paint('', text, colors
|
|
760
|
+
return ` ${paint('muted', text, colors)}`
|
|
516
761
|
}
|
|
517
762
|
|
|
518
763
|
/**
|
|
@@ -524,19 +769,21 @@ export function formatTaskSkippedLine(
|
|
|
524
769
|
node: TaskNode,
|
|
525
770
|
colors: ColorSupport = NO_COLOR,
|
|
526
771
|
skip?: Parameters<typeof skippedReason>[0],
|
|
772
|
+
blocks?: number,
|
|
527
773
|
): string {
|
|
528
774
|
const why = skippedReason(skip)
|
|
529
|
-
const suffix = why === '' ? '' : ` ${paint('', `• ${why}`, colors
|
|
775
|
+
const suffix = why === '' ? '' : ` ${paint('muted', `• ${why}`, colors)}`
|
|
530
776
|
return (
|
|
531
777
|
formatTaskRow(
|
|
532
|
-
paint(
|
|
778
|
+
paint('warn', '\u2298', colors),
|
|
533
779
|
null,
|
|
534
780
|
'skipped',
|
|
535
|
-
|
|
781
|
+
'warn',
|
|
536
782
|
'',
|
|
537
783
|
'',
|
|
538
784
|
paintTaskId(node, colors),
|
|
539
785
|
colors,
|
|
786
|
+
blocks === undefined ? {} : { blocks: String(blocks) },
|
|
540
787
|
) + suffix
|
|
541
788
|
)
|
|
542
789
|
}
|
|
@@ -550,11 +797,13 @@ export function formatTaskAbortedLine(
|
|
|
550
797
|
node: TaskNode,
|
|
551
798
|
o: TaskOutcome,
|
|
552
799
|
colors: ColorSupport = NO_COLOR,
|
|
800
|
+
blocks?: number,
|
|
553
801
|
): string {
|
|
554
802
|
const id = paintTaskId(node, colors)
|
|
803
|
+
const bl: RowCells = blocks === undefined ? {} : { blocks: String(blocks) }
|
|
555
804
|
if (neverStarted(o)) {
|
|
556
805
|
return formatTaskRow(
|
|
557
|
-
paint('', '\u25cc', colors
|
|
806
|
+
paint('muted', '\u25cc', colors),
|
|
558
807
|
null,
|
|
559
808
|
'not run',
|
|
560
809
|
'',
|
|
@@ -562,17 +811,19 @@ export function formatTaskAbortedLine(
|
|
|
562
811
|
'',
|
|
563
812
|
id,
|
|
564
813
|
colors,
|
|
814
|
+
bl,
|
|
565
815
|
)
|
|
566
816
|
}
|
|
567
817
|
return formatTaskRow(
|
|
568
|
-
paint(
|
|
818
|
+
paint('aborted', '\u2717', colors),
|
|
569
819
|
o.durationMs,
|
|
570
820
|
'aborted',
|
|
571
|
-
|
|
821
|
+
'aborted',
|
|
572
822
|
'',
|
|
573
823
|
'',
|
|
574
824
|
id,
|
|
575
825
|
colors,
|
|
826
|
+
ranCells(o, blocks),
|
|
576
827
|
)
|
|
577
828
|
}
|
|
578
829
|
|
|
@@ -588,7 +839,7 @@ export function formatFrameOpen(
|
|
|
588
839
|
colors: ColorSupport = NO_COLOR,
|
|
589
840
|
forwardArgs: readonly string[] = [],
|
|
590
841
|
): string {
|
|
591
|
-
const corner = (t: string) => paint('', t, colors
|
|
842
|
+
const corner = (t: string) => paint('muted', t, colors)
|
|
592
843
|
const cmd = shownCommand(node, forwardArgs)
|
|
593
844
|
const mark = persistentMark(node, colors)
|
|
594
845
|
return `${corner('┌─')} ${mark}${paintTaskId(node, colors, { bold: true })} ${corner('>')} $ ${cmd}`
|
|
@@ -620,7 +871,7 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
620
871
|
// glob matches and the user decides.
|
|
621
872
|
const fix = outcome.sandboxFix
|
|
622
873
|
const line = fix === undefined ? undefined : sandboxFixLine(fix)
|
|
623
|
-
const dim = (text: string): string => paint('', text, colors
|
|
874
|
+
const dim = (text: string): string => paint('muted', text, colors)
|
|
624
875
|
const note =
|
|
625
876
|
fix === undefined
|
|
626
877
|
? []
|
|
@@ -645,7 +896,7 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
645
896
|
// vx's own notes (`vx: …`) explain a failure; they are not denials.
|
|
646
897
|
const denials = body.filter((l) => !l.startsWith('vx: ')).length
|
|
647
898
|
return [
|
|
648
|
-
sectionLine(denials > 0 ? `SANDBOX VIOLATIONS (${denials})` : 'SANDBOX',
|
|
899
|
+
sectionLine(denials > 0 ? `SANDBOX VIOLATIONS (${denials})` : 'SANDBOX', 'error', colors),
|
|
649
900
|
'',
|
|
650
901
|
...body,
|
|
651
902
|
'',
|
|
@@ -658,7 +909,7 @@ export function formatFrameClose(
|
|
|
658
909
|
outcome: TaskOutcome,
|
|
659
910
|
colors: ColorSupport = NO_COLOR,
|
|
660
911
|
): string {
|
|
661
|
-
const corner = (t: string) => paint('', t, colors
|
|
912
|
+
const corner = (t: string) => paint('muted', t, colors)
|
|
662
913
|
const mark = persistentMark(node, colors)
|
|
663
914
|
// A persistent task's child keeps running past this "ready" outcome,
|
|
664
915
|
// so the close reads `running` (accent), not `success` — the ▸ marks
|
|
@@ -682,11 +933,11 @@ function shownCommand(node: TaskNode, forwardArgs: readonly string[]): string {
|
|
|
682
933
|
}
|
|
683
934
|
|
|
684
935
|
function persistentMark(node: TaskNode, colors: ColorSupport): string {
|
|
685
|
-
return node.config.exec?.persistent !== undefined ? `${paint(
|
|
936
|
+
return node.config.exec?.persistent !== undefined ? `${paint('info', '▸', colors)} ` : ''
|
|
686
937
|
}
|
|
687
938
|
|
|
688
939
|
function runningFooter(o: TaskOutcome, colors: ColorSupport): string {
|
|
689
|
-
return ` ${paint('', `(${formatElapsed(o.durationMs)})`, colors
|
|
940
|
+
return ` ${paint('muted', `(${formatElapsed(o.durationMs)})`, colors)} ${paint('info', 'running', colors)}`
|
|
690
941
|
}
|
|
691
942
|
|
|
692
943
|
/**
|
|
@@ -713,8 +964,8 @@ export function formatPersistentTailBlock(
|
|
|
713
964
|
const stderr = body.stderr ?? ''
|
|
714
965
|
if (stdout.trim().length === 0 && stderr.trim().length === 0) return ''
|
|
715
966
|
const lines: string[] = [formatFrameOpen(node, colors, forwardArgs)]
|
|
716
|
-
pushStreamSection(lines, stdout, 'STDOUT (since ready)',
|
|
717
|
-
pushStreamSection(lines, stderr, 'STDERR (since ready)',
|
|
967
|
+
pushStreamSection(lines, stdout, 'STDOUT (since ready)', 'ok', dropped.stdout ?? 0, colors)
|
|
968
|
+
pushStreamSection(lines, stderr, 'STDERR (since ready)', 'error', dropped.stderr ?? 0, colors)
|
|
718
969
|
lines.push(formatFrameClose(node, outcome, colors))
|
|
719
970
|
return lines.join('\n') + '\n'
|
|
720
971
|
}
|
|
@@ -725,40 +976,16 @@ export function formatPersistentTailBlock(
|
|
|
725
976
|
* logs read apart. `text` holds whole lines, its last newline cut off.
|
|
726
977
|
*/
|
|
727
978
|
export function formatKeptLines(node: TaskNode, text: string, colors: ColorSupport): string {
|
|
728
|
-
const prefix = `${paintTaskId(node, colors)} ${paint('', '│', colors
|
|
979
|
+
const prefix = `${paintTaskId(node, colors)} ${paint('muted', '│', colors)} `
|
|
729
980
|
return text
|
|
730
981
|
.split('\n')
|
|
731
982
|
.map((line) => {
|
|
732
983
|
const shown = lastRedraw(line)
|
|
733
|
-
return `${prefix}${shown}${leavesStyle(shown) ?
|
|
984
|
+
return `${prefix}${shown}${leavesStyle(shown) ? STYLE_END : ''}\n`
|
|
734
985
|
})
|
|
735
986
|
.join('')
|
|
736
987
|
}
|
|
737
988
|
|
|
738
|
-
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
739
|
-
const LINE_RESET = /\r|\x1b\[[12]K|\x1b\[[01]?G/g
|
|
740
|
-
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
741
|
-
const TRAILING_RESETS = /(?:\r|\x1b\[[12]K|\x1b\[[01]?G)+$/
|
|
742
|
-
// eslint-disable-next-line no-control-regex -- ESC is the point
|
|
743
|
-
const SGR = /\x1b\[[0-9;]*m/g
|
|
744
|
-
|
|
745
|
-
/**
|
|
746
|
-
* What a line shows once its redraws have played: the text after its last
|
|
747
|
-
* `\r`, line erase (`CSI 1K`/`2K`) or move to column 1 (`CSI G`), with the
|
|
748
|
-
* colours set before it. Written after an id prefix, a dev server's redraw
|
|
749
|
-
* (`\x1b[2K\x1b[1Gbuilt in 30ms`, a `10%\r100%` progress line) erased or
|
|
750
|
-
* overwrote the prefix on a terminal. A reset that ends the line, as in
|
|
751
|
-
* CRLF, redraws nothing.
|
|
752
|
-
*/
|
|
753
|
-
function lastRedraw(line: string): string {
|
|
754
|
-
if (!line.includes('\r') && !line.includes('\x1b')) return line
|
|
755
|
-
const text = line.replace(TRAILING_RESETS, '')
|
|
756
|
-
let cut = -1
|
|
757
|
-
for (const m of text.matchAll(LINE_RESET)) cut = m.index + m[0].length
|
|
758
|
-
if (cut === -1) return text
|
|
759
|
-
return (text.slice(0, cut).match(SGR) ?? []).join('') + text.slice(cut)
|
|
760
|
-
}
|
|
761
|
-
|
|
762
989
|
/**
|
|
763
990
|
* Between the requested task's frame and the summary footer, list the
|
|
764
991
|
* persistent tasks (dev servers / watchers) the run is keeping alive in
|
|
@@ -772,28 +999,28 @@ export function formatPersistentList(
|
|
|
772
999
|
): string[] {
|
|
773
1000
|
return nodes.map(
|
|
774
1001
|
(n) =>
|
|
775
|
-
` ${paint(
|
|
1002
|
+
` ${paint('info', '▸', colors)} ${paintTaskId(n, colors)} ${paint('muted', 'running', colors)}`,
|
|
776
1003
|
)
|
|
777
1004
|
}
|
|
778
1005
|
|
|
779
1006
|
function formatBlockHeader(o: TaskOutcome, colors: ColorSupport): string {
|
|
780
1007
|
const shortHash = o.hash ? o.hash.slice(0, 8) : ''
|
|
781
|
-
const dim = (s: string) => paint('', s, colors
|
|
1008
|
+
const dim = (s: string) => paint('muted', s, colors)
|
|
782
1009
|
const word = outcomeLabel(o)
|
|
783
1010
|
switch (o.status) {
|
|
784
1011
|
case 'cache-hit':
|
|
785
|
-
return `${paint(
|
|
1012
|
+
return `${paint('ok', word, colors)} ${dim(`• ${shortHash}`)}`
|
|
786
1013
|
case 'cache-hit-remote':
|
|
787
1014
|
// A remote restore is the one hit painted accent: bytes came off the wire.
|
|
788
|
-
return `${paint(o.restored === false ?
|
|
1015
|
+
return `${paint(o.restored === false ? 'ok' : 'info', word, colors)} ${dim(`• ${shortHash}`)}`
|
|
789
1016
|
case 'failed':
|
|
790
1017
|
// The command lives in its own `├─ command` section; the header
|
|
791
1018
|
// carries the outcome like every other status.
|
|
792
|
-
return paint(
|
|
1019
|
+
return paint('error', word, colors, true)
|
|
793
1020
|
case 'skipped':
|
|
794
|
-
return paint(
|
|
1021
|
+
return paint('warn', word, colors)
|
|
795
1022
|
case 'aborted':
|
|
796
|
-
return paint(
|
|
1023
|
+
return paint('aborted', word, colors)
|
|
797
1024
|
default:
|
|
798
1025
|
return dim(word)
|
|
799
1026
|
}
|
|
@@ -807,7 +1034,7 @@ function formatBlockFooter(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
807
1034
|
// saved" on the strength of it. The stored time lives on
|
|
808
1035
|
// `TaskOutcome.storedDurationMs`.) Status differs by outcome — see
|
|
809
1036
|
// formatStatusTag.
|
|
810
|
-
const dur = paint('', `(${formatElapsed(o.durationMs)})`, colors
|
|
1037
|
+
const dur = paint('muted', `(${formatElapsed(o.durationMs)})`, colors)
|
|
811
1038
|
const tag = formatStatusTag(o, colors)
|
|
812
1039
|
return ` ${dur} ${tag}${flakyNote(o, colors)}`
|
|
813
1040
|
}
|
|
@@ -816,13 +1043,13 @@ function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
816
1043
|
const word = outcomeLabel(o)
|
|
817
1044
|
switch (o.status) {
|
|
818
1045
|
case 'failed':
|
|
819
|
-
return paint(
|
|
1046
|
+
return paint('error', word, colors, true)
|
|
820
1047
|
case 'skipped':
|
|
821
|
-
return paint(
|
|
1048
|
+
return paint('warn', word, colors)
|
|
822
1049
|
case 'aborted':
|
|
823
|
-
return paint(
|
|
1050
|
+
return paint('aborted', word, colors)
|
|
824
1051
|
default:
|
|
825
|
-
return paint('', word, colors
|
|
1052
|
+
return paint('muted', word, colors)
|
|
826
1053
|
}
|
|
827
1054
|
}
|
|
828
1055
|
|