@vzn/vx 0.0.651 → 0.0.653
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 +3 -4
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +15 -20
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +37 -3
- package/src/cache/archive.ts +197 -32
- package/src/cache/cache.ts +882 -230
- package/src/cache/checkpointer.ts +102 -0
- package/src/cache/file-hashes.ts +2 -1
- package/src/cache/fold-memo.ts +24 -4
- package/src/cache/fs-clock.ts +26 -0
- package/src/cache/git-inputs.ts +188 -22
- package/src/cache/history-stream.ts +151 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +12 -1
- package/src/cache/inputs.ts +616 -135
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +29 -16
- package/src/cache/layered-cache.ts +28 -7
- package/src/cache/lockfile-memo.ts +2 -1
- package/src/cache/output-index.ts +106 -44
- package/src/cache/plugin-cache.ts +2 -2
- package/src/cache/policy.ts +6 -2
- package/src/cache/remote-lane.ts +3 -1
- package/src/cache/run-history.ts +94 -25
- package/src/cache/schema.ts +46 -10
- package/src/cli/cache.ts +149 -36
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +9 -2
- package/src/cli/docs.ts +4 -2
- package/src/cli/help.ts +27 -4
- package/src/cli/index.ts +62 -7
- package/src/cli/info.ts +5 -4
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +218 -43
- package/src/cli/lock.ts +187 -24
- package/src/cli/plan-format.ts +24 -21
- package/src/cli/plugin-commands.ts +6 -1
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +70 -28
- package/src/cli/task-verb.ts +12 -7
- package/src/cli/upgrade.ts +210 -76
- package/src/cli/watch-filter.ts +88 -15
- package/src/cli/watch-fs.ts +34 -47
- package/src/cli/watch-judge.ts +73 -9
- package/src/cli/watch-set.ts +10 -8
- package/src/cli/watch.ts +190 -33
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +56 -30
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +116 -16
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +7 -7
- package/src/exec/runner.ts +270 -108
- package/src/exec/sandbox-binds.ts +109 -18
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +446 -64
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +29 -7
- package/src/graph/definition.ts +16 -3
- package/src/graph/folds.ts +89 -16
- package/src/graph/index.ts +8 -0
- package/src/graph/priorities.ts +8 -5
- package/src/graph/scheduler.ts +176 -39
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +347 -160
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +39 -6
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +193 -73
- package/src/index.ts +6 -3
- package/src/orchestrator/admission.ts +14 -10
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +42 -13
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +8 -10
- package/src/orchestrator/excluded-keys.ts +15 -3
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +612 -138
- package/src/orchestrator/graph-memo.ts +26 -4
- package/src/orchestrator/graph-stage.ts +27 -4
- package/src/orchestrator/held-servers.ts +36 -14
- package/src/orchestrator/hit-restore.ts +141 -93
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +53 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +60 -61
- package/src/orchestrator/options.ts +13 -0
- package/src/orchestrator/output-host.ts +16 -4
- package/src/orchestrator/output-log.ts +1 -54
- package/src/orchestrator/persistent-task.ts +32 -16
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +82 -16
- package/src/orchestrator/plan.ts +39 -17
- package/src/orchestrator/plugin-host.ts +238 -110
- package/src/orchestrator/plugin.ts +59 -27
- package/src/orchestrator/prepare.ts +194 -38
- package/src/orchestrator/projects.ts +89 -10
- package/src/orchestrator/read-only.ts +115 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +101 -23
- package/src/orchestrator/run-id.ts +13 -2
- package/src/orchestrator/run-keys.ts +87 -28
- package/src/orchestrator/run-live.ts +16 -13
- package/src/orchestrator/run-lock.ts +27 -4
- package/src/orchestrator/run-records.ts +95 -74
- package/src/orchestrator/run-report.ts +3 -2
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +531 -122
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +314 -44
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +29 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +34 -27
- package/src/orchestrator/task-artifacts.ts +16 -3
- package/src/orchestrator/task-hash.ts +191 -34
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +85 -13
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +23 -6
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +179 -162
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +108 -31
- package/src/report/plain-output.ts +15 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +61 -18
- package/src/settings.ts +50 -14
- package/src/ui/components.ts +36 -42
- package/src/ui/index.ts +3 -1
- package/src/ui/live-region.ts +67 -8
- package/src/ui/terminal.ts +12 -0
- package/src/ui/theme.ts +16 -1
- package/src/util/config-frame.ts +142 -6
- package/src/util/edit-distance.ts +17 -1
- package/src/util/errors.ts +23 -7
- package/src/util/foreign-cache-dir.ts +60 -0
- package/src/util/git-ignored.ts +59 -5
- package/src/util/index.ts +15 -1
- package/src/util/paths.ts +51 -7
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +18 -1
- package/src/util/size.ts +12 -2
- package/src/util/tail.ts +4 -1
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-bytecode.ts +23 -4
- package/src/workspace/config-eval.ts +90 -16
- package/src/workspace/config-imports.ts +96 -41
- package/src/workspace/config-schema.ts +192 -86
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +18 -2
- package/src/workspace/index.ts +3 -1
- package/src/workspace/json-data.ts +34 -8
- package/src/workspace/lockfile.ts +86 -37
- package/src/workspace/migration.ts +58 -31
- package/src/workspace/module-state.ts +62 -0
- package/src/workspace/nested-dirs.ts +8 -0
- package/src/workspace/project-loader.ts +276 -32
- package/src/workspace/repo-id.ts +29 -6
- package/src/workspace/workspace.ts +190 -30
package/src/cli/last.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
// without re-executing anything. Comparison gap #12: with the dashboard's
|
|
3
3
|
// run-detail page gone (the 2026-08-23 cloud removal), the local run
|
|
4
4
|
// history in cache.db is the only replay surface, and this verb reads it.
|
|
5
|
-
// Read-only — no config evaluation, no re-hash, no cache probe
|
|
5
|
+
// Read-only — no config evaluation, no re-hash, no cache probe; `--stats`
|
|
6
|
+
// reads the lock only for each task's definition.
|
|
6
7
|
|
|
7
8
|
import type { Database } from 'bun:sqlite'
|
|
8
9
|
import { indexDb, Cache, keptSteps, keptWarnings, type RunStep } from '../cache/index.js'
|
|
9
|
-
import {
|
|
10
|
+
import { buildTaskGraph, expandRequested, isDefaultBuild, taskDefinitions } from '../graph/index.js'
|
|
10
11
|
import { flagHint, formatValue, seeHelp } from './help.js'
|
|
11
12
|
import {
|
|
12
13
|
type FailureMode,
|
|
@@ -18,7 +19,9 @@ import {
|
|
|
18
19
|
type RunSummaryRow,
|
|
19
20
|
} from '../history/index.js'
|
|
20
21
|
import {
|
|
22
|
+
applyGraphStage,
|
|
21
23
|
currentBranch,
|
|
24
|
+
hasHook,
|
|
22
25
|
type LiveSnapshot,
|
|
23
26
|
liveSocketPath,
|
|
24
27
|
exitSignal,
|
|
@@ -28,7 +31,13 @@ import {
|
|
|
28
31
|
shortRunId,
|
|
29
32
|
taskLog,
|
|
30
33
|
} from '../orchestrator/index.js'
|
|
31
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
formatRecordedRow,
|
|
36
|
+
formatSize,
|
|
37
|
+
outcomeWord,
|
|
38
|
+
taskList,
|
|
39
|
+
type WireEvent,
|
|
40
|
+
} from '../report/index.js'
|
|
32
41
|
import {
|
|
33
42
|
formatElapsed,
|
|
34
43
|
nearest,
|
|
@@ -39,9 +48,18 @@ import {
|
|
|
39
48
|
} from '../util/index.js'
|
|
40
49
|
import * as ui from '../ui/index.js'
|
|
41
50
|
import { sayNotice } from '../ui/index.js'
|
|
42
|
-
import {
|
|
51
|
+
import { runRules } from '../settings.js'
|
|
52
|
+
import { buildPackageGraph, findWorkspaceRoot } from '../workspace/index.js'
|
|
43
53
|
import { findCwdProject } from './select.js'
|
|
44
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
cliCacheDir,
|
|
56
|
+
discoverCliProjects,
|
|
57
|
+
loadCliProjects,
|
|
58
|
+
loadCliWorkspace,
|
|
59
|
+
parseCacheDirFlag,
|
|
60
|
+
rootSpelled,
|
|
61
|
+
stageCacheDir,
|
|
62
|
+
} from './workspace-config.js'
|
|
45
63
|
|
|
46
64
|
interface LastArgs {
|
|
47
65
|
runId?: string
|
|
@@ -68,6 +86,9 @@ interface LastArgs {
|
|
|
68
86
|
error?: string
|
|
69
87
|
}
|
|
70
88
|
|
|
89
|
+
/** The run grid's mark for a number history does not hold. */
|
|
90
|
+
const UNKNOWN = '\u2014'
|
|
91
|
+
|
|
71
92
|
/**
|
|
72
93
|
* A run id as typed beside `--list`: a UUIDv7's first group (8 hex digits)
|
|
73
94
|
* at least, whole or cut as `--list` prints it.
|
|
@@ -127,10 +148,16 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
127
148
|
}
|
|
128
149
|
if (a === '--sql' || a.startsWith('--sql=')) {
|
|
129
150
|
const v = a === '--sql' ? args[++i] : a.slice('--sql='.length)
|
|
130
|
-
|
|
151
|
+
// The space form took the next flag as the query, then blamed that
|
|
152
|
+
// flag for combining with --sql.
|
|
153
|
+
const flag =
|
|
154
|
+
a === '--sql' && v?.startsWith('-') === true
|
|
155
|
+
? ` (got flag ${v}; a query that starts with - needs --sql=${v})`
|
|
156
|
+
: ''
|
|
157
|
+
if (v === undefined || v.trim() === '' || flag !== '') {
|
|
131
158
|
return {
|
|
132
159
|
...out,
|
|
133
|
-
error: `--sql requires a query, like 'SELECT * FROM runs'${seeHelp('last')}`,
|
|
160
|
+
error: `--sql requires a query, like 'SELECT * FROM runs'${flag}${seeHelp('last')}`,
|
|
134
161
|
}
|
|
135
162
|
}
|
|
136
163
|
out.sql = v
|
|
@@ -276,18 +303,71 @@ export interface TaskStats {
|
|
|
276
303
|
maxCpuParallelism: number | null
|
|
277
304
|
}
|
|
278
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Each task's definition (`taskDefinitions`) as a run builds the graph
|
|
308
|
+
* now: the lock's configs with the `project` and `graph` stages applied.
|
|
309
|
+
* Undefined when none loads (no lock, a broken config): then every
|
|
310
|
+
* definition's rows are read, as before definitions were kept.
|
|
311
|
+
*/
|
|
312
|
+
async function definitionsNow(
|
|
313
|
+
root: string,
|
|
314
|
+
cacheDir: string | undefined,
|
|
315
|
+
): Promise<Map<string, string> | undefined> {
|
|
316
|
+
try {
|
|
317
|
+
// A plugin's warning is the run's to say, not a history read's.
|
|
318
|
+
const opts = { warn: () => {}, ...(cacheDir === undefined ? {} : { cacheDir }) }
|
|
319
|
+
const metas = await discoverCliProjects(root)
|
|
320
|
+
const projects = await loadCliProjects(root, metas, 'all', opts)
|
|
321
|
+
const ws = await loadCliWorkspace(root, opts.warn)
|
|
322
|
+
const rules = runRules(ws.workspaceConfig)
|
|
323
|
+
const names = new Set<string>()
|
|
324
|
+
for (const p of projects.values())
|
|
325
|
+
for (const t of Object.keys(p.config.tasks ?? {})) names.add(t)
|
|
326
|
+
const nodes = buildTaskGraph({
|
|
327
|
+
projects,
|
|
328
|
+
packageGraph: buildPackageGraph(metas),
|
|
329
|
+
requested: expandRequested([...names], [...projects.keys()], projects, isDefaultBuild),
|
|
330
|
+
workspaceRoot: root,
|
|
331
|
+
rules,
|
|
332
|
+
})
|
|
333
|
+
if (hasHook(ws.plugins, 'graph'))
|
|
334
|
+
await applyGraphStage(ws.plugins, nodes, {
|
|
335
|
+
workspaceRoot: root,
|
|
336
|
+
cacheDir: stageCacheDir(ws, opts),
|
|
337
|
+
projectMetas: metas,
|
|
338
|
+
warn: opts.warn,
|
|
339
|
+
rules,
|
|
340
|
+
})
|
|
341
|
+
return taskDefinitions(nodes.values())
|
|
342
|
+
} catch {
|
|
343
|
+
return undefined
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Each task's definition as run `runId` recorded it: what its live rows read history by. */
|
|
348
|
+
function runDefinitions(db: Database, runId: string): Map<string, string> {
|
|
349
|
+
const rows = db
|
|
350
|
+
.query(
|
|
351
|
+
`SELECT r.project, r.task, r.def FROM main.runs r JOIN main.run_ids k ON k.id = r.run
|
|
352
|
+
WHERE k.run_id = ? AND r.def IS NOT NULL`,
|
|
353
|
+
)
|
|
354
|
+
.all(runId) as { project: string; task: string; def: string }[]
|
|
355
|
+
return new Map(rows.map((r) => [`${r.project}#${r.task}`, r.def]))
|
|
356
|
+
}
|
|
357
|
+
|
|
279
358
|
/**
|
|
280
359
|
* `--stats`: what the scheduler and `--dry` learn from history, per task,
|
|
281
360
|
* over the same window (`LocalHistoryProvider`), with the p50 a run on
|
|
282
|
-
* `branch` forecasts
|
|
361
|
+
* `branch` forecasts from the timings of each task's definition `defs`.
|
|
283
362
|
*/
|
|
284
363
|
async function printStats(
|
|
285
364
|
db: Database,
|
|
286
365
|
target: string | undefined,
|
|
287
366
|
format: 'pretty' | 'json',
|
|
288
367
|
branch: string | null,
|
|
368
|
+
defs: ReadonlyMap<string, string> | undefined,
|
|
289
369
|
): Promise<number> {
|
|
290
|
-
const provider = new LocalHistoryProvider(db, undefined, branch)
|
|
370
|
+
const provider = new LocalHistoryProvider(db, undefined, branch, defs)
|
|
291
371
|
const matches = (id: string): boolean =>
|
|
292
372
|
target === undefined || (target.includes('#') ? id === target : splitTaskId(id)[1] === target)
|
|
293
373
|
const stats: TaskStats[] = [...(await provider.loadAll())]
|
|
@@ -319,7 +399,9 @@ async function printStats(
|
|
|
319
399
|
return 0
|
|
320
400
|
}
|
|
321
401
|
const pct = (r: number): string => `${Math.round(r * 100)}%`
|
|
322
|
-
|
|
402
|
+
// The run grid's units and its unknown mark, so one number reads alike
|
|
403
|
+
// in both: cpu `127%` (was `1.3×`), mem `3.6MB` (was `3.6 MB`), `—`.
|
|
404
|
+
const ms = (v: number | null): string => (v === null ? UNKNOWN : fmtMs(v))
|
|
323
405
|
const right = (title: string): ui.Column => ({ title, align: 'right' })
|
|
324
406
|
ui.print(
|
|
325
407
|
'out',
|
|
@@ -345,8 +427,8 @@ async function printStats(
|
|
|
345
427
|
pct(s.successRate),
|
|
346
428
|
pct(s.hitRate),
|
|
347
429
|
s.failureMode,
|
|
348
|
-
s.maxPeakRssBytes === null ?
|
|
349
|
-
s.maxCpuParallelism === null ?
|
|
430
|
+
s.maxPeakRssBytes === null ? UNKNOWN : formatSize(s.maxPeakRssBytes),
|
|
431
|
+
s.maxCpuParallelism === null ? UNKNOWN : pct(s.maxCpuParallelism),
|
|
350
432
|
]),
|
|
351
433
|
),
|
|
352
434
|
),
|
|
@@ -373,7 +455,8 @@ function printQuery(db: Database, sql: string, format: 'pretty' | 'json'): numbe
|
|
|
373
455
|
db.exec('PRAGMA query_only = ON')
|
|
374
456
|
const refused = (err: unknown): never => {
|
|
375
457
|
const message = err instanceof Error ? err.message : String(err)
|
|
376
|
-
|
|
458
|
+
// `runs` is a view: a write to it fails before query_only is asked.
|
|
459
|
+
if (/readonly|query_only|cannot modify/i.test(message)) {
|
|
377
460
|
throw new UserError(`vx last: --sql reads only: ${message}`, 'VX_E_USAGE')
|
|
378
461
|
}
|
|
379
462
|
throw new UserError(`vx last: --sql: ${message}`, 'VX_E_USAGE')
|
|
@@ -433,6 +516,15 @@ function printQuery(db: Database, sql: string, format: 'pretty' | 'json'): numbe
|
|
|
433
516
|
return 0
|
|
434
517
|
}
|
|
435
518
|
|
|
519
|
+
/** `text` as JSON, or undefined where it is not. */
|
|
520
|
+
function parsed(text: string): unknown {
|
|
521
|
+
try {
|
|
522
|
+
return JSON.parse(text)
|
|
523
|
+
} catch {
|
|
524
|
+
return undefined
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
436
528
|
/** How long a run has to answer before `--live` gives up on it. */
|
|
437
529
|
const LIVE_ANSWER_MS = 5_000
|
|
438
530
|
|
|
@@ -457,18 +549,36 @@ async function printLive(
|
|
|
457
549
|
}
|
|
458
550
|
let buffered = ''
|
|
459
551
|
let snapshot: LiveSnapshot | undefined
|
|
552
|
+
/** The run answered a snapshot this vx cannot read: another vx version's. */
|
|
553
|
+
let unreadable = false
|
|
460
554
|
let settle!: (answered: boolean) => void
|
|
461
555
|
const done = new Promise<boolean>((resolve) => (settle = resolve))
|
|
462
556
|
const line = (text: string): void => {
|
|
557
|
+
if (unreadable) return
|
|
463
558
|
if (snapshot === undefined) {
|
|
464
|
-
|
|
559
|
+
const s = parsed(text) as Partial<LiveSnapshot> | undefined
|
|
560
|
+
const l = s?.live
|
|
561
|
+
if (
|
|
562
|
+
typeof l?.runId !== 'string' ||
|
|
563
|
+
typeof l.command !== 'string' ||
|
|
564
|
+
typeof l.startedAt !== 'number' ||
|
|
565
|
+
typeof l.elapsedMs !== 'number' ||
|
|
566
|
+
!Array.isArray(s!.tasks) ||
|
|
567
|
+
!Array.isArray(s!.warnings)
|
|
568
|
+
) {
|
|
569
|
+
unreadable = true
|
|
570
|
+
return settle(false)
|
|
571
|
+
}
|
|
572
|
+
snapshot = s as LiveSnapshot
|
|
465
573
|
if (format === 'json') ui.data('out', `${text}\n`)
|
|
466
574
|
else ui.print('out', ...formatLive(snapshot))
|
|
467
575
|
return
|
|
468
576
|
}
|
|
469
577
|
if (format === 'json') ui.data('out', `${text}\n`)
|
|
470
578
|
else {
|
|
471
|
-
const
|
|
579
|
+
const e = parsed(text)
|
|
580
|
+
const event =
|
|
581
|
+
typeof e === 'object' && e !== null ? formatLiveEvent(e as WireEvent) : undefined
|
|
472
582
|
if (event !== undefined) ui.print('out', event)
|
|
473
583
|
}
|
|
474
584
|
}
|
|
@@ -503,6 +613,11 @@ async function printLive(
|
|
|
503
613
|
: setTimeout(() => settle(snapshot !== undefined), LIVE_ANSWER_MS)
|
|
504
614
|
const answered = await done
|
|
505
615
|
clearTimeout(timer)
|
|
616
|
+
if (unreadable)
|
|
617
|
+
throw refusal({
|
|
618
|
+
headline: 'the run in progress answered in a form this vx does not read',
|
|
619
|
+
fix: 'ask with the vx that started the run',
|
|
620
|
+
})
|
|
506
621
|
if (!answered) none()
|
|
507
622
|
return 0
|
|
508
623
|
}
|
|
@@ -682,9 +797,19 @@ export function formatTaskRows(
|
|
|
682
797
|
// The run's own grid (`taskList`): a replay reads as the run did. What
|
|
683
798
|
// the run said below the row (an exit, why it skipped) rides it dim.
|
|
684
799
|
const rows = ordered.map((t) => {
|
|
800
|
+
// A cached failure with no cache outcome is a hit whose restore failed: no
|
|
801
|
+
// exit. One whose command exited 0 failed in its save (a server that
|
|
802
|
+
// exited 0 before ready says so in its reason).
|
|
803
|
+
const failure =
|
|
804
|
+
t.status !== 'failed'
|
|
805
|
+
? ''
|
|
806
|
+
: t.cached === true && t.cacheHit === null
|
|
807
|
+
? 'cache restore failed'
|
|
808
|
+
: t.exitCode === 0 && t.notReady === null
|
|
809
|
+
? 'cache save failed'
|
|
810
|
+
: `exit ${t.exitCode}`
|
|
685
811
|
const note = (
|
|
686
|
-
(t
|
|
687
|
-
reasonParts(t, skippedHere, inputMoved.get(`${t.project}#${t.task}`))
|
|
812
|
+
failure + reasonParts(t, skippedHere, inputMoved.get(`${t.project}#${t.task}`))
|
|
688
813
|
).trim()
|
|
689
814
|
const id = `${t.project}#${t.task}`
|
|
690
815
|
const usualMs = usual === undefined ? undefined : (usual.get(id) ?? null)
|
|
@@ -766,7 +891,9 @@ export function formatRunList(
|
|
|
766
891
|
async function bareTaskId(db: Database, task: string): Promise<string> {
|
|
767
892
|
if (task.includes('#')) return task
|
|
768
893
|
const ids = (
|
|
769
|
-
db
|
|
894
|
+
db
|
|
895
|
+
.query('SELECT DISTINCT project FROM main.runs WHERE task = ? ORDER BY project')
|
|
896
|
+
.all(task) as {
|
|
770
897
|
project: string
|
|
771
898
|
}[]
|
|
772
899
|
).map((r) => `${r.project}#${task}`)
|
|
@@ -799,7 +926,13 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
799
926
|
if (parsed.stats === true) {
|
|
800
927
|
const branch = currentBranch(root, undefined, parsed.branch)
|
|
801
928
|
const task = parsed.task === undefined ? undefined : await rootSpelled(root, parsed.task)
|
|
802
|
-
return await printStats(
|
|
929
|
+
return await printStats(
|
|
930
|
+
db,
|
|
931
|
+
task,
|
|
932
|
+
parsed.format,
|
|
933
|
+
branch,
|
|
934
|
+
await definitionsNow(root, parsed.cacheDir),
|
|
935
|
+
)
|
|
803
936
|
}
|
|
804
937
|
|
|
805
938
|
if (parsed.log !== undefined) {
|
|
@@ -812,19 +945,22 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
812
945
|
// The id's form is hinted only when it lacks one: a task that
|
|
813
946
|
// never ran was told how to spell a task id. A typo of one that
|
|
814
947
|
// ran is named, as `vx why` names it.
|
|
815
|
-
const
|
|
816
|
-
?
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
948
|
+
const recorded = parsed.log.includes('#')
|
|
949
|
+
? new LocalHistoryProvider(db).recentTaskIds()
|
|
950
|
+
: []
|
|
951
|
+
const ran = nearest(parsed.log, recorded)
|
|
952
|
+
// No near name: what of that project has run, so the next try is a pick.
|
|
953
|
+
const project = parsed.log.slice(0, parsed.log.lastIndexOf('#') + 1)
|
|
954
|
+
const theirs = recorded
|
|
955
|
+
.filter((id) => project !== '' && id.startsWith(project))
|
|
956
|
+
.map((id) => id.slice(project.length))
|
|
957
|
+
.sort()
|
|
958
|
+
const listed =
|
|
959
|
+
ran === undefined && theirs.length > 0
|
|
960
|
+
? `; ${project.slice(0, -1)}'s recorded tasks: ${theirs.join(', ')}`
|
|
961
|
+
: ''
|
|
826
962
|
throw new UserError(
|
|
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}?`}`,
|
|
963
|
+
`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}?`}${listed}`,
|
|
828
964
|
'VX_E_NO_HISTORY',
|
|
829
965
|
)
|
|
830
966
|
}
|
|
@@ -833,8 +969,9 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
833
969
|
return 0
|
|
834
970
|
}
|
|
835
971
|
if (log.source === null) {
|
|
836
|
-
// A cached task with no entry was not saved (a read-only
|
|
837
|
-
//
|
|
972
|
+
// A cached task with no entry of its own was not saved (a read-only
|
|
973
|
+
// policy, a `--force` run that kept the entry it found), or its entry
|
|
974
|
+
// was pruned or saved over; calling it uncached sends the user to the config.
|
|
838
975
|
const [project, task] = splitTaskId(log.taskId)
|
|
839
976
|
const cached =
|
|
840
977
|
log.status !== 'failed' &&
|
|
@@ -843,14 +980,27 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
843
980
|
.query('SELECT cached FROM runs WHERE run_id = ? AND project = ? AND task = ?')
|
|
844
981
|
.get(log.runId, project, task) as { cached: number | null } | null
|
|
845
982
|
)?.cached === 1
|
|
983
|
+
// A skip ran nothing: the output to read is its blocker's.
|
|
984
|
+
const blocker =
|
|
985
|
+
log.status === 'skipped'
|
|
986
|
+
? (
|
|
987
|
+
db
|
|
988
|
+
.query(
|
|
989
|
+
'SELECT blocked_by AS b FROM runs WHERE run_id = ? AND project = ? AND task = ?',
|
|
990
|
+
)
|
|
991
|
+
.get(log.runId, project, task) as { b: string | null } | null
|
|
992
|
+
)?.b
|
|
993
|
+
: null
|
|
846
994
|
sayNotice(
|
|
847
995
|
'info',
|
|
848
996
|
'vx last',
|
|
849
|
-
|
|
850
|
-
? `${log.taskId}
|
|
851
|
-
:
|
|
852
|
-
? `${log.taskId} ended
|
|
853
|
-
:
|
|
997
|
+
typeof blocker === 'string'
|
|
998
|
+
? `${log.taskId} did not run: ${blocker} blocked it (vx last --log ${blocker})`
|
|
999
|
+
: log.status === 'failed'
|
|
1000
|
+
? `${log.taskId} ended failed, and vx kept no output for it: it keeps the newest 50 failed runs`
|
|
1001
|
+
: cached
|
|
1002
|
+
? `${log.taskId} ended ${log.status}, and the cache does not hold its output: the run did not save it, or it was pruned or saved over since`
|
|
1003
|
+
: `${log.taskId} ended ${log.status}. vx keeps output only for failed and cached tasks`,
|
|
854
1004
|
)
|
|
855
1005
|
} else if (log.output === '') {
|
|
856
1006
|
sayNotice('info', 'vx last', `${log.taskId} ended ${log.status} and printed nothing`)
|
|
@@ -885,18 +1035,34 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
885
1035
|
return 0
|
|
886
1036
|
}
|
|
887
1037
|
|
|
1038
|
+
const named = parsed.runId === undefined ? null : resolveRunId(db, parsed.runId, 'vx last')
|
|
888
1039
|
const inv =
|
|
889
1040
|
parsed.runId !== undefined
|
|
890
|
-
? getInvocation(db,
|
|
1041
|
+
? getInvocation(db, named ?? parsed.runId)
|
|
891
1042
|
: parsed.failed === true
|
|
892
1043
|
? (failedInvocations(db, 1)[0] ?? null)
|
|
893
1044
|
: (listInvocations(db, { limit: 1 })[0] ?? null)
|
|
1045
|
+
// A run with rows and no header is one a stop ended (item 854): `vx why`
|
|
1046
|
+
// printed its id, so it is named, not called unrecorded.
|
|
1047
|
+
if (named !== null && (inv === null || inv === undefined)) {
|
|
1048
|
+
throw new UserError(
|
|
1049
|
+
`vx last: run ${named} was stopped before it finished, so vx kept its tasks but no summary: \`vx last ${named} --log <task>\` prints one's output`,
|
|
1050
|
+
'VX_E_NO_HISTORY',
|
|
1051
|
+
)
|
|
1052
|
+
}
|
|
894
1053
|
if ((inv === null || inv === undefined) && noRuns()) {
|
|
895
1054
|
throw refusal(
|
|
896
1055
|
{ tone: 'info', headline: 'no runs yet', fix: 'vx run' },
|
|
897
1056
|
{ code: 'VX_E_NO_HISTORY', message: 'vx last: no recorded runs yet — run something first' },
|
|
898
1057
|
)
|
|
899
1058
|
}
|
|
1059
|
+
// Nothing failed is no fault: said as `--list --failed` says it.
|
|
1060
|
+
if ((inv === null || inv === undefined) && parsed.failed === true) {
|
|
1061
|
+
throw refusal(
|
|
1062
|
+
{ tone: 'info', headline: 'no failed runs', fix: 'vx last' },
|
|
1063
|
+
{ code: 'VX_E_NO_HISTORY', message: 'vx last: no failed runs' },
|
|
1064
|
+
)
|
|
1065
|
+
}
|
|
900
1066
|
if (inv === null || inv === undefined) {
|
|
901
1067
|
throw new UserError(
|
|
902
1068
|
noRuns()
|
|
@@ -905,9 +1071,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
905
1071
|
? `vx last: ${parsed.runId} is a task, not a run id — \`vx last --log ${parsed.runId}\` prints its output, \`vx last --stats ${parsed.runId}\` its durations`
|
|
906
1072
|
: parsed.runId !== undefined
|
|
907
1073
|
? `vx last: no recorded run ${parsed.runId} (vx last --list shows recent runs)`
|
|
908
|
-
:
|
|
909
|
-
? 'vx last: no recorded run failed'
|
|
910
|
-
: 'vx last: no recorded runs yet — run something first',
|
|
1074
|
+
: 'vx last: no recorded runs yet — run something first',
|
|
911
1075
|
'VX_E_NO_HISTORY',
|
|
912
1076
|
)
|
|
913
1077
|
}
|
|
@@ -975,7 +1139,18 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
975
1139
|
const ranIds = (detail?.tasks ?? [])
|
|
976
1140
|
.filter((t) => t.status === 'success' || t.status === 'failed')
|
|
977
1141
|
.map((t) => `${t.project}#${t.task}`)
|
|
978
|
-
|
|
1142
|
+
// Each task's timings of the definition it ran under, as its live row
|
|
1143
|
+
// read them: an older definition's timings are another task's.
|
|
1144
|
+
const history = new LocalHistoryProvider(
|
|
1145
|
+
db,
|
|
1146
|
+
undefined,
|
|
1147
|
+
currentBranch(root),
|
|
1148
|
+
runDefinitions(db, inv.runId),
|
|
1149
|
+
)
|
|
1150
|
+
const usual = new Map(history.p50sNow(ranIds))
|
|
1151
|
+
// A task whose one timing is this run's own has no usual to compare
|
|
1152
|
+
// with: `+0ms` claimed a baseline the live row rightly showed as `—`.
|
|
1153
|
+
for (const id of history.timedOnlySince(ranIds, inv.startedAt)) usual.delete(id)
|
|
979
1154
|
const taskRows = formatTaskRows(detail?.tasks ?? [], moved, usual)
|
|
980
1155
|
if (taskRows.length > 0)
|
|
981
1156
|
blocks.push(ui.section('Tasks', String(detail?.tasks.length ?? 0), ...taskRows))
|