@vzn/vx 0.0.649 → 0.0.651
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -20
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/src/bin.ts +9 -31
- package/src/cache/cache.ts +225 -51
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/fold-memo.ts +8 -1
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/index.ts +12 -0
- package/src/cache/inputs.ts +3 -0
- package/src/cache/layer.ts +11 -0
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +120 -0
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-hits.ts +9 -1
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +11 -1
- package/src/cache/run-notes.ts +123 -0
- package/src/cache/schema.ts +54 -1
- package/src/cli/cache.ts +26 -16
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +12 -23
- package/src/cli/index.ts +4 -9
- package/src/cli/info.ts +15 -7
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +224 -223
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +10 -3
- package/src/cli/plugin-templates.ts +2 -2
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +38 -42
- package/src/cli/task-verb.ts +22 -0
- package/src/cli/upgrade.ts +3 -1
- package/src/cli/watch-fs.ts +6 -1
- package/src/cli/watch-judge.ts +2 -1
- package/src/cli/watch.ts +83 -44
- package/src/cli/why.ts +126 -92
- package/src/cli/workspace-config.ts +2 -1
- package/src/exec/executor.ts +2 -1
- package/src/exec/kill-tree.ts +58 -5
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/pipes.ts +152 -0
- package/src/exec/runner.ts +199 -182
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-runtime.ts +11 -2
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +12 -2
- package/src/graph/task-graph.ts +27 -1
- package/src/handoff.ts +10 -7
- package/src/history/history.ts +39 -2
- package/src/history/index.ts +3 -0
- package/src/history/metrics.ts +9 -1
- package/src/index.ts +12 -3
- package/src/orchestrator/doctor.ts +3 -3
- package/src/orchestrator/execute-task.ts +16 -2
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +3 -0
- package/src/orchestrator/index.ts +3 -1
- package/src/orchestrator/live-usage.ts +98 -0
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +176 -102
- package/src/orchestrator/miss-save.ts +31 -3
- package/src/orchestrator/persistent-task.ts +3 -1
- package/src/orchestrator/plan.ts +5 -1
- package/src/orchestrator/plugin-host.ts +61 -0
- package/src/orchestrator/plugin.ts +49 -4
- package/src/orchestrator/prepare.ts +18 -2
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +35 -114
- package/src/orchestrator/run-records.ts +10 -0
- package/src/orchestrator/run.ts +89 -26
- package/src/orchestrator/sandbox-request.ts +2 -2
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-hash.ts +17 -3
- package/src/report/forecast.ts +12 -33
- package/src/report/framed-output.ts +388 -161
- package/src/report/index.ts +8 -2
- package/src/report/logger.ts +100 -36
- package/src/report/status-line.ts +81 -365
- package/src/report/summary.ts +104 -131
- package/src/ui/ansi.ts +48 -0
- package/src/ui/components.ts +506 -0
- package/src/ui/index.ts +79 -0
- package/src/ui/kit.ts +37 -0
- package/src/ui/live-region.ts +363 -0
- package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
- package/src/ui/terminal.ts +51 -0
- package/src/ui/theme.ts +224 -0
- package/src/ui/version-line.ts +28 -0
- package/src/util/config-frame.ts +65 -10
- package/src/util/edit-distance.ts +6 -1
- package/src/util/hidden-prompt.ts +8 -2
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +20 -2
- package/src/util/secret-mask.ts +39 -3
- package/src/util/secrets.ts +3 -0
- package/src/util/settle.ts +31 -0
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/config-schema.ts +4 -2
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +28 -0
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +4 -3
- package/src/workspace/workspace.ts +3 -0
- package/src/orchestrator/run-steps.ts +0 -93
- package/src/util/colors.ts +0 -130
- package/src/util/report.ts +0 -44
- package/src/util/ui.ts +0 -258
- /package/src/{report/path-links.ts → ui/links.ts} +0 -0
- /package/src/{report → ui}/terminal-signals.ts +0 -0
package/src/cli/last.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Read-only — no config evaluation, no re-hash, no cache probe.
|
|
6
6
|
|
|
7
7
|
import type { Database } from 'bun:sqlite'
|
|
8
|
-
import { Cache } from '../cache/index.js'
|
|
8
|
+
import { indexDb, Cache, keptSteps, keptWarnings, type RunStep } from '../cache/index.js'
|
|
9
9
|
import { formatBytes } from './format.js'
|
|
10
10
|
import { flagHint, formatValue, seeHelp } from './help.js'
|
|
11
11
|
import {
|
|
@@ -24,28 +24,21 @@ import {
|
|
|
24
24
|
exitSignal,
|
|
25
25
|
resolveRunId,
|
|
26
26
|
runFailures,
|
|
27
|
-
type RunStep,
|
|
28
|
-
runSteps,
|
|
29
|
-
runWarnings,
|
|
30
27
|
shellQuote,
|
|
31
28
|
shortRunId,
|
|
32
29
|
taskLog,
|
|
33
30
|
} from '../orchestrator/index.js'
|
|
34
|
-
import { outcomeWord, type WireEvent } from '../report/index.js'
|
|
31
|
+
import { formatRecordedRow, outcomeWord, taskList, type WireEvent } from '../report/index.js'
|
|
35
32
|
import {
|
|
36
|
-
detectColors,
|
|
37
|
-
ERROR,
|
|
38
33
|
formatElapsed,
|
|
39
|
-
|
|
34
|
+
nearest,
|
|
40
35
|
parseDecimalInt,
|
|
41
36
|
refusal,
|
|
42
37
|
splitTaskId,
|
|
43
|
-
SUCCESS,
|
|
44
38
|
UserError,
|
|
45
|
-
sayNotice,
|
|
46
|
-
ui,
|
|
47
|
-
type ColorSupport,
|
|
48
39
|
} from '../util/index.js'
|
|
40
|
+
import * as ui from '../ui/index.js'
|
|
41
|
+
import { sayNotice } from '../ui/index.js'
|
|
49
42
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
50
43
|
import { findCwdProject } from './select.js'
|
|
51
44
|
import { cliCacheDir, parseCacheDirFlag, rootSpelled } from './workspace-config.js'
|
|
@@ -318,7 +311,7 @@ async function printStats(
|
|
|
318
311
|
)
|
|
319
312
|
}
|
|
320
313
|
if (format === 'json') {
|
|
321
|
-
|
|
314
|
+
ui.data('out', `${JSON.stringify({ window: provider.recent, branch, stats })}\n`)
|
|
322
315
|
return 0
|
|
323
316
|
}
|
|
324
317
|
if (stats.length === 0) {
|
|
@@ -327,39 +320,44 @@ async function printStats(
|
|
|
327
320
|
}
|
|
328
321
|
const pct = (r: number): string => `${Math.round(r * 100)}%`
|
|
329
322
|
const ms = (v: number | null): string => (v === null ? '-' : fmtMs(v))
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
+
),
|
|
355
353
|
)
|
|
356
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
357
354
|
return 0
|
|
358
355
|
}
|
|
359
356
|
|
|
360
357
|
/**
|
|
361
|
-
* `--sql`: one SELECT over the history (`runs`, `invocations`,
|
|
362
|
-
* store's `entries`, `entry_inputs`,
|
|
358
|
+
* `--sql`: one SELECT over the history (`runs`, `invocations`, `steps`,
|
|
359
|
+
* `warnings`, `failures`, and the store's `entries`, `entry_inputs`,
|
|
360
|
+
* `task_durations`, `task_outcomes`),
|
|
363
361
|
* so an agent asks what no flag answers: a task's memory trend, the
|
|
364
362
|
* flakiest project, which input moved a key. `query_only` stops a write
|
|
365
363
|
* the statement hides (`WITH … DELETE`); it does not stop ATTACH, which
|
|
@@ -414,7 +412,7 @@ function printQuery(db: Database, sql: string, format: 'pretty' | 'json'): numbe
|
|
|
414
412
|
)
|
|
415
413
|
}
|
|
416
414
|
const rows = values.map((v) => Object.fromEntries(columns.map((c, i) => [c, v[i]])))
|
|
417
|
-
|
|
415
|
+
ui.data('out', `${JSON.stringify({ rows })}\n`)
|
|
418
416
|
return 0
|
|
419
417
|
}
|
|
420
418
|
if (values.length === 0) return 0
|
|
@@ -425,15 +423,13 @@ function printQuery(db: Database, sql: string, format: 'pretty' | 'json'): numbe
|
|
|
425
423
|
: v instanceof Uint8Array
|
|
426
424
|
? `<${v.length} bytes>`
|
|
427
425
|
: String(v)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
.map((v
|
|
433
|
-
|
|
434
|
-
.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
|
+
),
|
|
435
432
|
)
|
|
436
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
437
433
|
return 0
|
|
438
434
|
}
|
|
439
435
|
|
|
@@ -466,12 +462,15 @@ async function printLive(
|
|
|
466
462
|
const line = (text: string): void => {
|
|
467
463
|
if (snapshot === undefined) {
|
|
468
464
|
snapshot = JSON.parse(text) as LiveSnapshot
|
|
469
|
-
if (format === 'json')
|
|
470
|
-
else
|
|
465
|
+
if (format === 'json') ui.data('out', `${text}\n`)
|
|
466
|
+
else ui.print('out', ...formatLive(snapshot))
|
|
471
467
|
return
|
|
472
468
|
}
|
|
473
|
-
if (format === 'json')
|
|
474
|
-
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
|
+
}
|
|
475
474
|
}
|
|
476
475
|
try {
|
|
477
476
|
await Bun.connect({
|
|
@@ -508,49 +507,72 @@ async function printLive(
|
|
|
508
507
|
return 0
|
|
509
508
|
}
|
|
510
509
|
|
|
511
|
-
function formatLive(s: LiveSnapshot):
|
|
510
|
+
function formatLive(s: LiveSnapshot): ui.Block[] {
|
|
512
511
|
const count = (status: string): number => s.tasks.filter((t) => t.status === status).length
|
|
513
512
|
const running = count('running')
|
|
514
513
|
const waiting = count('waiting')
|
|
515
514
|
const failed = count('failed')
|
|
516
|
-
const lines = [
|
|
517
|
-
`run ${s.live.runId} — running ${fmtMs(s.live.elapsedMs)}`,
|
|
518
|
-
` $ ${s.live.command}`,
|
|
519
|
-
` ${running} running · ${s.tasks.length - running - waiting} ended${failed > 0 ? ` · ${failed} failed` : ''} · ${waiting} waiting`,
|
|
520
|
-
'',
|
|
521
|
-
]
|
|
522
|
-
const width = Math.max(...s.tasks.map((t) => t.status.length), 7)
|
|
523
|
-
const idWidth = Math.max(...s.tasks.map((t) => t.taskId.length), 0)
|
|
524
515
|
const now = s.live.startedAt + s.live.elapsedMs
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
|
536
552
|
}
|
|
537
553
|
|
|
538
|
-
/** One followed event
|
|
539
|
-
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 {
|
|
540
556
|
switch (e.kind) {
|
|
541
557
|
case 'task:stdout':
|
|
542
558
|
case 'task:stderr':
|
|
543
|
-
return
|
|
544
|
-
.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
)
|
|
548
565
|
case 'task:complete':
|
|
549
|
-
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
|
+
])
|
|
550
572
|
case 'run:status':
|
|
551
|
-
return 'warn' in e ?
|
|
573
|
+
return 'warn' in e ? ui.status('warn', 'warn', Bun.stripANSI(e.line)) : undefined
|
|
552
574
|
default:
|
|
553
|
-
return
|
|
575
|
+
return undefined
|
|
554
576
|
}
|
|
555
577
|
}
|
|
556
578
|
|
|
@@ -558,24 +580,6 @@ const fmtWhen = (ms: number): string => new Date(ms).toISOString()
|
|
|
558
580
|
|
|
559
581
|
const fmtMs = (ms: number): string => formatElapsed(ms, true)
|
|
560
582
|
|
|
561
|
-
/**
|
|
562
|
-
* What an executed task used — its peak RSS and its CPU parallelism (CPU
|
|
563
|
-
* time over wall time) — as the runner recorded them. This is the number
|
|
564
|
-
* `@vzn/vx-history` reserves from, so the replay shows it where
|
|
565
|
-
* a developer can read it; a hit spent nothing and shows nothing.
|
|
566
|
-
*/
|
|
567
|
-
function fmtUsage(t: {
|
|
568
|
-
cpuMs: number | null
|
|
569
|
-
peakRssBytes: number | null
|
|
570
|
-
durationMs: number
|
|
571
|
-
}): string {
|
|
572
|
-
const parts: string[] = []
|
|
573
|
-
if (t.peakRssBytes !== null) parts.push(formatBytes(t.peakRssBytes))
|
|
574
|
-
if (t.cpuMs !== null && t.durationMs > 0)
|
|
575
|
-
parts.push(`${(t.cpuMs / t.durationMs).toFixed(1)}× cpu`)
|
|
576
|
-
return parts.length > 0 ? ` ${parts.join(' · ')}` : ''
|
|
577
|
-
}
|
|
578
|
-
|
|
579
583
|
/**
|
|
580
584
|
* Why a row failed or was skipped, as the run's own footer said it (the
|
|
581
585
|
* v27 columns): a timeout or a never-ready server instead of the signal
|
|
@@ -634,16 +638,17 @@ const HITS_SHOWN = 16
|
|
|
634
638
|
* The steps the run did not wait on (`vx:prefetch`), which its own output
|
|
635
639
|
* never showed (2026-10-16): here for debugging, marked `internal`.
|
|
636
640
|
*/
|
|
637
|
-
function formatStepRows(steps: readonly RunStep[]
|
|
638
|
-
if (steps.length === 0) return
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
(s)
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
+
)
|
|
647
652
|
}
|
|
648
653
|
|
|
649
654
|
/**
|
|
@@ -657,10 +662,11 @@ function formatStepRows(steps: readonly RunStep[], c: ColorSupport): string[] {
|
|
|
657
662
|
|
|
658
663
|
export function formatTaskRows(
|
|
659
664
|
tasks: readonly RunSummaryRow[],
|
|
660
|
-
c: ColorSupport = { enabled: false },
|
|
661
665
|
/** Task id → the input that moved under it, from the run's kept failures. */
|
|
662
666
|
inputMoved: ReadonlyMap<string, string> = new Map(),
|
|
663
|
-
|
|
667
|
+
/** Task id → its usual time now, from history; a task absent was never timed. */
|
|
668
|
+
usual?: ReadonlyMap<string, number>,
|
|
669
|
+
): ui.Block[] {
|
|
664
670
|
if (tasks.length === 0) return []
|
|
665
671
|
const failed = tasks.filter((t) => t.status === 'failed')
|
|
666
672
|
const hits = tasks.filter((t) => t.status !== 'failed' && t.cacheHit === true)
|
|
@@ -670,50 +676,28 @@ export function formatTaskRows(
|
|
|
670
676
|
? [...hits].sort((a, b) => b.durationMs - a.durationMs).slice(0, HITS_SHOWN)
|
|
671
677
|
: hits
|
|
672
678
|
const ordered = [...failed, ...rest, ...shownHits]
|
|
673
|
-
const at = (t: (typeof ordered)[number]): string =>
|
|
674
|
-
t.placement !== null ? `@${t.placement}` : ''
|
|
675
|
-
const idW = Math.max(...ordered.map((t) => `${t.project}#${t.task}${at(t)}`.length), 4)
|
|
676
679
|
const skippedHere = new Set(
|
|
677
680
|
tasks.filter((t) => t.status === 'skipped').map((t) => `${t.project}#${t.task}`),
|
|
678
681
|
)
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
lines.push(
|
|
696
|
-
` ${paint(hue, status.padEnd(17), c, { dim: hue === '' })} ${ui.inline(ui.id(id), c)}${' '.repeat(idW - id.length)} ${fmtMs(t.durationMs).padStart(8)}` +
|
|
697
|
-
paint(
|
|
698
|
-
'',
|
|
699
|
-
`${t.hash !== '' ? ` ${t.hash}` : ''}${t.cached === false ? ' no-cache' : ''}${fmtUsage(t)}` +
|
|
700
|
-
reasonParts(t, skippedHere, inputMoved.get(`${t.project}#${t.task}`)),
|
|
701
|
-
c,
|
|
702
|
-
{ dim: true },
|
|
703
|
-
),
|
|
704
|
-
)
|
|
705
|
-
}
|
|
706
|
-
if (folded) {
|
|
707
|
-
lines.push(
|
|
708
|
-
paint(
|
|
709
|
-
'',
|
|
710
|
-
` … +${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
|
-
c,
|
|
712
|
-
{ 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`,
|
|
713
698
|
),
|
|
714
699
|
)
|
|
715
|
-
|
|
716
|
-
return lines
|
|
700
|
+
return blocks
|
|
717
701
|
}
|
|
718
702
|
|
|
719
703
|
/**
|
|
@@ -757,22 +741,20 @@ export function formatRunList(
|
|
|
757
741
|
>
|
|
758
742
|
id: string
|
|
759
743
|
}[],
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
(r) =>
|
|
775
|
-
`${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
|
+
]),
|
|
776
758
|
)
|
|
777
759
|
}
|
|
778
760
|
|
|
@@ -806,7 +788,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
806
788
|
const cacheDir = await cliCacheDir(root, parsed.cacheDir)
|
|
807
789
|
const cache = Cache.inspect(cacheDir)
|
|
808
790
|
try {
|
|
809
|
-
const db = cache
|
|
791
|
+
const db = indexDb(cache)
|
|
810
792
|
// Nothing recorded at all is its own answer: `--failed` said "no
|
|
811
793
|
// recorded run failed" and a run id pointed at a `--list` that lists
|
|
812
794
|
// nothing, while `--list --failed` past green runs said there were none.
|
|
@@ -825,22 +807,29 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
825
807
|
parsed.runId === undefined
|
|
826
808
|
? undefined
|
|
827
809
|
: (resolveRunId(db, parsed.runId, 'vx last') ?? parsed.runId)
|
|
828
|
-
const log = taskLog(
|
|
829
|
-
cacheDir,
|
|
830
|
-
db,
|
|
831
|
-
await bareTaskId(db, await rootSpelled(root, parsed.log)),
|
|
832
|
-
runId,
|
|
833
|
-
)
|
|
810
|
+
const log = taskLog(db, await bareTaskId(db, await rootSpelled(root, parsed.log)), runId)
|
|
834
811
|
if (log === null) {
|
|
835
812
|
// The id's form is hinted only when it lacks one: a task that
|
|
836
|
-
// 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
|
|
837
826
|
throw new UserError(
|
|
838
|
-
`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}?`}`,
|
|
839
828
|
'VX_E_NO_HISTORY',
|
|
840
829
|
)
|
|
841
830
|
}
|
|
842
831
|
if (parsed.format === 'json') {
|
|
843
|
-
|
|
832
|
+
ui.data('out', `${JSON.stringify(log)}\n`)
|
|
844
833
|
return 0
|
|
845
834
|
}
|
|
846
835
|
if (log.source === null) {
|
|
@@ -866,7 +855,8 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
866
855
|
} else if (log.output === '') {
|
|
867
856
|
sayNotice('info', 'vx last', `${log.taskId} ended ${log.status} and printed nothing`)
|
|
868
857
|
}
|
|
869
|
-
|
|
858
|
+
// The task's own bytes, as it wrote them.
|
|
859
|
+
ui.data('out', log.output)
|
|
870
860
|
return 0
|
|
871
861
|
}
|
|
872
862
|
|
|
@@ -876,7 +866,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
876
866
|
? failedInvocations(db, parsed.list)
|
|
877
867
|
: listInvocations(db, { limit: parsed.list })
|
|
878
868
|
if (parsed.format === 'json') {
|
|
879
|
-
|
|
869
|
+
ui.data('out', `${JSON.stringify(invocations)}\n`)
|
|
880
870
|
return 0
|
|
881
871
|
}
|
|
882
872
|
if (invocations.length === 0) {
|
|
@@ -884,11 +874,14 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
884
874
|
ui.print('out', ui.status('info', 'vx last', none))
|
|
885
875
|
return 0
|
|
886
876
|
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
+
),
|
|
890
884
|
)
|
|
891
|
-
process.stdout.write(`${lines.join('\n')}\n`)
|
|
892
885
|
return 0
|
|
893
886
|
}
|
|
894
887
|
|
|
@@ -921,9 +914,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
921
914
|
const detail = getRun(db, inv.runId)
|
|
922
915
|
|
|
923
916
|
if (parsed.format === 'json') {
|
|
924
|
-
const failures = new Map(
|
|
925
|
-
(runFailures(cacheDir, db, inv.runId)?.tasks ?? []).map((f) => [f.taskId, f]),
|
|
926
|
-
)
|
|
917
|
+
const failures = new Map((runFailures(db, inv.runId)?.tasks ?? []).map((f) => [f.taskId, f]))
|
|
927
918
|
const tasks = (detail?.tasks ?? []).map((t) => {
|
|
928
919
|
const f = failures.get(`${t.project}#${t.task}`)
|
|
929
920
|
return f === undefined
|
|
@@ -937,9 +928,10 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
937
928
|
...(f.fixedIn !== undefined ? { fixedIn: f.fixedIn } : {}),
|
|
938
929
|
}
|
|
939
930
|
})
|
|
940
|
-
const steps =
|
|
941
|
-
const warnings =
|
|
942
|
-
|
|
931
|
+
const steps = keptSteps(db, inv.runId)
|
|
932
|
+
const warnings = keptWarnings(db, inv.runId)
|
|
933
|
+
ui.data(
|
|
934
|
+
'out',
|
|
943
935
|
`${JSON.stringify({
|
|
944
936
|
invocation: inv,
|
|
945
937
|
tasks,
|
|
@@ -950,36 +942,48 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
950
942
|
return 0
|
|
951
943
|
}
|
|
952
944
|
|
|
953
|
-
const c = detectColors(process.stdout)
|
|
954
|
-
const dim = (s: string): string => paint('', s, c, { dim: true })
|
|
955
|
-
const lines: string[] = []
|
|
956
|
-
lines.push(
|
|
957
|
-
`${paint('', 'run', c, { bold: true })} ${paint('', inv.runId, c, { bold: true })} ${dim('—')} ${inv.exitOk ? paint(SUCCESS, 'ok', c) : paint(ERROR, 'FAILED', c)}`,
|
|
958
|
-
)
|
|
959
|
-
lines.push(` ${dim('$')} ${paint('', inv.command, c, { bold: true })}`)
|
|
960
|
-
lines.push(
|
|
961
|
-
dim(
|
|
962
|
-
` ${fmtWhen(inv.startedAt)} · ${fmtMs(inv.totalDurationMs)}` +
|
|
963
|
-
`${inv.branch !== null ? ` · ${inv.branch}` : ''}` +
|
|
964
|
-
`${inv.commitSha !== null ? ` @ ${inv.commitSha.slice(0, 8)}${inv.dirty === true ? '+dirty' : ''}` : ''}` +
|
|
965
|
-
`${inv.ci ? ` · CI${inv.ciProvider !== null ? ` (${inv.ciProvider})` : ''}` : ''}`,
|
|
966
|
-
),
|
|
967
|
-
)
|
|
968
|
-
lines.push(
|
|
969
|
-
dim(` ${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}`) +
|
|
970
|
-
`${inv.failedCount > 0 ? paint(ERROR, ` · ${inv.failedCount} failed`, c) : ''}`,
|
|
971
|
-
)
|
|
972
945
|
const moved = new Map<string, string>()
|
|
973
946
|
if (inv.failedCount > 0) {
|
|
974
|
-
for (const f of runFailures(
|
|
947
|
+
for (const f of runFailures(db, inv.runId)?.tasks ?? [])
|
|
975
948
|
if (f.inputMoved !== undefined) moved.set(f.taskId, f.inputMoved)
|
|
976
949
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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))
|
|
984
|
+
const warnings = keptWarnings(db, inv.runId)
|
|
985
|
+
if (warnings.length > 0)
|
|
986
|
+
blocks.push(ui.section('Warnings', String(warnings.length), ui.text(...warnings)))
|
|
983
987
|
// The next command after reading a failed run, with the arguments the
|
|
984
988
|
// run forwarded to the tasks that got them (a failure under `-- --shard
|
|
985
989
|
// 2` is that shard's); a dependency never got them, so its line has none.
|
|
@@ -992,14 +996,11 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
992
996
|
const ids = failed
|
|
993
997
|
.filter((t) => t.forwarded === withArgs)
|
|
994
998
|
.map((t) => shellQuote(`${t.project}#${t.task}`))
|
|
995
|
-
if (ids.length > 0)
|
|
996
|
-
rerun.push(
|
|
997
|
-
` ${dim('re-run what failed:')} ${paint('', `vx run ${ids.join(' ')}${withArgs ? args : ''}`, c, { bold: true })}`,
|
|
998
|
-
)
|
|
999
|
+
if (ids.length > 0) rerun.push(`vx run ${ids.join(' ')}${withArgs ? args : ''}`)
|
|
999
1000
|
}
|
|
1000
|
-
|
|
1001
|
+
blocks.push(ui.fields([['next', ...rerun.map((r) => ui.cmd(r))]]))
|
|
1001
1002
|
}
|
|
1002
|
-
|
|
1003
|
+
ui.print('out', ...blocks)
|
|
1003
1004
|
return 0
|
|
1004
1005
|
} finally {
|
|
1005
1006
|
cache.close()
|