@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/cli/last.ts
CHANGED
|
@@ -28,21 +28,17 @@ import {
|
|
|
28
28
|
shortRunId,
|
|
29
29
|
taskLog,
|
|
30
30
|
} from '../orchestrator/index.js'
|
|
31
|
-
import { outcomeWord, type WireEvent } from '../report/index.js'
|
|
31
|
+
import { formatRecordedRow, outcomeWord, taskList, type WireEvent } from '../report/index.js'
|
|
32
32
|
import {
|
|
33
|
-
detectColors,
|
|
34
|
-
ERROR,
|
|
35
33
|
formatElapsed,
|
|
36
|
-
|
|
34
|
+
nearest,
|
|
37
35
|
parseDecimalInt,
|
|
38
36
|
refusal,
|
|
39
37
|
splitTaskId,
|
|
40
|
-
SUCCESS,
|
|
41
38
|
UserError,
|
|
42
|
-
sayNotice,
|
|
43
|
-
ui,
|
|
44
|
-
type ColorSupport,
|
|
45
39
|
} from '../util/index.js'
|
|
40
|
+
import * as ui from '../ui/index.js'
|
|
41
|
+
import { sayNotice } from '../ui/index.js'
|
|
46
42
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
47
43
|
import { findCwdProject } from './select.js'
|
|
48
44
|
import { cliCacheDir, parseCacheDirFlag, rootSpelled } from './workspace-config.js'
|
|
@@ -315,7 +311,7 @@ async function printStats(
|
|
|
315
311
|
)
|
|
316
312
|
}
|
|
317
313
|
if (format === 'json') {
|
|
318
|
-
|
|
314
|
+
ui.data('out', `${JSON.stringify({ window: provider.recent, branch, stats })}\n`)
|
|
319
315
|
return 0
|
|
320
316
|
}
|
|
321
317
|
if (stats.length === 0) {
|
|
@@ -324,33 +320,37 @@ async function printStats(
|
|
|
324
320
|
}
|
|
325
321
|
const pct = (r: number): string => `${Math.round(r * 100)}%`
|
|
326
322
|
const ms = (v: number | null): string => (v === null ? '-' : fmtMs(v))
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
323
|
+
const right = (title: string): ui.Column => ({ title, align: 'right' })
|
|
324
|
+
ui.print(
|
|
325
|
+
'out',
|
|
326
|
+
ui.section(
|
|
327
|
+
'Task stats',
|
|
328
|
+
ui.table(
|
|
329
|
+
[
|
|
330
|
+
'task',
|
|
331
|
+
right('runs'),
|
|
332
|
+
right('p50'),
|
|
333
|
+
right('p99'),
|
|
334
|
+
right('pass'),
|
|
335
|
+
right('hit'),
|
|
336
|
+
'failure mode',
|
|
337
|
+
right('peak rss'),
|
|
338
|
+
right('cpu'),
|
|
339
|
+
],
|
|
340
|
+
stats.map((s) => [
|
|
341
|
+
ui.id(s.taskId),
|
|
342
|
+
String(s.runs),
|
|
343
|
+
ms(s.p50DurationMs),
|
|
344
|
+
ms(s.p99DurationMs),
|
|
345
|
+
pct(s.successRate),
|
|
346
|
+
pct(s.hitRate),
|
|
347
|
+
s.failureMode,
|
|
348
|
+
s.maxPeakRssBytes === null ? '-' : formatBytes(s.maxPeakRssBytes),
|
|
349
|
+
s.maxCpuParallelism === null ? '-' : `${s.maxCpuParallelism.toFixed(1)}×`,
|
|
350
|
+
]),
|
|
351
|
+
),
|
|
352
|
+
),
|
|
352
353
|
)
|
|
353
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
354
354
|
return 0
|
|
355
355
|
}
|
|
356
356
|
|
|
@@ -412,7 +412,7 @@ function printQuery(db: Database, sql: string, format: 'pretty' | 'json'): numbe
|
|
|
412
412
|
)
|
|
413
413
|
}
|
|
414
414
|
const rows = values.map((v) => Object.fromEntries(columns.map((c, i) => [c, v[i]])))
|
|
415
|
-
|
|
415
|
+
ui.data('out', `${JSON.stringify({ rows })}\n`)
|
|
416
416
|
return 0
|
|
417
417
|
}
|
|
418
418
|
if (values.length === 0) return 0
|
|
@@ -423,15 +423,13 @@ function printQuery(db: Database, sql: string, format: 'pretty' | 'json'): numbe
|
|
|
423
423
|
: v instanceof Uint8Array
|
|
424
424
|
? `<${v.length} bytes>`
|
|
425
425
|
: String(v)
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
.map((v
|
|
431
|
-
|
|
432
|
-
.trimEnd(),
|
|
426
|
+
ui.print(
|
|
427
|
+
'out',
|
|
428
|
+
ui.table(
|
|
429
|
+
columns,
|
|
430
|
+
values.map((r) => r.map((v) => ui.strong(cell(v as Parameters<typeof cell>[0])))),
|
|
431
|
+
),
|
|
433
432
|
)
|
|
434
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
435
433
|
return 0
|
|
436
434
|
}
|
|
437
435
|
|
|
@@ -464,12 +462,15 @@ async function printLive(
|
|
|
464
462
|
const line = (text: string): void => {
|
|
465
463
|
if (snapshot === undefined) {
|
|
466
464
|
snapshot = JSON.parse(text) as LiveSnapshot
|
|
467
|
-
if (format === 'json')
|
|
468
|
-
else
|
|
465
|
+
if (format === 'json') ui.data('out', `${text}\n`)
|
|
466
|
+
else ui.print('out', ...formatLive(snapshot))
|
|
469
467
|
return
|
|
470
468
|
}
|
|
471
|
-
if (format === 'json')
|
|
472
|
-
else
|
|
469
|
+
if (format === 'json') ui.data('out', `${text}\n`)
|
|
470
|
+
else {
|
|
471
|
+
const event = formatLiveEvent(JSON.parse(text) as WireEvent)
|
|
472
|
+
if (event !== undefined) ui.print('out', event)
|
|
473
|
+
}
|
|
473
474
|
}
|
|
474
475
|
try {
|
|
475
476
|
await Bun.connect({
|
|
@@ -506,49 +507,72 @@ async function printLive(
|
|
|
506
507
|
return 0
|
|
507
508
|
}
|
|
508
509
|
|
|
509
|
-
function formatLive(s: LiveSnapshot):
|
|
510
|
+
function formatLive(s: LiveSnapshot): ui.Block[] {
|
|
510
511
|
const count = (status: string): number => s.tasks.filter((t) => t.status === status).length
|
|
511
512
|
const running = count('running')
|
|
512
513
|
const waiting = count('waiting')
|
|
513
514
|
const failed = count('failed')
|
|
514
|
-
const lines = [
|
|
515
|
-
`run ${s.live.runId} — running ${fmtMs(s.live.elapsedMs)}`,
|
|
516
|
-
` $ ${s.live.command}`,
|
|
517
|
-
` ${running} running · ${s.tasks.length - running - waiting} ended${failed > 0 ? ` · ${failed} failed` : ''} · ${waiting} waiting`,
|
|
518
|
-
'',
|
|
519
|
-
]
|
|
520
|
-
const width = Math.max(...s.tasks.map((t) => t.status.length), 7)
|
|
521
|
-
const idWidth = Math.max(...s.tasks.map((t) => t.taskId.length), 0)
|
|
522
515
|
const now = s.live.startedAt + s.live.elapsedMs
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
516
|
+
const blocks = [
|
|
517
|
+
ui.section(
|
|
518
|
+
'Run',
|
|
519
|
+
s.live.runId,
|
|
520
|
+
ui.fields([
|
|
521
|
+
['status', ui.tone('info', `running ${fmtMs(s.live.elapsedMs)}`)],
|
|
522
|
+
['command', ui.cmd(s.live.command)],
|
|
523
|
+
[
|
|
524
|
+
'tasks',
|
|
525
|
+
`${running} running · ${s.tasks.length - running - waiting} ended${failed > 0 ? ` · ${failed} failed` : ''} · ${waiting} waiting`,
|
|
526
|
+
],
|
|
527
|
+
]),
|
|
528
|
+
),
|
|
529
|
+
ui.section(
|
|
530
|
+
'Tasks',
|
|
531
|
+
String(s.tasks.length),
|
|
532
|
+
ui.table(
|
|
533
|
+
['status', 'task', { title: 'time', align: 'right' }],
|
|
534
|
+
s.tasks.map((t) => {
|
|
535
|
+
const ms =
|
|
536
|
+
t.durationMs !== null ? t.durationMs : t.startedAt !== null ? now - t.startedAt : null
|
|
537
|
+
return [
|
|
538
|
+
ui.tone(
|
|
539
|
+
t.status === 'failed' ? 'error' : t.status === 'running' ? 'info' : 'muted',
|
|
540
|
+
t.status,
|
|
541
|
+
),
|
|
542
|
+
ui.id(t.taskId),
|
|
543
|
+
ms === null ? '' : fmtMs(ms),
|
|
544
|
+
]
|
|
545
|
+
}),
|
|
546
|
+
),
|
|
547
|
+
),
|
|
548
|
+
]
|
|
549
|
+
if (s.warnings.length > 0)
|
|
550
|
+
blocks.push(ui.section('Warnings', String(s.warnings.length), ui.text(...s.warnings)))
|
|
551
|
+
return blocks
|
|
534
552
|
}
|
|
535
553
|
|
|
536
|
-
/** One followed event
|
|
537
|
-
function formatLiveEvent(e: WireEvent):
|
|
554
|
+
/** One followed event: a task's output under its id, an end, a warning. */
|
|
555
|
+
function formatLiveEvent(e: WireEvent): ui.Block | undefined {
|
|
538
556
|
switch (e.kind) {
|
|
539
557
|
case 'task:stdout':
|
|
540
558
|
case 'task:stderr':
|
|
541
|
-
return
|
|
542
|
-
.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
559
|
+
return ui.text(
|
|
560
|
+
...e.chunk
|
|
561
|
+
.replace(/\n$/, '')
|
|
562
|
+
.split('\n')
|
|
563
|
+
.map((l): ui.Inline => [ui.id(e.taskId), ui.dim(' │ '), ui.tone('plain', l)]),
|
|
564
|
+
)
|
|
546
565
|
case 'task:complete':
|
|
547
|
-
return
|
|
566
|
+
return ui.text([
|
|
567
|
+
ui.tone(e.outcome.status === 'failed' ? 'error' : 'ok', outcomeWord(e.outcome)),
|
|
568
|
+
' ',
|
|
569
|
+
ui.id(e.outcome.taskId),
|
|
570
|
+
ui.dim(` ${fmtMs(e.outcome.durationMs)}`),
|
|
571
|
+
])
|
|
548
572
|
case 'run:status':
|
|
549
|
-
return 'warn' in e ?
|
|
573
|
+
return 'warn' in e ? ui.status('warn', 'warn', Bun.stripANSI(e.line)) : undefined
|
|
550
574
|
default:
|
|
551
|
-
return
|
|
575
|
+
return undefined
|
|
552
576
|
}
|
|
553
577
|
}
|
|
554
578
|
|
|
@@ -556,24 +580,6 @@ const fmtWhen = (ms: number): string => new Date(ms).toISOString()
|
|
|
556
580
|
|
|
557
581
|
const fmtMs = (ms: number): string => formatElapsed(ms, true)
|
|
558
582
|
|
|
559
|
-
/**
|
|
560
|
-
* What an executed task used — its peak RSS and its CPU parallelism (CPU
|
|
561
|
-
* time over wall time) — as the runner recorded them. This is the number
|
|
562
|
-
* `@vzn/vx-history` reserves from, so the replay shows it where
|
|
563
|
-
* a developer can read it; a hit spent nothing and shows nothing.
|
|
564
|
-
*/
|
|
565
|
-
function fmtUsage(t: {
|
|
566
|
-
cpuMs: number | null
|
|
567
|
-
peakRssBytes: number | null
|
|
568
|
-
durationMs: number
|
|
569
|
-
}): string {
|
|
570
|
-
const parts: string[] = []
|
|
571
|
-
if (t.peakRssBytes !== null) parts.push(formatBytes(t.peakRssBytes))
|
|
572
|
-
if (t.cpuMs !== null && t.durationMs > 0)
|
|
573
|
-
parts.push(`${(t.cpuMs / t.durationMs).toFixed(1)}× cpu`)
|
|
574
|
-
return parts.length > 0 ? ` ${parts.join(' · ')}` : ''
|
|
575
|
-
}
|
|
576
|
-
|
|
577
583
|
/**
|
|
578
584
|
* Why a row failed or was skipped, as the run's own footer said it (the
|
|
579
585
|
* v27 columns): a timeout or a never-ready server instead of the signal
|
|
@@ -632,16 +638,17 @@ const HITS_SHOWN = 16
|
|
|
632
638
|
* The steps the run did not wait on (`vx:prefetch`), which its own output
|
|
633
639
|
* never showed (2026-10-16): here for debugging, marked `internal`.
|
|
634
640
|
*/
|
|
635
|
-
function formatStepRows(steps: readonly RunStep[]
|
|
636
|
-
if (steps.length === 0) return
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
(s)
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
641
|
+
function formatStepRows(steps: readonly RunStep[]): ui.Block | undefined {
|
|
642
|
+
if (steps.length === 0) return undefined
|
|
643
|
+
return ui.table(
|
|
644
|
+
['', '', { title: '', align: 'right' }, ''],
|
|
645
|
+
steps.map((s) => [
|
|
646
|
+
ui.tone(s.ok ? 'ok' : 'error', s.ok ? 'success' : 'failed'),
|
|
647
|
+
ui.strong(s.id),
|
|
648
|
+
fmtMs(s.durationMs),
|
|
649
|
+
'internal',
|
|
650
|
+
]),
|
|
651
|
+
)
|
|
645
652
|
}
|
|
646
653
|
|
|
647
654
|
/**
|
|
@@ -655,10 +662,11 @@ function formatStepRows(steps: readonly RunStep[], c: ColorSupport): string[] {
|
|
|
655
662
|
|
|
656
663
|
export function formatTaskRows(
|
|
657
664
|
tasks: readonly RunSummaryRow[],
|
|
658
|
-
c: ColorSupport = { enabled: false },
|
|
659
665
|
/** Task id → the input that moved under it, from the run's kept failures. */
|
|
660
666
|
inputMoved: ReadonlyMap<string, string> = new Map(),
|
|
661
|
-
|
|
667
|
+
/** Task id → its usual time now, from history; a task absent was never timed. */
|
|
668
|
+
usual?: ReadonlyMap<string, number>,
|
|
669
|
+
): ui.Block[] {
|
|
662
670
|
if (tasks.length === 0) return []
|
|
663
671
|
const failed = tasks.filter((t) => t.status === 'failed')
|
|
664
672
|
const hits = tasks.filter((t) => t.status !== 'failed' && t.cacheHit === true)
|
|
@@ -668,50 +676,28 @@ export function formatTaskRows(
|
|
|
668
676
|
? [...hits].sort((a, b) => b.durationMs - a.durationMs).slice(0, HITS_SHOWN)
|
|
669
677
|
: hits
|
|
670
678
|
const ordered = [...failed, ...rest, ...shownHits]
|
|
671
|
-
const at = (t: (typeof ordered)[number]): string =>
|
|
672
|
-
t.placement !== null ? `@${t.placement}` : ''
|
|
673
|
-
const idW = Math.max(...ordered.map((t) => `${t.project}#${t.task}${at(t)}`.length), 4)
|
|
674
679
|
const skippedHere = new Set(
|
|
675
680
|
tasks.filter((t) => t.status === 'skipped').map((t) => `${t.project}#${t.task}`),
|
|
676
681
|
)
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
lines.push(
|
|
694
|
-
` ${paint(hue, status.padEnd(17), c, { dim: hue === '' })} ${ui.inline(ui.id(id), c)}${' '.repeat(idW - id.length)} ${fmtMs(t.durationMs).padStart(8)}` +
|
|
695
|
-
paint(
|
|
696
|
-
'',
|
|
697
|
-
`${t.hash !== '' ? ` ${t.hash}` : ''}${t.cached === false ? ' no-cache' : ''}${fmtUsage(t)}` +
|
|
698
|
-
reasonParts(t, skippedHere, inputMoved.get(`${t.project}#${t.task}`)),
|
|
699
|
-
c,
|
|
700
|
-
{ dim: true },
|
|
701
|
-
),
|
|
702
|
-
)
|
|
703
|
-
}
|
|
704
|
-
if (folded) {
|
|
705
|
-
lines.push(
|
|
706
|
-
paint(
|
|
707
|
-
'',
|
|
708
|
-
` … +${hits.length - HITS_SHOWN} more cache hit${hits.length - HITS_SHOWN === 1 ? '' : 's'} (the ${HITS_SHOWN} slowest restores shown) — vx last --format json lists every row`,
|
|
709
|
-
c,
|
|
710
|
-
{ dim: true },
|
|
682
|
+
// The run's own grid (`taskList`): a replay reads as the run did. What
|
|
683
|
+
// the run said below the row (an exit, why it skipped) rides it dim.
|
|
684
|
+
const rows = ordered.map((t) => {
|
|
685
|
+
const note = (
|
|
686
|
+
(t.status === 'failed' ? `exit ${t.exitCode}` : '') +
|
|
687
|
+
reasonParts(t, skippedHere, inputMoved.get(`${t.project}#${t.task}`))
|
|
688
|
+
).trim()
|
|
689
|
+
const id = `${t.project}#${t.task}`
|
|
690
|
+
const usualMs = usual === undefined ? undefined : (usual.get(id) ?? null)
|
|
691
|
+
return (c: ui.ColorSupport) => formatRecordedRow(t, c, note, usualMs)
|
|
692
|
+
})
|
|
693
|
+
const blocks = [taskList(rows)]
|
|
694
|
+
if (folded)
|
|
695
|
+
blocks.push(
|
|
696
|
+
ui.text(
|
|
697
|
+
`… +${hits.length - HITS_SHOWN} more cache hit${hits.length - HITS_SHOWN === 1 ? '' : 's'} (the ${HITS_SHOWN} slowest restores shown): \`vx last --format json\` lists every row`,
|
|
711
698
|
),
|
|
712
699
|
)
|
|
713
|
-
|
|
714
|
-
return lines
|
|
700
|
+
return blocks
|
|
715
701
|
}
|
|
716
702
|
|
|
717
703
|
/**
|
|
@@ -755,22 +741,20 @@ export function formatRunList(
|
|
|
755
741
|
>
|
|
756
742
|
id: string
|
|
757
743
|
}[],
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
(r) =>
|
|
773
|
-
`${paint(r.head.startsWith('ok') ? SUCCESS : ERROR, r.head.slice(0, 6), c)}${paint('', r.head.slice(6), c, { dim: true })} ${paint('', r.id.padEnd(idW), c, { bold: true })} ${paint('', r.counts, c, { dim: true })}${paint(ERROR, r.failed, c)}${paint('', `${' '.repeat(countsW - r.counts.length - r.failed.length)} ${r.ms.padStart(msW)} $`, c, { dim: true })} ${r.command}`,
|
|
744
|
+
): ui.Block {
|
|
745
|
+
return ui.table(
|
|
746
|
+
['', 'when', 'run', 'tasks', { title: 'time', align: 'right' }, 'command'],
|
|
747
|
+
runs.map(({ inv, id }) => [
|
|
748
|
+
inv.exitOk ? ui.tone('ok', 'ok') : ui.tone('error', 'FAILED'),
|
|
749
|
+
fmtWhen(inv.startedAt),
|
|
750
|
+
ui.strong(id),
|
|
751
|
+
[
|
|
752
|
+
ui.dim(`${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}`),
|
|
753
|
+
...(inv.failedCount > 0 ? [ui.tone('error', ` · ${inv.failedCount} failed`)] : []),
|
|
754
|
+
],
|
|
755
|
+
fmtMs(inv.totalDurationMs),
|
|
756
|
+
ui.cmd(inv.command),
|
|
757
|
+
]),
|
|
774
758
|
)
|
|
775
759
|
}
|
|
776
760
|
|
|
@@ -826,14 +810,26 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
826
810
|
const log = taskLog(db, await bareTaskId(db, await rootSpelled(root, parsed.log)), runId)
|
|
827
811
|
if (log === null) {
|
|
828
812
|
// The id's form is hinted only when it lacks one: a task that
|
|
829
|
-
// never ran was told how to spell a task id.
|
|
813
|
+
// never ran was told how to spell a task id. A typo of one that
|
|
814
|
+
// ran is named, as `vx why` names it.
|
|
815
|
+
const ran = parsed.log.includes('#')
|
|
816
|
+
? nearest(
|
|
817
|
+
parsed.log,
|
|
818
|
+
(
|
|
819
|
+
db.query('SELECT DISTINCT project, task FROM runs').all() as {
|
|
820
|
+
project: string
|
|
821
|
+
task: string
|
|
822
|
+
}[]
|
|
823
|
+
).map((r) => `${r.project}#${r.task}`),
|
|
824
|
+
)
|
|
825
|
+
: undefined
|
|
830
826
|
throw new UserError(
|
|
831
|
-
`vx last: no recorded run of ${parsed.log}${parsed.runId === undefined ? '' : ` in run ${parsed.runId}`}${parsed.log.includes('#') ? '' : ' (a task id is project#task)'}`,
|
|
827
|
+
`vx last: no recorded run of ${parsed.log}${parsed.runId === undefined ? '' : ` in run ${parsed.runId}`}${parsed.log.includes('#') ? '' : ' (a task id is project#task)'}${ran === undefined ? '' : ` — did you mean ${ran}?`}`,
|
|
832
828
|
'VX_E_NO_HISTORY',
|
|
833
829
|
)
|
|
834
830
|
}
|
|
835
831
|
if (parsed.format === 'json') {
|
|
836
|
-
|
|
832
|
+
ui.data('out', `${JSON.stringify(log)}\n`)
|
|
837
833
|
return 0
|
|
838
834
|
}
|
|
839
835
|
if (log.source === null) {
|
|
@@ -859,7 +855,8 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
859
855
|
} else if (log.output === '') {
|
|
860
856
|
sayNotice('info', 'vx last', `${log.taskId} ended ${log.status} and printed nothing`)
|
|
861
857
|
}
|
|
862
|
-
|
|
858
|
+
// The task's own bytes, as it wrote them.
|
|
859
|
+
ui.data('out', log.output)
|
|
863
860
|
return 0
|
|
864
861
|
}
|
|
865
862
|
|
|
@@ -869,7 +866,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
869
866
|
? failedInvocations(db, parsed.list)
|
|
870
867
|
: listInvocations(db, { limit: parsed.list })
|
|
871
868
|
if (parsed.format === 'json') {
|
|
872
|
-
|
|
869
|
+
ui.data('out', `${JSON.stringify(invocations)}\n`)
|
|
873
870
|
return 0
|
|
874
871
|
}
|
|
875
872
|
if (invocations.length === 0) {
|
|
@@ -877,11 +874,14 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
877
874
|
ui.print('out', ui.status('info', 'vx last', none))
|
|
878
875
|
return 0
|
|
879
876
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
877
|
+
ui.print(
|
|
878
|
+
'out',
|
|
879
|
+
ui.section(
|
|
880
|
+
'Runs',
|
|
881
|
+
String(invocations.length),
|
|
882
|
+
formatRunList(invocations.map((inv) => ({ inv, id: shortRunId(db, inv.runId) }))),
|
|
883
|
+
),
|
|
883
884
|
)
|
|
884
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
885
885
|
return 0
|
|
886
886
|
}
|
|
887
887
|
|
|
@@ -930,7 +930,8 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
930
930
|
})
|
|
931
931
|
const steps = keptSteps(db, inv.runId)
|
|
932
932
|
const warnings = keptWarnings(db, inv.runId)
|
|
933
|
-
|
|
933
|
+
ui.data(
|
|
934
|
+
'out',
|
|
934
935
|
`${JSON.stringify({
|
|
935
936
|
invocation: inv,
|
|
936
937
|
tasks,
|
|
@@ -941,36 +942,48 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
941
942
|
return 0
|
|
942
943
|
}
|
|
943
944
|
|
|
944
|
-
const c = detectColors(process.stdout)
|
|
945
|
-
const dim = (s: string): string => paint('', s, c, { dim: true })
|
|
946
|
-
const lines: string[] = []
|
|
947
|
-
lines.push(
|
|
948
|
-
`${paint('', 'run', c, { bold: true })} ${paint('', inv.runId, c, { bold: true })} ${dim('—')} ${inv.exitOk ? paint(SUCCESS, 'ok', c) : paint(ERROR, 'FAILED', c)}`,
|
|
949
|
-
)
|
|
950
|
-
lines.push(` ${dim('$')} ${paint('', inv.command, c, { bold: true })}`)
|
|
951
|
-
lines.push(
|
|
952
|
-
dim(
|
|
953
|
-
` ${fmtWhen(inv.startedAt)} · ${fmtMs(inv.totalDurationMs)}` +
|
|
954
|
-
`${inv.branch !== null ? ` · ${inv.branch}` : ''}` +
|
|
955
|
-
`${inv.commitSha !== null ? ` @ ${inv.commitSha.slice(0, 8)}${inv.dirty === true ? '+dirty' : ''}` : ''}` +
|
|
956
|
-
`${inv.ci ? ` · CI${inv.ciProvider !== null ? ` (${inv.ciProvider})` : ''}` : ''}`,
|
|
957
|
-
),
|
|
958
|
-
)
|
|
959
|
-
lines.push(
|
|
960
|
-
dim(` ${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}`) +
|
|
961
|
-
`${inv.failedCount > 0 ? paint(ERROR, ` · ${inv.failedCount} failed`, c) : ''}`,
|
|
962
|
-
)
|
|
963
945
|
const moved = new Map<string, string>()
|
|
964
946
|
if (inv.failedCount > 0) {
|
|
965
947
|
for (const f of runFailures(db, inv.runId)?.tasks ?? [])
|
|
966
948
|
if (f.inputMoved !== undefined) moved.set(f.taskId, f.inputMoved)
|
|
967
949
|
}
|
|
968
|
-
|
|
969
|
-
|
|
950
|
+
const blocks: ui.Block[] = [
|
|
951
|
+
ui.section(
|
|
952
|
+
'Run',
|
|
953
|
+
inv.runId,
|
|
954
|
+
ui.fields([
|
|
955
|
+
['status', inv.exitOk ? ui.tone('ok', 'ok') : ui.tone('error', 'FAILED', true)],
|
|
956
|
+
['command', ui.cmd(inv.command)],
|
|
957
|
+
[
|
|
958
|
+
'when',
|
|
959
|
+
`${fmtWhen(inv.startedAt)} · ${fmtMs(inv.totalDurationMs)}` +
|
|
960
|
+
`${inv.branch !== null ? ` · ${inv.branch}` : ''}` +
|
|
961
|
+
`${inv.commitSha !== null ? ` @ ${inv.commitSha.slice(0, 8)}${inv.dirty === true ? '+dirty' : ''}` : ''}` +
|
|
962
|
+
`${inv.ci ? ` · CI${inv.ciProvider !== null ? ` (${inv.ciProvider})` : ''}` : ''}`,
|
|
963
|
+
],
|
|
964
|
+
[
|
|
965
|
+
'tasks',
|
|
966
|
+
[
|
|
967
|
+
ui.dim(`${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}`),
|
|
968
|
+
...(inv.failedCount > 0 ? [ui.tone('error', ` · ${inv.failedCount} failed`)] : []),
|
|
969
|
+
],
|
|
970
|
+
],
|
|
971
|
+
]),
|
|
972
|
+
),
|
|
973
|
+
]
|
|
974
|
+
// ±usual against the task's usual time now, as a run on this branch reads it.
|
|
975
|
+
const ranIds = (detail?.tasks ?? [])
|
|
976
|
+
.filter((t) => t.status === 'success' || t.status === 'failed')
|
|
977
|
+
.map((t) => `${t.project}#${t.task}`)
|
|
978
|
+
const usual = new LocalHistoryProvider(db, undefined, currentBranch(root)).p50sNow(ranIds)
|
|
979
|
+
const taskRows = formatTaskRows(detail?.tasks ?? [], moved, usual)
|
|
980
|
+
if (taskRows.length > 0)
|
|
981
|
+
blocks.push(ui.section('Tasks', String(detail?.tasks.length ?? 0), ...taskRows))
|
|
982
|
+
const stepRows = formatStepRows(keptSteps(db, inv.runId))
|
|
983
|
+
if (stepRows !== undefined) blocks.push(ui.section('Steps', stepRows))
|
|
970
984
|
const warnings = keptWarnings(db, inv.runId)
|
|
971
|
-
if (warnings.length > 0)
|
|
972
|
-
|
|
973
|
-
}
|
|
985
|
+
if (warnings.length > 0)
|
|
986
|
+
blocks.push(ui.section('Warnings', String(warnings.length), ui.text(...warnings)))
|
|
974
987
|
// The next command after reading a failed run, with the arguments the
|
|
975
988
|
// run forwarded to the tasks that got them (a failure under `-- --shard
|
|
976
989
|
// 2` is that shard's); a dependency never got them, so its line has none.
|
|
@@ -983,14 +996,11 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
983
996
|
const ids = failed
|
|
984
997
|
.filter((t) => t.forwarded === withArgs)
|
|
985
998
|
.map((t) => shellQuote(`${t.project}#${t.task}`))
|
|
986
|
-
if (ids.length > 0)
|
|
987
|
-
rerun.push(
|
|
988
|
-
` ${dim('re-run what failed:')} ${paint('', `vx run ${ids.join(' ')}${withArgs ? args : ''}`, c, { bold: true })}`,
|
|
989
|
-
)
|
|
999
|
+
if (ids.length > 0) rerun.push(`vx run ${ids.join(' ')}${withArgs ? args : ''}`)
|
|
990
1000
|
}
|
|
991
|
-
|
|
1001
|
+
blocks.push(ui.fields([['next', ...rerun.map((r) => ui.cmd(r))]]))
|
|
992
1002
|
}
|
|
993
|
-
|
|
1003
|
+
ui.print('out', ...blocks)
|
|
994
1004
|
return 0
|
|
995
1005
|
} finally {
|
|
996
1006
|
cache.close()
|