@vzn/vx 0.0.645 → 0.0.648
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 +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
package/src/cli/last.ts
CHANGED
|
@@ -19,18 +19,36 @@ import {
|
|
|
19
19
|
} from '../history/index.js'
|
|
20
20
|
import {
|
|
21
21
|
currentBranch,
|
|
22
|
+
type LiveSnapshot,
|
|
23
|
+
liveSocketPath,
|
|
22
24
|
exitSignal,
|
|
23
25
|
resolveRunId,
|
|
24
26
|
runFailures,
|
|
25
27
|
type RunStep,
|
|
26
28
|
runSteps,
|
|
29
|
+
runWarnings,
|
|
30
|
+
shellQuote,
|
|
27
31
|
shortRunId,
|
|
28
32
|
taskLog,
|
|
29
33
|
} from '../orchestrator/index.js'
|
|
30
|
-
import { outcomeWord } from '../report/index.js'
|
|
31
|
-
import {
|
|
34
|
+
import { outcomeWord, type WireEvent } from '../report/index.js'
|
|
35
|
+
import {
|
|
36
|
+
detectColors,
|
|
37
|
+
ERROR,
|
|
38
|
+
formatElapsed,
|
|
39
|
+
paint,
|
|
40
|
+
parseDecimalInt,
|
|
41
|
+
refusal,
|
|
42
|
+
splitTaskId,
|
|
43
|
+
SUCCESS,
|
|
44
|
+
UserError,
|
|
45
|
+
sayNotice,
|
|
46
|
+
ui,
|
|
47
|
+
type ColorSupport,
|
|
48
|
+
} from '../util/index.js'
|
|
32
49
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
33
|
-
import {
|
|
50
|
+
import { findCwdProject } from './select.js'
|
|
51
|
+
import { cliCacheDir, parseCacheDirFlag, rootSpelled } from './workspace-config.js'
|
|
34
52
|
|
|
35
53
|
interface LastArgs {
|
|
36
54
|
runId?: string
|
|
@@ -48,6 +66,12 @@ interface LastArgs {
|
|
|
48
66
|
branch?: string
|
|
49
67
|
/** `--cache-dir`: read the history a run with the same flag wrote. */
|
|
50
68
|
cacheDir?: string
|
|
69
|
+
/** `--sql <query>`: the history's rows a read-only query selects. */
|
|
70
|
+
sql?: string
|
|
71
|
+
/** `--live`: the run in progress, from the run itself. */
|
|
72
|
+
live?: boolean
|
|
73
|
+
/** `--follow`: with `--live`, the run's events from now to its end. */
|
|
74
|
+
follow?: boolean
|
|
51
75
|
error?: string
|
|
52
76
|
}
|
|
53
77
|
|
|
@@ -77,8 +101,13 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
77
101
|
(!RUN_ID_SHAPE.test(next) && (!next.startsWith('-') || /^-\d/.test(next))))
|
|
78
102
|
const lv = a === '--list' ? (spaced ? next : '10') : a.slice(7)
|
|
79
103
|
if (spaced) i++
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
if (lv === '')
|
|
105
|
+
return {
|
|
106
|
+
...out,
|
|
107
|
+
error: `--list= requires a count from 1 to 500; bare --list shows 10${seeHelp('last')}`,
|
|
108
|
+
}
|
|
109
|
+
const n = parseDecimalInt(lv)
|
|
110
|
+
if (n === null || n < 1 || n > 500) {
|
|
82
111
|
return {
|
|
83
112
|
...out,
|
|
84
113
|
error: `--list must be a whole number from 1 to 500 (got ${lv})${seeHelp('last')}`,
|
|
@@ -95,6 +124,25 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
95
124
|
out.stats = true
|
|
96
125
|
continue
|
|
97
126
|
}
|
|
127
|
+
if (a === '--live') {
|
|
128
|
+
out.live = true
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if (a === '--follow') {
|
|
132
|
+
out.follow = true
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
if (a === '--sql' || a.startsWith('--sql=')) {
|
|
136
|
+
const v = a === '--sql' ? args[++i] : a.slice('--sql='.length)
|
|
137
|
+
if (v === undefined || v.trim() === '') {
|
|
138
|
+
return {
|
|
139
|
+
...out,
|
|
140
|
+
error: `--sql requires a query, like 'SELECT * FROM runs'${seeHelp('last')}`,
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
out.sql = v
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
98
146
|
if (a === '--branch' || a.startsWith('--branch=')) {
|
|
99
147
|
const v = a === '--branch' ? args[++i] : a.slice('--branch='.length)
|
|
100
148
|
if (v === undefined || v === '' || (a === '--branch' && v.startsWith('-'))) {
|
|
@@ -105,6 +153,9 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
105
153
|
}
|
|
106
154
|
if (a === '--log') {
|
|
107
155
|
const v = args[i + 1]
|
|
156
|
+
if (v === '') {
|
|
157
|
+
return { ...out, error: `--log requires a task id, like app#build${seeHelp('last')}` }
|
|
158
|
+
}
|
|
108
159
|
if (v === undefined || v.startsWith('-')) bareLog = true
|
|
109
160
|
else {
|
|
110
161
|
out.log = v
|
|
@@ -135,10 +186,35 @@ export function parseLastArgs(args: readonly string[]): LastArgs {
|
|
|
135
186
|
}
|
|
136
187
|
if (a.startsWith('-'))
|
|
137
188
|
return { ...out, error: `unknown flag: ${a}${flagHint('last', a)}${seeHelp('last')}` }
|
|
189
|
+
// Empty is no run id: '' is the start of every one.
|
|
190
|
+
if (a === '') continue
|
|
138
191
|
if (out.runId !== undefined)
|
|
139
192
|
return { ...out, error: `unexpected argument: ${a}${seeHelp('last')}` }
|
|
140
193
|
out.runId = a
|
|
141
194
|
}
|
|
195
|
+
if (out.follow === true && out.live !== true) {
|
|
196
|
+
return { ...out, error: `--follow needs --live${seeHelp('last')}` }
|
|
197
|
+
}
|
|
198
|
+
if (out.live === true) {
|
|
199
|
+
const other = [out.runId, out.list, out.failed, out.log, out.stats, out.branch, out.sql]
|
|
200
|
+
if (bareLog || other.some((v) => v !== undefined)) {
|
|
201
|
+
return {
|
|
202
|
+
...out,
|
|
203
|
+
error: `--live combines only with --follow, --format and --cache-dir${seeHelp('last')}`,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return out
|
|
207
|
+
}
|
|
208
|
+
if (out.sql !== undefined) {
|
|
209
|
+
const other = [out.runId, out.list, out.failed, out.log, out.stats, out.branch]
|
|
210
|
+
if (bareLog || other.some((v) => v !== undefined)) {
|
|
211
|
+
return {
|
|
212
|
+
...out,
|
|
213
|
+
error: `--sql combines only with --format and --cache-dir${seeHelp('last')}`,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out
|
|
217
|
+
}
|
|
142
218
|
if (bareLog) {
|
|
143
219
|
if (out.runId === undefined) {
|
|
144
220
|
return { ...out, error: `--log requires a task id, like app#build${seeHelp('last')}` }
|
|
@@ -266,9 +342,14 @@ async function printStats(
|
|
|
266
342
|
]),
|
|
267
343
|
]
|
|
268
344
|
const widths = rows[0]!.map((_, c) => Math.max(...rows.map((r) => r[c]!.length)))
|
|
269
|
-
const
|
|
345
|
+
const c = detectColors(process.stdout)
|
|
346
|
+
const lines = rows.map((r, i) =>
|
|
270
347
|
r
|
|
271
|
-
.map((cell,
|
|
348
|
+
.map((cell, col) => {
|
|
349
|
+
const padded = col === r.length - 1 ? cell : cell.padEnd(widths[col]!)
|
|
350
|
+
if (i === 0) return paint('', padded, c, { dim: true })
|
|
351
|
+
return col === 0 ? `${ui.inline(ui.id(cell), c)}${padded.slice(cell.length)}` : padded
|
|
352
|
+
})
|
|
272
353
|
.join(' ')
|
|
273
354
|
.trimEnd(),
|
|
274
355
|
)
|
|
@@ -276,6 +357,203 @@ async function printStats(
|
|
|
276
357
|
return 0
|
|
277
358
|
}
|
|
278
359
|
|
|
360
|
+
/**
|
|
361
|
+
* `--sql`: one SELECT over the history (`runs`, `invocations`, and the
|
|
362
|
+
* store's `entries`, `entry_inputs`, `task_durations`, `task_outcomes`),
|
|
363
|
+
* so an agent asks what no flag answers: a task's memory trend, the
|
|
364
|
+
* flakiest project, which input moved a key. `query_only` stops a write
|
|
365
|
+
* the statement hides (`WITH … DELETE`); it does not stop ATTACH, which
|
|
366
|
+
* creates a file, hence only SELECT or WITH may lead.
|
|
367
|
+
*/
|
|
368
|
+
function printQuery(db: Database, sql: string, format: 'pretty' | 'json'): number {
|
|
369
|
+
if (!/^\s*(select|with)\b/i.test(sql)) {
|
|
370
|
+
throw new UserError(
|
|
371
|
+
`vx last: --sql reads only: one SELECT (or WITH … SELECT) over the history`,
|
|
372
|
+
'VX_E_USAGE',
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
db.exec('PRAGMA query_only = ON')
|
|
376
|
+
const refused = (err: unknown): never => {
|
|
377
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
378
|
+
if (/readonly|query_only/i.test(message)) {
|
|
379
|
+
throw new UserError(`vx last: --sql reads only: ${message}`, 'VX_E_USAGE')
|
|
380
|
+
}
|
|
381
|
+
throw new UserError(`vx last: --sql: ${message}`, 'VX_E_USAGE')
|
|
382
|
+
}
|
|
383
|
+
let stmt: ReturnType<Database['query']>
|
|
384
|
+
try {
|
|
385
|
+
stmt = db.query(sql)
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return refused(err)
|
|
388
|
+
}
|
|
389
|
+
// SQLite prepares the first statement and drops the rest unread. Its
|
|
390
|
+
// text is the statement's own (a bound `?` expanded, hence the guard).
|
|
391
|
+
const own = stmt.toString()
|
|
392
|
+
const tail = sql.startsWith(own) ? sql.slice(own.length) : ''
|
|
393
|
+
if (tail.replace(/--[^\n]*|\/\*[^]*?\*\/|[\s;]/g, '') !== '') {
|
|
394
|
+
throw new UserError(
|
|
395
|
+
`vx last: --sql takes one statement, and ${JSON.stringify(tail.replace(/^[\s;]+/, '').trimEnd())} follows the first: run each in its own --sql`,
|
|
396
|
+
'VX_E_USAGE',
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
// Read by position: a join's `SELECT *` names `run_id` twice, and rows
|
|
400
|
+
// keyed by name kept one value per name.
|
|
401
|
+
const columns = stmt.columnNames
|
|
402
|
+
let values: unknown[][]
|
|
403
|
+
try {
|
|
404
|
+
values = stmt.values() as unknown[][]
|
|
405
|
+
} catch (err) {
|
|
406
|
+
return refused(err)
|
|
407
|
+
}
|
|
408
|
+
if (format === 'json') {
|
|
409
|
+
const twice = columns.find((c, i) => columns.indexOf(c) !== i)
|
|
410
|
+
if (twice !== undefined) {
|
|
411
|
+
throw new UserError(
|
|
412
|
+
`vx last: --sql: column ${JSON.stringify(twice)} is named twice, and a JSON row holds one value per name; name each with AS`,
|
|
413
|
+
'VX_E_USAGE',
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
const rows = values.map((v) => Object.fromEntries(columns.map((c, i) => [c, v[i]])))
|
|
417
|
+
process.stdout.write(`${JSON.stringify({ rows })}\n`)
|
|
418
|
+
return 0
|
|
419
|
+
}
|
|
420
|
+
if (values.length === 0) return 0
|
|
421
|
+
// A column's value as SQLite returns it: TEXT, a number, NULL or a BLOB.
|
|
422
|
+
const cell = (v: string | number | bigint | Uint8Array | null): string =>
|
|
423
|
+
v === null || v === undefined
|
|
424
|
+
? '-'
|
|
425
|
+
: v instanceof Uint8Array
|
|
426
|
+
? `<${v.length} bytes>`
|
|
427
|
+
: String(v)
|
|
428
|
+
const table = [columns, ...values.map((r) => r.map((v) => cell(v as Parameters<typeof cell>[0])))]
|
|
429
|
+
const widths = table[0]!.map((_, c) => Math.max(...table.map((r) => r[c]!.length)))
|
|
430
|
+
const lines = table.map((r) =>
|
|
431
|
+
r
|
|
432
|
+
.map((v, c) => v.padEnd(widths[c]!))
|
|
433
|
+
.join(' ')
|
|
434
|
+
.trimEnd(),
|
|
435
|
+
)
|
|
436
|
+
process.stdout.write(`${lines.join('\n')}\n`)
|
|
437
|
+
return 0
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** How long a run has to answer before `--live` gives up on it. */
|
|
441
|
+
const LIVE_ANSWER_MS = 5_000
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* `--live`: ask the run in progress (orchestrator/run-live.ts) for its
|
|
445
|
+
* snapshot, and with `--follow` print its events until it lets go. The
|
|
446
|
+
* history is not opened: the run itself answers.
|
|
447
|
+
*/
|
|
448
|
+
async function printLive(
|
|
449
|
+
root: string,
|
|
450
|
+
follow: boolean,
|
|
451
|
+
format: 'pretty' | 'json',
|
|
452
|
+
): Promise<number> {
|
|
453
|
+
const none = (): never => {
|
|
454
|
+
throw refusal(
|
|
455
|
+
{ tone: 'info', headline: 'no run in progress', fix: 'vx last' },
|
|
456
|
+
{
|
|
457
|
+
code: 'VX_E_NO_HISTORY',
|
|
458
|
+
message: 'vx last: no run in progress — `vx last` reads the last one',
|
|
459
|
+
},
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
let buffered = ''
|
|
463
|
+
let snapshot: LiveSnapshot | undefined
|
|
464
|
+
let settle!: (answered: boolean) => void
|
|
465
|
+
const done = new Promise<boolean>((resolve) => (settle = resolve))
|
|
466
|
+
const line = (text: string): void => {
|
|
467
|
+
if (snapshot === undefined) {
|
|
468
|
+
snapshot = JSON.parse(text) as LiveSnapshot
|
|
469
|
+
if (format === 'json') process.stdout.write(`${text}\n`)
|
|
470
|
+
else process.stdout.write(`${formatLive(snapshot).join('\n')}\n`)
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
if (format === 'json') process.stdout.write(`${text}\n`)
|
|
474
|
+
else process.stdout.write(formatLiveEvent(JSON.parse(text) as WireEvent))
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
await Bun.connect({
|
|
478
|
+
unix: liveSocketPath(root),
|
|
479
|
+
socket: {
|
|
480
|
+
open(sock) {
|
|
481
|
+
sock.write(follow ? 'follow\n' : 'snapshot\n')
|
|
482
|
+
},
|
|
483
|
+
data(_sock, chunk) {
|
|
484
|
+
buffered += chunk.toString()
|
|
485
|
+
for (let at = buffered.indexOf('\n'); at !== -1; at = buffered.indexOf('\n')) {
|
|
486
|
+
line(buffered.slice(0, at))
|
|
487
|
+
buffered = buffered.slice(at + 1)
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
close() {
|
|
491
|
+
settle(snapshot !== undefined)
|
|
492
|
+
},
|
|
493
|
+
error() {
|
|
494
|
+
settle(snapshot !== undefined)
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
})
|
|
498
|
+
} catch {
|
|
499
|
+
// ENOENT, ECONNREFUSED: no run listens there (a killed run's socket).
|
|
500
|
+
none()
|
|
501
|
+
}
|
|
502
|
+
const timer = follow
|
|
503
|
+
? undefined
|
|
504
|
+
: setTimeout(() => settle(snapshot !== undefined), LIVE_ANSWER_MS)
|
|
505
|
+
const answered = await done
|
|
506
|
+
clearTimeout(timer)
|
|
507
|
+
if (!answered) none()
|
|
508
|
+
return 0
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function formatLive(s: LiveSnapshot): string[] {
|
|
512
|
+
const count = (status: string): number => s.tasks.filter((t) => t.status === status).length
|
|
513
|
+
const running = count('running')
|
|
514
|
+
const waiting = count('waiting')
|
|
515
|
+
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
|
+
const now = s.live.startedAt + s.live.elapsedMs
|
|
525
|
+
for (const t of s.tasks) {
|
|
526
|
+
const ms =
|
|
527
|
+
t.durationMs !== null ? t.durationMs : t.startedAt !== null ? now - t.startedAt : null
|
|
528
|
+
lines.push(
|
|
529
|
+
` ${t.status.padEnd(width)} ${ms === null ? t.taskId : `${t.taskId.padEnd(idWidth)} ${fmtMs(ms)}`}`,
|
|
530
|
+
)
|
|
531
|
+
}
|
|
532
|
+
if (s.warnings.length > 0) {
|
|
533
|
+
lines.push('', ' warnings', ...s.warnings.flatMap((w) => w.split('\n').map((l) => ` ${l}`)))
|
|
534
|
+
}
|
|
535
|
+
return lines
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** One followed event as a line: a task's output prefixed by its id, an end, a warning. */
|
|
539
|
+
function formatLiveEvent(e: WireEvent): string {
|
|
540
|
+
switch (e.kind) {
|
|
541
|
+
case 'task:stdout':
|
|
542
|
+
case 'task:stderr':
|
|
543
|
+
return e.chunk
|
|
544
|
+
.replace(/\n$/, '')
|
|
545
|
+
.split('\n')
|
|
546
|
+
.map((l) => `${e.taskId} │ ${l}\n`)
|
|
547
|
+
.join('')
|
|
548
|
+
case 'task:complete':
|
|
549
|
+
return `${outcomeWord(e.outcome)} ${e.outcome.taskId} ${fmtMs(e.outcome.durationMs)}\n`
|
|
550
|
+
case 'run:status':
|
|
551
|
+
return 'warn' in e ? `warn ${Bun.stripANSI(e.line)}\n` : ''
|
|
552
|
+
default:
|
|
553
|
+
return ''
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
279
557
|
const fmtWhen = (ms: number): string => new Date(ms).toISOString()
|
|
280
558
|
|
|
281
559
|
const fmtMs = (ms: number): string => formatElapsed(ms, true)
|
|
@@ -283,7 +561,7 @@ const fmtMs = (ms: number): string => formatElapsed(ms, true)
|
|
|
283
561
|
/**
|
|
284
562
|
* What an executed task used — its peak RSS and its CPU parallelism (CPU
|
|
285
563
|
* time over wall time) — as the runner recorded them. This is the number
|
|
286
|
-
* `@vzn/vx-
|
|
564
|
+
* `@vzn/vx-history` reserves from, so the replay shows it where
|
|
287
565
|
* a developer can read it; a hit spent nothing and shows nothing.
|
|
288
566
|
*/
|
|
289
567
|
function fmtUsage(t: {
|
|
@@ -304,13 +582,21 @@ function fmtUsage(t: {
|
|
|
304
582
|
* their exit stands for, the sandbox's violation count, a skip's blocker.
|
|
305
583
|
* Older rows carry none and read as before.
|
|
306
584
|
*/
|
|
307
|
-
function reasonParts(
|
|
585
|
+
function reasonParts(
|
|
586
|
+
t: RunSummaryRow,
|
|
587
|
+
skippedHere: ReadonlySet<string>,
|
|
588
|
+
inputMoved: string | undefined,
|
|
589
|
+
): string {
|
|
308
590
|
const parts: string[] = []
|
|
309
591
|
if (t.status === 'failed') {
|
|
310
592
|
if (t.notReady !== null) {
|
|
311
593
|
parts.push(
|
|
312
594
|
`never ready: ${t.notReady === 'timeout' ? 'timed out' : t.notReady === 'exited' ? 'exited' : 'spawn failed'}`,
|
|
313
595
|
)
|
|
596
|
+
} else if (inputMoved !== undefined) {
|
|
597
|
+
// `inputStability.severity: 'error'` forced the exit; the run keeps
|
|
598
|
+
// the input with the failure's output, not in the row.
|
|
599
|
+
parts.push(`${inputMoved === '' ? 'an input' : inputMoved} changed during the run`)
|
|
314
600
|
} else if (t.timedOut === true) {
|
|
315
601
|
parts.push('timed out')
|
|
316
602
|
} else {
|
|
@@ -321,27 +607,41 @@ function reasonParts(t: RunSummaryRow): string {
|
|
|
321
607
|
parts.push(`${t.sandboxViolations} sandbox violation${t.sandboxViolations === 1 ? '' : 's'}`)
|
|
322
608
|
}
|
|
323
609
|
} else if (t.status === 'skipped' && t.blockedBy !== null) {
|
|
324
|
-
|
|
610
|
+
// A blocker that is itself a skip is a platform skip: a failure's
|
|
611
|
+
// dependants name the failure, never a skip between.
|
|
612
|
+
parts.push(
|
|
613
|
+
skippedHere.has(t.blockedBy)
|
|
614
|
+
? `after ${t.blockedBy} skipped: ${OTHER_PLATFORM}`
|
|
615
|
+
: `after ${t.blockedBy} failed`,
|
|
616
|
+
)
|
|
617
|
+
} else if (t.status === 'skipped' && t.exitCode === 0) {
|
|
618
|
+
// Exit 0 with no blocker is a platform skip; a fail-fast skip records 1.
|
|
619
|
+
parts.push(OTHER_PLATFORM)
|
|
325
620
|
}
|
|
621
|
+
// The run's own row said `flaky - 2 attempts`: a pass that needed a
|
|
622
|
+
// retry is the one sign of a flaky task a replay can give.
|
|
623
|
+
if (t.attempts !== null && t.attempts > 1) parts.push(`${t.attempts} attempts`)
|
|
326
624
|
if (t.remoteError !== null) parts.push(`remote \u2717 ${t.remoteError}`)
|
|
327
625
|
return parts.map((p) => ` ${p}`).join('')
|
|
328
626
|
}
|
|
329
627
|
|
|
628
|
+
const OTHER_PLATFORM = 'needs another platform'
|
|
629
|
+
|
|
330
630
|
/** Hit rows shown before the rest fold — the slowest restores. */
|
|
331
631
|
const HITS_SHOWN = 16
|
|
332
632
|
|
|
333
633
|
/**
|
|
334
634
|
* The steps the run did not wait on (`vx:prefetch`), which its own output
|
|
335
|
-
* never showed (
|
|
635
|
+
* never showed (2026-10-16): here for debugging, marked `internal`.
|
|
336
636
|
*/
|
|
337
|
-
function formatStepRows(steps: readonly RunStep[]): string[] {
|
|
637
|
+
function formatStepRows(steps: readonly RunStep[], c: ColorSupport): string[] {
|
|
338
638
|
if (steps.length === 0) return []
|
|
339
639
|
const idW = Math.max(...steps.map((s) => s.id.length), 4)
|
|
340
640
|
return [
|
|
341
641
|
'',
|
|
342
642
|
...steps.map(
|
|
343
643
|
(s) =>
|
|
344
|
-
` ${(s.ok ? 'success' : 'failed').padEnd(17)} ${s.id.padEnd(idW)} ${fmtMs(s.durationMs).padStart(8)} internal`,
|
|
644
|
+
` ${paint(s.ok ? SUCCESS : ERROR, (s.ok ? 'success' : 'failed').padEnd(17), c)} ${paint('', s.id.padEnd(idW), c, { bold: true })} ${paint('', `${fmtMs(s.durationMs).padStart(8)} internal`, c, { dim: true })}`,
|
|
345
645
|
),
|
|
346
646
|
]
|
|
347
647
|
}
|
|
@@ -355,7 +655,12 @@ function formatStepRows(steps: readonly RunStep[]): string[] {
|
|
|
355
655
|
* row tells. `--format json` lists every row.
|
|
356
656
|
*/
|
|
357
657
|
|
|
358
|
-
export function formatTaskRows(
|
|
658
|
+
export function formatTaskRows(
|
|
659
|
+
tasks: readonly RunSummaryRow[],
|
|
660
|
+
c: ColorSupport = { enabled: false },
|
|
661
|
+
/** Task id → the input that moved under it, from the run's kept failures. */
|
|
662
|
+
inputMoved: ReadonlyMap<string, string> = new Map(),
|
|
663
|
+
): string[] {
|
|
359
664
|
if (tasks.length === 0) return []
|
|
360
665
|
const failed = tasks.filter((t) => t.status === 'failed')
|
|
361
666
|
const hits = tasks.filter((t) => t.status !== 'failed' && t.cacheHit === true)
|
|
@@ -365,10 +670,15 @@ export function formatTaskRows(tasks: readonly RunSummaryRow[]): string[] {
|
|
|
365
670
|
? [...hits].sort((a, b) => b.durationMs - a.durationMs).slice(0, HITS_SHOWN)
|
|
366
671
|
: hits
|
|
367
672
|
const ordered = [...failed, ...rest, ...shownHits]
|
|
368
|
-
const
|
|
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
|
+
const skippedHere = new Set(
|
|
677
|
+
tasks.filter((t) => t.status === 'skipped').map((t) => `${t.project}#${t.task}`),
|
|
678
|
+
)
|
|
369
679
|
const lines = ['']
|
|
370
680
|
for (const t of ordered) {
|
|
371
|
-
const id = `${t.project}#${t.task}`
|
|
681
|
+
const id = `${t.project}#${t.task}${at(t)}`
|
|
372
682
|
// The terminal summary's own word for a task that runs every time
|
|
373
683
|
// by design; a reader must not take its row for a miss.
|
|
374
684
|
// A failure's row reads as the frame did — `failed (exit 137)` —
|
|
@@ -381,15 +691,26 @@ export function formatTaskRows(tasks: readonly RunSummaryRow[]): string[] {
|
|
|
381
691
|
: t.cacheHit === true && t.restored !== null
|
|
382
692
|
? outcomeWord({ status: t.status as 'cache-hit', restored: t.restored })
|
|
383
693
|
: t.status
|
|
694
|
+
const hue = t.status === 'failed' ? ERROR : t.status === 'skipped' ? '' : SUCCESS
|
|
384
695
|
lines.push(
|
|
385
|
-
` ${status.padEnd(17)} ${id.
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
),
|
|
388
704
|
)
|
|
389
705
|
}
|
|
390
706
|
if (folded) {
|
|
391
707
|
lines.push(
|
|
392
|
-
|
|
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 },
|
|
713
|
+
),
|
|
393
714
|
)
|
|
394
715
|
}
|
|
395
716
|
return lines
|
|
@@ -436,22 +757,43 @@ export function formatRunList(
|
|
|
436
757
|
>
|
|
437
758
|
id: string
|
|
438
759
|
}[],
|
|
760
|
+
c: ColorSupport = { enabled: false },
|
|
439
761
|
): string[] {
|
|
440
762
|
const rows = runs.map(({ inv, id }) => ({
|
|
441
763
|
head: `${inv.exitOk ? 'ok ' : 'FAILED'} ${fmtWhen(inv.startedAt)}`,
|
|
442
764
|
id,
|
|
443
|
-
counts:
|
|
444
|
-
|
|
445
|
-
(inv.failedCount > 0 ? ` · ${inv.failedCount} failed` : ''),
|
|
765
|
+
counts: `${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}`,
|
|
766
|
+
failed: inv.failedCount > 0 ? ` · ${inv.failedCount} failed` : '',
|
|
446
767
|
ms: fmtMs(inv.totalDurationMs),
|
|
447
768
|
command: inv.command,
|
|
448
769
|
}))
|
|
449
770
|
const idW = Math.max(...rows.map((r) => r.id.length))
|
|
450
|
-
const countsW = Math.max(...rows.map((r) => r.counts.length))
|
|
771
|
+
const countsW = Math.max(...rows.map((r) => r.counts.length + r.failed.length))
|
|
451
772
|
const msW = Math.max(...rows.map((r) => r.ms.length))
|
|
452
773
|
return rows.map(
|
|
453
774
|
(r) =>
|
|
454
|
-
`${r.head} ${r.id.padEnd(idW)} ${r.counts.
|
|
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}`,
|
|
776
|
+
)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* A bare task name as `--log` reads it: the one project that ran it, the
|
|
781
|
+
* cwd's among several (as `vx why` picks), else refused naming them. It
|
|
782
|
+
* read as an id and said "no recorded run of build" for a task that ran.
|
|
783
|
+
*/
|
|
784
|
+
async function bareTaskId(db: Database, task: string): Promise<string> {
|
|
785
|
+
if (task.includes('#')) return task
|
|
786
|
+
const ids = (
|
|
787
|
+
db.query('SELECT DISTINCT project FROM runs WHERE task = ? ORDER BY project').all(task) as {
|
|
788
|
+
project: string
|
|
789
|
+
}[]
|
|
790
|
+
).map((r) => `${r.project}#${task}`)
|
|
791
|
+
if (ids.length <= 1) return ids[0] ?? task
|
|
792
|
+
const here = await findCwdProject(process.cwd())
|
|
793
|
+
if (here !== null && ids.includes(`${here}#${task}`)) return `${here}#${task}`
|
|
794
|
+
throw new UserError(
|
|
795
|
+
`vx last: "${task}" ran in ${ids.length} projects — pick one:\n${ids.map((id) => ` ${id}`).join('\n')}`,
|
|
796
|
+
'VX_E_AMBIGUOUS',
|
|
455
797
|
)
|
|
456
798
|
}
|
|
457
799
|
|
|
@@ -460,6 +802,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
460
802
|
if (parsed.error !== undefined) throw new UserError(`vx last: ${parsed.error}`, 'VX_E_USAGE')
|
|
461
803
|
|
|
462
804
|
const root = await findWorkspaceRoot(process.cwd())
|
|
805
|
+
if (parsed.live === true) return await printLive(root, parsed.follow === true, parsed.format)
|
|
463
806
|
const cacheDir = await cliCacheDir(root, parsed.cacheDir)
|
|
464
807
|
const cache = Cache.inspect(cacheDir)
|
|
465
808
|
try {
|
|
@@ -469,9 +812,12 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
469
812
|
// nothing, while `--list --failed` past green runs said there were none.
|
|
470
813
|
const noRuns = (): boolean => listInvocations(db, { limit: 1 }).length === 0
|
|
471
814
|
|
|
815
|
+
if (parsed.sql !== undefined) return printQuery(db, parsed.sql, parsed.format)
|
|
816
|
+
|
|
472
817
|
if (parsed.stats === true) {
|
|
473
818
|
const branch = currentBranch(root, undefined, parsed.branch)
|
|
474
|
-
|
|
819
|
+
const task = parsed.task === undefined ? undefined : await rootSpelled(root, parsed.task)
|
|
820
|
+
return await printStats(db, task, parsed.format, branch)
|
|
475
821
|
}
|
|
476
822
|
|
|
477
823
|
if (parsed.log !== undefined) {
|
|
@@ -479,10 +825,18 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
479
825
|
parsed.runId === undefined
|
|
480
826
|
? undefined
|
|
481
827
|
: (resolveRunId(db, parsed.runId, 'vx last') ?? parsed.runId)
|
|
482
|
-
const log = taskLog(
|
|
828
|
+
const log = taskLog(
|
|
829
|
+
cacheDir,
|
|
830
|
+
db,
|
|
831
|
+
await bareTaskId(db, await rootSpelled(root, parsed.log)),
|
|
832
|
+
runId,
|
|
833
|
+
)
|
|
483
834
|
if (log === null) {
|
|
835
|
+
// 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.
|
|
484
837
|
throw new UserError(
|
|
485
|
-
`vx last: no recorded run of ${parsed.log}${parsed.runId === undefined ? '' : ` in run ${parsed.runId}`} (a task id is project#task)`,
|
|
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)'}`,
|
|
839
|
+
'VX_E_NO_HISTORY',
|
|
486
840
|
)
|
|
487
841
|
}
|
|
488
842
|
if (parsed.format === 'json') {
|
|
@@ -490,11 +844,27 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
490
844
|
return 0
|
|
491
845
|
}
|
|
492
846
|
if (log.source === null) {
|
|
847
|
+
// A cached task with no entry was not saved (a read-only policy) or
|
|
848
|
+
// was pruned; calling it uncached sends the user to the config.
|
|
849
|
+
const [project, task] = splitTaskId(log.taskId)
|
|
850
|
+
const cached =
|
|
851
|
+
log.status !== 'failed' &&
|
|
852
|
+
(
|
|
853
|
+
db
|
|
854
|
+
.query('SELECT cached FROM runs WHERE run_id = ? AND project = ? AND task = ?')
|
|
855
|
+
.get(log.runId, project, task) as { cached: number | null } | null
|
|
856
|
+
)?.cached === 1
|
|
493
857
|
sayNotice(
|
|
494
858
|
'info',
|
|
495
859
|
'vx last',
|
|
496
|
-
|
|
860
|
+
log.status === 'failed'
|
|
861
|
+
? `${log.taskId} ended failed, and vx kept no output for it: it keeps the newest 50 failed runs`
|
|
862
|
+
: cached
|
|
863
|
+
? `${log.taskId} ended ${log.status}, and its entry is not in the cache: the run did not save it, or it was pruned since`
|
|
864
|
+
: `${log.taskId} ended ${log.status}. vx keeps output only for failed and cached tasks`,
|
|
497
865
|
)
|
|
866
|
+
} else if (log.output === '') {
|
|
867
|
+
sayNotice('info', 'vx last', `${log.taskId} ended ${log.status} and printed nothing`)
|
|
498
868
|
}
|
|
499
869
|
process.stdout.write(log.output)
|
|
500
870
|
return 0
|
|
@@ -516,6 +886,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
516
886
|
}
|
|
517
887
|
const lines = formatRunList(
|
|
518
888
|
invocations.map((inv) => ({ inv, id: shortRunId(db, inv.runId) })),
|
|
889
|
+
detectColors(process.stdout),
|
|
519
890
|
)
|
|
520
891
|
process.stdout.write(`${lines.join('\n')}\n`)
|
|
521
892
|
return 0
|
|
@@ -537,11 +908,13 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
537
908
|
throw new UserError(
|
|
538
909
|
noRuns()
|
|
539
910
|
? 'vx last: no recorded runs yet — run something first'
|
|
540
|
-
: parsed.runId
|
|
541
|
-
? `vx last:
|
|
542
|
-
: parsed.
|
|
543
|
-
?
|
|
544
|
-
:
|
|
911
|
+
: parsed.runId?.includes('#') === true
|
|
912
|
+
? `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`
|
|
913
|
+
: parsed.runId !== undefined
|
|
914
|
+
? `vx last: no recorded run ${parsed.runId} (vx last --list shows recent runs)`
|
|
915
|
+
: parsed.failed === true
|
|
916
|
+
? 'vx last: no recorded run failed'
|
|
917
|
+
: 'vx last: no recorded runs yet — run something first',
|
|
545
918
|
'VX_E_NO_HISTORY',
|
|
546
919
|
)
|
|
547
920
|
}
|
|
@@ -565,27 +938,48 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
565
938
|
}
|
|
566
939
|
})
|
|
567
940
|
const steps = runSteps(cacheDir, inv.runId)
|
|
941
|
+
const warnings = runWarnings(cacheDir, inv.runId)
|
|
568
942
|
process.stdout.write(
|
|
569
|
-
`${JSON.stringify({
|
|
943
|
+
`${JSON.stringify({
|
|
944
|
+
invocation: inv,
|
|
945
|
+
tasks,
|
|
946
|
+
...(steps.length > 0 ? { steps } : {}),
|
|
947
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
948
|
+
})}\n`,
|
|
570
949
|
)
|
|
571
950
|
return 0
|
|
572
951
|
}
|
|
573
952
|
|
|
953
|
+
const c = detectColors(process.stdout)
|
|
954
|
+
const dim = (s: string): string => paint('', s, c, { dim: true })
|
|
574
955
|
const lines: string[] = []
|
|
575
|
-
lines.push(`run ${inv.runId} — ${inv.exitOk ? 'ok' : 'FAILED'}`)
|
|
576
|
-
lines.push(` $ ${inv.command}`)
|
|
577
956
|
lines.push(
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
+
),
|
|
582
967
|
)
|
|
583
968
|
lines.push(
|
|
584
|
-
` ${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}` +
|
|
585
|
-
`${inv.failedCount > 0 ? ` · ${inv.failedCount} failed
|
|
969
|
+
dim(` ${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}`) +
|
|
970
|
+
`${inv.failedCount > 0 ? paint(ERROR, ` · ${inv.failedCount} failed`, c) : ''}`,
|
|
586
971
|
)
|
|
587
|
-
|
|
588
|
-
|
|
972
|
+
const moved = new Map<string, string>()
|
|
973
|
+
if (inv.failedCount > 0) {
|
|
974
|
+
for (const f of runFailures(cacheDir, db, inv.runId)?.tasks ?? [])
|
|
975
|
+
if (f.inputMoved !== undefined) moved.set(f.taskId, f.inputMoved)
|
|
976
|
+
}
|
|
977
|
+
lines.push(...formatTaskRows(detail?.tasks ?? [], c, moved))
|
|
978
|
+
lines.push(...formatStepRows(runSteps(cacheDir, inv.runId), c))
|
|
979
|
+
const warnings = runWarnings(cacheDir, inv.runId)
|
|
980
|
+
if (warnings.length > 0) {
|
|
981
|
+
lines.push('', ' warnings', ...warnings.flatMap((w) => w.split('\n').map((l) => ` ${l}`)))
|
|
982
|
+
}
|
|
589
983
|
// The next command after reading a failed run, with the arguments the
|
|
590
984
|
// run forwarded to the tasks that got them (a failure under `-- --shard
|
|
591
985
|
// 2` is that shard's); a dependency never got them, so its line has none.
|
|
@@ -597,9 +991,11 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
597
991
|
for (const withArgs of [false, true]) {
|
|
598
992
|
const ids = failed
|
|
599
993
|
.filter((t) => t.forwarded === withArgs)
|
|
600
|
-
.map((t) => `${t.project}#${t.task}`)
|
|
994
|
+
.map((t) => shellQuote(`${t.project}#${t.task}`))
|
|
601
995
|
if (ids.length > 0)
|
|
602
|
-
rerun.push(
|
|
996
|
+
rerun.push(
|
|
997
|
+
` ${dim('re-run what failed:')} ${paint('', `vx run ${ids.join(' ')}${withArgs ? args : ''}`, c, { bold: true })}`,
|
|
998
|
+
)
|
|
603
999
|
}
|
|
604
1000
|
lines.push('', ...rerun)
|
|
605
1001
|
}
|