@vzn/vx 0.0.652 → 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 +2 -2
- package/launcher.sh +2 -1
- package/package.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/show.json +1 -0
- package/skills/vx/SKILL.md +3 -3
- package/src/bin.ts +25 -3
- package/src/cache/archive.ts +196 -31
- package/src/cache/cache.ts +863 -225
- 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 +22 -5
- package/src/cache/index.ts +10 -1
- package/src/cache/inputs.ts +573 -152
- package/src/cache/key-fold.ts +1 -1
- package/src/cache/layer.ts +23 -10
- package/src/cache/layered-cache.ts +28 -7
- 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 +50 -10
- package/src/cache/schema.ts +42 -9
- package/src/cli/cache.ts +147 -34
- 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 +25 -2
- package/src/cli/index.ts +38 -7
- package/src/cli/info.ts +4 -3
- package/src/cli/init.ts +55 -20
- package/src/cli/last.ts +184 -41
- 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 +1 -1
- package/src/cli/run.ts +68 -34
- package/src/cli/select.ts +160 -64
- package/src/cli/show.ts +63 -18
- 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 +72 -8
- package/src/cli/watch-set.ts +6 -4
- package/src/cli/watch.ts +179 -25
- package/src/cli/why.ts +107 -33
- package/src/cli/workspace-config.ts +11 -1
- package/src/config.ts +9 -0
- package/src/exec/env.ts +3 -1
- package/src/exec/executor.ts +76 -15
- 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-runtime.ts +134 -37
- package/src/exec/sandbox-violations.ts +250 -46
- package/src/exec/spawn-off.ts +10 -5
- package/src/exec/warm-shell.ts +29 -7
- 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 +173 -38
- package/src/graph/stand-ins.ts +36 -0
- package/src/graph/task-graph.ts +327 -141
- package/src/graph/task-status.ts +5 -1
- package/src/handoff.ts +45 -10
- package/src/history/failure-mode.ts +15 -5
- package/src/history/history.ts +103 -30
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +189 -69
- package/src/index.ts +2 -1
- package/src/orchestrator/admission.ts +13 -9
- package/src/orchestrator/affected-tasks.ts +35 -13
- package/src/orchestrator/command-windows.ts +8 -5
- package/src/orchestrator/deferred-outputs.ts +41 -12
- package/src/orchestrator/doctor.ts +21 -9
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/excluded-keys.ts +14 -2
- package/src/orchestrator/execute-args.ts +9 -2
- package/src/orchestrator/execute-task.ts +564 -123
- 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 +136 -87
- package/src/orchestrator/index.ts +4 -1
- package/src/orchestrator/keyed-projects.ts +25 -0
- package/src/orchestrator/local-floor.ts +21 -3
- package/src/orchestrator/lockfile-claim.ts +30 -4
- package/src/orchestrator/miss-save.ts +33 -53
- 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 +31 -17
- package/src/orchestrator/persistent.ts +152 -15
- package/src/orchestrator/placement.ts +81 -15
- package/src/orchestrator/plan.ts +37 -15
- package/src/orchestrator/plugin-host.ts +232 -108
- package/src/orchestrator/plugin.ts +53 -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/run-context.ts +6 -2
- package/src/orchestrator/run-failures.ts +43 -14
- 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 +12 -10
- package/src/orchestrator/run-report.ts +2 -1
- package/src/orchestrator/run-stop.ts +17 -9
- package/src/orchestrator/run.ts +443 -107
- package/src/orchestrator/sandbox-fix.ts +40 -12
- package/src/orchestrator/sandbox-request.ts +286 -59
- package/src/orchestrator/save-lane.ts +24 -13
- package/src/orchestrator/shell-verdict.ts +20 -1
- package/src/orchestrator/signals.ts +103 -8
- package/src/orchestrator/stable-keys.ts +17 -9
- package/src/orchestrator/task-artifacts.ts +15 -2
- package/src/orchestrator/task-hash.ts +187 -30
- package/src/orchestrator/telemetry-host.ts +25 -6
- package/src/orchestrator/telemetry.ts +80 -12
- package/src/orchestrator/upstream.ts +3 -1
- package/src/report/events.ts +20 -3
- package/src/report/forecast.ts +103 -37
- package/src/report/framed-output.ts +165 -157
- package/src/report/index.ts +2 -1
- package/src/report/logger.ts +107 -30
- package/src/report/plain-output.ts +13 -9
- package/src/report/status-line.ts +28 -55
- package/src/report/summary.ts +57 -14
- package/src/settings.ts +42 -6
- 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 +17 -3
- 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 +50 -6
- package/src/util/plain-text.ts +19 -0
- package/src/util/secret-mask.ts +57 -1
- package/src/util/settle.ts +16 -0
- package/src/util/size.ts +12 -2
- package/src/util/write-file.ts +86 -9
- package/src/workspace/affected.ts +225 -27
- 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 +126 -25
- package/src/workspace/dependencies-lock.ts +7 -14
- package/src/workspace/filter.ts +10 -1
- 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 +46 -25
- 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/index.ts
CHANGED
|
@@ -43,12 +43,21 @@ export async function run(argv: readonly string[]): Promise<number> {
|
|
|
43
43
|
await printHelp(await (await plugins()).pluginCommandHelp(), command)
|
|
44
44
|
return 0
|
|
45
45
|
}
|
|
46
|
+
if (command !== undefined && forwardsNothing(command, rest)) {
|
|
47
|
+
return refuse(
|
|
48
|
+
command,
|
|
49
|
+
rest,
|
|
50
|
+
'forwards nothing after -- (only vx run and vx watch pass arguments on to a task)',
|
|
51
|
+
'VX_E_USAGE',
|
|
52
|
+
)
|
|
53
|
+
}
|
|
46
54
|
// Each setting's variable once, before any verb: a renamed one is named,
|
|
47
55
|
// and a deep reader trusts what it reads. Not for help, as `vx run
|
|
48
56
|
// --help` above is not: help is where a setting's values are read. Nor
|
|
49
|
-
// for `vx info`, which reports a refused one (doctor.ts).
|
|
57
|
+
// for `vx info`, which reports a refused one (doctor.ts). Nor for one
|
|
58
|
+
// whose flag is given: the flag beats it.
|
|
50
59
|
if (command !== 'help' && command !== '--help' && command !== '-h' && command !== 'info')
|
|
51
|
-
checkSettingsEnv()
|
|
60
|
+
checkSettingsEnv(process.env, rest)
|
|
52
61
|
|
|
53
62
|
try {
|
|
54
63
|
return await dispatch(command, rest)
|
|
@@ -93,6 +102,21 @@ function refuseControlArgs(command: string, rest: readonly string[]): void {
|
|
|
93
102
|
}
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
/**
|
|
106
|
+
* A `--` given to a core verb that hands no task its words: each parser
|
|
107
|
+
* refused it as `unknown flag: --`, true of no flag. A `--sql` query is
|
|
108
|
+
* the one value that may be `--`.
|
|
109
|
+
*/
|
|
110
|
+
function forwardsNothing(command: string, rest: readonly string[]): boolean {
|
|
111
|
+
if (command === 'run' || command === 'watch') return false
|
|
112
|
+
if (!(CORE_VERBS as readonly string[]).includes(command)) return false
|
|
113
|
+
for (let i = 0; i < rest.length; i++) {
|
|
114
|
+
if (rest[i] === '--sql') i++
|
|
115
|
+
else if (rest[i] === '--') return true
|
|
116
|
+
}
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
|
|
96
120
|
/**
|
|
97
121
|
* A crash from a hand-broken lock entry is that entry's fault: runs validate
|
|
98
122
|
* no entry, so it surfaced as a raw TypeError (2026-10-14). The lock is
|
|
@@ -148,7 +172,9 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
148
172
|
// A name that is no verb here is the typo `vx rnu` is, not a request
|
|
149
173
|
// for the whole reference: it printed 145 lines and no hint.
|
|
150
174
|
const resolved = await (await plugins()).resolvePluginCommand(verb)
|
|
151
|
-
|
|
175
|
+
// A workspace that failed to load cannot say the verb exists: the
|
|
176
|
+
// whole reference and exit 0 hid the broken file (`vx okk` says so).
|
|
177
|
+
const unknown = resolved === null || !('plugin' in resolved)
|
|
152
178
|
if (unknown && verb.startsWith('-')) {
|
|
153
179
|
return refuse(
|
|
154
180
|
'help',
|
|
@@ -158,15 +184,20 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
158
184
|
)
|
|
159
185
|
}
|
|
160
186
|
if (unknown) {
|
|
161
|
-
const declared =
|
|
187
|
+
const declared =
|
|
188
|
+
resolved !== null && 'declaredVerbs' in resolved ? resolved.declaredVerbs : []
|
|
189
|
+
const loadNote =
|
|
190
|
+
resolved !== null && 'loadError' in resolved
|
|
191
|
+
? ` (plugin verbs could not be looked up: .vx/workspace failed to load: ${resolved.loadError})`
|
|
192
|
+
: ''
|
|
162
193
|
return refuse(
|
|
163
194
|
'help',
|
|
164
195
|
rest,
|
|
165
|
-
`unknown command: ${verb}${didYouMeanVerb(verb, declared)} (see \`vx help\`)`,
|
|
196
|
+
`unknown command: ${verb}${didYouMeanVerb(verb, declared)}${loadNote} (see \`vx help\`)`,
|
|
166
197
|
'VX_E_UNKNOWN_COMMAND',
|
|
167
198
|
)
|
|
168
199
|
}
|
|
169
|
-
|
|
200
|
+
return dispatch(verb, ['--help'])
|
|
170
201
|
}
|
|
171
202
|
await printHelp(await (await plugins()).pluginCommandHelp(), core ? verb : undefined)
|
|
172
203
|
return 0
|
|
@@ -239,7 +270,7 @@ async function dispatch(command: string | undefined, rest: string[]): Promise<nu
|
|
|
239
270
|
// that returned 256 (a count of failures, say) exited 0.
|
|
240
271
|
if (!Number.isInteger(code) || code < 0 || code > 255) {
|
|
241
272
|
throw new UserError(
|
|
242
|
-
`plugin '${resolved.plugin.name}': command '${command}' resolved ${JSON.stringify(code)} instead of an exit code (an integer 0–255)`,
|
|
273
|
+
`plugin '${resolved.plugin.name}': command '${command}' resolved ${typeof code === 'bigint' ? `${String(code)}n` : typeof code === 'number' || code === undefined ? String(code) : JSON.stringify(code)} instead of an exit code (an integer 0–255)`,
|
|
243
274
|
)
|
|
244
275
|
}
|
|
245
276
|
return code
|
package/src/cli/info.ts
CHANGED
|
@@ -101,7 +101,7 @@ function infoReport(f: InfoFacts): ui.Block {
|
|
|
101
101
|
],
|
|
102
102
|
['cache versions', `keys ${f.cacheVersion} · index schema ${f.schemaVersion}`],
|
|
103
103
|
['cache entries', describeEntries(f)],
|
|
104
|
-
// Only the settings a variable
|
|
104
|
+
// Only the settings a variable, the workspace or a plugin changed.
|
|
105
105
|
...(Object.values(f.settings).some((s) => s.source !== 'default')
|
|
106
106
|
? ([['settings', describeSettings(f.settings)]] as [string, string][])
|
|
107
107
|
: []),
|
|
@@ -141,14 +141,15 @@ function describeEntries(f: InfoFacts): string {
|
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
143
|
* The settings not at their default, with where each came from:
|
|
144
|
-
* `run.timeout 30m (VX_RUN_TIMEOUT), cache.policy local:rw (.vx/workspace)
|
|
144
|
+
* `run.timeout 30m (VX_RUN_TIMEOUT), cache.policy local:rw (.vx/workspace)`,
|
|
145
|
+
* `cache.scope pr-2 (plugin @org/ci)`.
|
|
145
146
|
*/
|
|
146
147
|
function describeSettings(settings: InfoFacts['settings']): string {
|
|
147
148
|
return Object.entries(settings)
|
|
148
149
|
.filter(([, s]) => s.source !== 'default')
|
|
149
150
|
.map(
|
|
150
151
|
([path, s]) =>
|
|
151
|
-
`${path} ${String(s.value)} (${s.source === 'env' ? envName(path) : '.vx/workspace'}${s.refused === undefined ? '' : `, refused: ${s.refused}`})`,
|
|
152
|
+
`${path} ${String(s.value)} (${s.source === 'env' ? envName(path) : s.source === 'plugin' ? `plugin ${s.plugin}` : '.vx/workspace'}${s.refused === undefined ? '' : `, refused: ${s.refused}`})`,
|
|
152
153
|
)
|
|
153
154
|
.join(', ')
|
|
154
155
|
}
|
package/src/cli/init.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// `vx init --plugin <hook>` writes a runnable plugin for one hook and its
|
|
7
7
|
// test instead (plugin-templates.ts, the examples the gate runs).
|
|
8
8
|
|
|
9
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
9
|
+
import { existsSync, lstatSync, readFileSync, statSync } from 'node:fs'
|
|
10
10
|
import { mkdir, unlink } from 'node:fs/promises'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import { gitTop, workspaceFileWritten } from '../handoff.js'
|
|
@@ -132,11 +132,17 @@ export async function initCmd(args: readonly string[]): Promise<number> {
|
|
|
132
132
|
dirs.push(d)
|
|
133
133
|
if (d === top || path.dirname(d) === d) break
|
|
134
134
|
}
|
|
135
|
-
|
|
135
|
+
const pkgRoot = packageRoot(dirs, top !== null)
|
|
136
|
+
// Outside git `dirs` reach `/`: a stray turbo.json or nx.json above the
|
|
137
|
+
// project took init over, as a stray package.json did (2026-10-28).
|
|
138
|
+
const own = top !== null ? dirs : dirs.slice(0, dirs.indexOf(pkgRoot ?? cwd) + 1)
|
|
139
|
+
if (own.some((d) => TOOL_CONFIGS.some((f) => existsSync(path.join(d, f))))) {
|
|
136
140
|
return migrate(dirs, parsed)
|
|
137
141
|
}
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
// Asked of git itself: a kept workspace file is `top` too, and the
|
|
143
|
+
// `git init` hint went missing where the next run would refuse.
|
|
144
|
+
const noGit = marked === undefined ? top === null : (await gitTop(cwd)) === null
|
|
145
|
+
return starter(marked ?? pkgRoot ?? cwd, pkgRoot, noGit, parsed)
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
/**
|
|
@@ -314,6 +320,20 @@ function ownerNamed(dir: string): boolean {
|
|
|
314
320
|
}
|
|
315
321
|
}
|
|
316
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Whether anything stands at `file`, a dangling symlink included: writing
|
|
325
|
+
* through one created its target outside the workspace.
|
|
326
|
+
*/
|
|
327
|
+
function occupied(file: string): boolean {
|
|
328
|
+
try {
|
|
329
|
+
lstatSync(file)
|
|
330
|
+
return true
|
|
331
|
+
} catch {
|
|
332
|
+
// quiet: nothing there (a parent that is a file is refused at the write)
|
|
333
|
+
return false
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
317
337
|
/** Write `plugins/<seam>.ts` and its test under `root`: the workspace root, else the cwd. */
|
|
318
338
|
async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promise<number> {
|
|
319
339
|
const template = PLUGIN_TEMPLATES[seam]!
|
|
@@ -329,13 +349,14 @@ async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promi
|
|
|
329
349
|
] as const
|
|
330
350
|
if (!args.force) {
|
|
331
351
|
for (const [rel] of files) {
|
|
332
|
-
if (
|
|
352
|
+
if (occupied(path.join(root, rel))) {
|
|
333
353
|
sayNotice('error', 'vx init', `${rel} exists. --force overwrites it`)
|
|
334
354
|
return 1
|
|
335
355
|
}
|
|
336
356
|
}
|
|
337
357
|
}
|
|
338
358
|
if (!args.dry) {
|
|
359
|
+
for (const [rel] of files) refuseSymlink(path.join(root, rel), root)
|
|
339
360
|
await mkdir(path.join(root, 'plugins'), { recursive: true }).catch(
|
|
340
361
|
(err: NodeJS.ErrnoException) => {
|
|
341
362
|
if (err.code !== 'EEXIST' && err.code !== 'ENOTDIR') throw err
|
|
@@ -344,7 +365,7 @@ async function scaffoldPlugin(seam: string, root: string, args: InitArgs): Promi
|
|
|
344
365
|
)
|
|
345
366
|
},
|
|
346
367
|
)
|
|
347
|
-
for (const [rel, text] of files) await
|
|
368
|
+
for (const [rel, text] of files) await writeNoFollow(path.join(root, rel), text, root)
|
|
348
369
|
}
|
|
349
370
|
const factory = /^export function (\w+)/m.exec(template.plugin)![1]!
|
|
350
371
|
const workspaceFile =
|
|
@@ -423,7 +444,7 @@ async function starter(
|
|
|
423
444
|
): Promise<number> {
|
|
424
445
|
if (args.mode !== undefined) {
|
|
425
446
|
throw new UserError(
|
|
426
|
-
`vx init: --${args.mode} answers a Turbo or Nx
|
|
447
|
+
`vx init: --${args.mode} answers a Turbo or Nx workspace's question, and there is no turbo.json or nx.json here`,
|
|
427
448
|
'VX_E_USAGE',
|
|
428
449
|
)
|
|
429
450
|
}
|
|
@@ -450,11 +471,20 @@ async function starter(
|
|
|
450
471
|
],
|
|
451
472
|
[PROJECT_CONFIG_FILENAMES, config, starterProject(ext)],
|
|
452
473
|
] as const) {
|
|
453
|
-
const existing = names.
|
|
454
|
-
|
|
474
|
+
const existing = names.filter((n) => existsSync(path.join(root, n)))
|
|
475
|
+
// Kept, a directory read as the workspace's marker, and every verb after
|
|
476
|
+
// then found none and sent the user back here.
|
|
477
|
+
const dir = existing.find((n) => statSync(path.join(root, n)).isDirectory())
|
|
478
|
+
if (dir !== undefined) {
|
|
479
|
+
throw new UserError(
|
|
480
|
+
`vx init: ${dir} is a directory, where the ${name === config ? 'project config' : 'workspace file'} goes; move it and re-run`,
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
if (existing.length > 0 && !args.force) kept.push(existing[0]!)
|
|
455
484
|
else {
|
|
456
485
|
files.push({ path: name, contents })
|
|
457
|
-
|
|
486
|
+
// Every other name goes too: a second config beside the one written.
|
|
487
|
+
replaced.push(...existing.filter((n) => n !== name))
|
|
458
488
|
}
|
|
459
489
|
}
|
|
460
490
|
// vx's own housekeeping, as a run's first cache open and `vx lock` write them.
|
|
@@ -465,7 +495,10 @@ async function starter(
|
|
|
465
495
|
if (!existsSync(path.join(root, name))) files.push({ path: name, contents })
|
|
466
496
|
}
|
|
467
497
|
const declare = `declare a task in ${kept.find((f) => f.startsWith('vx.config.')) ?? config}, then vx lock && vx run <task>`
|
|
468
|
-
|
|
498
|
+
// Run in the cwd, a git init below the root it chose hides the workspace from every verb.
|
|
499
|
+
const gitInit =
|
|
500
|
+
root === process.cwd() ? 'git init' : `git init ${path.relative(process.cwd(), root)}`
|
|
501
|
+
const next = `${noGit ? `${gitInit}, ` : ''}${hasScripts(pkgRoot) ? `bunx @vzn/vx-migrate${args.mjs ? ' --mjs' : ''} to make package.json scripts tasks, or ${declare}` : declare}`
|
|
469
502
|
if (args.json === true) {
|
|
470
503
|
ui.data(
|
|
471
504
|
'out',
|
|
@@ -473,7 +506,7 @@ async function starter(
|
|
|
473
506
|
)
|
|
474
507
|
return 0
|
|
475
508
|
}
|
|
476
|
-
const rows: ui.FieldRow[] = [['from', 'no turbo.json or nx.json
|
|
509
|
+
const rows: ui.FieldRow[] = [['from', 'no turbo.json or nx.json']]
|
|
477
510
|
// The files are root-relative: a root above the cwd is named, or they read as written here.
|
|
478
511
|
if (root !== process.cwd()) rows.push(['root', root])
|
|
479
512
|
if (args.dry) rows.push(['dry run', 'nothing written'])
|
|
@@ -486,17 +519,19 @@ async function starter(
|
|
|
486
519
|
ui.print('out', ui.report(`vx init ${VERSION}`, [...rows, null, ['next', next]]))
|
|
487
520
|
return 0
|
|
488
521
|
}
|
|
489
|
-
for (const f of files) refuseSymlink(path.join(root, f.path))
|
|
522
|
+
for (const f of files) refuseSymlink(path.join(root, f.path), root)
|
|
490
523
|
const ran = await install(pkgRoot, false, plugins)
|
|
491
524
|
if (ran.length > 0) rows.push(['installed', ...ran])
|
|
492
525
|
for (const f of files) {
|
|
493
|
-
await writeNoFollow(path.join(root, f.path), f.contents).catch(
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
526
|
+
await writeNoFollow(path.join(root, f.path), f.contents, root).catch(
|
|
527
|
+
(err: NodeJS.ErrnoException) => {
|
|
528
|
+
// writeNoFollow's mkdir of a parent that is a file says EEXIST.
|
|
529
|
+
if (err.code !== 'ENOTDIR' && err.code !== 'EEXIST') throw err
|
|
530
|
+
throw new UserError(
|
|
531
|
+
`vx init: cannot write ${f.path}: ${path.dirname(f.path)} is a file, not a directory; move it and re-run`,
|
|
532
|
+
)
|
|
533
|
+
},
|
|
534
|
+
)
|
|
500
535
|
}
|
|
501
536
|
for (const f of replaced) await unlink(path.join(root, f))
|
|
502
537
|
// The lock that follows loads the workspace file just written.
|
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')
|
|
@@ -714,9 +797,19 @@ export function formatTaskRows(
|
|
|
714
797
|
// The run's own grid (`taskList`): a replay reads as the run did. What
|
|
715
798
|
// the run said below the row (an exit, why it skipped) rides it dim.
|
|
716
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}`
|
|
717
811
|
const note = (
|
|
718
|
-
(t
|
|
719
|
-
reasonParts(t, skippedHere, inputMoved.get(`${t.project}#${t.task}`))
|
|
812
|
+
failure + reasonParts(t, skippedHere, inputMoved.get(`${t.project}#${t.task}`))
|
|
720
813
|
).trim()
|
|
721
814
|
const id = `${t.project}#${t.task}`
|
|
722
815
|
const usualMs = usual === undefined ? undefined : (usual.get(id) ?? null)
|
|
@@ -798,7 +891,9 @@ export function formatRunList(
|
|
|
798
891
|
async function bareTaskId(db: Database, task: string): Promise<string> {
|
|
799
892
|
if (task.includes('#')) return task
|
|
800
893
|
const ids = (
|
|
801
|
-
db
|
|
894
|
+
db
|
|
895
|
+
.query('SELECT DISTINCT project FROM main.runs WHERE task = ? ORDER BY project')
|
|
896
|
+
.all(task) as {
|
|
802
897
|
project: string
|
|
803
898
|
}[]
|
|
804
899
|
).map((r) => `${r.project}#${task}`)
|
|
@@ -831,7 +926,13 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
831
926
|
if (parsed.stats === true) {
|
|
832
927
|
const branch = currentBranch(root, undefined, parsed.branch)
|
|
833
928
|
const task = parsed.task === undefined ? undefined : await rootSpelled(root, parsed.task)
|
|
834
|
-
return await printStats(
|
|
929
|
+
return await printStats(
|
|
930
|
+
db,
|
|
931
|
+
task,
|
|
932
|
+
parsed.format,
|
|
933
|
+
branch,
|
|
934
|
+
await definitionsNow(root, parsed.cacheDir),
|
|
935
|
+
)
|
|
835
936
|
}
|
|
836
937
|
|
|
837
938
|
if (parsed.log !== undefined) {
|
|
@@ -844,19 +945,22 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
844
945
|
// The id's form is hinted only when it lacks one: a task that
|
|
845
946
|
// never ran was told how to spell a task id. A typo of one that
|
|
846
947
|
// ran is named, as `vx why` names it.
|
|
847
|
-
const
|
|
848
|
-
?
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
+
: ''
|
|
858
962
|
throw new UserError(
|
|
859
|
-
`vx last: no recorded run of ${parsed.log}${parsed.runId === undefined ? '' : ` in run ${parsed.runId}`}${parsed.log.includes('#') ? '' : ' (a task id is project#task)'}${ran === undefined ? '' : ` — did you mean ${ran}?`}`,
|
|
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}`,
|
|
860
964
|
'VX_E_NO_HISTORY',
|
|
861
965
|
)
|
|
862
966
|
}
|
|
@@ -865,8 +969,9 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
865
969
|
return 0
|
|
866
970
|
}
|
|
867
971
|
if (log.source === null) {
|
|
868
|
-
// A cached task with no entry was not saved (a read-only
|
|
869
|
-
//
|
|
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.
|
|
870
975
|
const [project, task] = splitTaskId(log.taskId)
|
|
871
976
|
const cached =
|
|
872
977
|
log.status !== 'failed' &&
|
|
@@ -875,14 +980,27 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
875
980
|
.query('SELECT cached FROM runs WHERE run_id = ? AND project = ? AND task = ?')
|
|
876
981
|
.get(log.runId, project, task) as { cached: number | null } | null
|
|
877
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
|
|
878
994
|
sayNotice(
|
|
879
995
|
'info',
|
|
880
996
|
'vx last',
|
|
881
|
-
|
|
882
|
-
? `${log.taskId}
|
|
883
|
-
:
|
|
884
|
-
? `${log.taskId} ended
|
|
885
|
-
:
|
|
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`,
|
|
886
1004
|
)
|
|
887
1005
|
} else if (log.output === '') {
|
|
888
1006
|
sayNotice('info', 'vx last', `${log.taskId} ended ${log.status} and printed nothing`)
|
|
@@ -917,18 +1035,34 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
917
1035
|
return 0
|
|
918
1036
|
}
|
|
919
1037
|
|
|
1038
|
+
const named = parsed.runId === undefined ? null : resolveRunId(db, parsed.runId, 'vx last')
|
|
920
1039
|
const inv =
|
|
921
1040
|
parsed.runId !== undefined
|
|
922
|
-
? getInvocation(db,
|
|
1041
|
+
? getInvocation(db, named ?? parsed.runId)
|
|
923
1042
|
: parsed.failed === true
|
|
924
1043
|
? (failedInvocations(db, 1)[0] ?? null)
|
|
925
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
|
+
}
|
|
926
1053
|
if ((inv === null || inv === undefined) && noRuns()) {
|
|
927
1054
|
throw refusal(
|
|
928
1055
|
{ tone: 'info', headline: 'no runs yet', fix: 'vx run' },
|
|
929
1056
|
{ code: 'VX_E_NO_HISTORY', message: 'vx last: no recorded runs yet — run something first' },
|
|
930
1057
|
)
|
|
931
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
|
+
}
|
|
932
1066
|
if (inv === null || inv === undefined) {
|
|
933
1067
|
throw new UserError(
|
|
934
1068
|
noRuns()
|
|
@@ -937,9 +1071,7 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
937
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`
|
|
938
1072
|
: parsed.runId !== undefined
|
|
939
1073
|
? `vx last: no recorded run ${parsed.runId} (vx last --list shows recent runs)`
|
|
940
|
-
:
|
|
941
|
-
? 'vx last: no recorded run failed'
|
|
942
|
-
: 'vx last: no recorded runs yet — run something first',
|
|
1074
|
+
: 'vx last: no recorded runs yet — run something first',
|
|
943
1075
|
'VX_E_NO_HISTORY',
|
|
944
1076
|
)
|
|
945
1077
|
}
|
|
@@ -1007,7 +1139,18 @@ export async function lastCmd(args: readonly string[]): Promise<number> {
|
|
|
1007
1139
|
const ranIds = (detail?.tasks ?? [])
|
|
1008
1140
|
.filter((t) => t.status === 'success' || t.status === 'failed')
|
|
1009
1141
|
.map((t) => `${t.project}#${t.task}`)
|
|
1010
|
-
|
|
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)
|
|
1011
1154
|
const taskRows = formatTaskRows(detail?.tasks ?? [], moved, usual)
|
|
1012
1155
|
if (taskRows.length > 0)
|
|
1013
1156
|
blocks.push(ui.section('Tasks', String(detail?.tasks.length ?? 0), ...taskRows))
|