@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
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
|
|
@@ -227,6 +190,11 @@ export interface RunContext {
|
|
|
227
190
|
usage?: (taskId: string) => { cores: number; rssBytes: number } | undefined
|
|
228
191
|
/** Each task's count of the tasks downstream of it in the run, built on first ask: the rows' `blocks`. */
|
|
229
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
|
|
230
198
|
/** Worker-pool size for this run; shown on the `info` row. */
|
|
231
199
|
concurrency?: number
|
|
232
200
|
remoteCacheEnabled: boolean
|
|
@@ -277,12 +245,12 @@ export function formatSummarySection(
|
|
|
277
245
|
const blocked = stats.blocked ?? 0
|
|
278
246
|
const remainder = {
|
|
279
247
|
n: Math.max(0, left - running - scheduled - blocked),
|
|
280
|
-
color: '',
|
|
248
|
+
color: 'muted' as const,
|
|
281
249
|
glyph: '\u25b1',
|
|
282
250
|
dim: true,
|
|
283
251
|
}
|
|
284
252
|
|
|
285
|
-
const dim = (txt: string) => paint('', txt, colors
|
|
253
|
+
const dim = (txt: string) => paint('muted', txt, colors)
|
|
286
254
|
const row = (label: string, value: string): string => ` ${dim(label.padEnd(8))} ${value}`
|
|
287
255
|
const join = (parts: string[]): string => parts.join(` ${dim('\u00b7')} `)
|
|
288
256
|
|
|
@@ -291,13 +259,12 @@ export function formatSummarySection(
|
|
|
291
259
|
// the run counts, each state its own segment (2026-10-14).
|
|
292
260
|
const runTotal = stats.total + aborted + notRun
|
|
293
261
|
const taskParts: string[] = []
|
|
294
|
-
if (stats.failed > 0)
|
|
295
|
-
|
|
296
|
-
if (stats.
|
|
297
|
-
if (
|
|
298
|
-
if (
|
|
299
|
-
if (
|
|
300
|
-
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))
|
|
301
268
|
if (scheduled > 0) taskParts.push(dim(`${scheduled} scheduled`))
|
|
302
269
|
if (blocked > 0) taskParts.push(dim(`${blocked} blocked`))
|
|
303
270
|
// What is not done yet, running included: the count the bar's unfilled part shows.
|
|
@@ -305,14 +272,14 @@ export function formatSummarySection(
|
|
|
305
272
|
if (runTotal > 0) taskParts.push(dim(`${runTotal} total`))
|
|
306
273
|
const taskBar = segmentBar(
|
|
307
274
|
[
|
|
308
|
-
{ n: stats.failed, color:
|
|
309
|
-
{ n: stats.successful, color:
|
|
310
|
-
{ n: stats.skipped, color:
|
|
311
|
-
{ n: aborted, color:
|
|
312
|
-
{ n: notRun, color:
|
|
313
|
-
{ n: running, color:
|
|
314
|
-
{ n: scheduled, color: '', dim: true },
|
|
315
|
-
{ 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 },
|
|
316
283
|
remainder,
|
|
317
284
|
],
|
|
318
285
|
colors,
|
|
@@ -320,12 +287,8 @@ export function formatSummarySection(
|
|
|
320
287
|
// Bars on the label row, color-coded legend on its own line below,
|
|
321
288
|
// indented to the bar column (2 indent + 8 label + 2 gap = 12).
|
|
322
289
|
const legend = (parts: string[]): string => `${' '.repeat(12)}${join(parts)}`
|
|
323
|
-
//
|
|
324
|
-
|
|
325
|
-
const lines: string[] = [
|
|
326
|
-
'',
|
|
327
|
-
gradientRule(colors, context !== undefined ? `vx ${context.version}` : 'vx'),
|
|
328
|
-
]
|
|
290
|
+
// The panel's body; its box goes round it at the end.
|
|
291
|
+
const lines: string[] = []
|
|
329
292
|
|
|
330
293
|
// Run context (final footer only): the projects bar leads the meter
|
|
331
294
|
// stack — the projects in the run (yellow) vs the rest of the workspace (dim).
|
|
@@ -337,12 +300,12 @@ export function formatSummarySection(
|
|
|
337
300
|
context.packageCount === 0
|
|
338
301
|
? 0
|
|
339
302
|
: Math.min(width, Math.max(1, Math.round((context.packageCount / wsTotal) * width)))
|
|
340
|
-
const bar =
|
|
341
|
-
paint(WARN, '▰'.repeat(cells), colors) +
|
|
342
|
-
paint('', '▱'.repeat(width - cells), colors, { dim: true })
|
|
343
303
|
lines.push(
|
|
344
|
-
row('projects',
|
|
345
|
-
legend([
|
|
304
|
+
row('projects', gauge(cells, 'warn', colors)),
|
|
305
|
+
legend([
|
|
306
|
+
paint('warn', `${context.packageCount} affected`, colors),
|
|
307
|
+
dim(`${wsTotal} total`),
|
|
308
|
+
]),
|
|
346
309
|
)
|
|
347
310
|
}
|
|
348
311
|
}
|
|
@@ -371,17 +334,17 @@ export function formatSummarySection(
|
|
|
371
334
|
const b = cacheBuckets(stats, layers)
|
|
372
335
|
const cacheBar = segmentBar(
|
|
373
336
|
[
|
|
374
|
-
{ n: b.miss, color:
|
|
375
|
-
{ n: b.notSaved, color:
|
|
376
|
-
{ n: b.noCache, color: '', dim: true },
|
|
377
|
-
{ n: b.upToDate, color:
|
|
378
|
-
{ n: b.restoredLocal, color:
|
|
379
|
-
{ n: b.saved, color:
|
|
380
|
-
{ n: b.restoredRemote, color:
|
|
381
|
-
{ n: b.uploaded, color:
|
|
382
|
-
{ n: b.uploading, color:
|
|
383
|
-
{ n: b.remoteFailed, color:
|
|
384
|
-
{ 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' },
|
|
385
348
|
remainder,
|
|
386
349
|
],
|
|
387
350
|
colors,
|
|
@@ -392,13 +355,13 @@ export function formatSummarySection(
|
|
|
392
355
|
// either layer) and `∅` no cache, both local; `↓` restored from a
|
|
393
356
|
// layer, `↑` saved to it; `✗` a remote read or write failed, `⇡` still
|
|
394
357
|
// uploading. Each count is its bar segment's, glyph and number in its colour.
|
|
395
|
-
const n = (count: number, glyph: string, color:
|
|
396
|
-
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)] : []
|
|
397
360
|
const local = [
|
|
398
|
-
...(layers.local ? n(b.upToDate, '\u2713',
|
|
399
|
-
...n(b.noCache, '\u2205', ''
|
|
400
|
-
...(layers.local ? n(b.restoredLocal, '\u2193',
|
|
401
|
-
...(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') : []),
|
|
402
365
|
]
|
|
403
366
|
// The layer words are dim; only the counts carry colour (2026-10-15).
|
|
404
367
|
if (local.length > 0) cacheParts.push(`${dim('local')} ${local.join(' ')}`)
|
|
@@ -408,29 +371,29 @@ export function formatSummarySection(
|
|
|
408
371
|
count === 0
|
|
409
372
|
? []
|
|
410
373
|
: bytes === undefined || bytes === 0
|
|
411
|
-
? n(count, glyph,
|
|
412
|
-
: [...n(count, glyph,
|
|
374
|
+
? n(count, glyph, 'remote')
|
|
375
|
+
: [...n(count, glyph, 'remote'), dim(formatBytes(bytes))]
|
|
413
376
|
const remote = [
|
|
414
377
|
...sized(b.restoredRemote, '\u2193', layers.downloads?.bytes),
|
|
415
378
|
...sized(b.uploaded, '\u2191', layers.uploads?.bytes),
|
|
416
|
-
...n(b.remoteFailed, '\u2717',
|
|
417
|
-
...n(b.uploading, '\u21e1',
|
|
379
|
+
...n(b.remoteFailed, '\u2717', 'error'),
|
|
380
|
+
...n(b.uploading, '\u21e1', 'info'),
|
|
418
381
|
]
|
|
419
382
|
if (layers.remote && remote.length > 0)
|
|
420
383
|
cacheParts.push(`${dim('remote')} ${remote.join(' ')}`)
|
|
421
|
-
if (b.miss > 0) cacheParts.push(paint(
|
|
422
|
-
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))
|
|
423
386
|
const notDownloaded = layers.notDownloaded()
|
|
424
|
-
if (notDownloaded > 0)
|
|
387
|
+
if (notDownloaded > 0)
|
|
388
|
+
cacheParts.push(paint('warn', `${notDownloaded} not downloaded`, colors))
|
|
425
389
|
} else {
|
|
426
|
-
if (miss > 0) cacheParts.push(paint(
|
|
427
|
-
if (noCache > 0) cacheParts.push(paint('', `${noCache} no-cache`, colors
|
|
428
|
-
if (stats.upToDate > 0)
|
|
429
|
-
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))
|
|
430
393
|
if (stats.restoredLocal > 0)
|
|
431
|
-
cacheParts.push(paint(
|
|
394
|
+
cacheParts.push(paint('local', `${stats.restoredLocal} local`, colors))
|
|
432
395
|
if (stats.restoredRemote > 0)
|
|
433
|
-
cacheParts.push(paint(
|
|
396
|
+
cacheParts.push(paint('remote', `${stats.restoredRemote} remote`, colors))
|
|
434
397
|
}
|
|
435
398
|
// Skipped tasks ride the bar, yellow like the tasks meter, so the two
|
|
436
399
|
// bars fill alike; their count is the tasks legend's alone (the footer
|
|
@@ -467,9 +430,9 @@ export function formatSummarySection(
|
|
|
467
430
|
// The live run's forecast leads the spread: the one number on the row
|
|
468
431
|
// that is about the future.
|
|
469
432
|
if (stats.etaMs !== undefined)
|
|
470
|
-
timeParts.push(paint(
|
|
433
|
+
timeParts.push(paint('info', `${formatEta(stats.etaMs)} left`, colors))
|
|
471
434
|
else if (stats.overMs !== undefined)
|
|
472
|
-
timeParts.push(paint(
|
|
435
|
+
timeParts.push(paint('info', `${formatOver(stats.overMs)} over`, colors))
|
|
473
436
|
for (const p of spreadParts) timeParts.push(dim(p))
|
|
474
437
|
const timeRow = row('time', join(timeParts))
|
|
475
438
|
// Run-shape footer (final summary only): worker pool + cache mode,
|
|
@@ -513,7 +476,18 @@ export function formatSummarySection(
|
|
|
513
476
|
} else {
|
|
514
477
|
lines.push('', timeRow)
|
|
515
478
|
}
|
|
516
|
-
|
|
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
|
+
]
|
|
517
491
|
}
|
|
518
492
|
|
|
519
493
|
/**
|
|
@@ -532,7 +506,7 @@ function warnRows(
|
|
|
532
506
|
warnings.map((w) => Bun.stripANSI(w).split('\n')[0]!.trim()).filter((w) => w !== ''),
|
|
533
507
|
).size
|
|
534
508
|
if (n === 0) return []
|
|
535
|
-
return [row('warn', paint(
|
|
509
|
+
return [row('warn', paint('warn', `${n} ${n === 1 ? 'warning' : 'warnings'} above`, colors))]
|
|
536
510
|
}
|
|
537
511
|
|
|
538
512
|
/**
|
|
@@ -576,18 +550,16 @@ export function formatOutcomeTable(
|
|
|
576
550
|
}
|
|
577
551
|
const pad = (text: string, width: number) => ' '.repeat(Math.max(0, width - text.length))
|
|
578
552
|
const header = `${'TASK'.padEnd(widths.task)} ${'STATUS'.padEnd(widths.status)} ${'DURATION'.padStart(widths.duration)}`
|
|
579
|
-
const dim = (t: string) => paint('', t, colors
|
|
553
|
+
const dim = (t: string) => paint('muted', t, colors)
|
|
580
554
|
return [
|
|
581
555
|
'',
|
|
582
|
-
paint(
|
|
556
|
+
paint('heading', header, colors, true),
|
|
583
557
|
dim('-'.repeat(header.length)),
|
|
584
558
|
...rows.map((r) => {
|
|
585
559
|
const { node } = r.o
|
|
586
560
|
const id =
|
|
587
561
|
node.projectName !== undefined && node.taskName !== undefined
|
|
588
|
-
?
|
|
589
|
-
dim('#') +
|
|
590
|
-
paint(identityColor(node.taskName), node.taskName, colors)
|
|
562
|
+
? idParts(node.projectName, node.projectName, node.taskName, colors)
|
|
591
563
|
: r.task
|
|
592
564
|
const status = paint(tableStatusColor(r.o), r.status, colors)
|
|
593
565
|
return `${id}${pad(r.task, widths.task)} ${status}${pad(r.status, widths.status)} ${pad(r.duration, widths.duration)}${dim(r.duration)}`
|
|
@@ -595,19 +567,16 @@ export function formatOutcomeTable(
|
|
|
595
567
|
]
|
|
596
568
|
}
|
|
597
569
|
|
|
598
|
-
|
|
599
|
-
const TABLE_HEADER = '#a78bfa'
|
|
600
|
-
|
|
601
|
-
function tableStatusColor(o: TaskOutcome): string {
|
|
570
|
+
function tableStatusColor(o: TaskOutcome): Style {
|
|
602
571
|
switch (o.status) {
|
|
603
572
|
case 'failed':
|
|
604
|
-
return
|
|
573
|
+
return 'error'
|
|
605
574
|
case 'skipped':
|
|
606
|
-
return
|
|
575
|
+
return 'warn'
|
|
607
576
|
case 'aborted':
|
|
608
|
-
return neverStarted(o) ?
|
|
577
|
+
return neverStarted(o) ? 'notRun' : 'aborted'
|
|
609
578
|
default:
|
|
610
|
-
return
|
|
579
|
+
return 'ok'
|
|
611
580
|
}
|
|
612
581
|
}
|
|
613
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
|
+
}
|