@vzn/vx 0.0.636 → 0.0.638
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -11
- package/docs/caching.md +246 -213
- package/docs/cli.md +791 -705
- package/docs/execution.md +68 -76
- package/docs/features.md +47 -42
- package/docs/patterns.md +26 -25
- package/docs/schema.md +392 -205
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +4 -4
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +28 -13
- package/src/bin.ts +30 -9
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +124 -53
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +15 -1
- package/src/cache/inputs.ts +102 -36
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +15 -18
- package/src/cache/layered-cache.ts +20 -102
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +32 -27
- package/src/cache/run-history.ts +104 -14
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +58 -31
- package/src/cache/scoped-cache.ts +237 -0
- package/src/cli/cache.ts +173 -34
- package/src/cli/completions.ts +11 -7
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +110 -70
- package/src/cli/index.ts +67 -24
- package/src/cli/info.ts +43 -27
- package/src/cli/init.ts +115 -407
- package/src/cli/last.ts +157 -9
- package/src/cli/lock.ts +356 -122
- package/src/cli/plan-format.ts +7 -5
- package/src/cli/plugin-commands.ts +81 -14
- package/src/cli/plugin-templates.ts +11 -11
- package/src/cli/run.ts +116 -160
- package/src/cli/select.ts +50 -47
- package/src/cli/show.ts +83 -59
- package/src/cli/task-verb.ts +6 -13
- package/src/cli/upgrade.ts +415 -149
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +39 -61
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +18 -4
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +227 -230
- package/src/cli/why.ts +38 -18
- package/src/cli/workspace-config.ts +33 -70
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +27 -7
- package/src/graph/task-graph.ts +76 -24
- package/src/handoff.ts +169 -0
- package/src/index.ts +35 -58
- package/src/orchestrator/affected-tasks.ts +15 -8
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +147 -69
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +9 -5
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +126 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +20 -5
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +80 -41
- package/src/orchestrator/metrics.ts +78 -9
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +100 -66
- package/src/orchestrator/plugin.ts +173 -67
- package/src/orchestrator/prepare.ts +163 -78
- package/src/orchestrator/projects.ts +105 -99
- package/src/orchestrator/remote-prefetch.ts +11 -6
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-records.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +244 -119
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +49 -7
- package/src/orchestrator/summary.ts +115 -106
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +4 -4
- package/src/settings.ts +428 -0
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +33 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +16 -3
- package/src/util/paths.ts +10 -0
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ui.ts +186 -0
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +84 -232
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +65 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +307 -215
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +363 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/src/orchestrator/run.ts
CHANGED
|
@@ -3,12 +3,7 @@
|
|
|
3
3
|
// so the layers can be swapped without touching the others.
|
|
4
4
|
|
|
5
5
|
import type { ProjectEntry } from '../workspace/index.js'
|
|
6
|
-
import {
|
|
7
|
-
inputStabilityEnv,
|
|
8
|
-
loadWorkspace,
|
|
9
|
-
unreachedHint,
|
|
10
|
-
unreachedPackages,
|
|
11
|
-
} from '../workspace/index.js'
|
|
6
|
+
import { inputStabilityEnv, LOCK_INVALID, lockEntryBroken } from '../workspace/index.js'
|
|
12
7
|
import { realpathSync } from 'node:fs'
|
|
13
8
|
import path from 'node:path'
|
|
14
9
|
import {
|
|
@@ -16,6 +11,11 @@ import {
|
|
|
16
11
|
type CacheLayer,
|
|
17
12
|
type CachePolicy,
|
|
18
13
|
cachesNothing,
|
|
14
|
+
formatCachePolicy,
|
|
15
|
+
packRunOutputs,
|
|
16
|
+
resolveOutputs,
|
|
17
|
+
resolveWorkspaceOutputs,
|
|
18
|
+
runEntryName,
|
|
19
19
|
stopRuntimeProbes,
|
|
20
20
|
} from '../cache/index.js'
|
|
21
21
|
import { VERSION } from '../version.js'
|
|
@@ -42,8 +42,6 @@ import {
|
|
|
42
42
|
stageTimes,
|
|
43
43
|
killGraceMs,
|
|
44
44
|
MAX_TIMEOUT_MS,
|
|
45
|
-
parseDuration,
|
|
46
|
-
parseSize,
|
|
47
45
|
printTimings,
|
|
48
46
|
ulid,
|
|
49
47
|
isLiteralPattern,
|
|
@@ -54,23 +52,28 @@ import {
|
|
|
54
52
|
teardownTimeoutMs,
|
|
55
53
|
secretMask,
|
|
56
54
|
isFsRefusal,
|
|
55
|
+
isUserError,
|
|
56
|
+
detectColors,
|
|
57
|
+
relPosix,
|
|
58
|
+
type ColorSupport,
|
|
57
59
|
} from '../util/index.js'
|
|
58
60
|
import { keyedProjects } from './keyed-projects.js'
|
|
59
61
|
import { isDefaultBuild } from './projects.js'
|
|
60
|
-
import { prepareSandbox } from './sandbox-request.js'
|
|
62
|
+
import { commandWriteReach, prepareSandbox } from './sandbox-request.js'
|
|
61
63
|
import type { OutputDirSnapshot, SaveFacts } from './miss-save.js'
|
|
62
64
|
import { admitTasks, taintTracker } from './admission.js'
|
|
63
65
|
import { unkeyedGroupOutcome, type ExecuteArgs } from './execute-task.js'
|
|
64
66
|
import { excludedTaint } from './excluded-keys.js'
|
|
65
67
|
import { keyUpstream } from './upstream.js'
|
|
66
68
|
import { busLogger, createEventBus, terminalSubscriber, type EventBus } from './events.js'
|
|
67
|
-
import { installPlugins, PluginSetupError } from './plugin.js'
|
|
69
|
+
import { installPlugins, PluginSetupError, type RunReport, type RunReportTask } from './plugin.js'
|
|
70
|
+
import { decodeOutputLog } from './output-log.js'
|
|
68
71
|
import { buildAdmission, executorLabel, resolveExecutors, teardownPlugins } from './plugin-host.js'
|
|
69
72
|
import { subscribeTelemetry, type TelemetryHandle } from './telemetry-host.js'
|
|
73
|
+
import { claimOutput, guardedOutput } from './output-host.js'
|
|
70
74
|
import { assembleRunSummary, isPassStatus } from './telemetry.js'
|
|
71
75
|
import type { RunContextRecord } from './telemetry.js'
|
|
72
76
|
import { defaultLogger, type DefaultLogger, resolveOutputView, type Logger } from './logger.js'
|
|
73
|
-
import { detectColors, type ColorSupport } from './colors.js'
|
|
74
77
|
import { plainOutput } from './plain-output.js'
|
|
75
78
|
import { formatPersistentList } from './framed-output.js'
|
|
76
79
|
import { createForecast } from './forecast.js'
|
|
@@ -126,10 +129,13 @@ import {
|
|
|
126
129
|
type RunSummaryJson,
|
|
127
130
|
} from './run-artifacts.js'
|
|
128
131
|
import { createSaveLane } from './save-lane.js'
|
|
132
|
+
import { commandWindows } from './command-windows.js'
|
|
129
133
|
import { formatOutcomeTable, formatRunSummary } from './summary.js'
|
|
130
134
|
import { detectFlaky, type FlakyCandidate, type FlakyFinding } from './failure-mode.js'
|
|
131
135
|
import { createMissExplainer } from './miss-reason.js'
|
|
132
136
|
import type { HeldServer, RunOptions, RunSummary } from './options.js'
|
|
137
|
+
import { withRunSettings } from './run-settings.js'
|
|
138
|
+
import { resolveSetting } from '../settings.js'
|
|
133
139
|
|
|
134
140
|
// Per run, never shared: a `vx watch` process runs many, and a shared map
|
|
135
141
|
// is one `preProbed.set` away from leaking a hit across cycles.
|
|
@@ -140,40 +146,19 @@ const emptyShortCircuit = (): ShortCircuit => ({
|
|
|
140
146
|
groupKeys: new Map(),
|
|
141
147
|
})
|
|
142
148
|
|
|
143
|
-
/**
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (!Number.isInteger(n) || n <= 0) return undefined
|
|
157
|
-
// CLAMPED, not refused — unlike `exec.timeout` and `--timeout`, this rung's
|
|
158
|
-
// contract is already "unusable value → fall back", so it never throws. But
|
|
159
|
-
// falling back is wrong here: someone who typed a huge number wants a long
|
|
160
|
-
// timeout, and the largest expressible one IS long (~24.8 days). Handing it
|
|
161
|
-
// to `setTimeout` unbounded would mean 1 ms — killing every task instantly.
|
|
162
|
-
return Math.min(n, MAX_TIMEOUT_MS)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Compact the 4-axis cache policy into the `invocations.cache_policy`
|
|
167
|
-
* column string. Each enabled axis contributes its flag; a fully-on
|
|
168
|
-
* policy reads `'lR,lW,rR,rW'`. Pure presentation — never affects the key.
|
|
169
|
-
*/
|
|
170
|
-
function compactCachePolicy(p: CachePolicy): string {
|
|
171
|
-
const parts: string[] = []
|
|
172
|
-
if (p.localRead) parts.push('lR')
|
|
173
|
-
if (p.localWrite) parts.push('lW')
|
|
174
|
-
if (p.remoteRead) parts.push('rR')
|
|
175
|
-
if (p.remoteWrite) parts.push('rW')
|
|
176
|
-
return parts.join(',')
|
|
149
|
+
/** The files `node`'s declared outputs match on disk now, workspace-relative and sorted. */
|
|
150
|
+
async function outputsLeft(
|
|
151
|
+
node: TaskNode,
|
|
152
|
+
workspaceRoot: string,
|
|
153
|
+
nestedProjectDirs: string[],
|
|
154
|
+
): Promise<string[]> {
|
|
155
|
+
const declared = node.config.cache?.outputs
|
|
156
|
+
if (declared === undefined) return []
|
|
157
|
+
const [own, ws] = await Promise.all([
|
|
158
|
+
resolveOutputs({ projectDir: node.projectDir, outputs: declared.files, nestedProjectDirs }),
|
|
159
|
+
resolveWorkspaceOutputs({ workspaceRoot, outputs: declared.workspaceFiles ?? [] }),
|
|
160
|
+
])
|
|
161
|
+
return [...new Set([...own, ...ws].map((p) => relPosix(workspaceRoot, p)))].sort()
|
|
177
162
|
}
|
|
178
163
|
|
|
179
164
|
/**
|
|
@@ -182,7 +167,7 @@ function compactCachePolicy(p: CachePolicy): string {
|
|
|
182
167
|
* local cache writes NOWHERE, yet the write axis reads as on. Normalising
|
|
183
168
|
* here — the ONE place both `run()` and `planRun()` derive it from — is
|
|
184
169
|
* what keeps `--dry` describing the run you are about to get: reading the
|
|
185
|
-
* raw request made the plan label `--cache=local:,remote:rw` "cache miss —
|
|
170
|
+
* raw request made the plan label `--cache-policy=local:,remote:rw` "cache miss —
|
|
186
171
|
* would exec" (a result that will be stored) for a run in which caching is
|
|
187
172
|
* entirely off.
|
|
188
173
|
*/
|
|
@@ -201,7 +186,7 @@ function effectiveCachePolicy(requested: CachePolicy, hasRemoteLayer: boolean):
|
|
|
201
186
|
* ingests hits into local for execute-task's lazy probe. The gate asks
|
|
202
187
|
* the layer (`hasRemote`), not `instanceof LayeredCache` — a
|
|
203
188
|
* third-party remote layer stalls on the classify just as hard.
|
|
204
|
-
* - the policy reads locally (a `--no-cache`/`--force`/`--cache=local:`
|
|
189
|
+
* - the policy reads locally (a `--no-cache`/`--force`/`--cache-policy=local:`
|
|
205
190
|
* run reads nothing locally → nothing to restore → skip);
|
|
206
191
|
* - there is at least one task.
|
|
207
192
|
* A flat graph has no ordering to bypass, but the classify pass is worth
|
|
@@ -294,6 +279,12 @@ function refuseRunOptions(options: RunOptions): void {
|
|
|
294
279
|
oneOf('outputLogs', options.outputLogs, ['full', 'errors-only', 'none', 'hash-only'])
|
|
295
280
|
oneOf('download', options.download, ['all', 'toplevel', 'none'])
|
|
296
281
|
oneOf('flow', options.flow, ['focused', 'broad'])
|
|
282
|
+
oneOf('schedule', options.schedule, [
|
|
283
|
+
'most-work',
|
|
284
|
+
'critical-path',
|
|
285
|
+
'direct-dependents',
|
|
286
|
+
'ready-order',
|
|
287
|
+
])
|
|
297
288
|
const exclude = options.excludeDependencies
|
|
298
289
|
if (exclude !== undefined && exclude !== 'all' && !names(exclude))
|
|
299
290
|
refuse('excludeDependencies', exclude, "'all' or an array of task names")
|
|
@@ -304,11 +295,15 @@ function refuseRunOptions(options: RunOptions): void {
|
|
|
304
295
|
refuse('concurrency', concurrency, 'a positive integer')
|
|
305
296
|
if (retries !== undefined && !(Number.isInteger(retries) && retries >= 0))
|
|
306
297
|
refuse('retries', retries, 'a non-negative integer')
|
|
307
|
-
|
|
308
|
-
timeout
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
298
|
+
for (const [name, ms] of [
|
|
299
|
+
['timeout', timeout],
|
|
300
|
+
['killGrace', options.killGrace],
|
|
301
|
+
['teardownTimeout', options.teardownTimeout],
|
|
302
|
+
['readyNotice', options.readyNotice],
|
|
303
|
+
] as const) {
|
|
304
|
+
if (ms !== undefined && !(Number.isInteger(ms) && ms > 0 && ms <= MAX_TIMEOUT_MS))
|
|
305
|
+
refuse(name, ms, `a positive integer of ms, at most ${MAX_TIMEOUT_MS}`)
|
|
306
|
+
}
|
|
312
307
|
}
|
|
313
308
|
|
|
314
309
|
export async function run(options: RunOptions): Promise<RunSummary> {
|
|
@@ -337,6 +332,7 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
337
332
|
coalesce: true,
|
|
338
333
|
...(options.tty === true ? { tty: true } : {}),
|
|
339
334
|
...(options.forwardArgs !== undefined ? { forwardArgs: options.forwardArgs } : {}),
|
|
335
|
+
...(options.footer === false ? { footer: false } : {}),
|
|
340
336
|
},
|
|
341
337
|
)
|
|
342
338
|
: null
|
|
@@ -349,16 +345,26 @@ export async function run(options: RunOptions): Promise<RunSummary> {
|
|
|
349
345
|
// Tasks run with colour forced (exec/env.ts); where the terminal prints
|
|
350
346
|
// plain, their escapes are stripped. A custom logger gets the task's
|
|
351
347
|
// bytes as written, as the cache holds them: it formats its own output.
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
)
|
|
348
|
+
const render = terminalSubscriber(terminal === null || colors.enabled ? sink : plainOutput(sink))
|
|
349
|
+
let unsubscribeTerminal = bus.subscribe(render)
|
|
355
350
|
const detach = (): void => {
|
|
356
351
|
unsubscribeTerminal()
|
|
357
352
|
terminal?.settle()
|
|
358
353
|
}
|
|
354
|
+
// A plugin's `output` renders the run from its start; core's renderer
|
|
355
|
+
// stays the fallback. Only core's own renderer is replaceable: an
|
|
356
|
+
// embedder's `log` is never consulted away.
|
|
357
|
+
const takeOutput =
|
|
358
|
+
terminal === null
|
|
359
|
+
? undefined
|
|
360
|
+
: (claimed: { logger: Logger; owner: string }): void => {
|
|
361
|
+
terminal.settle()
|
|
362
|
+
unsubscribeTerminal()
|
|
363
|
+
unsubscribeTerminal = bus.subscribe(guardedOutput(claimed, render))
|
|
364
|
+
}
|
|
359
365
|
let held = false
|
|
360
366
|
try {
|
|
361
|
-
const summary = await runOnBus(options, bus, colors, terminal)
|
|
367
|
+
const summary = await runOnBus(options, bus, colors, terminal, takeOutput)
|
|
362
368
|
if (summary.persistent === undefined) return summary
|
|
363
369
|
// Servers handed back still running still write: `vx watch dev` printed
|
|
364
370
|
// none of its server's log while it idled, the renderer gone with this
|
|
@@ -382,6 +388,7 @@ async function runOnBus(
|
|
|
382
388
|
bus: EventBus,
|
|
383
389
|
colors: ColorSupport,
|
|
384
390
|
terminal: DefaultLogger | null,
|
|
391
|
+
takeOutput?: (claimed: { logger: Logger; owner: string }) => void,
|
|
385
392
|
): Promise<RunSummary> {
|
|
386
393
|
const log = busLogger(bus)
|
|
387
394
|
// A server's output goes through a cell of its own: a later watch cycle
|
|
@@ -398,6 +405,7 @@ async function runOnBus(
|
|
|
398
405
|
}
|
|
399
406
|
|
|
400
407
|
const prepared = await prepareRun(options, log)
|
|
408
|
+
options = withRunSettings(options, prepared.workspaceConfig)
|
|
401
409
|
mark('plugin stages')
|
|
402
410
|
// Every exit from here tears the plugins down: their capability
|
|
403
411
|
// factories ran in prepareRun, and teardown is what releases what those
|
|
@@ -406,11 +414,11 @@ async function runOnBus(
|
|
|
406
414
|
// per cycle under `vx watch`. A plugin whose own setup threw set up
|
|
407
415
|
// nothing, and is the one left out.
|
|
408
416
|
let tornDown = false
|
|
409
|
-
const teardown = async (except?: unknown): Promise<void> => {
|
|
417
|
+
const teardown = async (except?: unknown, report?: RunReport): Promise<void> => {
|
|
410
418
|
if (tornDown) return
|
|
411
419
|
tornDown = true
|
|
412
420
|
const plugins = prepared.plugins.filter((p) => p !== except)
|
|
413
|
-
await teardownPlugins(plugins, (m) => log.status(m))
|
|
421
|
+
await teardownPlugins(plugins, (m) => log.status(m), report)
|
|
414
422
|
}
|
|
415
423
|
// A task whose command re-enters `vx run` in the workspace running it
|
|
416
424
|
// is refused. When the inner run reaches this task again it forks a run
|
|
@@ -438,7 +446,7 @@ async function runOnBus(
|
|
|
438
446
|
// A refusal is the caller's to print (on stderr, as `vx run:`): as a
|
|
439
447
|
// status line it went to stdout, into whatever a script piped there.
|
|
440
448
|
if (prepared.unresolvedTasks.length > 0) {
|
|
441
|
-
const refused = `no projects declare task(s): ${prepared.unresolvedTasks.join(', ')}.${
|
|
449
|
+
const refused = `no projects declare task(s): ${prepared.unresolvedTasks.join(', ')}.${await unresolvedHint(prepared)}`
|
|
442
450
|
await teardown()
|
|
443
451
|
prepared.cache.close()
|
|
444
452
|
return { ok: false, outcomes: [], refused }
|
|
@@ -468,6 +476,13 @@ async function runOnBus(
|
|
|
468
476
|
// the plugin. A plugin without `setup` subscribes nothing.
|
|
469
477
|
let disposePlugins: (() => void) | undefined
|
|
470
478
|
let telemetry: TelemetryHandle | undefined
|
|
479
|
+
// One run-id per `vx run` invocation. Every task in the resulting graph
|
|
480
|
+
// carries it so analytics queries can group by invocation; plugins get it
|
|
481
|
+
// in their teardown's `RunReport`.
|
|
482
|
+
const runId = ulid()
|
|
483
|
+
const head = captureGitContext(prepared.workspaceRoot)
|
|
484
|
+
const ciContext = detectCi(process.env)
|
|
485
|
+
const keepRunLog = prepared.keepRunEntry !== undefined
|
|
471
486
|
try {
|
|
472
487
|
disposePlugins = await installPlugins({
|
|
473
488
|
plugins: prepared.plugins,
|
|
@@ -475,6 +490,9 @@ async function runOnBus(
|
|
|
475
490
|
workspaceRoot: prepared.workspaceRoot,
|
|
476
491
|
cacheDir: prepared.cacheDir,
|
|
477
492
|
warn: (m) => log.status(m),
|
|
493
|
+
cache: prepared.cacheFor,
|
|
494
|
+
ci: ciContext.ci,
|
|
495
|
+
branch: prepared.branch(),
|
|
478
496
|
})
|
|
479
497
|
} catch (err) {
|
|
480
498
|
disposePlugins?.()
|
|
@@ -499,14 +517,13 @@ async function runOnBus(
|
|
|
499
517
|
cacheDir,
|
|
500
518
|
cache,
|
|
501
519
|
nodes,
|
|
502
|
-
workspaceFingerprint,
|
|
503
520
|
fingerprintWatch,
|
|
504
521
|
nestedDirsByProject,
|
|
505
522
|
gitFilesCache,
|
|
506
523
|
hashCache,
|
|
507
524
|
workspaceProjectCount,
|
|
508
525
|
} = prepared
|
|
509
|
-
const concurrency = options.concurrency ??
|
|
526
|
+
const concurrency = options.concurrency ?? machineParallelism()
|
|
510
527
|
// An earlier cycle's servers: the ones this graph starts again unchanged
|
|
511
528
|
// stay up; the others go now, before anything binds their ports. A stop
|
|
512
529
|
// that lands meanwhile ends the run here, having run nothing: an aborted
|
|
@@ -598,7 +615,7 @@ async function runOnBus(
|
|
|
598
615
|
}
|
|
599
616
|
}
|
|
600
617
|
|
|
601
|
-
//
|
|
618
|
+
// `cache.download` (default `all`) decides ONCE per task, here, whether a
|
|
602
619
|
// remote execution's outputs come home.
|
|
603
620
|
const downloadPolicy = options.download ?? 'all'
|
|
604
621
|
const localPlaced = locallyPlaced(placements, nodes.keys())
|
|
@@ -609,20 +626,20 @@ async function runOnBus(
|
|
|
609
626
|
remoteOnly: placements.remoteOnly,
|
|
610
627
|
})
|
|
611
628
|
|
|
612
|
-
//
|
|
613
|
-
|
|
614
|
-
// → `VX_TASK_TIMEOUT` env → workspace `timeout`. A malformed env value is
|
|
615
|
-
// ignored (a bad timeout must not silently disable a task's own limit).
|
|
616
|
-
const taskTimeoutDefault =
|
|
617
|
-
options.timeout ?? readTaskTimeoutEnv() ?? workspaceConfig?.timeout ?? undefined
|
|
629
|
+
// `run.timeout` (ms), for any task WITHOUT its own `exec.timeout`.
|
|
630
|
+
const taskTimeoutDefault = options.timeout
|
|
618
631
|
// Read once, before any task: a bad value is refused here, not mid-run.
|
|
619
632
|
const inputStability = {
|
|
620
633
|
run: inputStabilityEnv(),
|
|
621
|
-
...(workspaceConfig?.inputStability !== undefined
|
|
622
|
-
? { workspace: workspaceConfig.inputStability }
|
|
634
|
+
...(workspaceConfig?.cache?.inputStability !== undefined
|
|
635
|
+
? { workspace: workspaceConfig.cache.inputStability }
|
|
623
636
|
: {}),
|
|
624
637
|
}
|
|
625
638
|
|
|
639
|
+
const cleanOutputs = resolveSetting('cache.outputs.clean', workspaceConfig).value as
|
|
640
|
+
| boolean
|
|
641
|
+
| undefined
|
|
642
|
+
|
|
626
643
|
// Run-scoped registries of live subprocesses:
|
|
627
644
|
// - `liveChildren`: in-flight children. The runner adds/removes
|
|
628
645
|
// each child around its spawn (persistent children stay until
|
|
@@ -710,7 +727,7 @@ async function runOnBus(
|
|
|
710
727
|
// bumps are flushed, so a throw between opening the cache and the
|
|
711
728
|
// normal close leaked the SQLite handle (it matters in a long-lived
|
|
712
729
|
// host) AND lost this run's touch record, after which an LRU
|
|
713
|
-
// `vx cache
|
|
730
|
+
// `vx cache clean` can evict entries the run just hit. Once-only
|
|
714
731
|
// because the normal path closes before the persistent-task wait —
|
|
715
732
|
// holding the handle open for a dev server's whole lifetime would be
|
|
716
733
|
// worse — and because close() re-runs its retention DELETEs.
|
|
@@ -737,9 +754,6 @@ async function runOnBus(
|
|
|
737
754
|
}),
|
|
738
755
|
)
|
|
739
756
|
try {
|
|
740
|
-
// One run-id per `vx run` invocation. Every task in the resulting
|
|
741
|
-
// graph carries it so analytics queries can group by invocation.
|
|
742
|
-
const runId = ulid()
|
|
743
757
|
const runStartHrTimeNs = process.hrtime.bigint()
|
|
744
758
|
const endedAtMsAtStart = Date.now()
|
|
745
759
|
const remoteCacheEnabled = prepared.hasRemoteLayer
|
|
@@ -751,7 +765,7 @@ async function runOnBus(
|
|
|
751
765
|
const policy: CachePolicy = effectiveCachePolicy(prepared.cachePolicy, prepared.hasRemoteLayer)
|
|
752
766
|
if (options.remoteRequested === true && !prepared.hasRemoteLayer) {
|
|
753
767
|
log.status(
|
|
754
|
-
'vx: --cache named the remote cache, but no cache plugin in vx
|
|
768
|
+
'vx: --cache-policy named the remote cache, but no cache plugin in .vx/workspace.* supplies one — the remote axes are off for this run',
|
|
755
769
|
)
|
|
756
770
|
}
|
|
757
771
|
const deferredOutputs = new DeferredOutputs({
|
|
@@ -768,8 +782,12 @@ async function runOnBus(
|
|
|
768
782
|
// fails a run). `dirty` reuses the `git status --porcelain` the
|
|
769
783
|
// GitFilesCache populate already ran for input enumeration — no
|
|
770
784
|
// second status spawn.
|
|
771
|
-
|
|
772
|
-
const
|
|
785
|
+
// `run.branch` names the branch over git's (`prepared.branch`).
|
|
786
|
+
const gitContext = {
|
|
787
|
+
...head,
|
|
788
|
+
dirty: gitFilesCache.worktreeDirty,
|
|
789
|
+
branch: prepared.branch(),
|
|
790
|
+
}
|
|
773
791
|
const hostContext = captureHostContext()
|
|
774
792
|
|
|
775
793
|
// The canonical run-context record — the same git/CI/host data the
|
|
@@ -784,7 +802,7 @@ async function runOnBus(
|
|
|
784
802
|
let runContextRecord: RunContextRecord | undefined
|
|
785
803
|
// Gate on a plugin that can actually CONTRIBUTE a sink, not on declaring
|
|
786
804
|
// any plugin at all: a cache-only plugin has no telemetry hook to
|
|
787
|
-
// consult, so paying for the record (2 git spawns + a `.vx/workspace-id`
|
|
805
|
+
// consult, so paying for the record (2 git spawns + a `.vx/cache/workspace-id`
|
|
788
806
|
// write on a remote-less repo) buys nothing. A plugin that has the hook
|
|
789
807
|
// but declines still pays — its answer is only knowable by asking.
|
|
790
808
|
const hasTelemetryPlugin = prepared.plugins.some((p) => p.telemetry !== undefined)
|
|
@@ -807,7 +825,7 @@ async function runOnBus(
|
|
|
807
825
|
invocationCommand(options.command?.split(' ') ?? process.argv.slice(1)),
|
|
808
826
|
),
|
|
809
827
|
requestedTasks: [...options.tasks],
|
|
810
|
-
cachePolicy:
|
|
828
|
+
cachePolicy: formatCachePolicy(policy),
|
|
811
829
|
concurrency,
|
|
812
830
|
flow: options.flow ?? null,
|
|
813
831
|
commitSha: gitContext.commitSha,
|
|
@@ -869,6 +887,13 @@ async function runOnBus(
|
|
|
869
887
|
log.status(`[vx] cache save failed: ${err instanceof Error ? err.message : String(err)}`),
|
|
870
888
|
)
|
|
871
889
|
const deferredSaves = new Map<string, Promise<SaveFacts>>()
|
|
890
|
+
const windows = commandWindows()
|
|
891
|
+
// A server held from an earlier run (`vx watch`) runs through this one
|
|
892
|
+
// too, so it may write a mutable input while it is described.
|
|
893
|
+
for (const id of heldBefore.kept.keys()) {
|
|
894
|
+
const node = nodes.get(id)!
|
|
895
|
+
windows.open(id, node.projectDir, commandWriteReach(node, workspaceRoot))
|
|
896
|
+
}
|
|
872
897
|
|
|
873
898
|
// Focused flow: a requested GROUP has no output of its own, so
|
|
874
899
|
// surface the same-project, non-group tasks it chains (one level)
|
|
@@ -903,10 +928,31 @@ async function runOnBus(
|
|
|
903
928
|
version: VERSION,
|
|
904
929
|
packageCount: packagesInScope.size,
|
|
905
930
|
remoteCacheEnabled,
|
|
931
|
+
cache: {
|
|
932
|
+
local: policy.localRead || policy.localWrite,
|
|
933
|
+
localWrite: policy.localWrite,
|
|
934
|
+
remote: remoteCacheEnabled && (policy.remoteRead || policy.remoteWrite),
|
|
935
|
+
uploads: prepared.localCache.uploads,
|
|
936
|
+
notDownloaded: () => deferredOutputs.pending().length,
|
|
937
|
+
},
|
|
906
938
|
concurrency,
|
|
907
939
|
workspaceProjectCount,
|
|
908
940
|
}
|
|
909
941
|
|
|
942
|
+
// Asked only when a plugin declares `output`: a plain run pays nothing.
|
|
943
|
+
if (takeOutput !== undefined && prepared.plugins.some((p) => p.output !== undefined)) {
|
|
944
|
+
const json = options.json === true
|
|
945
|
+
const claimed = await claimOutput(prepared.plugins, {
|
|
946
|
+
workspaceRoot,
|
|
947
|
+
cacheDir,
|
|
948
|
+
warn: (m: string) => log.status(m),
|
|
949
|
+
isTTY: (json ? process.stderr : process.stdout).isTTY === true,
|
|
950
|
+
ci: ciContext.ci,
|
|
951
|
+
json,
|
|
952
|
+
})
|
|
953
|
+
if (claimed !== undefined) takeOutput(claimed)
|
|
954
|
+
}
|
|
955
|
+
|
|
910
956
|
// Lifecycle hooks drive the default logger's dynamic status line
|
|
911
957
|
// (TTY-only); custom loggers may ignore them.
|
|
912
958
|
log.runStart?.({
|
|
@@ -921,7 +967,11 @@ async function runOnBus(
|
|
|
921
967
|
terminal?.forecast(async () => {
|
|
922
968
|
const ids: string[] = []
|
|
923
969
|
for (const n of nodes.values()) if (!isGroupTask(n)) ids.push(n.id)
|
|
924
|
-
const p50s = await new LocalHistoryProvider(
|
|
970
|
+
const p50s = await new LocalHistoryProvider(
|
|
971
|
+
prepared.localCache.dbHandle(),
|
|
972
|
+
undefined,
|
|
973
|
+
prepared.branch(),
|
|
974
|
+
).p50sFor(ids)
|
|
925
975
|
return createForecast(nodes, p50s, concurrency)
|
|
926
976
|
})
|
|
927
977
|
|
|
@@ -938,7 +988,7 @@ async function runOnBus(
|
|
|
938
988
|
nodes,
|
|
939
989
|
cache,
|
|
940
990
|
workspaceRoot,
|
|
941
|
-
|
|
991
|
+
claimedFiles: fingerprintWatch.names,
|
|
942
992
|
forwardArgs: options.forwardArgs,
|
|
943
993
|
nestedDirsByProject,
|
|
944
994
|
gitFilesCache,
|
|
@@ -970,7 +1020,7 @@ async function runOnBus(
|
|
|
970
1020
|
nodes,
|
|
971
1021
|
cache,
|
|
972
1022
|
workspaceRoot,
|
|
973
|
-
|
|
1023
|
+
claimedFiles: fingerprintWatch.names,
|
|
974
1024
|
forwardArgs: options.forwardArgs,
|
|
975
1025
|
nestedDirsByProject,
|
|
976
1026
|
gitFilesCache,
|
|
@@ -1043,7 +1093,6 @@ async function runOnBus(
|
|
|
1043
1093
|
node,
|
|
1044
1094
|
upstream,
|
|
1045
1095
|
workspaceRoot,
|
|
1046
|
-
workspaceFingerprint,
|
|
1047
1096
|
cache,
|
|
1048
1097
|
cachePolicy: policy,
|
|
1049
1098
|
forwardArgs: options.forwardArgs,
|
|
@@ -1063,12 +1112,15 @@ async function runOnBus(
|
|
|
1063
1112
|
outputDirSnapshots,
|
|
1064
1113
|
deferSave: saveLane.defer,
|
|
1065
1114
|
deferredSaves,
|
|
1115
|
+
commandWindows: windows,
|
|
1066
1116
|
stopSignal: stopRun.signal,
|
|
1067
1117
|
failFast: failFast.signal,
|
|
1068
1118
|
}
|
|
1069
1119
|
if (options.retries !== undefined) a.retries = options.retries
|
|
1070
1120
|
if (taskTimeoutDefault !== undefined) a.timeout = taskTimeoutDefault
|
|
1121
|
+
if (options.readyNotice !== undefined) a.readyNotice = options.readyNotice
|
|
1071
1122
|
a.inputStability = inputStability
|
|
1123
|
+
if (cleanOutputs !== undefined) a.cleanOutputs = cleanOutputs
|
|
1072
1124
|
if (download.modeOf.get(node.id) === 'deferred') a.download = 'deferred'
|
|
1073
1125
|
if (placements.remoteOnlyNoop.has(node.id)) a.remoteOnlyNoop = true
|
|
1074
1126
|
if (placements.remoteOnly.has(node.id)) a.remoteOnly = true
|
|
@@ -1078,6 +1130,7 @@ async function runOnBus(
|
|
|
1078
1130
|
if (upfrontKey !== undefined) a.upfrontKey = upfrontKey
|
|
1079
1131
|
if (upfrontGroupKey !== undefined) a.upfrontGroupKey = upfrontGroupKey
|
|
1080
1132
|
if (tainted) a.taintedUpstream = true
|
|
1133
|
+
if (keepRunLog) a.keepRunLog = true
|
|
1081
1134
|
if (!dependedOn.has(node.id)) a.noDependants = true
|
|
1082
1135
|
if (holders.has(node.id)) a.terminal = true
|
|
1083
1136
|
if (armSandbox !== undefined) a.armSandbox = armSandbox
|
|
@@ -1090,7 +1143,6 @@ async function runOnBus(
|
|
|
1090
1143
|
shortCircuit,
|
|
1091
1144
|
hashArgs: {
|
|
1092
1145
|
workspaceRoot,
|
|
1093
|
-
workspaceFingerprint,
|
|
1094
1146
|
cache,
|
|
1095
1147
|
forwardArgs: options.forwardArgs,
|
|
1096
1148
|
nestedDirsByProject,
|
|
@@ -1148,6 +1200,7 @@ async function runOnBus(
|
|
|
1148
1200
|
wallclockEndNs: at,
|
|
1149
1201
|
}
|
|
1150
1202
|
}
|
|
1203
|
+
let lockBroken: boolean | undefined
|
|
1151
1204
|
const outcomes = await runGraph({
|
|
1152
1205
|
nodes,
|
|
1153
1206
|
concurrency,
|
|
@@ -1169,7 +1222,15 @@ async function runOnBus(
|
|
|
1169
1222
|
log.taskComplete(o.node, o)
|
|
1170
1223
|
narrowDemand(o.node.id)
|
|
1171
1224
|
},
|
|
1172
|
-
|
|
1225
|
+
// A crash a hand-broken lock entry caused is named as that; asked
|
|
1226
|
+
// only here, so a run that does not crash pays nothing.
|
|
1227
|
+
onError: (node, line, err) =>
|
|
1228
|
+
log.taskStderr(
|
|
1229
|
+
node,
|
|
1230
|
+
!isUserError(err) && !isFsRefusal(err) && (lockBroken ??= lockEntryBroken(workspaceRoot))
|
|
1231
|
+
? `[vx] ${node.id}: ${LOCK_INVALID}\n`
|
|
1232
|
+
: line,
|
|
1233
|
+
),
|
|
1173
1234
|
execute: (node, upstream) => {
|
|
1174
1235
|
const kept = keepServer(node)
|
|
1175
1236
|
return kept !== undefined
|
|
@@ -1195,9 +1256,7 @@ async function runOnBus(
|
|
|
1195
1256
|
// baseline as the tie-break. Empty map → baseline only.
|
|
1196
1257
|
...(priorities.size > 0 ? { priorities } : {}),
|
|
1197
1258
|
// The baseline under them; `most-work` when the workspace names none.
|
|
1198
|
-
...(
|
|
1199
|
-
? { strategy: prepared.workspaceConfig.schedule }
|
|
1200
|
-
: {}),
|
|
1259
|
+
...(options.schedule !== undefined ? { strategy: options.schedule } : {}),
|
|
1201
1260
|
// Local short-circuit: confirmed stable local hits the scheduler
|
|
1202
1261
|
// runs ahead of their deps as low-priority worker-slot backfill.
|
|
1203
1262
|
// Empty when the short-circuit didn't fire → byte-identical.
|
|
@@ -1289,7 +1348,7 @@ async function runOnBus(
|
|
|
1289
1348
|
const stillDeferred = deferredOutputs.pending()
|
|
1290
1349
|
if (stillDeferred.length > 0) {
|
|
1291
1350
|
log.status(
|
|
1292
|
-
` Deferred:
|
|
1351
|
+
` Deferred: left outputs remote (--cache-download=none): ${stillDeferred.join(', ')}`,
|
|
1293
1352
|
)
|
|
1294
1353
|
}
|
|
1295
1354
|
|
|
@@ -1297,6 +1356,55 @@ async function runOnBus(
|
|
|
1297
1356
|
// change the run's exit code — the run already happened.
|
|
1298
1357
|
// Written again after the keep-alive wait: a kept server's crash or a
|
|
1299
1358
|
// Ctrl-C there is the process's exit, and the first write said ok.
|
|
1359
|
+
// The run's own bytes, kept in a remote that takes its writes (owner
|
|
1360
|
+
// 2026-10-15): a failed task's log and the files its outputs left, an
|
|
1361
|
+
// uncached task's log. Then the report every plugin's teardown reads.
|
|
1362
|
+
const keepRunBytes = async (final: readonly TaskOutcome[]): Promise<RunReport> => {
|
|
1363
|
+
const keep = prepared.keepRunEntry
|
|
1364
|
+
const tasks: RunReportTask[] = []
|
|
1365
|
+
const work: (() => Promise<void>)[] = []
|
|
1366
|
+
for (const o of final) {
|
|
1367
|
+
if (isGroupTask(o.node)) continue
|
|
1368
|
+
const task: { -readonly [K in keyof RunReportTask]: RunReportTask[K] } = {
|
|
1369
|
+
id: o.node.id,
|
|
1370
|
+
status: o.status,
|
|
1371
|
+
durationMs: Math.round(o.durationMs),
|
|
1372
|
+
...(o.hash !== undefined ? { key: o.hash } : {}),
|
|
1373
|
+
}
|
|
1374
|
+
tasks.push(task)
|
|
1375
|
+
if (keep === undefined) continue
|
|
1376
|
+
const runLog = o.runLog
|
|
1377
|
+
if (runLog !== undefined) {
|
|
1378
|
+
work.push(async () => {
|
|
1379
|
+
const text = decodeOutputLog(runLog)
|
|
1380
|
+
.map((c) => c.text)
|
|
1381
|
+
.join('')
|
|
1382
|
+
if (text.length === 0) return
|
|
1383
|
+
const name = runEntryName(runId, o.node.id, 'log')
|
|
1384
|
+
if (await keep(name, new TextEncoder().encode(text))) task.log = name
|
|
1385
|
+
})
|
|
1386
|
+
}
|
|
1387
|
+
if (o.status === 'failed' && o.where === undefined) {
|
|
1388
|
+
work.push(async () => {
|
|
1389
|
+
const bytes = await packRunOutputs(
|
|
1390
|
+
prepared.workspaceRoot,
|
|
1391
|
+
await outputsLeft(
|
|
1392
|
+
o.node,
|
|
1393
|
+
prepared.workspaceRoot,
|
|
1394
|
+
prepared.nestedDirsByProject.get(o.node.projectName) ?? [],
|
|
1395
|
+
),
|
|
1396
|
+
)
|
|
1397
|
+
if (bytes === null) return
|
|
1398
|
+
const name = runEntryName(runId, o.node.id, 'outputs')
|
|
1399
|
+
if (await keep(name, bytes)) task.outputs = name
|
|
1400
|
+
})
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
for (let i = 0; i < work.length; i += 8) {
|
|
1404
|
+
await Promise.all(work.slice(i, i + 8).map((w) => w()))
|
|
1405
|
+
}
|
|
1406
|
+
return { runId, commit: head.commitSha, ciRun: ciContext, ok, tasks }
|
|
1407
|
+
}
|
|
1300
1408
|
let json: RunSummaryJson | undefined
|
|
1301
1409
|
const summarize = async (runOk: boolean, final = list): Promise<void> => {
|
|
1302
1410
|
if (options.json === true) {
|
|
@@ -1370,7 +1478,7 @@ async function runOnBus(
|
|
|
1370
1478
|
// own output masks it, so cache.db holds no plaintext value.
|
|
1371
1479
|
command: maskInvocation(options.command ?? process.argv.slice(1).join(' ')),
|
|
1372
1480
|
requestedTasks: options.tasks,
|
|
1373
|
-
cachePolicy:
|
|
1481
|
+
cachePolicy: formatCachePolicy(policy),
|
|
1374
1482
|
concurrency,
|
|
1375
1483
|
flow: options.flow ?? null,
|
|
1376
1484
|
forwardArgs: options.forwardArgs,
|
|
@@ -1435,6 +1543,7 @@ async function runOnBus(
|
|
|
1435
1543
|
// drain of the lane stood here until item 634; deleting it changed
|
|
1436
1544
|
// nothing the suite could see, because this gate is the rule.
|
|
1437
1545
|
await cache.drainUploads?.()
|
|
1546
|
+
const report = await keepRunBytes(list)
|
|
1438
1547
|
// Hand the per-run summary to the telemetry sinks + drain them. Only
|
|
1439
1548
|
// when a sink is active (telemetry !== undefined) — otherwise this
|
|
1440
1549
|
// whole block is skipped and the run is byte-identical to before.
|
|
@@ -1493,7 +1602,7 @@ async function runOnBus(
|
|
|
1493
1602
|
// whole keep-alive wait below (C-66). Idempotent; the finally's stay.
|
|
1494
1603
|
disposePlugins?.()
|
|
1495
1604
|
telemetry?.dispose()
|
|
1496
|
-
await teardown()
|
|
1605
|
+
await teardown(undefined, report)
|
|
1497
1606
|
await closeCache()
|
|
1498
1607
|
mark('close')
|
|
1499
1608
|
printTimings()
|
|
@@ -1519,7 +1628,8 @@ async function runOnBus(
|
|
|
1519
1628
|
if (options.summaryTable === true) for (const line of formatOutcomeTable(list)) log.status(line)
|
|
1520
1629
|
const above = options.beforeFooter?.(list, ok)
|
|
1521
1630
|
if (above) log.status(above.replace(/\n$/, ''))
|
|
1522
|
-
|
|
1631
|
+
if (options.footer !== false)
|
|
1632
|
+
for (const line of formatRunSummary(list, totalMs, colors, runContext)) log.status(line)
|
|
1523
1633
|
|
|
1524
1634
|
// Edge case the summary already reported: the user requested a
|
|
1525
1635
|
// persistent task (dev server / watcher). The run is "done" in every
|
|
@@ -1665,18 +1775,16 @@ async function runOnBus(
|
|
|
1665
1775
|
}
|
|
1666
1776
|
|
|
1667
1777
|
/**
|
|
1668
|
-
*
|
|
1778
|
+
* `cache.retention`, after every save and upload of this run
|
|
1669
1779
|
* has landed (so nothing this run wrote is mid-flight) and before the cache
|
|
1670
1780
|
* closes. Housekeeping, not the run's work: silent, and a failure is never
|
|
1671
1781
|
* a failed run. Declared nowhere → one property read.
|
|
1672
1782
|
*/
|
|
1673
1783
|
async function applyCacheRetention(prepared: PreparedRun): Promise<void> {
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
retention.olderThan === undefined ? undefined : parseDuration(retention.olderThan)!
|
|
1679
|
-
const maxBytes = retention.maxSize === undefined ? undefined : parseSize(retention.maxSize)!
|
|
1784
|
+
const ws = prepared.workspaceConfig
|
|
1785
|
+
const maxAgeMs = resolveSetting('cache.retention.olderThan', ws).value as number | undefined
|
|
1786
|
+
const maxBytes = resolveSetting('cache.retention.maxSize', ws).value as number | undefined
|
|
1787
|
+
if (maxAgeMs === undefined && maxBytes === undefined) return
|
|
1680
1788
|
try {
|
|
1681
1789
|
await prepared.localCache.evictIfDue({
|
|
1682
1790
|
...(maxAgeMs !== undefined ? { maxAgeMs } : {}),
|
|
@@ -1721,12 +1829,13 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1721
1829
|
const log = options.log ?? defaultLogger(undefined, undefined, process.stderr)
|
|
1722
1830
|
const reasons = new Map<string, AffectedReason>()
|
|
1723
1831
|
const prepared = await prepareRun({ ...options, affectedReasons: reasons }, log)
|
|
1832
|
+
options = withRunSettings(options, prepared.workspaceConfig)
|
|
1724
1833
|
try {
|
|
1725
1834
|
if (prepared.unresolvedTasks.length > 0) {
|
|
1726
1835
|
return {
|
|
1727
1836
|
tasks: [],
|
|
1728
1837
|
unresolvedTasks: prepared.unresolvedTasks,
|
|
1729
|
-
unresolvedHint:
|
|
1838
|
+
unresolvedHint: await unresolvedHint(prepared),
|
|
1730
1839
|
}
|
|
1731
1840
|
}
|
|
1732
1841
|
if (prepared.empty === 'no-tasks-declared' && prepared.declaredElsewhere.length > 0) {
|
|
@@ -1748,7 +1857,6 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1748
1857
|
const planned = await plan({
|
|
1749
1858
|
nodes: prepared.nodes,
|
|
1750
1859
|
workspaceRoot: prepared.workspaceRoot,
|
|
1751
|
-
workspaceFingerprint: prepared.workspaceFingerprint,
|
|
1752
1860
|
cache: prepared.cache,
|
|
1753
1861
|
cachePolicy: policy,
|
|
1754
1862
|
forwardArgs: options.forwardArgs,
|
|
@@ -1760,7 +1868,11 @@ export async function planRun(options: RunOptions): Promise<RunPlan> {
|
|
|
1760
1868
|
// per-task p50s + a predicted wall-clock. `--dry` is an explicit
|
|
1761
1869
|
// inspection command, so the history read's cost is fine here even
|
|
1762
1870
|
// though it stays OFF the default run path.
|
|
1763
|
-
history: new LocalHistoryProvider(
|
|
1871
|
+
history: new LocalHistoryProvider(
|
|
1872
|
+
prepared.localCache.dbHandle(),
|
|
1873
|
+
undefined,
|
|
1874
|
+
prepared.branch(),
|
|
1875
|
+
),
|
|
1764
1876
|
// Placement, by the same rules the run applies. Only worth showing
|
|
1765
1877
|
// when there is a choice to show — with one declared executor every
|
|
1766
1878
|
// line would carry the same label. Resolving the executors here is
|
|
@@ -1810,7 +1922,7 @@ async function planRestorable(
|
|
|
1810
1922
|
nodes,
|
|
1811
1923
|
cache: prepared.cache,
|
|
1812
1924
|
workspaceRoot: prepared.workspaceRoot,
|
|
1813
|
-
|
|
1925
|
+
claimedFiles: prepared.fingerprintWatch.names,
|
|
1814
1926
|
forwardArgs,
|
|
1815
1927
|
nestedDirsByProject: prepared.nestedDirsByProject,
|
|
1816
1928
|
gitFilesCache: prepared.gitFilesCache,
|
|
@@ -1828,25 +1940,38 @@ function elsewhereHint(names: readonly string[]): string {
|
|
|
1828
1940
|
}
|
|
1829
1941
|
|
|
1830
1942
|
/**
|
|
1831
|
-
*
|
|
1832
|
-
*
|
|
1833
|
-
*
|
|
1834
|
-
* told that instead — `vx init` would find no package to write for.
|
|
1943
|
+
* What follows "no projects declare task(s): …": where the name is, its
|
|
1944
|
+
* nearest spelling, and, with neither, the lock a run reads its configs
|
|
1945
|
+
* from: a task just added to a vx.config is not in it until `vx lock`.
|
|
1835
1946
|
*/
|
|
1836
|
-
async function
|
|
1947
|
+
async function unresolvedHint(prepared: {
|
|
1948
|
+
unresolvedTasks: readonly string[]
|
|
1949
|
+
hintProjects: ReadonlyMap<string, ProjectEntry>
|
|
1950
|
+
declaredElsewhere: readonly string[]
|
|
1837
1951
|
anyProjectConfig: boolean
|
|
1838
|
-
workspaceProjectCount: number
|
|
1839
|
-
workspaceRoot: string
|
|
1840
1952
|
}): Promise<string> {
|
|
1953
|
+
const elsewhere = elsewhereHint(prepared.declaredElsewhere)
|
|
1954
|
+
const near = didYouMean(
|
|
1955
|
+
prepared.unresolvedTasks,
|
|
1956
|
+
prepared.hintProjects,
|
|
1957
|
+
prepared.declaredElsewhere,
|
|
1958
|
+
)
|
|
1959
|
+
const init = await initHint(prepared)
|
|
1960
|
+
const lock =
|
|
1961
|
+
init === '' && elsewhere === '' && !near.startsWith(' Did you mean')
|
|
1962
|
+
? ' A run reads the lock: after editing a vx.config, run `vx lock`.'
|
|
1963
|
+
: ''
|
|
1964
|
+
return `${elsewhere}${near}${init}${lock}`
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
/**
|
|
1968
|
+
* The first-run case, told apart from a typo: no project in the workspace
|
|
1969
|
+
* has a `vx.config.*` at all, so no name could have resolved. Reached only
|
|
1970
|
+
* on the error path.
|
|
1971
|
+
*/
|
|
1972
|
+
async function initHint(prepared: { anyProjectConfig: boolean }): Promise<string> {
|
|
1841
1973
|
if (prepared.anyProjectConfig) return ''
|
|
1842
|
-
|
|
1843
|
-
return " No package matched the workspace's package globs — check `workspaces` in package.json (or pnpm-workspace.yaml)."
|
|
1844
|
-
}
|
|
1845
|
-
// Single-project mode with packages the root's missing `workspaces`
|
|
1846
|
-
// never reaches: `vx init` would find no scripts either (item 248).
|
|
1847
|
-
const unreached = await unreachedPackages(await loadWorkspace(prepared.workspaceRoot))
|
|
1848
|
-
if (unreached.length > 0) return ` ${unreachedHint(unreached, prepared.workspaceRoot)}`
|
|
1849
|
-
return ' No package declares a vx.config — run `vx init` to write one per package from its package.json scripts.'
|
|
1974
|
+
return ' No directory holds a vx.config — run `vx init` to write them, then `vx lock`.'
|
|
1850
1975
|
}
|
|
1851
1976
|
|
|
1852
1977
|
/**
|
|
@@ -1892,7 +2017,7 @@ function didYouMean(
|
|
|
1892
2017
|
// `//` is two edits from any two-letter name: it hinted `ui#build` (X-13).
|
|
1893
2018
|
if (proj === '//') {
|
|
1894
2019
|
lists.add(
|
|
1895
|
-
|
|
2020
|
+
'`//` is the workspace root, which is no project here: add a vx.config there with that task and run `vx lock`.',
|
|
1896
2021
|
)
|
|
1897
2022
|
continue
|
|
1898
2023
|
}
|