@vzn/vx 0.0.650 → 0.0.652
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 +7 -23
- package/package.json +5 -5
- package/schemas/last.json +15 -0
- package/schemas/show.json +14 -20
- package/src/bin.ts +21 -31
- package/src/cache/archive.ts +1 -1
- package/src/cache/cache.ts +221 -30
- package/src/cache/file-hashes.ts +3 -0
- package/src/cache/git-inputs.ts +188 -39
- package/src/cache/history-stream.ts +134 -0
- package/src/cache/history-writer.ts +76 -0
- package/src/cache/index.ts +3 -0
- package/src/cache/inputs.ts +77 -14
- package/src/cache/layer.ts +17 -6
- package/src/cache/layered-cache.ts +4 -0
- package/src/cache/linked-inputs.ts +3 -0
- package/src/cache/lockfile-memo.ts +5 -3
- package/src/cache/output-index.ts +5 -2
- package/src/cache/remote-lane.ts +2 -1
- package/src/cache/run-history.ts +56 -17
- package/src/cache/schema.ts +15 -2
- package/src/cli/cache.ts +28 -18
- package/src/cli/completions.ts +4 -2
- package/src/cli/docs.ts +10 -11
- package/src/cli/help.ts +14 -25
- package/src/cli/index.ts +28 -9
- package/src/cli/info.ts +16 -8
- package/src/cli/init.ts +23 -15
- package/src/cli/last.ts +246 -204
- package/src/cli/lock.ts +55 -5
- package/src/cli/plan-format.ts +100 -118
- package/src/cli/plugin-commands.ts +8 -2
- package/src/cli/plugin-templates.ts +36 -45
- package/src/cli/run.ts +46 -27
- package/src/cli/select.ts +43 -5
- package/src/cli/show.ts +45 -52
- package/src/cli/task-verb.ts +22 -0
- package/src/cli/upgrade.ts +3 -1
- package/src/cli/watch-fs.ts +6 -1
- package/src/cli/watch-judge.ts +3 -2
- package/src/cli/watch-set.ts +4 -4
- package/src/cli/watch.ts +94 -52
- package/src/cli/why.ts +123 -89
- package/src/cli/workspace-config.ts +2 -1
- package/src/config.ts +47 -30
- package/src/exec/executor.ts +42 -2
- package/src/exec/index.ts +1 -0
- package/src/exec/kill-tree.ts +58 -5
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/pipes.ts +152 -0
- package/src/exec/runner.ts +199 -182
- package/src/exec/sandbox-binds.ts +1 -1
- package/src/exec/sandbox-paths.ts +3 -1
- package/src/exec/sandbox-runtime.ts +317 -23
- package/src/exec/spawn-off.ts +154 -0
- package/src/exec/task-proxy.ts +93 -0
- package/src/exec/warm-shell.ts +146 -0
- package/src/graph/critical-path.ts +40 -28
- package/src/graph/definition.ts +16 -3
- package/src/graph/priorities.ts +98 -44
- package/src/graph/scheduler.ts +17 -11
- package/src/graph/task-graph.ts +51 -24
- package/src/handoff.ts +10 -7
- package/src/history/failure-mode.ts +24 -1
- package/src/history/history.ts +5 -0
- package/src/history/index.ts +1 -0
- package/src/history/metrics.ts +14 -6
- package/src/index.ts +7 -3
- package/src/orchestrator/admission.ts +1 -1
- package/src/orchestrator/command-windows.ts +1 -1
- package/src/orchestrator/deferred-outputs.ts +1 -1
- package/src/orchestrator/doctor.ts +1 -1
- package/src/orchestrator/download-policy.ts +5 -7
- package/src/orchestrator/excluded-keys.ts +1 -1
- package/src/orchestrator/execute-task.ts +67 -20
- package/src/orchestrator/graph-memo.ts +50 -1
- package/src/orchestrator/graph-stage.ts +6 -0
- package/src/orchestrator/hit-restore.ts +5 -6
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/keyed-projects.ts +28 -0
- package/src/orchestrator/live-usage.ts +16 -11
- package/src/orchestrator/local-shortcircuit.ts +2 -2
- package/src/orchestrator/locked-digests.ts +63 -0
- package/src/orchestrator/lockfile-claim.ts +108 -29
- package/src/orchestrator/miss-save.ts +58 -11
- package/src/orchestrator/persistent-task.ts +5 -1
- package/src/orchestrator/placement.ts +1 -1
- package/src/orchestrator/plan.ts +7 -3
- package/src/orchestrator/plugin-host.ts +67 -2
- package/src/orchestrator/plugin.ts +53 -2
- package/src/orchestrator/prepare.ts +16 -1
- package/src/orchestrator/projects.ts +4 -1
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-context.ts +29 -10
- package/src/orchestrator/run-failures.ts +62 -13
- package/src/orchestrator/run-keys.ts +1 -1
- package/src/orchestrator/run-records.ts +88 -59
- package/src/orchestrator/run-report.ts +1 -1
- package/src/orchestrator/run.ts +145 -41
- package/src/orchestrator/sandbox-request.ts +52 -9
- package/src/orchestrator/shell-verdict.ts +9 -0
- package/src/orchestrator/signals.ts +8 -5
- package/src/orchestrator/stable-keys.ts +17 -18
- package/src/orchestrator/task-artifacts.ts +1 -1
- package/src/orchestrator/task-hash.ts +21 -7
- package/src/orchestrator/telemetry.ts +5 -1
- package/src/report/events.ts +6 -14
- package/src/report/forecast.ts +10 -32
- package/src/report/framed-output.ts +283 -175
- package/src/report/index.ts +8 -3
- package/src/report/logger.ts +74 -39
- package/src/report/plain-output.ts +3 -1
- package/src/report/status-line.ts +49 -357
- package/src/report/summary.ts +105 -136
- package/src/settings.ts +8 -8
- package/src/ui/ansi.ts +48 -0
- package/src/ui/components.ts +506 -0
- package/src/ui/index.ts +79 -0
- package/src/ui/kit.ts +37 -0
- package/src/ui/live-region.ts +363 -0
- package/src/{util/refusal-notice.ts → ui/refusal.ts} +7 -5
- package/src/ui/terminal.ts +51 -0
- package/src/ui/theme.ts +224 -0
- package/src/ui/version-line.ts +28 -0
- package/src/util/config-frame.ts +191 -14
- package/src/util/edit-distance.ts +6 -1
- package/src/util/index.ts +2 -15
- package/src/util/paths.ts +21 -3
- package/src/util/secret-mask.ts +39 -3
- package/src/util/settle.ts +33 -1
- package/src/util/tail.ts +4 -1
- package/src/util/timing.ts +4 -16
- package/src/workspace/affected.ts +89 -30
- package/src/workspace/bracket-literal.ts +2 -2
- package/src/workspace/config-schema.ts +70 -63
- package/src/workspace/dependencies-lock.ts +191 -0
- package/src/workspace/filter.ts +36 -1
- package/src/workspace/index.ts +9 -0
- package/src/workspace/lockfile.ts +159 -6
- package/src/workspace/migration.ts +16 -9
- package/src/workspace/workspace.ts +3 -0
- package/src/util/colors.ts +0 -133
- package/src/util/report.ts +0 -44
- package/src/util/ui.ts +0 -258
- /package/src/{report/path-links.ts → ui/links.ts} +0 -0
- /package/src/{report → ui}/terminal-signals.ts +0 -0
package/src/cli/why.ts
CHANGED
|
@@ -18,19 +18,9 @@ import {
|
|
|
18
18
|
whyDidThisRerun,
|
|
19
19
|
} from '../history/index.js'
|
|
20
20
|
import { resolveRunId } from '../orchestrator/index.js'
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
MASKED,
|
|
25
|
-
nearMatches,
|
|
26
|
-
paint,
|
|
27
|
-
printable,
|
|
28
|
-
refusal,
|
|
29
|
-
SUCCESS,
|
|
30
|
-
ui,
|
|
31
|
-
UserError,
|
|
32
|
-
WARN,
|
|
33
|
-
} from '../util/index.js'
|
|
21
|
+
import { MASKED, nearMatches, printable, refusal, UserError } from '../util/index.js'
|
|
22
|
+
import * as ui from '../ui/index.js'
|
|
23
|
+
import type { Style } from '../ui/index.js'
|
|
34
24
|
import { runRules } from '../settings.js'
|
|
35
25
|
import { buildPackageGraph, findWorkspaceRoot } from '../workspace/index.js'
|
|
36
26
|
import { findCwdProject } from './select.js'
|
|
@@ -165,6 +155,7 @@ async function resolveTarget(
|
|
|
165
155
|
cache: Cache,
|
|
166
156
|
target: string,
|
|
167
157
|
cwdProject: () => Promise<string | null>,
|
|
158
|
+
declared: () => Promise<boolean>,
|
|
168
159
|
): Promise<string> {
|
|
169
160
|
const db = indexDb(cache)
|
|
170
161
|
if (target.includes('#')) {
|
|
@@ -202,11 +193,21 @@ async function resolveTarget(
|
|
|
202
193
|
const ids = pairs.map((p) => `${p.project}#${p.task}`)
|
|
203
194
|
const invoked = ids.length === 0 && db.query('SELECT 1 FROM invocations LIMIT 1').get() !== null
|
|
204
195
|
if (ids.length === 0 && !invoked) {
|
|
196
|
+
// `vx run` of a task no project declares is refused: its advice was that.
|
|
197
|
+
const known = await declared()
|
|
205
198
|
throw refusal(
|
|
206
|
-
|
|
199
|
+
known
|
|
200
|
+
? { tone: 'info', headline: `no runs of ${target} yet`, fix: `vx run ${target}` }
|
|
201
|
+
: {
|
|
202
|
+
tone: 'info',
|
|
203
|
+
headline: `no project declares ${target}`,
|
|
204
|
+
fix: 'vx show lists the tasks',
|
|
205
|
+
},
|
|
207
206
|
{
|
|
208
207
|
code: 'VX_E_NO_HISTORY',
|
|
209
|
-
message:
|
|
208
|
+
message: known
|
|
209
|
+
? `vx why: no recorded runs for "${target}"${suggest(target, ids, invoked)}`
|
|
210
|
+
: `vx why: no project declares "${target}" (vx show lists the tasks)`,
|
|
210
211
|
},
|
|
211
212
|
)
|
|
212
213
|
}
|
|
@@ -264,8 +265,12 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
264
265
|
const cache = Cache.inspect(await cliCacheDir(root, parsed.cacheDir))
|
|
265
266
|
try {
|
|
266
267
|
const db = indexDb(cache)
|
|
267
|
-
const
|
|
268
|
-
|
|
268
|
+
const target = await rootSpelled(root, parsed.target)
|
|
269
|
+
const taskId = await resolveTarget(
|
|
270
|
+
cache,
|
|
271
|
+
target,
|
|
272
|
+
() => findCwdProject(process.cwd()),
|
|
273
|
+
() => declares(root, target),
|
|
269
274
|
)
|
|
270
275
|
let runId: string | null
|
|
271
276
|
if (parsed.runId !== undefined) {
|
|
@@ -290,14 +295,23 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
290
295
|
// the persisted entry metadata so the verb still says something useful.
|
|
291
296
|
const explanation = explainCacheKey(db, taskId)
|
|
292
297
|
if (parsed.format === 'json') {
|
|
293
|
-
|
|
298
|
+
ui.data('out', `${JSON.stringify({ taskId, why: null, diff: null, explanation })}\n`)
|
|
294
299
|
} else {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
300
|
+
ui.print(
|
|
301
|
+
'out',
|
|
302
|
+
ui.section(
|
|
303
|
+
'Why',
|
|
304
|
+
taskId,
|
|
305
|
+
ui.text('recorded runs carry no run id: the latest cache entry instead'),
|
|
306
|
+
ui.fields(
|
|
307
|
+
explanation.latestEntry !== null
|
|
308
|
+
? [
|
|
309
|
+
['key', explanation.latestEntry.hash],
|
|
310
|
+
['command', ui.cmd(explanation.latestEntry.command)],
|
|
311
|
+
]
|
|
312
|
+
: [['key', 'no cache entry either']],
|
|
313
|
+
),
|
|
314
|
+
),
|
|
301
315
|
)
|
|
302
316
|
}
|
|
303
317
|
return 0
|
|
@@ -326,27 +340,22 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
326
340
|
const writers = inputWriters(db, root, taskId, diff.entries, roots)
|
|
327
341
|
|
|
328
342
|
if (parsed.format === 'json') {
|
|
329
|
-
|
|
343
|
+
ui.data('out', `${JSON.stringify({ taskId, runId, why, diff, roots, writers })}\n`)
|
|
330
344
|
return 0
|
|
331
345
|
}
|
|
332
346
|
|
|
333
|
-
const lines: string[] = []
|
|
334
|
-
const c = detectColors(process.stdout)
|
|
335
|
-
const dim = (text: string): string => paint('', text, c, { dim: true })
|
|
336
|
-
const head = (text: string): string => paint('', text, c, { bold: true })
|
|
337
347
|
const t = why.thisRun!
|
|
338
|
-
|
|
339
|
-
|
|
348
|
+
const head: ui.FieldRow[] = [
|
|
349
|
+
['run', runId],
|
|
350
|
+
['this run', runFacts(t)],
|
|
351
|
+
]
|
|
340
352
|
if (why.previousRun == null && t.hash === '') {
|
|
341
353
|
// Only keyed runs are compared: a persistent task's tenth run read
|
|
342
354
|
// as its first, and the note saying why went unprinted.
|
|
343
|
-
|
|
344
|
-
lines.push(` ${dim('verdict')} ${why.note}`)
|
|
355
|
+
head.push(['previous', 'none with a cache key'], ['verdict', why.note])
|
|
345
356
|
} else if (why.previousRun == null) {
|
|
346
|
-
|
|
357
|
+
head.push(['previous', 'none: first recorded run of this task'])
|
|
347
358
|
} else {
|
|
348
|
-
const p = why.previousRun
|
|
349
|
-
lines.push(` ${dim('previous')} ${runFacts(p)}`)
|
|
350
359
|
// The key moved and the diff says by what: the verdict names it, so
|
|
351
360
|
// an env-only change reads as one line, not "inputs differ" above a
|
|
352
361
|
// table to scan.
|
|
@@ -354,56 +363,71 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
354
363
|
const moved = diff.entries.map((e) =>
|
|
355
364
|
e.name === e.kind ? e.kind : `${e.kind} ${printable(e.name)}`,
|
|
356
365
|
)
|
|
357
|
-
|
|
358
|
-
|
|
366
|
+
head.push(
|
|
367
|
+
['previous', runFacts(why.previousRun)],
|
|
368
|
+
[
|
|
369
|
+
'verdict',
|
|
359
370
|
why.hashChanged === true && moved.length > 0
|
|
360
|
-
?
|
|
361
|
-
|
|
371
|
+
? ui.tone(
|
|
372
|
+
'warn',
|
|
362
373
|
`cache key changed: ${moved.slice(0, 3).join(', ')}${moved.length > 3 ? ` and ${moved.length - 3} more` : ''}`,
|
|
363
|
-
c,
|
|
364
374
|
)
|
|
365
|
-
: why.note
|
|
366
|
-
|
|
375
|
+
: why.note,
|
|
376
|
+
],
|
|
367
377
|
)
|
|
368
378
|
}
|
|
379
|
+
if (diff.entries.length === 0 && why.hashChanged === true) head.push(['detail', diff.note])
|
|
380
|
+
const blocks: ui.Block[] = [ui.section('Why', taskId, ui.fields(head))]
|
|
369
381
|
|
|
370
382
|
if (diff.entries.length > 0) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
383
|
+
blocks.push(
|
|
384
|
+
ui.section(
|
|
385
|
+
'What changed',
|
|
386
|
+
`${diff.entries.length} of ${diff.entries.length + diff.unchangedCount}`,
|
|
387
|
+
ui.table(
|
|
388
|
+
['change', 'kind', 'name', 'before → after'],
|
|
389
|
+
diff.entries.map((e) => [
|
|
390
|
+
ui.tone(CHANGE_HUE[e.change] ?? 'warn', e.change),
|
|
391
|
+
e.kind,
|
|
392
|
+
ui.strong(printable(e.name)),
|
|
393
|
+
// A secret-named env input shows its name and its change, no hash.
|
|
394
|
+
e.before === MASKED || e.after === MASKED
|
|
395
|
+
? ''
|
|
396
|
+
: e.change === 'added'
|
|
397
|
+
? `+ ${e.after}`
|
|
398
|
+
: e.change === 'removed'
|
|
399
|
+
? `- ${e.before}`
|
|
400
|
+
: `${e.before} → ${e.after}`,
|
|
401
|
+
]),
|
|
402
|
+
),
|
|
403
|
+
),
|
|
374
404
|
)
|
|
375
|
-
const kindW = Math.max(...diff.entries.map((e) => e.kind.length), 4)
|
|
376
|
-
for (const e of diff.entries) {
|
|
377
|
-
// A secret-named env input shows its name and its change, no hash.
|
|
378
|
-
const beforeAfter =
|
|
379
|
-
e.before === MASKED || e.after === MASKED
|
|
380
|
-
? ''
|
|
381
|
-
: e.change === 'added'
|
|
382
|
-
? `+ ${e.after}`
|
|
383
|
-
: e.change === 'removed'
|
|
384
|
-
? `- ${e.before}`
|
|
385
|
-
: `${e.before} → ${e.after}`
|
|
386
|
-
lines.push(
|
|
387
|
-
` ${paint(CHANGE_HUE[e.change] ?? WARN, e.change.padEnd(7), c)} ${dim(e.kind.padEnd(kindW))} ${printable(e.name)} ${dim(beforeAfter)}`.trimEnd(),
|
|
388
|
-
)
|
|
389
|
-
}
|
|
390
405
|
if (roots.length > 0) {
|
|
391
|
-
lines.
|
|
392
|
-
for (const r of roots.slice(0, ROOTS_SHOWN)) {
|
|
406
|
+
const lines: ui.Inline[] = roots.slice(0, ROOTS_SHOWN).map((r) => {
|
|
393
407
|
const what = r.entries.map((e) =>
|
|
394
408
|
e.name === e.kind ? e.kind : `${e.kind} ${printable(e.name)}`,
|
|
395
409
|
)
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
410
|
+
return [
|
|
411
|
+
...r.chain.flatMap((id, i) => [...(i > 0 ? [ui.dim(' \u2190 ')] : []), ui.id(id)]),
|
|
412
|
+
ui.dim(' \u2190 '),
|
|
413
|
+
ui.strong(
|
|
414
|
+
`${what.slice(0, 3).join(', ')}${what.length > 3 ? ` and ${what.length - 3} more` : ''}`,
|
|
415
|
+
),
|
|
416
|
+
]
|
|
417
|
+
})
|
|
418
|
+
if (roots.length > ROOTS_SHOWN) lines.push(`and ${roots.length - ROOTS_SHOWN} more`)
|
|
419
|
+
blocks.push(ui.section('Root cause', ui.text(...lines)))
|
|
401
420
|
}
|
|
402
421
|
if (writers.length > 0) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
422
|
+
blocks.push(
|
|
423
|
+
ui.section(
|
|
424
|
+
'Changed during a run',
|
|
425
|
+
ui.table(
|
|
426
|
+
['file', 'written while'],
|
|
427
|
+
writers.map((w) => [ui.path(printable(w.file)), w.writers.join(', ')]),
|
|
428
|
+
),
|
|
429
|
+
),
|
|
430
|
+
)
|
|
407
431
|
}
|
|
408
432
|
const rootKinds = roots.flatMap((r) => r.entries.map((e) => e.kind))
|
|
409
433
|
// An upstream row names the task to ask next: the command, not a
|
|
@@ -418,31 +442,26 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
418
442
|
k in WHAT_TO_DO && !(k === 'upstream' && (roots.length > 0 || upstream.length === 0)),
|
|
419
443
|
)
|
|
420
444
|
if (kinds.length > 0) {
|
|
421
|
-
|
|
422
|
-
// Its own column: a root cause's kind and the `written` row are
|
|
423
|
-
// not in the table above, and overflowed its width.
|
|
424
|
-
const todoW = Math.max(kindW, ...kinds.map((k) => k.length), writers.length > 0 ? 7 : 0)
|
|
445
|
+
const todo: ui.FieldRow[] = []
|
|
425
446
|
for (const k of kinds) {
|
|
426
447
|
if (k === 'upstream') {
|
|
427
|
-
|
|
448
|
+
todo.push([k, ...upstream])
|
|
428
449
|
continue
|
|
429
450
|
}
|
|
430
|
-
|
|
451
|
+
todo.push([
|
|
452
|
+
k,
|
|
431
453
|
k === 'config'
|
|
432
454
|
? WHAT_TO_DO[k]!.replace('`vx show <that task>`', `\`vx show ${taskId}\``)
|
|
433
|
-
: WHAT_TO_DO[k]
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
if (writers.length > 0) {
|
|
437
|
-
// A task re-writing a file a key reads moves that key every run:
|
|
438
|
-
// the miss is right, and only moving the write ends it.
|
|
439
|
-
lines.push(` ${dim('written'.padEnd(todoW))} ${WRITTEN_TODO}`)
|
|
455
|
+
: WHAT_TO_DO[k]!,
|
|
456
|
+
])
|
|
440
457
|
}
|
|
458
|
+
// A task re-writing a file a key reads moves that key every run:
|
|
459
|
+
// the miss is right, and only moving the write ends it.
|
|
460
|
+
if (writers.length > 0) todo.push(['written', WRITTEN_TODO])
|
|
461
|
+
blocks.push(ui.section('What to do', ui.fields(todo)))
|
|
441
462
|
}
|
|
442
|
-
} else if (why.hashChanged === true) {
|
|
443
|
-
lines.push(` ${dim('detail')} ${diff.note}`)
|
|
444
463
|
}
|
|
445
|
-
|
|
464
|
+
ui.print('out', ...blocks)
|
|
446
465
|
return 0
|
|
447
466
|
} finally {
|
|
448
467
|
cache.close()
|
|
@@ -451,7 +470,7 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
451
470
|
|
|
452
471
|
const ROOTS_SHOWN = 5
|
|
453
472
|
|
|
454
|
-
const CHANGE_HUE: Record<string,
|
|
473
|
+
const CHANGE_HUE: Record<string, Style> = { added: 'ok', removed: 'error', changed: 'warn' }
|
|
455
474
|
|
|
456
475
|
type DiffEntry = ReturnType<typeof cacheKeyDiff>['entries'][number]
|
|
457
476
|
|
|
@@ -489,6 +508,21 @@ function upstreamTodo(upstream: readonly DiffEntry[], unrecorded: readonly DiffE
|
|
|
489
508
|
return out
|
|
490
509
|
}
|
|
491
510
|
|
|
511
|
+
/** Whether a project declares `target` (`pkg#task`, or a bare task); true when no config loads. */
|
|
512
|
+
async function declares(root: string, target: string): Promise<boolean> {
|
|
513
|
+
try {
|
|
514
|
+
const projects = await loadCliProjects(root, await discoverCliProjects(root), 'all')
|
|
515
|
+
const [project, task] = target.includes('#') ? splitTaskId(target) : [undefined, target]
|
|
516
|
+
return [...projects.values()].some(
|
|
517
|
+
(p) =>
|
|
518
|
+
(project === undefined || p.name === project) && Object.hasOwn(p.config.tasks ?? {}, task),
|
|
519
|
+
)
|
|
520
|
+
} catch {
|
|
521
|
+
// quiet: with no lock or a broken config, the run says why itself
|
|
522
|
+
return true
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
492
526
|
/**
|
|
493
527
|
* Each group in `taskId`'s graph with the tasks it gathers. Empty when a
|
|
494
528
|
* config no longer loads: the advice then names the group to `vx show`.
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import type { WorkspaceConfig } from '../config.js'
|
|
10
|
-
import { UserError
|
|
10
|
+
import { UserError } from '../util/index.js'
|
|
11
|
+
import { sayLine } from '../ui/index.js'
|
|
11
12
|
import {
|
|
12
13
|
discoverProjects,
|
|
13
14
|
loadProjects,
|
package/src/config.ts
CHANGED
|
@@ -28,6 +28,10 @@ export interface WorkspaceConfig {
|
|
|
28
28
|
baseBranch?: string
|
|
29
29
|
run?: RunSettings
|
|
30
30
|
cache?: CacheSettings
|
|
31
|
+
/** Input defaults for every task. */
|
|
32
|
+
inputs?: WorkspaceInputs
|
|
33
|
+
/** Output defaults for every task. */
|
|
34
|
+
outputs?: WorkspaceOutputs
|
|
31
35
|
lock?: LockSettings
|
|
32
36
|
watch?: WatchSettings
|
|
33
37
|
/**
|
|
@@ -76,6 +80,18 @@ export interface RunSettings {
|
|
|
76
80
|
rules?: WorkspaceRules
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/** `WorkspaceConfig.inputs`. */
|
|
84
|
+
export interface WorkspaceInputs {
|
|
85
|
+
/** The `inputs.stability` of every task that does not set its own. */
|
|
86
|
+
stability?: InputStability
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `WorkspaceConfig.outputs`. */
|
|
90
|
+
export interface WorkspaceOutputs {
|
|
91
|
+
/** The `outputs.clean` of every task that does not set its own. */
|
|
92
|
+
clean?: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
/** `WorkspaceConfig.cache`: where the cache lives and what a run does with it. */
|
|
80
96
|
export interface CacheSettings {
|
|
81
97
|
/**
|
|
@@ -93,10 +109,6 @@ export interface CacheSettings {
|
|
|
93
109
|
policy?: string
|
|
94
110
|
/** Which remote hits land on disk: `all` (the default), only the requested tasks' (`toplevel`), or `none`. */
|
|
95
111
|
download?: 'all' | 'toplevel' | 'none'
|
|
96
|
-
/** The `cache.inputStability` of every task that does not set its own. */
|
|
97
|
-
inputStability?: InputStability
|
|
98
|
-
/** The `cache.outputs.clean` of every task that does not set its own. */
|
|
99
|
-
outputs?: { clean?: boolean }
|
|
100
112
|
/**
|
|
101
113
|
* Which runs keep a failed task's log and the files its outputs left in
|
|
102
114
|
* the remote: CI runs (`'ci'`, the default), runs off CI (`'local'`), or
|
|
@@ -305,13 +317,15 @@ export interface TaskConfig {
|
|
|
305
317
|
* keyed group: its inputs are a key its dependents fold, with no process.
|
|
306
318
|
*/
|
|
307
319
|
inputs?: InputsConfig
|
|
320
|
+
/** What the task produces: saved on a miss, restored on a hit. Needs `cache: true`. */
|
|
321
|
+
outputs?: TaskOutputs
|
|
308
322
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
* Requires `exec` and `
|
|
312
|
-
* runs nothing to cache.
|
|
323
|
+
* `true` caches the task. **Caching is opt-in.** Omitted or `false`, the
|
|
324
|
+
* task always runs and nothing is read from or written to the cache.
|
|
325
|
+
* Requires `exec`, `inputs` and `outputs`: the key is the task's inputs,
|
|
326
|
+
* and a group runs nothing to cache.
|
|
313
327
|
*/
|
|
314
|
-
cache?:
|
|
328
|
+
cache?: boolean
|
|
315
329
|
}
|
|
316
330
|
|
|
317
331
|
/**
|
|
@@ -335,7 +349,7 @@ export interface TaskConfig {
|
|
|
335
349
|
* from the other coupled them both ways — a declaration added for
|
|
336
350
|
* caching silently widened the sandbox, and a path the task needed had
|
|
337
351
|
* to be laundered through the cache key to get it (2026-09-05).
|
|
338
|
-
* A declared `
|
|
352
|
+
* A declared `outputs` is NOT a write grant; the request
|
|
339
353
|
* `sandbox-request.ts` builds carries no write of its own and binds
|
|
340
354
|
* `allow.write` alone. This comment claimed the opposite — here and on
|
|
341
355
|
* both grant fields — while the file beside it and `schema.md` both said
|
|
@@ -391,6 +405,13 @@ export interface SandboxConfig {
|
|
|
391
405
|
weakerWhenNested?: boolean
|
|
392
406
|
/** Accept weaker network isolation (host proxy) for lower overhead. */
|
|
393
407
|
weakerNetworkIsolation?: boolean
|
|
408
|
+
/**
|
|
409
|
+
* Refuse every read outside the workspace too (`/usr`, `~/.cache`, the
|
|
410
|
+
* system toolchain), not only the workspace's other projects. A path
|
|
411
|
+
* outside it is readable only when `allow.read` names it; the shell a
|
|
412
|
+
* task runs in stays readable, so a task can start.
|
|
413
|
+
*/
|
|
414
|
+
hermetic?: boolean
|
|
394
415
|
}
|
|
395
416
|
|
|
396
417
|
/** The denials `sandbox.ignore` can leave out of the report: patterns per class. */
|
|
@@ -410,9 +431,9 @@ export interface SandboxGrants {
|
|
|
410
431
|
read?: readonly string[]
|
|
411
432
|
/**
|
|
412
433
|
* Writable paths or globs. The task can write NOTHING without these:
|
|
413
|
-
* declaring `
|
|
434
|
+
* declaring `outputs.files` grants no write, and a task whose
|
|
414
435
|
* only declaration is its outputs writes into a tmpfs that evaporates
|
|
415
|
-
* (vx then warns that `
|
|
436
|
+
* (vx then warns that `outputs` matched no files).
|
|
416
437
|
*/
|
|
417
438
|
write?: readonly string[]
|
|
418
439
|
/**
|
|
@@ -585,17 +606,6 @@ export interface ExecEnv {
|
|
|
585
606
|
secret?: readonly string[]
|
|
586
607
|
}
|
|
587
608
|
|
|
588
|
-
export interface CacheConfig {
|
|
589
|
-
/** What this task produces. Captured for restore on a cache hit. */
|
|
590
|
-
outputs: CacheOutputs
|
|
591
|
-
/**
|
|
592
|
-
* What a write to an input while the command runs means. Each field,
|
|
593
|
-
* highest first: its env var, this, the workspace's `inputStability`,
|
|
594
|
-
* the default (`'immutable'`, `'warn'`).
|
|
595
|
-
*/
|
|
596
|
-
inputStability?: InputStability
|
|
597
|
-
}
|
|
598
|
-
|
|
599
609
|
export interface InputStability {
|
|
600
610
|
/**
|
|
601
611
|
* `'immutable'` (the default): any write to an input during the command
|
|
@@ -615,7 +625,7 @@ export interface InputStability {
|
|
|
615
625
|
severity?: 'warn' | 'error'
|
|
616
626
|
}
|
|
617
627
|
|
|
618
|
-
export interface
|
|
628
|
+
export interface TaskOutputs {
|
|
619
629
|
/**
|
|
620
630
|
* Files produced by this task, as project-relative globs. Pass `[]`
|
|
621
631
|
* when the task has no files to capture (e.g. `lint`, `typecheck`).
|
|
@@ -640,7 +650,7 @@ export interface CacheOutputs {
|
|
|
640
650
|
* (tsc's `.tsbuildinfo`) instead of removing them first. A run that
|
|
641
651
|
* leaves a file it found untouched saves nothing, so an entry holds only
|
|
642
652
|
* what a clean run would leave. A retry still starts clean. Omitted →
|
|
643
|
-
* the workspace's `
|
|
653
|
+
* the workspace's `outputs.clean`, else `true`.
|
|
644
654
|
*/
|
|
645
655
|
clean?: boolean
|
|
646
656
|
}
|
|
@@ -716,6 +726,13 @@ export interface InputsConfig {
|
|
|
716
726
|
* tool versions, OS info, etc.
|
|
717
727
|
*/
|
|
718
728
|
workspaceRuntime?: readonly string[]
|
|
729
|
+
/**
|
|
730
|
+
* What a write to an input while the command runs means for the save.
|
|
731
|
+
* Each field, highest first: its env var, this, the workspace's
|
|
732
|
+
* `inputs.stability`, the default (`'immutable'`, `'warn'`). Needs
|
|
733
|
+
* `cache: true`.
|
|
734
|
+
*/
|
|
735
|
+
stability?: InputStability
|
|
719
736
|
}
|
|
720
737
|
|
|
721
738
|
/**
|
|
@@ -785,9 +802,7 @@ export function defineProject<const T extends ProjectConfig>(
|
|
|
785
802
|
dependsOn?: readonly DependsOnEntry<Extract<keyof NonNullable<T['tasks']>, string>>[]
|
|
786
803
|
exec?: Known<At<NonNullable<T['tasks']>[K], 'exec'>, ExecConfig>
|
|
787
804
|
inputs?: Known<At<NonNullable<T['tasks']>[K], 'inputs'>, InputsConfig>
|
|
788
|
-
|
|
789
|
-
outputs?: Known<At<At<NonNullable<T['tasks']>[K], 'cache'>, 'outputs'>, CacheOutputs>
|
|
790
|
-
}
|
|
805
|
+
outputs?: Known<At<NonNullable<T['tasks']>[K], 'outputs'>, TaskOutputs>
|
|
791
806
|
}
|
|
792
807
|
}
|
|
793
808
|
},
|
|
@@ -801,9 +816,11 @@ export function defineWorkspace<T extends WorkspaceConfig>(
|
|
|
801
816
|
run?: Known<At<T, 'run'>, RunSettings> & {
|
|
802
817
|
rules?: Known<At<At<T, 'run'>, 'rules'>, WorkspaceRules>
|
|
803
818
|
}
|
|
804
|
-
cache?: Known<At<T, 'cache'>, CacheSettings>
|
|
805
|
-
|
|
819
|
+
cache?: Known<At<T, 'cache'>, CacheSettings>
|
|
820
|
+
inputs?: Known<At<T, 'inputs'>, WorkspaceInputs> & {
|
|
821
|
+
stability?: Known<At<At<T, 'inputs'>, 'stability'>, InputStability>
|
|
806
822
|
}
|
|
823
|
+
outputs?: Known<At<T, 'outputs'>, WorkspaceOutputs>
|
|
807
824
|
lock?: Known<At<T, 'lock'>, LockSettings>
|
|
808
825
|
watch?: Known<At<T, 'watch'>, WatchSettings>
|
|
809
826
|
},
|
package/src/exec/executor.ts
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
import type { SandboxConfig } from '../config.js'
|
|
17
17
|
import type { CaptureConfig, RunResult } from './runner.js'
|
|
18
18
|
import type { ResolvedSandboxConfig, SandboxViolation } from './sandbox-runtime.js'
|
|
19
|
-
import { thrownMessage, UserError
|
|
19
|
+
import { thrownMessage, UserError } from '../util/index.js'
|
|
20
|
+
import * as ui from '../ui/index.js'
|
|
20
21
|
|
|
21
22
|
/** Sandbox baselines + the user's resolved sandbox block, when the task is sandboxed. */
|
|
22
23
|
export interface ExecuteSandbox {
|
|
@@ -42,6 +43,11 @@ export interface ExecuteSandbox {
|
|
|
42
43
|
* machine's paths and platform, hands `sandboxRequestFor`.
|
|
43
44
|
*/
|
|
44
45
|
readonly keyed?: readonly string[]
|
|
46
|
+
/**
|
|
47
|
+
* The dependencies' outputs it may read (absolute), which an executor
|
|
48
|
+
* that builds the request again hands `sandboxRequestFor` too.
|
|
49
|
+
*/
|
|
50
|
+
readonly dependencyReads?: readonly string[]
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
/** One declared input file, as the cache key saw it. */
|
|
@@ -107,7 +113,7 @@ export interface ExecuteRequest {
|
|
|
107
113
|
/** `${project}#${task}` — for executors that route or log by task. */
|
|
108
114
|
readonly taskId: string
|
|
109
115
|
readonly workspaceRoot: string
|
|
110
|
-
/** Present for cacheable tasks (the miss path); absent
|
|
116
|
+
/** Present for cacheable tasks (the miss path); absent for a task without `cache: true`. */
|
|
111
117
|
readonly inputs?: TaskInputs
|
|
112
118
|
/**
|
|
113
119
|
* The task's cache key (present when the task is cacheable). An executor
|
|
@@ -144,6 +150,12 @@ export interface ExecuteRequest {
|
|
|
144
150
|
* executor's `platforms`: its key includes it. Absent when the task names none.
|
|
145
151
|
*/
|
|
146
152
|
readonly platform?: string
|
|
153
|
+
/**
|
|
154
|
+
* Which attempt of the task this is, from 2 when it retries; absent on
|
|
155
|
+
* the first. An executor that can, runs a retry somewhere other than
|
|
156
|
+
* the attempt before it, so the machine is not the flake.
|
|
157
|
+
*/
|
|
158
|
+
readonly attempt?: number
|
|
147
159
|
/**
|
|
148
160
|
* Whether this task's outputs must land on the submitter's disk now.
|
|
149
161
|
* 'eager' (or absent) = materialise as today. 'deferred' = leave them
|
|
@@ -376,6 +388,13 @@ export interface TaskExecutor {
|
|
|
376
388
|
* anything but a finite number, reports none.
|
|
377
389
|
*/
|
|
378
390
|
idealMs?(): number | undefined
|
|
391
|
+
/**
|
|
392
|
+
* The workers that would have run the run best, judged after it from
|
|
393
|
+
* what each task took: for the run summary, so its reader can change
|
|
394
|
+
* how many workers the executor may start and of what size. Asked once
|
|
395
|
+
* at the end, only when a sink is on. A throw reports none.
|
|
396
|
+
*/
|
|
397
|
+
advice?(): readonly WorkerAdvice[]
|
|
379
398
|
/**
|
|
380
399
|
* Its own Chrome-trace events for `vx run --profile` (the machines it ran
|
|
381
400
|
* tasks on, in time), asked once at the end, only when the flag is set.
|
|
@@ -422,6 +441,27 @@ export interface MachineReport {
|
|
|
422
441
|
startMs?: number
|
|
423
442
|
}
|
|
424
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Workers that would have run the run best (`TaskExecutor.advice`): `of`
|
|
446
|
+
* says on what. `declared`: as many of each size the executor may start
|
|
447
|
+
* as end it soonest; `more`: past each size's limit, when that ends it
|
|
448
|
+
* sooner; `any`: one shape a platform, of any cores, ending as soon for
|
|
449
|
+
* the least core time.
|
|
450
|
+
*/
|
|
451
|
+
export interface WorkerAdvice {
|
|
452
|
+
of: 'declared' | 'more' | 'any'
|
|
453
|
+
/** The run's end on them, from its start. */
|
|
454
|
+
endMs: number
|
|
455
|
+
workers: readonly {
|
|
456
|
+
count: number
|
|
457
|
+
cores: number
|
|
458
|
+
memoryBytes: number
|
|
459
|
+
platform?: string
|
|
460
|
+
/** The size it is, for `declared` and `more`. */
|
|
461
|
+
size?: string
|
|
462
|
+
}[]
|
|
463
|
+
}
|
|
464
|
+
|
|
425
465
|
/**
|
|
426
466
|
* What a remote executor rejects with when it gives a task back: core runs
|
|
427
467
|
* the same request on the local floor and says `reason` once (a remote that
|