@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
package/src/report/summary.ts
CHANGED
|
@@ -7,72 +7,35 @@ import type { TaskOutcome } from '../graph/index.js'
|
|
|
7
7
|
import { tallyOutcomes } from './tally.js'
|
|
8
8
|
import { outcomeLabel, projectOutcome, ranNoCache } from './events.js'
|
|
9
9
|
import { isGroupTask } from '../graph/index.js'
|
|
10
|
+
import { formatBytes, formatElapsed } from '../util/index.js'
|
|
11
|
+
import * as ui from '../ui/index.js'
|
|
10
12
|
import {
|
|
13
|
+
bar,
|
|
11
14
|
barWidth,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
formatElapsed,
|
|
15
|
-
gradientRule,
|
|
16
|
-
identityColor,
|
|
15
|
+
gauge,
|
|
16
|
+
idParts,
|
|
17
17
|
paint,
|
|
18
|
-
|
|
19
|
-
ui,
|
|
20
|
-
WARN,
|
|
18
|
+
panelTop,
|
|
21
19
|
type ColorSupport,
|
|
22
|
-
|
|
20
|
+
type Segment,
|
|
21
|
+
type Style,
|
|
22
|
+
} from '../ui/index.js'
|
|
23
23
|
|
|
24
24
|
const NO_COLOR: ColorSupport = { enabled: false }
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
// yellow is skipped's color on both meters.
|
|
28
|
-
const LOCAL = '#38bdf8' // sky-400
|
|
29
|
-
const REMOTE = '#2563eb' // blue-600
|
|
30
|
-
const ABORTED = '#f97316' // orange-500: red's neighbour, not a failure
|
|
31
|
-
const NOT_RUN = '#a855f7' // purple-500: stopped before it began, apart from every other state
|
|
32
|
-
const RUNNING = '#06b6d4' // cyan-500: ui's info
|
|
33
|
-
const ACCENT = '#06b6d4' // cyan-500: the live region's running word
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Stacked state meter: one ▰ run per bucket, proportional cells via
|
|
37
|
-
* largest-remainder, every non-zero bucket guaranteed at least one
|
|
38
|
-
* cell. Color carries the state; the color-coded numbers ride beside
|
|
39
|
-
* the bar.
|
|
40
|
-
*/
|
|
26
|
+
/** A meter of the run's states; a segment with no style is dim, `▱` the work left. */
|
|
41
27
|
function segmentBar(
|
|
42
|
-
segments: readonly { n: number; color:
|
|
28
|
+
segments: readonly { n: number; color: Style | ''; glyph?: string; dim?: boolean }[],
|
|
43
29
|
colors: ColorSupport,
|
|
44
30
|
): string {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
.sort((x, y) => y.frac - x.frac)
|
|
54
|
-
for (const { i } of order) {
|
|
55
|
-
if (leftover === 0) break
|
|
56
|
-
cells[i]!++
|
|
57
|
-
leftover--
|
|
58
|
-
}
|
|
59
|
-
for (let i = 0; i < segments.length; i++) {
|
|
60
|
-
if (segments[i]!.n > 0 && cells[i] === 0) {
|
|
61
|
-
const biggest = cells.indexOf(Math.max(...cells))
|
|
62
|
-
cells[biggest]!--
|
|
63
|
-
cells[i] = 1
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return segments
|
|
67
|
-
.map((seg, i) =>
|
|
68
|
-
// An empty segment paints nothing: no escape pair around no cell.
|
|
69
|
-
cells[i] === 0
|
|
70
|
-
? ''
|
|
71
|
-
: paint(seg.color, (seg.glyph ?? '\u25b0').repeat(cells[i]!), colors, {
|
|
72
|
-
...(seg.dim === true ? { dim: true } : {}),
|
|
73
|
-
}),
|
|
74
|
-
)
|
|
75
|
-
.join('')
|
|
31
|
+
return bar(
|
|
32
|
+
segments.map((seg): Segment => ({
|
|
33
|
+
n: seg.n,
|
|
34
|
+
style: seg.color === '' || seg.dim === true ? 'muted' : seg.color,
|
|
35
|
+
...(seg.glyph !== undefined ? { cell: seg.glyph } : {}),
|
|
36
|
+
})),
|
|
37
|
+
colors,
|
|
38
|
+
)
|
|
76
39
|
}
|
|
77
40
|
|
|
78
41
|
/** Aggregate counts feeding the summary section — buildable from a
|
|
@@ -223,6 +186,15 @@ export interface RunContext {
|
|
|
223
186
|
executorRows?: () => readonly (readonly [string, ui.Inline])[]
|
|
224
187
|
/** Where a running task runs, `null` while it waits for a machine (`TaskExecutor.running`): its live row. */
|
|
225
188
|
whereRunning?: (taskId: string) => string | null | undefined
|
|
189
|
+
/** A running task's live CPU and memory: its live row's `cpu` and `mem`. */
|
|
190
|
+
usage?: (taskId: string) => { cores: number; rssBytes: number } | undefined
|
|
191
|
+
/** Each task's count of the tasks downstream of it in the run, built on first ask: the rows' `blocks`. */
|
|
192
|
+
blocks?: () => ReadonlyMap<string, number>
|
|
193
|
+
/**
|
|
194
|
+
* A task's usual duration from history, read once on the first ask;
|
|
195
|
+
* undefined for a task it never timed: every row's `usual`.
|
|
196
|
+
*/
|
|
197
|
+
usual?: (taskId: string) => number | undefined
|
|
226
198
|
/** Worker-pool size for this run; shown on the `info` row. */
|
|
227
199
|
concurrency?: number
|
|
228
200
|
remoteCacheEnabled: boolean
|
|
@@ -273,12 +245,12 @@ export function formatSummarySection(
|
|
|
273
245
|
const blocked = stats.blocked ?? 0
|
|
274
246
|
const remainder = {
|
|
275
247
|
n: Math.max(0, left - running - scheduled - blocked),
|
|
276
|
-
color: '',
|
|
248
|
+
color: 'muted' as const,
|
|
277
249
|
glyph: '\u25b1',
|
|
278
250
|
dim: true,
|
|
279
251
|
}
|
|
280
252
|
|
|
281
|
-
const dim = (txt: string) => paint('', txt, colors
|
|
253
|
+
const dim = (txt: string) => paint('muted', txt, colors)
|
|
282
254
|
const row = (label: string, value: string): string => ` ${dim(label.padEnd(8))} ${value}`
|
|
283
255
|
const join = (parts: string[]): string => parts.join(` ${dim('\u00b7')} `)
|
|
284
256
|
|
|
@@ -287,13 +259,12 @@ export function formatSummarySection(
|
|
|
287
259
|
// the run counts, each state its own segment (2026-10-14).
|
|
288
260
|
const runTotal = stats.total + aborted + notRun
|
|
289
261
|
const taskParts: string[] = []
|
|
290
|
-
if (stats.failed > 0)
|
|
291
|
-
|
|
292
|
-
if (stats.
|
|
293
|
-
if (
|
|
294
|
-
if (
|
|
295
|
-
if (
|
|
296
|
-
if (running > 0) taskParts.push(paint(RUNNING, `${running} running`, colors))
|
|
262
|
+
if (stats.failed > 0) taskParts.push(paint('error', `${stats.failed} failed`, colors, true))
|
|
263
|
+
if (stats.successful > 0) taskParts.push(paint('ok', `${stats.successful} success`, colors))
|
|
264
|
+
if (stats.skipped > 0) taskParts.push(paint('warn', `${stats.skipped} skipped`, colors))
|
|
265
|
+
if (aborted > 0) taskParts.push(paint('aborted', `${aborted} aborted`, colors))
|
|
266
|
+
if (notRun > 0) taskParts.push(paint('notRun', `${notRun} not run`, colors))
|
|
267
|
+
if (running > 0) taskParts.push(paint('info', `${running} running`, colors))
|
|
297
268
|
if (scheduled > 0) taskParts.push(dim(`${scheduled} scheduled`))
|
|
298
269
|
if (blocked > 0) taskParts.push(dim(`${blocked} blocked`))
|
|
299
270
|
// What is not done yet, running included: the count the bar's unfilled part shows.
|
|
@@ -301,14 +272,14 @@ export function formatSummarySection(
|
|
|
301
272
|
if (runTotal > 0) taskParts.push(dim(`${runTotal} total`))
|
|
302
273
|
const taskBar = segmentBar(
|
|
303
274
|
[
|
|
304
|
-
{ n: stats.failed, color:
|
|
305
|
-
{ n: stats.successful, color:
|
|
306
|
-
{ n: stats.skipped, color:
|
|
307
|
-
{ n: aborted, color:
|
|
308
|
-
{ n: notRun, color:
|
|
309
|
-
{ n: running, color:
|
|
310
|
-
{ n: scheduled, color: '', dim: true },
|
|
311
|
-
{ n: blocked, color: '', dim: true },
|
|
275
|
+
{ n: stats.failed, color: 'error' },
|
|
276
|
+
{ n: stats.successful, color: 'ok' },
|
|
277
|
+
{ n: stats.skipped, color: 'warn' },
|
|
278
|
+
{ n: aborted, color: 'aborted' },
|
|
279
|
+
{ n: notRun, color: 'notRun' },
|
|
280
|
+
{ n: running, color: 'info' },
|
|
281
|
+
{ n: scheduled, color: 'muted', dim: true },
|
|
282
|
+
{ n: blocked, color: 'muted', dim: true },
|
|
312
283
|
remainder,
|
|
313
284
|
],
|
|
314
285
|
colors,
|
|
@@ -316,12 +287,8 @@ export function formatSummarySection(
|
|
|
316
287
|
// Bars on the label row, color-coded legend on its own line below,
|
|
317
288
|
// indented to the bar column (2 indent + 8 label + 2 gap = 12).
|
|
318
289
|
const legend = (parts: string[]): string => `${' '.repeat(12)}${join(parts)}`
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
const lines: string[] = [
|
|
322
|
-
'',
|
|
323
|
-
gradientRule(colors, context !== undefined ? `vx ${context.version}` : 'vx'),
|
|
324
|
-
]
|
|
290
|
+
// The panel's body; its box goes round it at the end.
|
|
291
|
+
const lines: string[] = []
|
|
325
292
|
|
|
326
293
|
// Run context (final footer only): the projects bar leads the meter
|
|
327
294
|
// stack — the projects in the run (yellow) vs the rest of the workspace (dim).
|
|
@@ -333,12 +300,12 @@ export function formatSummarySection(
|
|
|
333
300
|
context.packageCount === 0
|
|
334
301
|
? 0
|
|
335
302
|
: Math.min(width, Math.max(1, Math.round((context.packageCount / wsTotal) * width)))
|
|
336
|
-
const bar =
|
|
337
|
-
paint(WARN, '▰'.repeat(cells), colors) +
|
|
338
|
-
paint('', '▱'.repeat(width - cells), colors, { dim: true })
|
|
339
303
|
lines.push(
|
|
340
|
-
row('projects',
|
|
341
|
-
legend([
|
|
304
|
+
row('projects', gauge(cells, 'warn', colors)),
|
|
305
|
+
legend([
|
|
306
|
+
paint('warn', `${context.packageCount} affected`, colors),
|
|
307
|
+
dim(`${wsTotal} total`),
|
|
308
|
+
]),
|
|
342
309
|
)
|
|
343
310
|
}
|
|
344
311
|
}
|
|
@@ -367,17 +334,17 @@ export function formatSummarySection(
|
|
|
367
334
|
const b = cacheBuckets(stats, layers)
|
|
368
335
|
const cacheBar = segmentBar(
|
|
369
336
|
[
|
|
370
|
-
{ n: b.miss, color:
|
|
371
|
-
{ n: b.notSaved, color:
|
|
372
|
-
{ n: b.noCache, color: '', dim: true },
|
|
373
|
-
{ n: b.upToDate, color:
|
|
374
|
-
{ n: b.restoredLocal, color:
|
|
375
|
-
{ n: b.saved, color:
|
|
376
|
-
{ n: b.restoredRemote, color:
|
|
377
|
-
{ n: b.uploaded, color:
|
|
378
|
-
{ n: b.uploading, color:
|
|
379
|
-
{ n: b.remoteFailed, color:
|
|
380
|
-
{ n: stats.skipped, color:
|
|
337
|
+
{ n: b.miss, color: 'error' },
|
|
338
|
+
{ n: b.notSaved, color: 'warn' },
|
|
339
|
+
{ n: b.noCache, color: 'muted', dim: true },
|
|
340
|
+
{ n: b.upToDate, color: 'ok' },
|
|
341
|
+
{ n: b.restoredLocal, color: 'local' },
|
|
342
|
+
{ n: b.saved, color: 'local' },
|
|
343
|
+
{ n: b.restoredRemote, color: 'remote' },
|
|
344
|
+
{ n: b.uploaded, color: 'remote' },
|
|
345
|
+
{ n: b.uploading, color: 'info' },
|
|
346
|
+
{ n: b.remoteFailed, color: 'error' },
|
|
347
|
+
{ n: stats.skipped, color: 'warn' },
|
|
381
348
|
remainder,
|
|
382
349
|
],
|
|
383
350
|
colors,
|
|
@@ -388,13 +355,13 @@ export function formatSummarySection(
|
|
|
388
355
|
// either layer) and `∅` no cache, both local; `↓` restored from a
|
|
389
356
|
// layer, `↑` saved to it; `✗` a remote read or write failed, `⇡` still
|
|
390
357
|
// uploading. Each count is its bar segment's, glyph and number in its colour.
|
|
391
|
-
const n = (count: number, glyph: string, color:
|
|
392
|
-
count > 0 ? [paint(color, `${count}${glyph}`, colors
|
|
358
|
+
const n = (count: number, glyph: string, color: Style): string[] =>
|
|
359
|
+
count > 0 ? [paint(color, `${count}${glyph}`, colors)] : []
|
|
393
360
|
const local = [
|
|
394
|
-
...(layers.local ? n(b.upToDate, '\u2713',
|
|
395
|
-
...n(b.noCache, '\u2205', ''
|
|
396
|
-
...(layers.local ? n(b.restoredLocal, '\u2193',
|
|
397
|
-
...(layers.localWrite ? n(b.saved, '\u2191',
|
|
361
|
+
...(layers.local ? n(b.upToDate, '\u2713', 'ok') : []),
|
|
362
|
+
...n(b.noCache, '\u2205', 'muted'),
|
|
363
|
+
...(layers.local ? n(b.restoredLocal, '\u2193', 'local') : []),
|
|
364
|
+
...(layers.localWrite ? n(b.saved, '\u2191', 'local') : []),
|
|
398
365
|
]
|
|
399
366
|
// The layer words are dim; only the counts carry colour (2026-10-15).
|
|
400
367
|
if (local.length > 0) cacheParts.push(`${dim('local')} ${local.join(' ')}`)
|
|
@@ -404,29 +371,29 @@ export function formatSummarySection(
|
|
|
404
371
|
count === 0
|
|
405
372
|
? []
|
|
406
373
|
: bytes === undefined || bytes === 0
|
|
407
|
-
? n(count, glyph,
|
|
408
|
-
: [...n(count, glyph,
|
|
374
|
+
? n(count, glyph, 'remote')
|
|
375
|
+
: [...n(count, glyph, 'remote'), dim(formatBytes(bytes))]
|
|
409
376
|
const remote = [
|
|
410
377
|
...sized(b.restoredRemote, '\u2193', layers.downloads?.bytes),
|
|
411
378
|
...sized(b.uploaded, '\u2191', layers.uploads?.bytes),
|
|
412
|
-
...n(b.remoteFailed, '\u2717',
|
|
413
|
-
...n(b.uploading, '\u21e1',
|
|
379
|
+
...n(b.remoteFailed, '\u2717', 'error'),
|
|
380
|
+
...n(b.uploading, '\u21e1', 'info'),
|
|
414
381
|
]
|
|
415
382
|
if (layers.remote && remote.length > 0)
|
|
416
383
|
cacheParts.push(`${dim('remote')} ${remote.join(' ')}`)
|
|
417
|
-
if (b.miss > 0) cacheParts.push(paint(
|
|
418
|
-
if (b.notSaved > 0) cacheParts.push(paint(
|
|
384
|
+
if (b.miss > 0) cacheParts.push(paint('error', `${b.miss} miss`, colors))
|
|
385
|
+
if (b.notSaved > 0) cacheParts.push(paint('warn', `${b.notSaved} not saved`, colors))
|
|
419
386
|
const notDownloaded = layers.notDownloaded()
|
|
420
|
-
if (notDownloaded > 0)
|
|
387
|
+
if (notDownloaded > 0)
|
|
388
|
+
cacheParts.push(paint('warn', `${notDownloaded} not downloaded`, colors))
|
|
421
389
|
} else {
|
|
422
|
-
if (miss > 0) cacheParts.push(paint(
|
|
423
|
-
if (noCache > 0) cacheParts.push(paint('', `${noCache} no-cache`, colors
|
|
424
|
-
if (stats.upToDate > 0)
|
|
425
|
-
cacheParts.push(paint(SUCCESS, `${stats.upToDate} up-to-date`, colors))
|
|
390
|
+
if (miss > 0) cacheParts.push(paint('error', `${miss} miss`, colors))
|
|
391
|
+
if (noCache > 0) cacheParts.push(paint('muted', `${noCache} no-cache`, colors))
|
|
392
|
+
if (stats.upToDate > 0) cacheParts.push(paint('ok', `${stats.upToDate} up-to-date`, colors))
|
|
426
393
|
if (stats.restoredLocal > 0)
|
|
427
|
-
cacheParts.push(paint(
|
|
394
|
+
cacheParts.push(paint('local', `${stats.restoredLocal} local`, colors))
|
|
428
395
|
if (stats.restoredRemote > 0)
|
|
429
|
-
cacheParts.push(paint(
|
|
396
|
+
cacheParts.push(paint('remote', `${stats.restoredRemote} remote`, colors))
|
|
430
397
|
}
|
|
431
398
|
// Skipped tasks ride the bar, yellow like the tasks meter, so the two
|
|
432
399
|
// bars fill alike; their count is the tasks legend's alone (the footer
|
|
@@ -463,9 +430,9 @@ export function formatSummarySection(
|
|
|
463
430
|
// The live run's forecast leads the spread: the one number on the row
|
|
464
431
|
// that is about the future.
|
|
465
432
|
if (stats.etaMs !== undefined)
|
|
466
|
-
timeParts.push(paint(
|
|
433
|
+
timeParts.push(paint('info', `${formatEta(stats.etaMs)} left`, colors))
|
|
467
434
|
else if (stats.overMs !== undefined)
|
|
468
|
-
timeParts.push(paint(
|
|
435
|
+
timeParts.push(paint('info', `${formatOver(stats.overMs)} over`, colors))
|
|
469
436
|
for (const p of spreadParts) timeParts.push(dim(p))
|
|
470
437
|
const timeRow = row('time', join(timeParts))
|
|
471
438
|
// Run-shape footer (final summary only): worker pool + cache mode,
|
|
@@ -509,7 +476,18 @@ export function formatSummarySection(
|
|
|
509
476
|
} else {
|
|
510
477
|
lines.push('', timeRow)
|
|
511
478
|
}
|
|
512
|
-
|
|
479
|
+
// The Summary panel; the final one names the vx that ran it (the live
|
|
480
|
+
// one has no context yet).
|
|
481
|
+
return [
|
|
482
|
+
'',
|
|
483
|
+
panelTop(
|
|
484
|
+
'Summary',
|
|
485
|
+
colors,
|
|
486
|
+
undefined,
|
|
487
|
+
context !== undefined ? `vx ${context.version}` : undefined,
|
|
488
|
+
),
|
|
489
|
+
...lines,
|
|
490
|
+
]
|
|
513
491
|
}
|
|
514
492
|
|
|
515
493
|
/**
|
|
@@ -528,7 +506,7 @@ function warnRows(
|
|
|
528
506
|
warnings.map((w) => Bun.stripANSI(w).split('\n')[0]!.trim()).filter((w) => w !== ''),
|
|
529
507
|
).size
|
|
530
508
|
if (n === 0) return []
|
|
531
|
-
return [row('warn', paint(
|
|
509
|
+
return [row('warn', paint('warn', `${n} ${n === 1 ? 'warning' : 'warnings'} above`, colors))]
|
|
532
510
|
}
|
|
533
511
|
|
|
534
512
|
/**
|
|
@@ -572,18 +550,16 @@ export function formatOutcomeTable(
|
|
|
572
550
|
}
|
|
573
551
|
const pad = (text: string, width: number) => ' '.repeat(Math.max(0, width - text.length))
|
|
574
552
|
const header = `${'TASK'.padEnd(widths.task)} ${'STATUS'.padEnd(widths.status)} ${'DURATION'.padStart(widths.duration)}`
|
|
575
|
-
const dim = (t: string) => paint('', t, colors
|
|
553
|
+
const dim = (t: string) => paint('muted', t, colors)
|
|
576
554
|
return [
|
|
577
555
|
'',
|
|
578
|
-
paint(
|
|
556
|
+
paint('heading', header, colors, true),
|
|
579
557
|
dim('-'.repeat(header.length)),
|
|
580
558
|
...rows.map((r) => {
|
|
581
559
|
const { node } = r.o
|
|
582
560
|
const id =
|
|
583
561
|
node.projectName !== undefined && node.taskName !== undefined
|
|
584
|
-
?
|
|
585
|
-
dim('#') +
|
|
586
|
-
paint(identityColor(node.taskName), node.taskName, colors)
|
|
562
|
+
? idParts(node.projectName, node.projectName, node.taskName, colors)
|
|
587
563
|
: r.task
|
|
588
564
|
const status = paint(tableStatusColor(r.o), r.status, colors)
|
|
589
565
|
return `${id}${pad(r.task, widths.task)} ${status}${pad(r.status, widths.status)} ${pad(r.duration, widths.duration)}${dim(r.duration)}`
|
|
@@ -591,19 +567,16 @@ export function formatOutcomeTable(
|
|
|
591
567
|
]
|
|
592
568
|
}
|
|
593
569
|
|
|
594
|
-
|
|
595
|
-
const TABLE_HEADER = '#a78bfa'
|
|
596
|
-
|
|
597
|
-
function tableStatusColor(o: TaskOutcome): string {
|
|
570
|
+
function tableStatusColor(o: TaskOutcome): Style {
|
|
598
571
|
switch (o.status) {
|
|
599
572
|
case 'failed':
|
|
600
|
-
return
|
|
573
|
+
return 'error'
|
|
601
574
|
case 'skipped':
|
|
602
|
-
return
|
|
575
|
+
return 'warn'
|
|
603
576
|
case 'aborted':
|
|
604
|
-
return neverStarted(o) ?
|
|
577
|
+
return neverStarted(o) ? 'notRun' : 'aborted'
|
|
605
578
|
default:
|
|
606
|
-
return
|
|
579
|
+
return 'ok'
|
|
607
580
|
}
|
|
608
581
|
}
|
|
609
582
|
|
package/src/ui/ansi.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// A task's own bytes, as a screen shows them: vx never restyles them, but
|
|
2
|
+
// it ends a colour they leave on and plays a redraw they hold before its
|
|
3
|
+
// own lines go below or beside them.
|
|
4
|
+
|
|
5
|
+
import { RESET } from './theme.js'
|
|
6
|
+
|
|
7
|
+
/** Whether `text` holds a terminal escape. */
|
|
8
|
+
export const hasEscape = (text: string): boolean => text.includes('\x1b')
|
|
9
|
+
|
|
10
|
+
/** Whether `text`'s last colour code sets a style rather than resetting it. */
|
|
11
|
+
export function leavesStyle(text: string): boolean {
|
|
12
|
+
const at = text.lastIndexOf('\x1b[')
|
|
13
|
+
if (at < 0) return false
|
|
14
|
+
const sgr = /^\[([\d;]*)m/.exec(text.slice(at + 1, at + 64))
|
|
15
|
+
// Not a colour code (a cursor move): look no further, as a reset costs nothing.
|
|
16
|
+
if (sgr === null) return true
|
|
17
|
+
return sgr[1] !== '' && sgr[1] !== '0'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `text`, with any colour it leaves on ended. */
|
|
21
|
+
export const endStyle = (text: string): string => (leavesStyle(text) ? `${text}${RESET}` : text)
|
|
22
|
+
|
|
23
|
+
/** The bytes that end any colour left on: a task killed mid-write leaves one open. */
|
|
24
|
+
export const STYLE_END = RESET
|
|
25
|
+
|
|
26
|
+
// oxlint-disable-next-line no-control-regex -- ESC is the point
|
|
27
|
+
const LINE_RESET = /\r|\x1b\[[12]K|\x1b\[[01]?G/g
|
|
28
|
+
// oxlint-disable-next-line no-control-regex -- ESC is the point
|
|
29
|
+
const TRAILING_RESETS = /(?:\r|\x1b\[[12]K|\x1b\[[01]?G)+$/
|
|
30
|
+
// oxlint-disable-next-line no-control-regex -- ESC is the point
|
|
31
|
+
const SGR = /\x1b\[[0-9;]*m/g
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What a line shows once its redraws have played: the text after its last
|
|
35
|
+
* `\r`, line erase (`CSI 1K`/`2K`) or move to column 1 (`CSI G`), with the
|
|
36
|
+
* colours set before it. Written after an id prefix, a dev server's redraw
|
|
37
|
+
* (`\x1b[2K\x1b[1Gbuilt in 30ms`, a `10%\r100%` progress line) erased or
|
|
38
|
+
* overwrote the prefix on a terminal. A reset that ends the line, as in
|
|
39
|
+
* CRLF, redraws nothing.
|
|
40
|
+
*/
|
|
41
|
+
export function lastRedraw(line: string): string {
|
|
42
|
+
if (!line.includes('\r') && !line.includes('\x1b')) return line
|
|
43
|
+
const text = line.replace(TRAILING_RESETS, '')
|
|
44
|
+
let cut = -1
|
|
45
|
+
for (const m of text.matchAll(LINE_RESET)) cut = m.index + m[0].length
|
|
46
|
+
if (cut === -1) return text
|
|
47
|
+
return (text.slice(0, cut).match(SGR) ?? []).join('') + text.slice(cut)
|
|
48
|
+
}
|