@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/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,18 +423,25 @@ 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
|
|
|
436
|
+
/** `text` as JSON, or undefined where it is not. */
|
|
437
|
+
function parsed(text: string): unknown {
|
|
438
|
+
try {
|
|
439
|
+
return JSON.parse(text)
|
|
440
|
+
} catch {
|
|
441
|
+
return undefined
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
438
445
|
/** How long a run has to answer before `--live` gives up on it. */
|
|
439
446
|
const LIVE_ANSWER_MS = 5_000
|
|
440
447
|
|
|
@@ -459,17 +466,38 @@ async function printLive(
|
|
|
459
466
|
}
|
|
460
467
|
let buffered = ''
|
|
461
468
|
let snapshot: LiveSnapshot | undefined
|
|
469
|
+
/** The run answered a snapshot this vx cannot read: another vx version's. */
|
|
470
|
+
let unreadable = false
|
|
462
471
|
let settle!: (answered: boolean) => void
|
|
463
472
|
const done = new Promise<boolean>((resolve) => (settle = resolve))
|
|
464
473
|
const line = (text: string): void => {
|
|
474
|
+
if (unreadable) return
|
|
465
475
|
if (snapshot === undefined) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
476
|
+
const s = parsed(text) as Partial<LiveSnapshot> | undefined
|
|
477
|
+
const l = s?.live
|
|
478
|
+
if (
|
|
479
|
+
typeof l?.runId !== 'string' ||
|
|
480
|
+
typeof l.command !== 'string' ||
|
|
481
|
+
typeof l.startedAt !== 'number' ||
|
|
482
|
+
typeof l.elapsedMs !== 'number' ||
|
|
483
|
+
!Array.isArray(s!.tasks) ||
|
|
484
|
+
!Array.isArray(s!.warnings)
|
|
485
|
+
) {
|
|
486
|
+
unreadable = true
|
|
487
|
+
return settle(false)
|
|
488
|
+
}
|
|
489
|
+
snapshot = s as LiveSnapshot
|
|
490
|
+
if (format === 'json') ui.data('out', `${text}\n`)
|
|
491
|
+
else ui.print('out', ...formatLive(snapshot))
|
|
469
492
|
return
|
|
470
493
|
}
|
|
471
|
-
if (format === 'json')
|
|
472
|
-
else
|
|
494
|
+
if (format === 'json') ui.data('out', `${text}\n`)
|
|
495
|
+
else {
|
|
496
|
+
const e = parsed(text)
|
|
497
|
+
const event =
|
|
498
|
+
typeof e === 'object' && e !== null ? formatLiveEvent(e as WireEvent) : undefined
|
|
499
|
+
if (event !== undefined) ui.print('out', event)
|
|
500
|
+
}
|
|
473
501
|
}
|
|
474
502
|
try {
|
|
475
503
|
await Bun.connect({
|
|
@@ -502,53 +530,81 @@ async function printLive(
|
|
|
502
530
|
: setTimeout(() => settle(snapshot !== undefined), LIVE_ANSWER_MS)
|
|
503
531
|
const answered = await done
|
|
504
532
|
clearTimeout(timer)
|
|
533
|
+
if (unreadable)
|
|
534
|
+
throw refusal({
|
|
535
|
+
headline: 'the run in progress answered in a form this vx does not read',
|
|
536
|
+
fix: 'ask with the vx that started the run',
|
|
537
|
+
})
|
|
505
538
|
if (!answered) none()
|
|
506
539
|
return 0
|
|
507
540
|
}
|
|
508
541
|
|
|
509
|
-
function formatLive(s: LiveSnapshot):
|
|
542
|
+
function formatLive(s: LiveSnapshot): ui.Block[] {
|
|
510
543
|
const count = (status: string): number => s.tasks.filter((t) => t.status === status).length
|
|
511
544
|
const running = count('running')
|
|
512
545
|
const waiting = count('waiting')
|
|
513
546
|
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
547
|
const now = s.live.startedAt + s.live.elapsedMs
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
548
|
+
const blocks = [
|
|
549
|
+
ui.section(
|
|
550
|
+
'Run',
|
|
551
|
+
s.live.runId,
|
|
552
|
+
ui.fields([
|
|
553
|
+
['status', ui.tone('info', `running ${fmtMs(s.live.elapsedMs)}`)],
|
|
554
|
+
['command', ui.cmd(s.live.command)],
|
|
555
|
+
[
|
|
556
|
+
'tasks',
|
|
557
|
+
`${running} running · ${s.tasks.length - running - waiting} ended${failed > 0 ? ` · ${failed} failed` : ''} · ${waiting} waiting`,
|
|
558
|
+
],
|
|
559
|
+
]),
|
|
560
|
+
),
|
|
561
|
+
ui.section(
|
|
562
|
+
'Tasks',
|
|
563
|
+
String(s.tasks.length),
|
|
564
|
+
ui.table(
|
|
565
|
+
['status', 'task', { title: 'time', align: 'right' }],
|
|
566
|
+
s.tasks.map((t) => {
|
|
567
|
+
const ms =
|
|
568
|
+
t.durationMs !== null ? t.durationMs : t.startedAt !== null ? now - t.startedAt : null
|
|
569
|
+
return [
|
|
570
|
+
ui.tone(
|
|
571
|
+
t.status === 'failed' ? 'error' : t.status === 'running' ? 'info' : 'muted',
|
|
572
|
+
t.status,
|
|
573
|
+
),
|
|
574
|
+
ui.id(t.taskId),
|
|
575
|
+
ms === null ? '' : fmtMs(ms),
|
|
576
|
+
]
|
|
577
|
+
}),
|
|
578
|
+
),
|
|
579
|
+
),
|
|
580
|
+
]
|
|
581
|
+
if (s.warnings.length > 0)
|
|
582
|
+
blocks.push(ui.section('Warnings', String(s.warnings.length), ui.text(...s.warnings)))
|
|
583
|
+
return blocks
|
|
534
584
|
}
|
|
535
585
|
|
|
536
|
-
/** One followed event
|
|
537
|
-
function formatLiveEvent(e: WireEvent):
|
|
586
|
+
/** One followed event: a task's output under its id, an end, a warning. */
|
|
587
|
+
function formatLiveEvent(e: WireEvent): ui.Block | undefined {
|
|
538
588
|
switch (e.kind) {
|
|
539
589
|
case 'task:stdout':
|
|
540
590
|
case 'task:stderr':
|
|
541
|
-
return
|
|
542
|
-
.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
591
|
+
return ui.text(
|
|
592
|
+
...e.chunk
|
|
593
|
+
.replace(/\n$/, '')
|
|
594
|
+
.split('\n')
|
|
595
|
+
.map((l): ui.Inline => [ui.id(e.taskId), ui.dim(' │ '), ui.tone('plain', l)]),
|
|
596
|
+
)
|
|
546
597
|
case 'task:complete':
|
|
547
|
-
return
|
|
598
|
+
return ui.text([
|
|
599
|
+
ui.tone(e.outcome.status === 'failed' ? 'error' : 'ok', outcomeWord(e.outcome)),
|
|
600
|
+
' ',
|
|
601
|
+
ui.id(e.outcome.taskId),
|
|
602
|
+
ui.dim(` ${fmtMs(e.outcome.durationMs)}`),
|
|
603
|
+
])
|
|
548
604
|
case 'run:status':
|
|
549
|
-
return 'warn' in e ?
|
|
605
|
+
return 'warn' in e ? ui.status('warn', 'warn', Bun.stripANSI(e.line)) : undefined
|
|
550
606
|
default:
|
|
551
|
-
return
|
|
607
|
+
return undefined
|
|
552
608
|
}
|
|
553
609
|
}
|
|
554
610
|
|
|
@@ -556,24 +612,6 @@ const fmtWhen = (ms: number): string => new Date(ms).toISOString()
|
|
|
556
612
|
|
|
557
613
|
const fmtMs = (ms: number): string => formatElapsed(ms, true)
|
|
558
614
|
|
|
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
615
|
/**
|
|
578
616
|
* Why a row failed or was skipped, as the run's own footer said it (the
|
|
579
617
|
* v27 columns): a timeout or a never-ready server instead of the signal
|
|
@@ -632,16 +670,17 @@ const HITS_SHOWN = 16
|
|
|
632
670
|
* The steps the run did not wait on (`vx:prefetch`), which its own output
|
|
633
671
|
* never showed (2026-10-16): here for debugging, marked `internal`.
|
|
634
672
|
*/
|
|
635
|
-
function formatStepRows(steps: readonly RunStep[]
|
|
636
|
-
if (steps.length === 0) return
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
(s)
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
673
|
+
function formatStepRows(steps: readonly RunStep[]): ui.Block | undefined {
|
|
674
|
+
if (steps.length === 0) return undefined
|
|
675
|
+
return ui.table(
|
|
676
|
+
['', '', { title: '', align: 'right' }, ''],
|
|
677
|
+
steps.map((s) => [
|
|
678
|
+
ui.tone(s.ok ? 'ok' : 'error', s.ok ? 'success' : 'failed'),
|
|
679
|
+
ui.strong(s.id),
|
|
680
|
+
fmtMs(s.durationMs),
|
|
681
|
+
'internal',
|
|
682
|
+
]),
|
|
683
|
+
)
|
|
645
684
|
}
|
|
646
685
|
|
|
647
686
|
/**
|
|
@@ -655,10 +694,11 @@ function formatStepRows(steps: readonly RunStep[], c: ColorSupport): string[] {
|
|
|
655
694
|
|
|
656
695
|
export function formatTaskRows(
|
|
657
696
|
tasks: readonly RunSummaryRow[],
|
|
658
|
-
c: ColorSupport = { enabled: false },
|
|
659
697
|
/** Task id → the input that moved under it, from the run's kept failures. */
|
|
660
698
|
inputMoved: ReadonlyMap<string, string> = new Map(),
|
|
661
|
-
|
|
699
|
+
/** Task id → its usual time now, from history; a task absent was never timed. */
|
|
700
|
+
usual?: ReadonlyMap<string, number>,
|
|
701
|
+
): ui.Block[] {
|
|
662
702
|
if (tasks.length === 0) return []
|
|
663
703
|
const failed = tasks.filter((t) => t.status === 'failed')
|
|
664
704
|
const hits = tasks.filter((t) => t.status !== 'failed' && t.cacheHit === true)
|
|
@@ -668,50 +708,28 @@ export function formatTaskRows(
|
|
|
668
708
|
? [...hits].sort((a, b) => b.durationMs - a.durationMs).slice(0, HITS_SHOWN)
|
|
669
709
|
: hits
|
|
670
710
|
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
711
|
const skippedHere = new Set(
|
|
675
712
|
tasks.filter((t) => t.status === 'skipped').map((t) => `${t.project}#${t.task}`),
|
|
676
713
|
)
|
|
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 },
|
|
714
|
+
// The run's own grid (`taskList`): a replay reads as the run did. What
|
|
715
|
+
// the run said below the row (an exit, why it skipped) rides it dim.
|
|
716
|
+
const rows = ordered.map((t) => {
|
|
717
|
+
const note = (
|
|
718
|
+
(t.status === 'failed' ? `exit ${t.exitCode}` : '') +
|
|
719
|
+
reasonParts(t, skippedHere, inputMoved.get(`${t.project}#${t.task}`))
|
|
720
|
+
).trim()
|
|
721
|
+
const id = `${t.project}#${t.task}`
|
|
722
|
+
const usualMs = usual === undefined ? undefined : (usual.get(id) ?? null)
|
|
723
|
+
return (c: ui.ColorSupport) => formatRecordedRow(t, c, note, usualMs)
|
|
724
|
+
})
|
|
725
|
+
const blocks = [taskList(rows)]
|
|
726
|
+
if (folded)
|
|
727
|
+
blocks.push(
|
|
728
|
+
ui.text(
|
|
729
|
+
`… +${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
730
|
),
|
|
712
731
|
)
|
|
713
|
-
|
|
714
|
-
return lines
|
|
732
|
+
return blocks
|
|
715
733
|
}
|
|
716
734
|
|
|
717
735
|
/**
|
|
@@ -755,22 +773,20 @@ export function formatRunList(
|
|
|
755
773
|
>
|
|
756
774
|
id: string
|
|
757
775
|
}[],
|
|
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}`,
|
|
776
|
+
): ui.Block {
|
|
777
|
+
return ui.table(
|
|
778
|
+
['', 'when', 'run', 'tasks', { title: 'time', align: 'right' }, 'command'],
|
|
779
|
+
runs.map(({ inv, id }) => [
|
|
780
|
+
inv.exitOk ? ui.tone('ok', 'ok') : ui.tone('error', 'FAILED'),
|
|
781
|
+
fmtWhen(inv.startedAt),
|
|
782
|
+
ui.strong(id),
|
|
783
|
+
[
|
|
784
|
+
ui.dim(`${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}`),
|
|
785
|
+
...(inv.failedCount > 0 ? [ui.tone('error', ` · ${inv.failedCount} failed`)] : []),
|
|
786
|
+
],
|
|
787
|
+
fmtMs(inv.totalDurationMs),
|
|
788
|
+
ui.cmd(inv.command),
|
|
789
|
+
]),
|
|
774
790
|
)
|
|
775
791
|
}
|
|
776
792
|
|
|
@@ -826,14 +842,26 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
826
842
|
const log = taskLog(db, await bareTaskId(db, await rootSpelled(root, parsed.log)), runId)
|
|
827
843
|
if (log === null) {
|
|
828
844
|
// 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.
|
|
845
|
+
// never ran was told how to spell a task id. A typo of one that
|
|
846
|
+
// ran is named, as `vx why` names it.
|
|
847
|
+
const ran = parsed.log.includes('#')
|
|
848
|
+
? nearest(
|
|
849
|
+
parsed.log,
|
|
850
|
+
(
|
|
851
|
+
db.query('SELECT DISTINCT project, task FROM runs').all() as {
|
|
852
|
+
project: string
|
|
853
|
+
task: string
|
|
854
|
+
}[]
|
|
855
|
+
).map((r) => `${r.project}#${r.task}`),
|
|
856
|
+
)
|
|
857
|
+
: undefined
|
|
830
858
|
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)'}`,
|
|
859
|
+
`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
860
|
'VX_E_NO_HISTORY',
|
|
833
861
|
)
|
|
834
862
|
}
|
|
835
863
|
if (parsed.format === 'json') {
|
|
836
|
-
|
|
864
|
+
ui.data('out', `${JSON.stringify(log)}\n`)
|
|
837
865
|
return 0
|
|
838
866
|
}
|
|
839
867
|
if (log.source === null) {
|
|
@@ -859,7 +887,8 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
859
887
|
} else if (log.output === '') {
|
|
860
888
|
sayNotice('info', 'vx last', `${log.taskId} ended ${log.status} and printed nothing`)
|
|
861
889
|
}
|
|
862
|
-
|
|
890
|
+
// The task's own bytes, as it wrote them.
|
|
891
|
+
ui.data('out', log.output)
|
|
863
892
|
return 0
|
|
864
893
|
}
|
|
865
894
|
|
|
@@ -869,7 +898,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
869
898
|
? failedInvocations(db, parsed.list)
|
|
870
899
|
: listInvocations(db, { limit: parsed.list })
|
|
871
900
|
if (parsed.format === 'json') {
|
|
872
|
-
|
|
901
|
+
ui.data('out', `${JSON.stringify(invocations)}\n`)
|
|
873
902
|
return 0
|
|
874
903
|
}
|
|
875
904
|
if (invocations.length === 0) {
|
|
@@ -877,11 +906,14 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
877
906
|
ui.print('out', ui.status('info', 'vx last', none))
|
|
878
907
|
return 0
|
|
879
908
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
909
|
+
ui.print(
|
|
910
|
+
'out',
|
|
911
|
+
ui.section(
|
|
912
|
+
'Runs',
|
|
913
|
+
String(invocations.length),
|
|
914
|
+
formatRunList(invocations.map((inv) => ({ inv, id: shortRunId(db, inv.runId) }))),
|
|
915
|
+
),
|
|
883
916
|
)
|
|
884
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
885
917
|
return 0
|
|
886
918
|
}
|
|
887
919
|
|
|
@@ -930,7 +962,8 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
930
962
|
})
|
|
931
963
|
const steps = keptSteps(db, inv.runId)
|
|
932
964
|
const warnings = keptWarnings(db, inv.runId)
|
|
933
|
-
|
|
965
|
+
ui.data(
|
|
966
|
+
'out',
|
|
934
967
|
`${JSON.stringify({
|
|
935
968
|
invocation: inv,
|
|
936
969
|
tasks,
|
|
@@ -941,36 +974,48 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
941
974
|
return 0
|
|
942
975
|
}
|
|
943
976
|
|
|
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
977
|
const moved = new Map<string, string>()
|
|
964
978
|
if (inv.failedCount > 0) {
|
|
965
979
|
for (const f of runFailures(db, inv.runId)?.tasks ?? [])
|
|
966
980
|
if (f.inputMoved !== undefined) moved.set(f.taskId, f.inputMoved)
|
|
967
981
|
}
|
|
968
|
-
|
|
969
|
-
|
|
982
|
+
const blocks: ui.Block[] = [
|
|
983
|
+
ui.section(
|
|
984
|
+
'Run',
|
|
985
|
+
inv.runId,
|
|
986
|
+
ui.fields([
|
|
987
|
+
['status', inv.exitOk ? ui.tone('ok', 'ok') : ui.tone('error', 'FAILED', true)],
|
|
988
|
+
['command', ui.cmd(inv.command)],
|
|
989
|
+
[
|
|
990
|
+
'when',
|
|
991
|
+
`${fmtWhen(inv.startedAt)} · ${fmtMs(inv.totalDurationMs)}` +
|
|
992
|
+
`${inv.branch !== null ? ` · ${inv.branch}` : ''}` +
|
|
993
|
+
`${inv.commitSha !== null ? ` @ ${inv.commitSha.slice(0, 8)}${inv.dirty === true ? '+dirty' : ''}` : ''}` +
|
|
994
|
+
`${inv.ci ? ` · CI${inv.ciProvider !== null ? ` (${inv.ciProvider})` : ''}` : ''}`,
|
|
995
|
+
],
|
|
996
|
+
[
|
|
997
|
+
'tasks',
|
|
998
|
+
[
|
|
999
|
+
ui.dim(`${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}`),
|
|
1000
|
+
...(inv.failedCount > 0 ? [ui.tone('error', ` · ${inv.failedCount} failed`)] : []),
|
|
1001
|
+
],
|
|
1002
|
+
],
|
|
1003
|
+
]),
|
|
1004
|
+
),
|
|
1005
|
+
]
|
|
1006
|
+
// ±usual against the task's usual time now, as a run on this branch reads it.
|
|
1007
|
+
const ranIds = (detail?.tasks ?? [])
|
|
1008
|
+
.filter((t) => t.status === 'success' || t.status === 'failed')
|
|
1009
|
+
.map((t) => `${t.project}#${t.task}`)
|
|
1010
|
+
const usual = new LocalHistoryProvider(db, undefined, currentBranch(root)).p50sNow(ranIds)
|
|
1011
|
+
const taskRows = formatTaskRows(detail?.tasks ?? [], moved, usual)
|
|
1012
|
+
if (taskRows.length > 0)
|
|
1013
|
+
blocks.push(ui.section('Tasks', String(detail?.tasks.length ?? 0), ...taskRows))
|
|
1014
|
+
const stepRows = formatStepRows(keptSteps(db, inv.runId))
|
|
1015
|
+
if (stepRows !== undefined) blocks.push(ui.section('Steps', stepRows))
|
|
970
1016
|
const warnings = keptWarnings(db, inv.runId)
|
|
971
|
-
if (warnings.length > 0)
|
|
972
|
-
|
|
973
|
-
}
|
|
1017
|
+
if (warnings.length > 0)
|
|
1018
|
+
blocks.push(ui.section('Warnings', String(warnings.length), ui.text(...warnings)))
|
|
974
1019
|
// The next command after reading a failed run, with the arguments the
|
|
975
1020
|
// run forwarded to the tasks that got them (a failure under `-- --shard
|
|
976
1021
|
// 2` is that shard's); a dependency never got them, so its line has none.
|
|
@@ -983,14 +1028,11 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
983
1028
|
const ids = failed
|
|
984
1029
|
.filter((t) => t.forwarded === withArgs)
|
|
985
1030
|
.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
|
-
)
|
|
1031
|
+
if (ids.length > 0) rerun.push(`vx run ${ids.join(' ')}${withArgs ? args : ''}`)
|
|
990
1032
|
}
|
|
991
|
-
|
|
1033
|
+
blocks.push(ui.fields([['next', ...rerun.map((r) => ui.cmd(r))]]))
|
|
992
1034
|
}
|
|
993
|
-
|
|
1035
|
+
ui.print('out', ...blocks)
|
|
994
1036
|
return 0
|
|
995
1037
|
} finally {
|
|
996
1038
|
cache.close()
|