@vzn/vx 0.0.650 → 0.0.652
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -23
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/schemas/show.json +14 -20
- package/src/bin.ts +21 -31
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +221 -30
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +77 -14
- package/src/cache/layer.ts +17 -6
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +5 -3
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +56 -17
- package/src/cache/schema.ts +15 -2
- package/src/cli/cache.ts +28 -18
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +14 -25
- package/src/cli/index.ts +28 -9
- package/src/cli/info.ts +16 -8
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +246 -204
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +8 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +45 -52
- package/src/cli/task-verb.ts +22 -0
- package/src/cli/upgrade.ts +3 -1
- package/src/cli/watch-fs.ts +6 -1
- package/src/cli/watch-judge.ts +3 -2
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +94 -52
- package/src/cli/why.ts +123 -89
- package/src/cli/workspace-config.ts +2 -1
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +42 -2
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +58 -5
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/pipes.ts +152 -0
- package/src/exec/runner.ts +199 -182
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +317 -23
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/definition.ts +16 -3
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +17 -11
- package/src/graph/task-graph.ts +51 -24
- package/src/handoff.ts +10 -7
- package/src/history/failure-mode.ts +24 -1
- package/src/history/history.ts +5 -0
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +14 -6
- package/src/index.ts +7 -3
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +67 -20
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/live-usage.ts +16 -11
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +108 -29
- package/src/orchestrator/miss-save.ts +58 -11
- package/src/orchestrator/persistent-task.ts +5 -1
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +7 -3
- package/src/orchestrator/plugin-host.ts +67 -2
- package/src/orchestrator/plugin.ts +53 -2
- package/src/orchestrator/prepare.ts +16 -1
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +62 -13
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +88 -59
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +145 -41
- package/src/orchestrator/sandbox-request.ts +52 -9
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +21 -7
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +6 -14
- package/src/report/forecast.ts +10 -32
- package/src/report/framed-output.ts +283 -175
- package/src/report/index.ts +8 -3
- package/src/report/logger.ts +74 -39
- package/src/report/plain-output.ts +3 -1
- package/src/report/status-line.ts +49 -357
- package/src/report/summary.ts +105 -136
- package/src/settings.ts +8 -8
- package/src/ui/ansi.ts +48 -0
- package/src/ui/components.ts +506 -0
- package/src/ui/index.ts +79 -0
- package/src/ui/kit.ts +37 -0
- package/src/ui/live-region.ts +363 -0
- package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
- package/src/ui/terminal.ts +51 -0
- package/src/ui/theme.ts +224 -0
- package/src/ui/version-line.ts +28 -0
- package/src/util/config-frame.ts +191 -14
- package/src/util/edit-distance.ts +6 -1
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +21 -3
- package/src/util/secret-mask.ts +39 -3
- package/src/util/settle.ts +33 -1
- package/src/util/tail.ts +4 -1
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +70 -63
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +36 -1
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +16 -9
- package/src/workspace/workspace.ts +3 -0
- package/src/util/colors.ts +0 -133
- package/src/util/report.ts +0 -44
- package/src/util/ui.ts +0 -258
- /package/src/{report/path-links.ts → ui/links.ts} +0 -0
- /package/src/{report → ui}/terminal-signals.ts +0 -0
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
|
|
@@ -99,11 +62,11 @@ export interface SummaryStats {
|
|
|
99
62
|
restoredRemote: number
|
|
100
63
|
/** Ran here and saved to the cache: the legend's `↑`. */
|
|
101
64
|
saved?: number
|
|
102
|
-
/** Ran here with
|
|
65
|
+
/** Ran here with `cache: true`, succeeded, and saved nothing. */
|
|
103
66
|
notSaved?: number
|
|
104
|
-
/** Tasks that executed (success + failed) with
|
|
67
|
+
/** Tasks that executed (success + failed) with `cache: true` — the misses. */
|
|
105
68
|
miss: number
|
|
106
|
-
/** Executed
|
|
69
|
+
/** Executed without `cache: true`: never consulted the cache, not a miss. */
|
|
107
70
|
noCache?: number
|
|
108
71
|
/** Still to run (live section only; 0 in the final summary) — renders
|
|
109
72
|
* as a gray ▱ remainder so the live meters FILL toward the final. */
|
|
@@ -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,25 +567,22 @@ 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
|
|
|
614
583
|
// Local byte formatter — the orchestrator can't import cli/format.ts
|
|
615
584
|
// (module boundary), and the footer only needs one compact form.
|
|
616
|
-
/** Ran here with
|
|
585
|
+
/** Ran here with `cache: true`, succeeded, and its result was not saved. */
|
|
617
586
|
export function ranUnsaved(o: TaskOutcome): boolean {
|
|
618
587
|
return o.status === 'success' && !ranNoCache(o) && o.saved !== true && o.where === undefined
|
|
619
588
|
}
|
|
@@ -624,7 +593,7 @@ export function formatRunSummary(
|
|
|
624
593
|
colors: ColorSupport = NO_COLOR,
|
|
625
594
|
context?: RunContext,
|
|
626
595
|
): string[] {
|
|
627
|
-
// Counted here, not in the Tally: a task
|
|
596
|
+
// Counted here, not in the Tally: a task without `cache: true` never
|
|
628
597
|
// consulted the cache, so it is not a miss.
|
|
629
598
|
const noCache = outcomes.filter(
|
|
630
599
|
(o) =>
|
package/src/settings.ts
CHANGED
|
@@ -118,7 +118,7 @@ export const SETTINGS: readonly Setting[] = [
|
|
|
118
118
|
{
|
|
119
119
|
path: 'cache.dir',
|
|
120
120
|
kind: { type: 'string' },
|
|
121
|
-
default: '.vx/cache, entries in ~/.vx/<
|
|
121
|
+
default: '.vx/cache, entries in ~/.vx/<id>/cache',
|
|
122
122
|
doc: 'Where the cache lives, relative to the workspace root; named, it holds the whole cache.',
|
|
123
123
|
flag: { verb: 'run', name: '--cache-dir' },
|
|
124
124
|
},
|
|
@@ -165,25 +165,25 @@ export const SETTINGS: readonly Setting[] = [
|
|
|
165
165
|
flag: { verb: 'run', name: '--cache-download' },
|
|
166
166
|
},
|
|
167
167
|
{
|
|
168
|
-
path: '
|
|
168
|
+
path: 'inputs.stability.strategy',
|
|
169
169
|
kind: { type: 'enum', values: ['immutable', 'mutable', 'unchecked'] },
|
|
170
170
|
default: 'immutable',
|
|
171
171
|
doc: 'What a write to an input during the command means for the save, for tasks without their own.',
|
|
172
|
-
task: '
|
|
172
|
+
task: 'inputs.stability.strategy',
|
|
173
173
|
},
|
|
174
174
|
{
|
|
175
|
-
path: '
|
|
175
|
+
path: 'inputs.stability.severity',
|
|
176
176
|
kind: { type: 'enum', values: ['warn', 'error'] },
|
|
177
177
|
default: 'warn',
|
|
178
178
|
doc: 'Whether a skipped save warns or fails the task, for tasks without their own.',
|
|
179
|
-
task: '
|
|
179
|
+
task: 'inputs.stability.severity',
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
|
-
path: '
|
|
182
|
+
path: 'outputs.clean',
|
|
183
183
|
kind: { type: 'bool' },
|
|
184
184
|
default: 'true',
|
|
185
185
|
doc: "Remove a task's declared outputs before it runs on a miss, for tasks without their own.",
|
|
186
|
-
task: '
|
|
186
|
+
task: 'outputs.clean',
|
|
187
187
|
env: false,
|
|
188
188
|
},
|
|
189
189
|
{
|
|
@@ -249,7 +249,7 @@ export const SETTINGS: readonly Setting[] = [
|
|
|
249
249
|
]
|
|
250
250
|
|
|
251
251
|
/** The `.vx/workspace` groups that hold settings; `baseBranch` and `plugins` are the other keys. */
|
|
252
|
-
export const SETTING_GROUPS = ['run', 'cache', 'lock', 'watch'] as const
|
|
252
|
+
export const SETTING_GROUPS = ['run', 'cache', 'inputs', 'outputs', 'lock', 'watch'] as const
|
|
253
253
|
|
|
254
254
|
const BY_PATH = new Map(SETTINGS.map((s) => [s.path, s]))
|
|
255
255
|
|
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
|
+
}
|