@vzn/vx 0.0.646 → 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 +29 -22
- 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 +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -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/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- 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 +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- 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 +270 -60
- 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 +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- 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 +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- 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 +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- 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 +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- 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 +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- 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 +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- 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 +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- 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 +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -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 +61 -3
- 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 +17 -11
- 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 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
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)
|
|
@@ -368,6 +673,9 @@ export function formatTaskRows(tasks: readonly RunSummaryRow[]): string[] {
|
|
|
368
673
|
const at = (t: (typeof ordered)[number]): string =>
|
|
369
674
|
t.placement !== null ? `@${t.placement}` : ''
|
|
370
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
|
+
)
|
|
371
679
|
const lines = ['']
|
|
372
680
|
for (const t of ordered) {
|
|
373
681
|
const id = `${t.project}#${t.task}${at(t)}`
|
|
@@ -383,15 +691,26 @@ export function formatTaskRows(tasks: readonly RunSummaryRow[]): string[] {
|
|
|
383
691
|
: t.cacheHit === true && t.restored !== null
|
|
384
692
|
? outcomeWord({ status: t.status as 'cache-hit', restored: t.restored })
|
|
385
693
|
: t.status
|
|
694
|
+
const hue = t.status === 'failed' ? ERROR : t.status === 'skipped' ? '' : SUCCESS
|
|
386
695
|
lines.push(
|
|
387
|
-
` ${status.padEnd(17)} ${id.
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
),
|
|
390
704
|
)
|
|
391
705
|
}
|
|
392
706
|
if (folded) {
|
|
393
707
|
lines.push(
|
|
394
|
-
|
|
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
|
+
),
|
|
395
714
|
)
|
|
396
715
|
}
|
|
397
716
|
return lines
|
|
@@ -438,22 +757,43 @@ export function formatRunList(
|
|
|
438
757
|
>
|
|
439
758
|
id: string
|
|
440
759
|
}[],
|
|
760
|
+
c: ColorSupport = { enabled: false },
|
|
441
761
|
): string[] {
|
|
442
762
|
const rows = runs.map(({ inv, id }) => ({
|
|
443
763
|
head: `${inv.exitOk ? 'ok ' : 'FAILED'} ${fmtWhen(inv.startedAt)}`,
|
|
444
764
|
id,
|
|
445
|
-
counts:
|
|
446
|
-
|
|
447
|
-
(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` : '',
|
|
448
767
|
ms: fmtMs(inv.totalDurationMs),
|
|
449
768
|
command: inv.command,
|
|
450
769
|
}))
|
|
451
770
|
const idW = Math.max(...rows.map((r) => r.id.length))
|
|
452
|
-
const countsW = Math.max(...rows.map((r) => r.counts.length))
|
|
771
|
+
const countsW = Math.max(...rows.map((r) => r.counts.length + r.failed.length))
|
|
453
772
|
const msW = Math.max(...rows.map((r) => r.ms.length))
|
|
454
773
|
return rows.map(
|
|
455
774
|
(r) =>
|
|
456
|
-
`${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',
|
|
457
797
|
)
|
|
458
798
|
}
|
|
459
799
|
|
|
@@ -462,6 +802,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
462
802
|
if (parsed.error !== undefined) throw new UserError(`vx last: ${parsed.error}`, 'VX_E_USAGE')
|
|
463
803
|
|
|
464
804
|
const root = await findWorkspaceRoot(process.cwd())
|
|
805
|
+
if (parsed.live === true) return await printLive(root, parsed.follow === true, parsed.format)
|
|
465
806
|
const cacheDir = await cliCacheDir(root, parsed.cacheDir)
|
|
466
807
|
const cache = Cache.inspect(cacheDir)
|
|
467
808
|
try {
|
|
@@ -471,9 +812,12 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
471
812
|
// nothing, while `--list --failed` past green runs said there were none.
|
|
472
813
|
const noRuns = (): boolean => listInvocations(db, { limit: 1 }).length === 0
|
|
473
814
|
|
|
815
|
+
if (parsed.sql !== undefined) return printQuery(db, parsed.sql, parsed.format)
|
|
816
|
+
|
|
474
817
|
if (parsed.stats === true) {
|
|
475
818
|
const branch = currentBranch(root, undefined, parsed.branch)
|
|
476
|
-
|
|
819
|
+
const task = parsed.task === undefined ? undefined : await rootSpelled(root, parsed.task)
|
|
820
|
+
return await printStats(db, task, parsed.format, branch)
|
|
477
821
|
}
|
|
478
822
|
|
|
479
823
|
if (parsed.log !== undefined) {
|
|
@@ -481,10 +825,18 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
481
825
|
parsed.runId === undefined
|
|
482
826
|
? undefined
|
|
483
827
|
: (resolveRunId(db, parsed.runId, 'vx last') ?? parsed.runId)
|
|
484
|
-
const log = taskLog(
|
|
828
|
+
const log = taskLog(
|
|
829
|
+
cacheDir,
|
|
830
|
+
db,
|
|
831
|
+
await bareTaskId(db, await rootSpelled(root, parsed.log)),
|
|
832
|
+
runId,
|
|
833
|
+
)
|
|
485
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.
|
|
486
837
|
throw new UserError(
|
|
487
|
-
`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',
|
|
488
840
|
)
|
|
489
841
|
}
|
|
490
842
|
if (parsed.format === 'json') {
|
|
@@ -492,11 +844,27 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
492
844
|
return 0
|
|
493
845
|
}
|
|
494
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
|
|
495
857
|
sayNotice(
|
|
496
858
|
'info',
|
|
497
859
|
'vx last',
|
|
498
|
-
|
|
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`,
|
|
499
865
|
)
|
|
866
|
+
} else if (log.output === '') {
|
|
867
|
+
sayNotice('info', 'vx last', `${log.taskId} ended ${log.status} and printed nothing`)
|
|
500
868
|
}
|
|
501
869
|
process.stdout.write(log.output)
|
|
502
870
|
return 0
|
|
@@ -518,6 +886,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
518
886
|
}
|
|
519
887
|
const lines = formatRunList(
|
|
520
888
|
invocations.map((inv) => ({ inv, id: shortRunId(db, inv.runId) })),
|
|
889
|
+
detectColors(process.stdout),
|
|
521
890
|
)
|
|
522
891
|
process.stdout.write(`${lines.join('\n')}\n`)
|
|
523
892
|
return 0
|
|
@@ -539,11 +908,13 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
539
908
|
throw new UserError(
|
|
540
909
|
noRuns()
|
|
541
910
|
? 'vx last: no recorded runs yet — run something first'
|
|
542
|
-
: parsed.runId
|
|
543
|
-
? `vx last:
|
|
544
|
-
: parsed.
|
|
545
|
-
?
|
|
546
|
-
:
|
|
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',
|
|
547
918
|
'VX_E_NO_HISTORY',
|
|
548
919
|
)
|
|
549
920
|
}
|
|
@@ -567,27 +938,48 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
567
938
|
}
|
|
568
939
|
})
|
|
569
940
|
const steps = runSteps(cacheDir, inv.runId)
|
|
941
|
+
const warnings = runWarnings(cacheDir, inv.runId)
|
|
570
942
|
process.stdout.write(
|
|
571
|
-
`${JSON.stringify({
|
|
943
|
+
`${JSON.stringify({
|
|
944
|
+
invocation: inv,
|
|
945
|
+
tasks,
|
|
946
|
+
...(steps.length > 0 ? { steps } : {}),
|
|
947
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
948
|
+
})}\n`,
|
|
572
949
|
)
|
|
573
950
|
return 0
|
|
574
951
|
}
|
|
575
952
|
|
|
953
|
+
const c = detectColors(process.stdout)
|
|
954
|
+
const dim = (s: string): string => paint('', s, c, { dim: true })
|
|
576
955
|
const lines: string[] = []
|
|
577
|
-
lines.push(`run ${inv.runId} — ${inv.exitOk ? 'ok' : 'FAILED'}`)
|
|
578
|
-
lines.push(` $ ${inv.command}`)
|
|
579
956
|
lines.push(
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
),
|
|
584
967
|
)
|
|
585
968
|
lines.push(
|
|
586
|
-
` ${inv.taskCount} task${inv.taskCount === 1 ? '' : 's'} · ${hitsLine(inv)}` +
|
|
587
|
-
`${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) : ''}`,
|
|
588
971
|
)
|
|
589
|
-
|
|
590
|
-
|
|
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
|
+
}
|
|
591
983
|
// The next command after reading a failed run, with the arguments the
|
|
592
984
|
// run forwarded to the tasks that got them (a failure under `-- --shard
|
|
593
985
|
// 2` is that shard's); a dependency never got them, so its line has none.
|
|
@@ -599,9 +991,11 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
599
991
|
for (const withArgs of [false, true]) {
|
|
600
992
|
const ids = failed
|
|
601
993
|
.filter((t) => t.forwarded === withArgs)
|
|
602
|
-
.map((t) => `${t.project}#${t.task}`)
|
|
994
|
+
.map((t) => shellQuote(`${t.project}#${t.task}`))
|
|
603
995
|
if (ids.length > 0)
|
|
604
|
-
rerun.push(
|
|
996
|
+
rerun.push(
|
|
997
|
+
` ${dim('re-run what failed:')} ${paint('', `vx run ${ids.join(' ')}${withArgs ? args : ''}`, c, { bold: true })}`,
|
|
998
|
+
)
|
|
605
999
|
}
|
|
606
1000
|
lines.push('', ...rerun)
|
|
607
1001
|
}
|