@vzn/vx 0.0.650 → 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 +202 -25
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/index.ts +1 -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 +3 -2
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +11 -1
- package/src/cache/schema.ts +11 -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 +213 -203
- 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 +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 +123 -89
- 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 +14 -10
- package/src/graph/task-graph.ts +27 -1
- package/src/handoff.ts +10 -7
- package/src/history/history.ts +5 -0
- package/src/history/metrics.ts +9 -1
- package/src/index.ts +3 -1
- package/src/orchestrator/doctor.ts +1 -1
- 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/index.ts +3 -0
- package/src/orchestrator/live-usage.ts +16 -11
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +108 -29
- 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 +47 -2
- package/src/orchestrator/prepare.ts +16 -1
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +1 -1
- package/src/orchestrator/run-records.ts +10 -0
- package/src/orchestrator/run.ts +53 -22
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/task-hash.ts +17 -3
- package/src/report/events.ts +3 -11
- package/src/report/forecast.ts +10 -32
- package/src/report/framed-output.ts +272 -173
- package/src/report/index.ts +8 -3
- package/src/report/logger.ts +73 -38
- package/src/report/plain-output.ts +1 -1
- package/src/report/status-line.ts +49 -357
- package/src/report/summary.ts +100 -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/index.ts +2 -15
- package/src/util/paths.ts +20 -2
- package/src/util/secret-mask.ts +39 -3
- 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/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,18 @@ 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
|
-
/** The list header's colour: violet-400, the summary rule's first stop. */
|
|
303
270
|
|
|
304
271
|
/** Right-align the duration in a TIME_COL cell (pad on the left). */
|
|
305
272
|
function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
306
273
|
let raw = ms === null ? '' : formatElapsed(ms)
|
|
307
274
|
// `1000.00s` is wider than the cell; minutes fit it up to `99m 59s`.
|
|
308
275
|
if (ms !== null && raw.length > TIME_COL) raw = formatElapsed(ms, true)
|
|
309
|
-
|
|
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)
|
|
310
278
|
}
|
|
311
279
|
|
|
312
280
|
/**
|
|
@@ -314,13 +282,8 @@ function timeCell(ms: number | null, colors: ColorSupport): string {
|
|
|
314
282
|
* An empty color string renders the text dim (used for the `miss` cache
|
|
315
283
|
* word); empty text is a blank cell.
|
|
316
284
|
*/
|
|
317
|
-
function cell(text: string, color:
|
|
318
|
-
const painted =
|
|
319
|
-
text === ''
|
|
320
|
-
? ''
|
|
321
|
-
: color === ''
|
|
322
|
-
? paint('', text, colors, { dim: true })
|
|
323
|
-
: 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)
|
|
324
287
|
return painted + ' '.repeat(Math.max(0, width - text.length))
|
|
325
288
|
}
|
|
326
289
|
|
|
@@ -343,34 +306,34 @@ function glyphShape(o: TaskOutcome): string {
|
|
|
343
306
|
}
|
|
344
307
|
|
|
345
308
|
/** Task-axis word + color (a cache hit is a success). */
|
|
346
|
-
function statusOf(o: TaskOutcome): { word: string; color:
|
|
309
|
+
function statusOf(o: TaskOutcome): { word: string; color: Style } {
|
|
347
310
|
switch (o.status) {
|
|
348
311
|
case 'failed':
|
|
349
|
-
return { word: 'failed', color:
|
|
312
|
+
return { word: 'failed', color: 'error' }
|
|
350
313
|
case 'skipped':
|
|
351
|
-
return { word: 'skipped', color:
|
|
314
|
+
return { word: 'skipped', color: 'warn' }
|
|
352
315
|
default:
|
|
353
|
-
return { word: 'success', color:
|
|
316
|
+
return { word: 'success', color: 'ok' }
|
|
354
317
|
}
|
|
355
318
|
}
|
|
356
319
|
|
|
357
320
|
/** Cache-axis word + color. Empty for states that never reached the cache. */
|
|
358
|
-
function cacheOf(o: TaskOutcome): { word: string; color:
|
|
321
|
+
function cacheOf(o: TaskOutcome): { word: string; color: Style } {
|
|
359
322
|
switch (o.status) {
|
|
360
323
|
case 'success':
|
|
361
324
|
case 'failed':
|
|
362
325
|
// A task no cache answered for never consulted one: not a miss.
|
|
363
|
-
return { word: ranNoCache(o) ? 'no-cache' : 'miss', color: '' } // dim
|
|
326
|
+
return { word: ranNoCache(o) ? 'no-cache' : 'miss', color: 'muted' } // dim
|
|
364
327
|
case 'cache-hit':
|
|
365
328
|
return o.restored === false
|
|
366
|
-
? { word: 'fresh', color:
|
|
367
|
-
: { word: 'local', color:
|
|
329
|
+
? { word: 'fresh', color: 'ok' }
|
|
330
|
+
: { word: 'local', color: 'local' }
|
|
368
331
|
case 'cache-hit-remote':
|
|
369
332
|
return o.restored === false
|
|
370
|
-
? { word: 'fresh', color:
|
|
371
|
-
: { word: 'remote', color:
|
|
333
|
+
? { word: 'fresh', color: 'ok' }
|
|
334
|
+
: { word: 'remote', color: 'remote' }
|
|
372
335
|
default:
|
|
373
|
-
return { word: '', color: '' }
|
|
336
|
+
return { word: '', color: 'muted' }
|
|
374
337
|
}
|
|
375
338
|
}
|
|
376
339
|
|
|
@@ -380,7 +343,7 @@ function taskGlyph(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
380
343
|
}
|
|
381
344
|
|
|
382
345
|
/**
|
|
383
|
-
* One reported task row on the list grid: `<glyph> <time> <usual>
|
|
346
|
+
* One reported task row on the list grid: `<glyph> <time> <usual>
|
|
384
347
|
* <status> <cache> <cpu> <mem> <blocks> <worker> <name>`. `glyph` is
|
|
385
348
|
* pre-painted; status/cache are raw text + color, padded to their
|
|
386
349
|
* columns; `paintedId` carries identity coloring. `ms = null` blanks the
|
|
@@ -390,9 +353,9 @@ export function formatTaskRow(
|
|
|
390
353
|
glyph: string,
|
|
391
354
|
ms: number | null,
|
|
392
355
|
status: string,
|
|
393
|
-
statusColor:
|
|
356
|
+
statusColor: Style | '',
|
|
394
357
|
cache: string,
|
|
395
|
-
cacheColor:
|
|
358
|
+
cacheColor: Style | '',
|
|
396
359
|
paintedId: string,
|
|
397
360
|
colors: ColorSupport = NO_COLOR,
|
|
398
361
|
cells: RowCells = {},
|
|
@@ -402,30 +365,39 @@ export function formatTaskRow(
|
|
|
402
365
|
const ca = cell(cache, cacheColor, CACHE_COL - Math.max(0, status.length - STATUS_COL), colors)
|
|
403
366
|
const usual = cells.usual ?? { text: '', painted: '' }
|
|
404
367
|
const us = `${' '.repeat(Math.max(0, USUAL_COL - usual.text.length))}${usual.painted}`
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
const mem =
|
|
368
|
+
const cpu =
|
|
369
|
+
cells.cpu === undefined ? '' : cells.cpu === null ? UNKNOWN : `${Math.round(cells.cpu * 100)}%`
|
|
370
|
+
const mem =
|
|
371
|
+
cells.memBytes === undefined
|
|
372
|
+
? ''
|
|
373
|
+
: cells.memBytes === null
|
|
374
|
+
? UNKNOWN
|
|
375
|
+
: formatSize(cells.memBytes)
|
|
408
376
|
const out = cells.outputBytes === undefined ? '' : formatSize(cells.outputBytes)
|
|
409
377
|
const blocks = cells.blocks ?? ''
|
|
410
378
|
const worker = workerName(cells.worker ?? '')
|
|
411
379
|
return (
|
|
412
|
-
` ${glyph} ${timeCell(ms, colors)} ${us} ${
|
|
380
|
+
` ${glyph} ${timeCell(ms, colors)} ${us} ${st} ${ca} ` +
|
|
413
381
|
`${right(cpu, CPU_COL, colors)} ${right(mem, MEM_COL, colors)} ${right(out, OUT_COL, colors)} ` +
|
|
414
|
-
`${' '.repeat(Math.max(0, BLOCKED_COL - blocks.length))}${paint(
|
|
415
|
-
`${
|
|
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}`
|
|
416
384
|
)
|
|
417
385
|
}
|
|
418
386
|
|
|
419
|
-
/**
|
|
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. */
|
|
420
394
|
export interface RowCells {
|
|
421
395
|
/** After the time, pre-painted: `~18s` live, `+6.0s` / `−1.4s` after. */
|
|
422
396
|
usual?: { text: string; painted: string }
|
|
423
|
-
/** How long the task waited ready for a slot. */
|
|
424
|
-
waitedMs?: number
|
|
425
397
|
/** Cores in use (live) or on average (after): 1.8 prints `180%`. */
|
|
426
|
-
cpu?: number
|
|
398
|
+
cpu?: number | null
|
|
427
399
|
/** Resident memory now (live) or at its peak (after). */
|
|
428
|
-
memBytes?: number
|
|
400
|
+
memBytes?: number | null
|
|
429
401
|
/** The bytes of the task's outputs: what its save stored, or its hit restored. */
|
|
430
402
|
outputBytes?: number
|
|
431
403
|
/** How many tasks are downstream. */
|
|
@@ -435,7 +407,7 @@ export interface RowCells {
|
|
|
435
407
|
}
|
|
436
408
|
|
|
437
409
|
function right(text: string, width: number, colors: ColorSupport): string {
|
|
438
|
-
return ' '.repeat(Math.max(0, width - text.length)) + paint('', text, colors
|
|
410
|
+
return ' '.repeat(Math.max(0, width - text.length)) + paint('muted', text, colors)
|
|
439
411
|
}
|
|
440
412
|
|
|
441
413
|
/** `900B`, `412MB`, `1.1GB`: one width for a size, whatever it is. */
|
|
@@ -449,19 +421,24 @@ export function formatSize(bytes: number): string {
|
|
|
449
421
|
return gb < 10 ? `${gb.toFixed(1)}GB` : `${Math.round(gb)}GB`
|
|
450
422
|
}
|
|
451
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
|
+
|
|
452
437
|
/** A worker's name in its 12 columns: a longer one ends in `…`. */
|
|
453
438
|
function workerName(name: string): string {
|
|
454
439
|
return name.length > WORKER_COL ? `${name.slice(0, WORKER_COL - 1)}\u2026` : name
|
|
455
440
|
}
|
|
456
441
|
|
|
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
442
|
/** A task's average cores over its run: CPU time over wall time. */
|
|
466
443
|
function coresOf(o: TaskOutcome): number | undefined {
|
|
467
444
|
return o.cpuMs === undefined || o.durationMs <= 0 ? undefined : o.cpuMs / o.durationMs
|
|
@@ -469,19 +446,27 @@ function coresOf(o: TaskOutcome): number | undefined {
|
|
|
469
446
|
|
|
470
447
|
/**
|
|
471
448
|
* 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
|
-
*
|
|
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).
|
|
474
453
|
*/
|
|
475
|
-
function
|
|
454
|
+
export function usualCell(
|
|
476
455
|
ms: number,
|
|
477
|
-
usualMs: number | undefined,
|
|
456
|
+
usualMs: number | null | undefined,
|
|
478
457
|
colors: ColorSupport = NO_COLOR,
|
|
479
458
|
): { text: string; painted: string } {
|
|
480
|
-
if (usualMs === undefined
|
|
481
|
-
|
|
459
|
+
if (usualMs === undefined) return { text: '', painted: '' }
|
|
460
|
+
if (usualMs === null || usualMs <= 0) return unknownUsual(colors)
|
|
482
461
|
const d = ms - usualMs
|
|
483
|
-
const text = `${d
|
|
484
|
-
return { text, painted: paint(
|
|
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) }
|
|
485
470
|
}
|
|
486
471
|
|
|
487
472
|
/**
|
|
@@ -503,10 +488,137 @@ export function formatRunningHeader(colors: ColorSupport = NO_COLOR): string {
|
|
|
503
488
|
|
|
504
489
|
function header(usual: string, colors: ColorSupport): string {
|
|
505
490
|
const head =
|
|
506
|
-
` ${'time'.padStart(TIME_COL)} ${usual.padStart(USUAL_COL)}
|
|
491
|
+
` ${'time'.padStart(TIME_COL)} ${usual.padStart(USUAL_COL)} ` +
|
|
507
492
|
`${'status'.padEnd(STATUS_COL)} ${'cache'.padEnd(CACHE_COL)} ${'cpu'.padStart(CPU_COL)} ` +
|
|
508
493
|
`${'mem'.padStart(MEM_COL)} ${'outputs'.padStart(OUT_COL)} ${'blocks'.padStart(BLOCKED_COL)} ${'worker'.padEnd(WORKER_COL)} task`
|
|
509
|
-
return paint('', head, colors
|
|
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,
|
|
529
|
+
): string {
|
|
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
|
+
}
|
|
510
622
|
}
|
|
511
623
|
|
|
512
624
|
/** Compact one-liner for a cache hit with nothing to replay. */
|
|
@@ -532,8 +644,8 @@ export function formatTaskHashLine(
|
|
|
532
644
|
o: TaskOutcome,
|
|
533
645
|
colors: ColorSupport = NO_COLOR,
|
|
534
646
|
): string {
|
|
535
|
-
const color = o.status === 'aborted' ?
|
|
536
|
-
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)}`
|
|
537
649
|
return `${paint(color, outcomeWord(o), colors)} ${paintTaskId(node, colors)}${key}`
|
|
538
650
|
}
|
|
539
651
|
|
|
@@ -544,21 +656,32 @@ export function formatTaskExecutedLine(
|
|
|
544
656
|
colors: ColorSupport = NO_COLOR,
|
|
545
657
|
onPath = false,
|
|
546
658
|
blocks?: number,
|
|
547
|
-
usualMs?: number,
|
|
659
|
+
usualMs?: number | null,
|
|
548
660
|
): string {
|
|
549
661
|
return formatOutcomeRow(o, paintTaskId(node, colors), o.durationMs, colors, onPath, {
|
|
550
662
|
...executedCells(o, blocks),
|
|
551
|
-
|
|
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
|
+
: {}),
|
|
552
667
|
})
|
|
553
668
|
}
|
|
554
669
|
|
|
555
|
-
/**
|
|
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
|
+
*/
|
|
556
674
|
export function executedCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
557
675
|
const cores = coresOf(o)
|
|
676
|
+
const ran = o.status === 'success' || o.status === 'failed'
|
|
558
677
|
return {
|
|
559
678
|
...ranCells(o, blocks),
|
|
560
|
-
...(cores !== undefined ? { cpu: cores } : {}),
|
|
561
|
-
...(o.peakRssBytes !== undefined
|
|
679
|
+
...(cores !== undefined ? { cpu: cores } : ran ? { cpu: null } : {}),
|
|
680
|
+
...(o.peakRssBytes !== undefined
|
|
681
|
+
? { memBytes: o.peakRssBytes }
|
|
682
|
+
: ran
|
|
683
|
+
? { memBytes: null }
|
|
684
|
+
: {}),
|
|
562
685
|
}
|
|
563
686
|
}
|
|
564
687
|
|
|
@@ -568,7 +691,6 @@ function ranCells(o: TaskOutcome, blocks: number | undefined): RowCells {
|
|
|
568
691
|
// A skip reached no machine.
|
|
569
692
|
...(o.status === 'skipped' ? {} : { worker: o.where ?? 'local' }),
|
|
570
693
|
...(blocks !== undefined ? { blocks: String(blocks) } : {}),
|
|
571
|
-
...(waited(o.queuedMs) ? { waitedMs: o.queuedMs! } : {}),
|
|
572
694
|
...(o.outputBytes !== undefined ? { outputBytes: o.outputBytes } : {}),
|
|
573
695
|
}
|
|
574
696
|
}
|
|
@@ -603,7 +725,8 @@ function formatOutcomeRow(
|
|
|
603
725
|
|
|
604
726
|
/** `critical path`: the row's task held the run's end while others ran beside it. */
|
|
605
727
|
export function pathNote(colors: ColorSupport = NO_COLOR): string {
|
|
606
|
-
|
|
728
|
+
// A fact worth knowing, not a failure: never red.
|
|
729
|
+
return ` ${paint('info', 'critical path', colors)}`
|
|
607
730
|
}
|
|
608
731
|
|
|
609
732
|
/** Where an executor ran the task, when not here, right after its id: `@small#2` (2026-10-21). */
|
|
@@ -616,7 +739,7 @@ function whereNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): string {
|
|
|
616
739
|
* on the row, as a worker's name has no width to pad to (2026-10-21).
|
|
617
740
|
*/
|
|
618
741
|
function placeNote(where: string, colors: ColorSupport = NO_COLOR): string {
|
|
619
|
-
return `${paint('', ' \u00b7', colors
|
|
742
|
+
return `${paint('muted', ' \u00b7', colors)} ${ui.inline(ui.place(where), colors)}`
|
|
620
743
|
}
|
|
621
744
|
|
|
622
745
|
/**
|
|
@@ -634,7 +757,7 @@ export function flakyNote(o: TaskOutcome, colors: ColorSupport = NO_COLOR): stri
|
|
|
634
757
|
}
|
|
635
758
|
if ((o.attempts ?? 1) > 1) said.push(`${o.attempts} attempts`)
|
|
636
759
|
const text = said.length === 0 ? 'flaky' : `flaky - ${said.join(' \u00b7 ')}`
|
|
637
|
-
return ` ${paint('', text, colors
|
|
760
|
+
return ` ${paint('muted', text, colors)}`
|
|
638
761
|
}
|
|
639
762
|
|
|
640
763
|
/**
|
|
@@ -649,13 +772,13 @@ export function formatTaskSkippedLine(
|
|
|
649
772
|
blocks?: number,
|
|
650
773
|
): string {
|
|
651
774
|
const why = skippedReason(skip)
|
|
652
|
-
const suffix = why === '' ? '' : ` ${paint('', `• ${why}`, colors
|
|
775
|
+
const suffix = why === '' ? '' : ` ${paint('muted', `• ${why}`, colors)}`
|
|
653
776
|
return (
|
|
654
777
|
formatTaskRow(
|
|
655
|
-
paint(
|
|
778
|
+
paint('warn', '\u2298', colors),
|
|
656
779
|
null,
|
|
657
780
|
'skipped',
|
|
658
|
-
|
|
781
|
+
'warn',
|
|
659
782
|
'',
|
|
660
783
|
'',
|
|
661
784
|
paintTaskId(node, colors),
|
|
@@ -680,7 +803,7 @@ export function formatTaskAbortedLine(
|
|
|
680
803
|
const bl: RowCells = blocks === undefined ? {} : { blocks: String(blocks) }
|
|
681
804
|
if (neverStarted(o)) {
|
|
682
805
|
return formatTaskRow(
|
|
683
|
-
paint('', '\u25cc', colors
|
|
806
|
+
paint('muted', '\u25cc', colors),
|
|
684
807
|
null,
|
|
685
808
|
'not run',
|
|
686
809
|
'',
|
|
@@ -692,10 +815,10 @@ export function formatTaskAbortedLine(
|
|
|
692
815
|
)
|
|
693
816
|
}
|
|
694
817
|
return formatTaskRow(
|
|
695
|
-
paint(
|
|
818
|
+
paint('aborted', '\u2717', colors),
|
|
696
819
|
o.durationMs,
|
|
697
820
|
'aborted',
|
|
698
|
-
|
|
821
|
+
'aborted',
|
|
699
822
|
'',
|
|
700
823
|
'',
|
|
701
824
|
id,
|
|
@@ -716,7 +839,7 @@ export function formatFrameOpen(
|
|
|
716
839
|
colors: ColorSupport = NO_COLOR,
|
|
717
840
|
forwardArgs: readonly string[] = [],
|
|
718
841
|
): string {
|
|
719
|
-
const corner = (t: string) => paint('', t, colors
|
|
842
|
+
const corner = (t: string) => paint('muted', t, colors)
|
|
720
843
|
const cmd = shownCommand(node, forwardArgs)
|
|
721
844
|
const mark = persistentMark(node, colors)
|
|
722
845
|
return `${corner('┌─')} ${mark}${paintTaskId(node, colors, { bold: true })} ${corner('>')} $ ${cmd}`
|
|
@@ -748,7 +871,7 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
748
871
|
// glob matches and the user decides.
|
|
749
872
|
const fix = outcome.sandboxFix
|
|
750
873
|
const line = fix === undefined ? undefined : sandboxFixLine(fix)
|
|
751
|
-
const dim = (text: string): string => paint('', text, colors
|
|
874
|
+
const dim = (text: string): string => paint('muted', text, colors)
|
|
752
875
|
const note =
|
|
753
876
|
fix === undefined
|
|
754
877
|
? []
|
|
@@ -773,7 +896,7 @@ function violationSection(outcome: TaskOutcome, colors: ColorSupport): string[]
|
|
|
773
896
|
// vx's own notes (`vx: …`) explain a failure; they are not denials.
|
|
774
897
|
const denials = body.filter((l) => !l.startsWith('vx: ')).length
|
|
775
898
|
return [
|
|
776
|
-
sectionLine(denials > 0 ? `SANDBOX VIOLATIONS (${denials})` : 'SANDBOX',
|
|
899
|
+
sectionLine(denials > 0 ? `SANDBOX VIOLATIONS (${denials})` : 'SANDBOX', 'error', colors),
|
|
777
900
|
'',
|
|
778
901
|
...body,
|
|
779
902
|
'',
|
|
@@ -786,7 +909,7 @@ export function formatFrameClose(
|
|
|
786
909
|
outcome: TaskOutcome,
|
|
787
910
|
colors: ColorSupport = NO_COLOR,
|
|
788
911
|
): string {
|
|
789
|
-
const corner = (t: string) => paint('', t, colors
|
|
912
|
+
const corner = (t: string) => paint('muted', t, colors)
|
|
790
913
|
const mark = persistentMark(node, colors)
|
|
791
914
|
// A persistent task's child keeps running past this "ready" outcome,
|
|
792
915
|
// so the close reads `running` (accent), not `success` — the ▸ marks
|
|
@@ -810,11 +933,11 @@ function shownCommand(node: TaskNode, forwardArgs: readonly string[]): string {
|
|
|
810
933
|
}
|
|
811
934
|
|
|
812
935
|
function persistentMark(node: TaskNode, colors: ColorSupport): string {
|
|
813
|
-
return node.config.exec?.persistent !== undefined ? `${paint(
|
|
936
|
+
return node.config.exec?.persistent !== undefined ? `${paint('info', '▸', colors)} ` : ''
|
|
814
937
|
}
|
|
815
938
|
|
|
816
939
|
function runningFooter(o: TaskOutcome, colors: ColorSupport): string {
|
|
817
|
-
return ` ${paint('', `(${formatElapsed(o.durationMs)})`, colors
|
|
940
|
+
return ` ${paint('muted', `(${formatElapsed(o.durationMs)})`, colors)} ${paint('info', 'running', colors)}`
|
|
818
941
|
}
|
|
819
942
|
|
|
820
943
|
/**
|
|
@@ -841,8 +964,8 @@ export function formatPersistentTailBlock(
|
|
|
841
964
|
const stderr = body.stderr ?? ''
|
|
842
965
|
if (stdout.trim().length === 0 && stderr.trim().length === 0) return ''
|
|
843
966
|
const lines: string[] = [formatFrameOpen(node, colors, forwardArgs)]
|
|
844
|
-
pushStreamSection(lines, stdout, 'STDOUT (since ready)',
|
|
845
|
-
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)
|
|
846
969
|
lines.push(formatFrameClose(node, outcome, colors))
|
|
847
970
|
return lines.join('\n') + '\n'
|
|
848
971
|
}
|
|
@@ -853,40 +976,16 @@ export function formatPersistentTailBlock(
|
|
|
853
976
|
* logs read apart. `text` holds whole lines, its last newline cut off.
|
|
854
977
|
*/
|
|
855
978
|
export function formatKeptLines(node: TaskNode, text: string, colors: ColorSupport): string {
|
|
856
|
-
const prefix = `${paintTaskId(node, colors)} ${paint('', '│', colors
|
|
979
|
+
const prefix = `${paintTaskId(node, colors)} ${paint('muted', '│', colors)} `
|
|
857
980
|
return text
|
|
858
981
|
.split('\n')
|
|
859
982
|
.map((line) => {
|
|
860
983
|
const shown = lastRedraw(line)
|
|
861
|
-
return `${prefix}${shown}${leavesStyle(shown) ?
|
|
984
|
+
return `${prefix}${shown}${leavesStyle(shown) ? STYLE_END : ''}\n`
|
|
862
985
|
})
|
|
863
986
|
.join('')
|
|
864
987
|
}
|
|
865
988
|
|
|
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
989
|
/**
|
|
891
990
|
* Between the requested task's frame and the summary footer, list the
|
|
892
991
|
* persistent tasks (dev servers / watchers) the run is keeping alive in
|
|
@@ -900,28 +999,28 @@ export function formatPersistentList(
|
|
|
900
999
|
): string[] {
|
|
901
1000
|
return nodes.map(
|
|
902
1001
|
(n) =>
|
|
903
|
-
` ${paint(
|
|
1002
|
+
` ${paint('info', '▸', colors)} ${paintTaskId(n, colors)} ${paint('muted', 'running', colors)}`,
|
|
904
1003
|
)
|
|
905
1004
|
}
|
|
906
1005
|
|
|
907
1006
|
function formatBlockHeader(o: TaskOutcome, colors: ColorSupport): string {
|
|
908
1007
|
const shortHash = o.hash ? o.hash.slice(0, 8) : ''
|
|
909
|
-
const dim = (s: string) => paint('', s, colors
|
|
1008
|
+
const dim = (s: string) => paint('muted', s, colors)
|
|
910
1009
|
const word = outcomeLabel(o)
|
|
911
1010
|
switch (o.status) {
|
|
912
1011
|
case 'cache-hit':
|
|
913
|
-
return `${paint(
|
|
1012
|
+
return `${paint('ok', word, colors)} ${dim(`• ${shortHash}`)}`
|
|
914
1013
|
case 'cache-hit-remote':
|
|
915
1014
|
// A remote restore is the one hit painted accent: bytes came off the wire.
|
|
916
|
-
return `${paint(o.restored === false ?
|
|
1015
|
+
return `${paint(o.restored === false ? 'ok' : 'info', word, colors)} ${dim(`• ${shortHash}`)}`
|
|
917
1016
|
case 'failed':
|
|
918
1017
|
// The command lives in its own `├─ command` section; the header
|
|
919
1018
|
// carries the outcome like every other status.
|
|
920
|
-
return paint(
|
|
1019
|
+
return paint('error', word, colors, true)
|
|
921
1020
|
case 'skipped':
|
|
922
|
-
return paint(
|
|
1021
|
+
return paint('warn', word, colors)
|
|
923
1022
|
case 'aborted':
|
|
924
|
-
return paint(
|
|
1023
|
+
return paint('aborted', word, colors)
|
|
925
1024
|
default:
|
|
926
1025
|
return dim(word)
|
|
927
1026
|
}
|
|
@@ -935,7 +1034,7 @@ function formatBlockFooter(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
935
1034
|
// saved" on the strength of it. The stored time lives on
|
|
936
1035
|
// `TaskOutcome.storedDurationMs`.) Status differs by outcome — see
|
|
937
1036
|
// formatStatusTag.
|
|
938
|
-
const dur = paint('', `(${formatElapsed(o.durationMs)})`, colors
|
|
1037
|
+
const dur = paint('muted', `(${formatElapsed(o.durationMs)})`, colors)
|
|
939
1038
|
const tag = formatStatusTag(o, colors)
|
|
940
1039
|
return ` ${dur} ${tag}${flakyNote(o, colors)}`
|
|
941
1040
|
}
|
|
@@ -944,13 +1043,13 @@ function formatStatusTag(o: TaskOutcome, colors: ColorSupport): string {
|
|
|
944
1043
|
const word = outcomeLabel(o)
|
|
945
1044
|
switch (o.status) {
|
|
946
1045
|
case 'failed':
|
|
947
|
-
return paint(
|
|
1046
|
+
return paint('error', word, colors, true)
|
|
948
1047
|
case 'skipped':
|
|
949
|
-
return paint(
|
|
1048
|
+
return paint('warn', word, colors)
|
|
950
1049
|
case 'aborted':
|
|
951
|
-
return paint(
|
|
1050
|
+
return paint('aborted', word, colors)
|
|
952
1051
|
default:
|
|
953
|
-
return paint('', word, colors
|
|
1052
|
+
return paint('muted', word, colors)
|
|
954
1053
|
}
|
|
955
1054
|
}
|
|
956
1055
|
|