@vzn/vx 0.0.645 → 0.0.648
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Core's local floor for a plugin that runs tasks itself: `@vzn/vx-dte`'s
|
|
2
|
+
// workers, which run tasks another machine placed. A run arms the sandbox
|
|
3
|
+
// runtime before it builds a sandboxed request; a worker is no run, so it
|
|
4
|
+
// arms here. It starts a request's servers (`ExecuteRequest.servers`) once
|
|
5
|
+
// each, before the task, each inside its own sandbox, and stops them when
|
|
6
|
+
// it closes.
|
|
7
|
+
|
|
8
|
+
import type { ExecConfig } from '../config.js'
|
|
9
|
+
import {
|
|
10
|
+
localExecutor,
|
|
11
|
+
PersistentReadyError,
|
|
12
|
+
releaseBridges,
|
|
13
|
+
resetSandbox,
|
|
14
|
+
runPersistent,
|
|
15
|
+
signalExitCode,
|
|
16
|
+
type ExecuteRequest,
|
|
17
|
+
type ExecuteResult,
|
|
18
|
+
type ServerSpec,
|
|
19
|
+
wrapSandboxedCommand,
|
|
20
|
+
} from '../exec/index.js'
|
|
21
|
+
import { placeholderSweeper, prepareSandbox, sandboxRequestFor } from './sandbox-request.js'
|
|
22
|
+
import { forwardedSignal, terminateChildren } from './signals.js'
|
|
23
|
+
|
|
24
|
+
/** How much of a server's output before ready a failure to start quotes. */
|
|
25
|
+
const HEARD_BYTES = 8192
|
|
26
|
+
|
|
27
|
+
export interface LocalFloor {
|
|
28
|
+
execute(req: ExecuteRequest): Promise<ExecuteResult>
|
|
29
|
+
/** Arms the sandbox runtime for `sandboxes` from the next sandboxed task on; its servers keep running. */
|
|
30
|
+
rearm(sandboxes: readonly NonNullable<ExecConfig['sandbox']>[]): Promise<void>
|
|
31
|
+
/** Stops the servers this started and the sandbox runtime this armed. */
|
|
32
|
+
close(): Promise<void>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `sandboxes`, in a process that is no run, are the `exec.sandbox` configs
|
|
37
|
+
* of every sandboxed task the run holds: the runtime's proxy and its
|
|
38
|
+
* unix-socket filter are one per process, set from their union before the
|
|
39
|
+
* first sandboxed task (`prepareSandbox`). A request that carries a
|
|
40
|
+
* `signal` but no `liveChildren` has its children stopped here when it
|
|
41
|
+
* aborts, the way a run's stop ends its own.
|
|
42
|
+
*/
|
|
43
|
+
export function localFloor(
|
|
44
|
+
sandboxes: readonly NonNullable<ExecConfig['sandbox']>[] = [],
|
|
45
|
+
): LocalFloor {
|
|
46
|
+
const local = localExecutor()
|
|
47
|
+
const prepare = (list: typeof sandboxes) =>
|
|
48
|
+
prepareSandbox(list.map((sandbox) => ({ config: { exec: { sandbox } } })))
|
|
49
|
+
let armer = prepare(sandboxes)
|
|
50
|
+
// Each server, by task id: shared by every task here that depends on it.
|
|
51
|
+
const servers = new Map<string, Promise<ReturnType<typeof Bun.spawn>>>()
|
|
52
|
+
const children: ReturnType<typeof Bun.spawn>[] = []
|
|
53
|
+
// A sandboxed one starts inside its sandbox, built here as a one-shot
|
|
54
|
+
// task's; its port bridges and untouched placeholders go when it exits.
|
|
55
|
+
const start = async (spec: ServerSpec, root: string): Promise<ReturnType<typeof Bun.spawn>> => {
|
|
56
|
+
let command = spec.command
|
|
57
|
+
let signalChannel = false
|
|
58
|
+
let gone = async (): Promise<unknown> => undefined
|
|
59
|
+
if (spec.sandbox !== undefined) {
|
|
60
|
+
await armer?.arm()
|
|
61
|
+
const sb = await sandboxRequestFor(
|
|
62
|
+
{ projectDir: spec.cwd },
|
|
63
|
+
spec.sandbox.config,
|
|
64
|
+
root,
|
|
65
|
+
undefined,
|
|
66
|
+
spec.sandbox.nested,
|
|
67
|
+
)
|
|
68
|
+
const swept = placeholderSweeper(sb.placeholders)
|
|
69
|
+
const wrapped = await wrapSandboxedCommand({
|
|
70
|
+
command,
|
|
71
|
+
cwd: spec.cwd,
|
|
72
|
+
env: spec.env,
|
|
73
|
+
...sb.sandbox,
|
|
74
|
+
server: true,
|
|
75
|
+
}).catch(async (err: unknown) => {
|
|
76
|
+
await swept()
|
|
77
|
+
throw err
|
|
78
|
+
})
|
|
79
|
+
command = wrapped.wrapped
|
|
80
|
+
signalChannel = wrapped.forwardsSignals
|
|
81
|
+
gone = async () => {
|
|
82
|
+
if (wrapped.tag !== undefined) releaseBridges(wrapped.tag)
|
|
83
|
+
return swept()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Its output until ready, the tail kept: a server that dies first says
|
|
87
|
+
// why (`docker run` exiting 125 said nothing on CI, 2026-10-26).
|
|
88
|
+
let heard: string | undefined = ''
|
|
89
|
+
const hear = (t: string): void => {
|
|
90
|
+
if (heard !== undefined) heard = (heard + t).slice(-HEARD_BYTES)
|
|
91
|
+
}
|
|
92
|
+
const spawn = runPersistent({
|
|
93
|
+
command,
|
|
94
|
+
cwd: spec.cwd,
|
|
95
|
+
env: spec.env,
|
|
96
|
+
onStdout: hear,
|
|
97
|
+
onStderr: hear,
|
|
98
|
+
...(spec.readyWhen === undefined ? {} : { readyWhen: spec.readyWhen }),
|
|
99
|
+
...(spec.timeoutMs === undefined ? {} : { timeoutMs: spec.timeoutMs }),
|
|
100
|
+
...(signalChannel ? { signalChannel } : {}),
|
|
101
|
+
})
|
|
102
|
+
// A spawn that threw hands back no child, only a rejected `ready`.
|
|
103
|
+
if (spawn.child === undefined) void gone()
|
|
104
|
+
else {
|
|
105
|
+
children.push(spawn.child)
|
|
106
|
+
void spawn.child.exited.then(gone, gone)
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await spawn.ready
|
|
110
|
+
} catch (err) {
|
|
111
|
+
// A shell that exited before ready can leave its group running
|
|
112
|
+
// (`server & …; exit`), holding the port of the one the next task starts.
|
|
113
|
+
if (
|
|
114
|
+
err instanceof PersistentReadyError &&
|
|
115
|
+
err.reason === 'exited' &&
|
|
116
|
+
spawn.child !== undefined
|
|
117
|
+
)
|
|
118
|
+
await terminateChildren(() => [spawn.child!])
|
|
119
|
+
const why = err instanceof Error ? err.message : String(err)
|
|
120
|
+
const said = (heard ?? '').trimEnd()
|
|
121
|
+
throw new Error(said === '' ? why : `${why}; its output:\n${said}`)
|
|
122
|
+
}
|
|
123
|
+
heard = undefined
|
|
124
|
+
// Ready, so it spawned: only a failed spawn has no child.
|
|
125
|
+
return spawn.child!
|
|
126
|
+
}
|
|
127
|
+
const serve = (spec: ServerSpec, root: string): Promise<ReturnType<typeof Bun.spawn>> => {
|
|
128
|
+
let up = servers.get(spec.taskId)
|
|
129
|
+
if (up === undefined) {
|
|
130
|
+
up = start(spec, root)
|
|
131
|
+
servers.set(spec.taskId, up)
|
|
132
|
+
// One that never became ready is asked again by the next task.
|
|
133
|
+
up.catch(() => servers.delete(spec.taskId))
|
|
134
|
+
}
|
|
135
|
+
return up
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
async execute(req) {
|
|
139
|
+
// A stop while a server is not yet ready ends the wait: one that never
|
|
140
|
+
// prints its `readyWhen` line held the task, and a worker's shutdown
|
|
141
|
+
// waits on its tasks before `close()` stops the servers. The aborted
|
|
142
|
+
// request then reads as one stopped before its spawn.
|
|
143
|
+
const signal = req.signal
|
|
144
|
+
const stopped =
|
|
145
|
+
signal === undefined
|
|
146
|
+
? undefined
|
|
147
|
+
: new Promise<void>((resolve) => {
|
|
148
|
+
if (signal.aborted) resolve()
|
|
149
|
+
else signal.addEventListener('abort', () => resolve(), { once: true })
|
|
150
|
+
})
|
|
151
|
+
for (const spec of req.servers ?? []) {
|
|
152
|
+
if (signal?.aborted === true) break
|
|
153
|
+
try {
|
|
154
|
+
const up = serve(spec, req.workspaceRoot)
|
|
155
|
+
const child = await (stopped === undefined ? up : Promise.race([up, stopped]))
|
|
156
|
+
// One that crashed after it was ready serves nobody: the task ran
|
|
157
|
+
// against nothing and failed on its own "connection refused". A
|
|
158
|
+
// clean exit is a setup step that did its work (runPersistent).
|
|
159
|
+
const died = child === undefined ? null : (child.exitCode ?? child.signalCode)
|
|
160
|
+
if (died !== null && died !== 0) {
|
|
161
|
+
const code = typeof died === 'number' ? died : signalExitCode(died)
|
|
162
|
+
const text = `${spec.taskId}, which ${req.taskId} depends on, exited with code ${code} after it was ready\n`
|
|
163
|
+
req.onStderr(text)
|
|
164
|
+
return { exitCode: 1, durationMs: 0, stdout: '', stderr: text, violations: [] }
|
|
165
|
+
}
|
|
166
|
+
} catch (err) {
|
|
167
|
+
const why = err instanceof Error ? err.message : String(err)
|
|
168
|
+
const text = `${spec.taskId}, which ${req.taskId} depends on, did not start: ${why}\n`
|
|
169
|
+
// Through `onStderr` too, as a spawn failure's text goes: a worker
|
|
170
|
+
// ships only what its callbacks heard, and a dependant failed
|
|
171
|
+
// "exit 1, 0ms" with no word of why (CI, 2026-10-25).
|
|
172
|
+
req.onStderr(text)
|
|
173
|
+
return { exitCode: 1, durationMs: 0, stdout: '', stderr: text, violations: [] }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (req.sandbox !== undefined) await armer?.arm()
|
|
177
|
+
const stop = req.signal
|
|
178
|
+
if (stop === undefined || req.liveChildren !== undefined) return local.execute(req)
|
|
179
|
+
// No run holds this task's children (a worker's): its stop is ours to carry out.
|
|
180
|
+
const live = new Set<ReturnType<typeof Bun.spawn>>()
|
|
181
|
+
const end = (): void => void terminateChildren(() => [...live], forwardedSignal(stop.reason))
|
|
182
|
+
stop.addEventListener('abort', end, { once: true })
|
|
183
|
+
try {
|
|
184
|
+
return await local.execute({ ...req, liveChildren: live })
|
|
185
|
+
} finally {
|
|
186
|
+
stop.removeEventListener('abort', end)
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
async rearm(list) {
|
|
190
|
+
if (armer?.armed === true) await resetSandbox()
|
|
191
|
+
armer = prepare(list)
|
|
192
|
+
},
|
|
193
|
+
async close() {
|
|
194
|
+
if (children.length > 0) await terminateChildren(() => children)
|
|
195
|
+
if (armer?.armed === true) await resetSandbox()
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
import { normalizeGlob, relPosix, span, staticPrefix } from '../util/index.js'
|
|
45
45
|
import type { CacheEntry, CacheLayer, GitFilesCache } from '../cache/index.js'
|
|
46
46
|
import type { TaskNode } from '../graph/index.js'
|
|
47
|
-
import { deriveStableKeys, workspaceInputsReach } from './stable-keys.js'
|
|
47
|
+
import { deriveStableKeys, rederiveKeys, workspaceInputsReach } from './stable-keys.js'
|
|
48
|
+
import { readRunDelta, readRunKeys, runKeysToken, writeRunDelta, writeRunKeys } from './run-keys.js'
|
|
48
49
|
import type { HashCache, UpfrontGroupKey } from './task-hash.js'
|
|
49
50
|
import { getContext } from './remote-prefetch.js'
|
|
50
51
|
|
|
@@ -100,6 +101,8 @@ export interface ShortCircuit {
|
|
|
100
101
|
uncachedKeys: Map<string, string>
|
|
101
102
|
/** Unkeyed group id → its up-front key (`deriveStableKeys`' `groupKeys`). */
|
|
102
103
|
groupKeys: Map<string, UpfrontGroupKey>
|
|
104
|
+
/** Behind a remote, each stable task the local store missed → its key: the remote may hold it. */
|
|
105
|
+
remoteAsk: Map<string, string>
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
/**
|
|
@@ -117,13 +120,46 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
|
|
|
117
120
|
const groupKeys = new Map<string, UpfrontGroupKey>()
|
|
118
121
|
const endKeys = span('stable keys')
|
|
119
122
|
try {
|
|
120
|
-
|
|
123
|
+
const at = runKeysToken(args)
|
|
124
|
+
const remembered = at === undefined ? undefined : readRunKeys(args, at)
|
|
125
|
+
if (remembered !== undefined) {
|
|
126
|
+
stableKeys = remembered.stableKeys
|
|
127
|
+
for (const [id, key] of remembered.uncachedKeys) uncachedKeys.set(id, key)
|
|
128
|
+
} else {
|
|
129
|
+
// An edit moved some files tokens: the last derivation under the same
|
|
130
|
+
// graph keys again only the tasks the edit reaches.
|
|
131
|
+
const delta = at === undefined ? undefined : readRunDelta(args, at)
|
|
132
|
+
let derivation
|
|
133
|
+
if (delta !== undefined) {
|
|
134
|
+
derivation = delta.derivation
|
|
135
|
+
await rederiveKeys(args, derivation.keys, delta.dirty)
|
|
136
|
+
const keys = derivation.keys
|
|
137
|
+
stableKeys = derivation.stable.map((id) => ({
|
|
138
|
+
hash: keys.get(id)!,
|
|
139
|
+
node: args.nodes.get(id)!,
|
|
140
|
+
}))
|
|
141
|
+
for (const id of derivation.uncached) uncachedKeys.set(id, keys.get(id)!)
|
|
142
|
+
} else {
|
|
143
|
+
const keys = new Map<string, string>()
|
|
144
|
+
stableKeys = await deriveStableKeys({ ...args, uncachedKeys, groupKeys, keys })
|
|
145
|
+
derivation = {
|
|
146
|
+
keys,
|
|
147
|
+
stable: stableKeys.map((k) => k.node.id),
|
|
148
|
+
uncached: [...uncachedKeys.keys()],
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (at !== undefined) {
|
|
152
|
+
writeRunKeys(args, at, { stableKeys, uncachedKeys })
|
|
153
|
+
writeRunDelta(args, at, derivation)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
121
156
|
} catch {
|
|
122
157
|
return {
|
|
123
158
|
preProbed: new Map(),
|
|
124
159
|
restoreTier: new Set(),
|
|
125
160
|
uncachedKeys: new Map(),
|
|
126
161
|
groupKeys: new Map(),
|
|
162
|
+
remoteAsk: new Map(),
|
|
127
163
|
}
|
|
128
164
|
} finally {
|
|
129
165
|
endKeys()
|
|
@@ -134,15 +170,16 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
|
|
|
134
170
|
const candidates = stableKeys
|
|
135
171
|
const preProbed = new Map<string, ProbedEntry>()
|
|
136
172
|
const restoreTier = new Set<string>()
|
|
173
|
+
const remoteAsk = new Map<string, string>()
|
|
137
174
|
if (candidates.length === 0 && uncachedKeys.size === 0) {
|
|
138
|
-
return { preProbed, restoreTier, uncachedKeys, groupKeys }
|
|
175
|
+
return { preProbed, restoreTier, uncachedKeys, groupKeys, remoteAsk }
|
|
139
176
|
}
|
|
140
177
|
|
|
141
178
|
const keptOut = restoreTierExclusions(args.nodes, args.workspaceRoot)
|
|
142
179
|
// A root-anchored output may land in a project no edge leads from, so an
|
|
143
180
|
// uncached key there is derived again once the writer may have run.
|
|
144
181
|
for (const id of keptOut) uncachedKeys.delete(id)
|
|
145
|
-
if (candidates.length === 0) return { preProbed, restoreTier, uncachedKeys, groupKeys }
|
|
182
|
+
if (candidates.length === 0) return { preProbed, restoreTier, uncachedKeys, groupKeys, remoteAsk }
|
|
146
183
|
|
|
147
184
|
// Bounded pool over the stable candidates: probe local ONCE each. A
|
|
148
185
|
// confirmed hit becomes restore-tier (unless workspace outputs disable
|
|
@@ -165,11 +202,14 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
|
|
|
165
202
|
endProbe()
|
|
166
203
|
for (const { hash, node } of candidates) {
|
|
167
204
|
const hit = hits.get(hash) ?? null
|
|
168
|
-
if (hit === null && args.localOnly === true)
|
|
205
|
+
if (hit === null && args.localOnly === true) {
|
|
206
|
+
if (!keptOut.has(node.id)) remoteAsk.set(node.id, hash)
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
169
209
|
preProbed.set(node.id, { hash, hit })
|
|
170
210
|
if (hit !== null && !keptOut.has(node.id)) restoreTier.add(node.id)
|
|
171
211
|
}
|
|
172
|
-
return { preProbed, restoreTier, uncachedKeys, groupKeys }
|
|
212
|
+
return { preProbed, restoreTier, uncachedKeys, groupKeys, remoteAsk }
|
|
173
213
|
} catch {
|
|
174
214
|
// Fall through to the per-hash pool, which isolates a failing probe
|
|
175
215
|
// to its own task.
|
|
@@ -182,7 +222,10 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
|
|
|
182
222
|
const { hash, node } = candidates[next++]!
|
|
183
223
|
try {
|
|
184
224
|
const hit = await args.cache.get(hash, getContext(node, node.config.exec?.command ?? ''))
|
|
185
|
-
if (hit === null && args.localOnly === true)
|
|
225
|
+
if (hit === null && args.localOnly === true) {
|
|
226
|
+
if (!keptOut.has(node.id)) remoteAsk.set(node.id, hash)
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
186
229
|
preProbed.set(node.id, { hash, hit })
|
|
187
230
|
if (hit !== null && !keptOut.has(node.id)) restoreTier.add(node.id)
|
|
188
231
|
} catch {
|
|
@@ -193,7 +236,7 @@ export async function startLocalShortCircuit(args: ShortCircuitArgs): Promise<Sh
|
|
|
193
236
|
}
|
|
194
237
|
await Promise.all(Array.from({ length: workers }, () => pump()))
|
|
195
238
|
|
|
196
|
-
return { preProbed, restoreTier, uncachedKeys, groupKeys }
|
|
239
|
+
return { preProbed, restoreTier, uncachedKeys, groupKeys, remoteAsk }
|
|
197
240
|
}
|
|
198
241
|
|
|
199
242
|
/**
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import path from 'node:path'
|
|
17
17
|
import { rename, stat } from 'node:fs/promises'
|
|
18
|
-
import { relPosix, xxh3, xxh3hex } from '../util/index.js'
|
|
18
|
+
import { relPosix, UserError, xxh3, xxh3hex } from '../util/index.js'
|
|
19
19
|
import type { TaskNode } from '../graph/index.js'
|
|
20
20
|
import type {
|
|
21
21
|
FingerprintChange,
|
|
@@ -37,7 +37,7 @@ export interface LockfileClaimOptions {
|
|
|
37
37
|
/**
|
|
38
38
|
* Root-relative files the lockfile names whose BYTES the install uses but
|
|
39
39
|
* the lockfile does not pin (bun.lock's `patchedDependencies` names a patch
|
|
40
|
-
* file and records no hash of it). Their content hashes ('' when absent)
|
|
40
|
+
* file and records no hash of it). Their content hashes ('' when absent; unreadable refuses)
|
|
41
41
|
* reach `digest` as `files` and join the memo's identity, so an edited
|
|
42
42
|
* patch moves the key (item 1014). A directory (a `file:` dependency the
|
|
43
43
|
* install copies) hashes every file under it but `node_modules` and
|
|
@@ -108,18 +108,30 @@ interface Memo {
|
|
|
108
108
|
edges?: Record<string, LockfileImporter>
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* A root-relative extra file's content hash, '' when absent; a path leaving
|
|
113
|
+
* the root is never read. One there but unreadable (EACCES, a symlink loop)
|
|
114
|
+
* refuses, as a settings file does: folded as '', it keyed as no file at all.
|
|
115
|
+
*/
|
|
112
116
|
async function hashExtra(workspaceRoot: string, rel: string): Promise<string> {
|
|
113
117
|
if (path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) return 'outside-root'
|
|
114
118
|
const abs = path.join(workspaceRoot, rel)
|
|
115
119
|
try {
|
|
116
120
|
return xxh3hex(await Bun.file(abs).bytes())
|
|
117
121
|
} catch (err) {
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
123
|
+
if (code === 'EISDIR') return hashDir(abs)
|
|
124
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') return ''
|
|
125
|
+
throw unreadable(rel, err)
|
|
120
126
|
}
|
|
121
127
|
}
|
|
122
128
|
|
|
129
|
+
function unreadable(rel: string, err: unknown): UserError {
|
|
130
|
+
return new UserError(
|
|
131
|
+
`${rel}: cannot be read (${err instanceof Error ? err.message : String(err)})`,
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
123
135
|
/**
|
|
124
136
|
* Every file under `dir` but its `node_modules` and `.git`, as `rel\0hash`
|
|
125
137
|
* lines in sorted order. Links are followed: a linked file is what a task
|
|
@@ -322,7 +334,18 @@ export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooks
|
|
|
322
334
|
return names
|
|
323
335
|
},
|
|
324
336
|
...(options.extraFiles !== undefined
|
|
325
|
-
? {
|
|
337
|
+
? {
|
|
338
|
+
// The memo records which files these exact bytes name, for this
|
|
339
|
+
// parser: `--affected` asks on every diff, and a cold parse of
|
|
340
|
+
// a large lockfile was most of its selection's cost.
|
|
341
|
+
async extraFiles(_file: string, bytes: Uint8Array, ctx: FingerprintContext) {
|
|
342
|
+
const memoFile = path.join(ctx.cacheDir, 'lockfile-claims', `${file}.json`)
|
|
343
|
+
const memo = await readMemo(memoFile, version, claimant, xxh3hex(bytes))
|
|
344
|
+
return memo !== undefined
|
|
345
|
+
? Object.keys(memo.extras)
|
|
346
|
+
: options.extraFiles!(decode(bytes))
|
|
347
|
+
},
|
|
348
|
+
}
|
|
326
349
|
: {}),
|
|
327
350
|
},
|
|
328
351
|
async key(task, ctx) {
|
|
@@ -347,15 +370,22 @@ export function lockfileClaim(options: LockfileClaimOptions): LockfileClaimHooks
|
|
|
347
370
|
* Node's resolution walking up to the root `node_modules` (`@types/*`,
|
|
348
371
|
* a config's plugins) — so a root devDependency bump that moved only the
|
|
349
372
|
* root's digest replayed the old tool's output (nx#36415 class,
|
|
350
|
-
* 2026-09-24). A project the lockfile has no importer for
|
|
351
|
-
*
|
|
352
|
-
*
|
|
373
|
+
* 2026-09-24). A project the lockfile has no importer for folds its
|
|
374
|
+
* nearest listed ancestor's, as Node resolves from that ancestor's
|
|
375
|
+
* `node_modules` (`packages/a/e2e` from `packages/a`'s): the root's alone
|
|
376
|
+
* keyed it on no bump only `a` reached. Outside every importer it folds
|
|
377
|
+
* the root's alone; a file with neither folds a constant.
|
|
353
378
|
*/
|
|
354
379
|
function digestFor(importers: ReadonlyMap<string, string>, importer: string): string {
|
|
355
|
-
|
|
380
|
+
let dir = importer
|
|
381
|
+
while (dir !== '.' && !importers.has(dir)) {
|
|
382
|
+
const slash = dir.lastIndexOf('/')
|
|
383
|
+
dir = slash === -1 ? '.' : dir.slice(0, slash)
|
|
384
|
+
}
|
|
385
|
+
const own = importers.get(dir)
|
|
356
386
|
const root = importers.get('.')
|
|
357
387
|
if (own === undefined) return root ?? 'no-importer'
|
|
358
|
-
if (root === undefined ||
|
|
388
|
+
if (root === undefined || dir === '.') return own
|
|
359
389
|
return xxh3hex(`${own}\0${root}`)
|
|
360
390
|
}
|
|
361
391
|
|
|
@@ -423,6 +453,49 @@ function decode(bytes: Uint8Array): string {
|
|
|
423
453
|
|
|
424
454
|
// --- Reach digests -----------------------------------------------------------
|
|
425
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Each member of a cycle as what it is and where its edges land, by
|
|
458
|
+
* colour refinement: a member starts as its material and the child
|
|
459
|
+
* digests its edges leave the cycle for, then folds its in-cycle
|
|
460
|
+
* targets' labels until no two labels part further. npm and bun key two
|
|
461
|
+
* copies of one version alike (`a/node_modules/foo` → bar@1,
|
|
462
|
+
* `node_modules/foo` → bar@2), so by material alone two members swapping
|
|
463
|
+
* copies, in the cycle or out of it, moved nothing. Two members left with
|
|
464
|
+
* one label have the same material, reach the same children, and their
|
|
465
|
+
* in-cycle edges land on labels alike to every depth. Telling such a pair
|
|
466
|
+
* apart takes an isomorphism test; it needs a cycle where copies of one
|
|
467
|
+
* version reach alike at every step, which no swap of what one reaches
|
|
468
|
+
* produces.
|
|
469
|
+
*/
|
|
470
|
+
function cycleLabels(
|
|
471
|
+
g: ReachGraph,
|
|
472
|
+
members: readonly number[],
|
|
473
|
+
childOf: (w: number) => string | undefined,
|
|
474
|
+
): string[] {
|
|
475
|
+
let label = new Map<number, string>()
|
|
476
|
+
for (const m of members) {
|
|
477
|
+
const out = new Set<string>()
|
|
478
|
+
for (const w of g.edges[m]!) {
|
|
479
|
+
const child = w === m ? undefined : childOf(w)
|
|
480
|
+
if (child !== undefined) out.add(child)
|
|
481
|
+
}
|
|
482
|
+
label.set(m, xxh3hex(`${g.material[m]!}\0${[...out].sort().join('\0')}`))
|
|
483
|
+
}
|
|
484
|
+
let parts = new Set(label.values()).size
|
|
485
|
+
for (;;) {
|
|
486
|
+
const next = new Map<number, string>()
|
|
487
|
+
for (const m of members) {
|
|
488
|
+
const inner = new Set<string>()
|
|
489
|
+
for (const w of g.edges[m]!) if (w !== m && label.has(w)) inner.add(label.get(w)!)
|
|
490
|
+
next.set(m, xxh3hex(`${label.get(m)!}\0${[...inner].sort().join('\0')}`))
|
|
491
|
+
}
|
|
492
|
+
label = next
|
|
493
|
+
const now = new Set(label.values()).size
|
|
494
|
+
if (now === parts) return members.map((m) => label.get(m)!)
|
|
495
|
+
parts = now
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
426
499
|
/** A dependency graph: one material string per node and its out-edges by index. */
|
|
427
500
|
export interface ReachGraph {
|
|
428
501
|
readonly material: readonly string[]
|
|
@@ -493,11 +566,18 @@ export function reachDigests(g: ReachGraph): string[] {
|
|
|
493
566
|
// share one digest, so retargeting an edge between two of them (an
|
|
494
567
|
// importer's `y@1.0.0` → `y@1.1.0` inside one cycle) moved nothing
|
|
495
568
|
// and replayed a stale output (item 1013). A self-loop reaches
|
|
496
|
-
// nothing new and is left out.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
569
|
+
// nothing new and is left out. A singleton's children already say
|
|
570
|
+
// where its edges land, and its line stays as it was, so an acyclic
|
|
571
|
+
// digest keeps its value.
|
|
572
|
+
const lines =
|
|
573
|
+
members.length === 1
|
|
574
|
+
? members.map((m) => {
|
|
575
|
+
const targets = [
|
|
576
|
+
...new Set(g.edges[m]!.filter((w) => w !== m).map((w) => g.material[w]!)),
|
|
577
|
+
]
|
|
578
|
+
return `${g.material[m]!}\0${targets.sort().join('\0')}`
|
|
579
|
+
})
|
|
580
|
+
: cycleLabels(g, members, (w) => (comp[w] === id ? undefined : compHash[comp[w]!]!))
|
|
501
581
|
let h = xxh3(`members:${members.length}`)
|
|
502
582
|
for (const line of lines.sort()) h = xxh3(`${line}\n`, h)
|
|
503
583
|
h = xxh3(`children:${children.size}`, h)
|
|
@@ -7,24 +7,27 @@
|
|
|
7
7
|
// a later run replays. Moved out of execute-task.ts 2026-09-09 as pure
|
|
8
8
|
// code motion.
|
|
9
9
|
|
|
10
|
-
import { lstatSync, realpathSync, statSync } from 'node:fs'
|
|
10
|
+
import { existsSync, lstatSync, realpathSync, statSync } from 'node:fs'
|
|
11
11
|
import path from 'node:path'
|
|
12
12
|
import {
|
|
13
13
|
type CacheLayer,
|
|
14
14
|
type GitFilesCache,
|
|
15
15
|
resolveOutputTree,
|
|
16
|
+
outputWalkTakes,
|
|
16
17
|
resolveOutputs,
|
|
17
18
|
resolveWorkspaceOutputs,
|
|
19
|
+
resolveWorkspaceOutputTree,
|
|
18
20
|
WORKSPACE_OUTPUT_PREFIX,
|
|
19
21
|
} from '../cache/index.js'
|
|
20
22
|
import type { TaskNode } from '../graph/index.js'
|
|
21
23
|
import {
|
|
22
24
|
asTrees,
|
|
25
|
+
outputDirPrefixes,
|
|
23
26
|
relPosix,
|
|
24
27
|
span,
|
|
28
|
+
splitNegations,
|
|
25
29
|
staticPrefix,
|
|
26
30
|
taskGlob,
|
|
27
|
-
wholeSubtreePrefixes,
|
|
28
31
|
} from '../util/index.js'
|
|
29
32
|
import { type Logger, warnOf } from '../report/index.js'
|
|
30
33
|
import type { TaskInputComponent } from '../cache/index.js'
|
|
@@ -49,8 +52,12 @@ export interface OutputDirSnapshot {
|
|
|
49
52
|
*/
|
|
50
53
|
export function entryHolds(
|
|
51
54
|
node: TaskNode,
|
|
55
|
+
takes: (rel: string) => boolean,
|
|
52
56
|
): (files: readonly string[], rows: ReadonlyArray<{ path: string }>) => boolean {
|
|
53
|
-
return (
|
|
57
|
+
return (walked, rows) => {
|
|
58
|
+
// A root-anchored glob's snapshot walks the whole project: only what
|
|
59
|
+
// the glob takes is the entry's.
|
|
60
|
+
const files = walked.filter(takes)
|
|
54
61
|
const expected = new Set(
|
|
55
62
|
rows
|
|
56
63
|
.map((r) => r.path)
|
|
@@ -160,12 +167,11 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
160
167
|
nestedProjectDirs: a.nestedProjectDirs,
|
|
161
168
|
})
|
|
162
169
|
const outputFiles = tree.files
|
|
163
|
-
const
|
|
164
|
-
a.ownWsOutputFiles
|
|
165
|
-
|
|
166
|
-
workspaceRoot: a.workspaceRoot,
|
|
167
|
-
|
|
168
|
-
}))
|
|
170
|
+
const wsTree =
|
|
171
|
+
a.ownWsOutputFiles !== undefined
|
|
172
|
+
? { files: a.ownWsOutputFiles, emptyDirs: [] }
|
|
173
|
+
: await resolveWorkspaceOutputTree({ workspaceRoot: a.workspaceRoot, outputs: a.wsOutputs })
|
|
174
|
+
const wsOutputFiles = wsTree.files
|
|
169
175
|
endResolve()
|
|
170
176
|
// The mirror of the input warning: declared outputs that resolve to
|
|
171
177
|
// nothing save an empty artifact, and the next hit "restores" it —
|
|
@@ -196,7 +202,11 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
196
202
|
? ` — ${linkedOut.dir} is a symlink to ${linkedOut.target}, outside ${linkedOut.base}, and vx keeps only outputs inside it: make ${linkedOut.dir} a directory`
|
|
197
203
|
: sandboxed && !grantsWrite
|
|
198
204
|
? ` — the task is sandboxed and declares no exec.sandbox.allow.write, so its writes never reached disk`
|
|
199
|
-
: '')
|
|
205
|
+
: '') +
|
|
206
|
+
bracketLiteralHint([
|
|
207
|
+
...a.outputs.map((g) => [node.projectDir, g] as const),
|
|
208
|
+
...a.wsOutputs.map((g) => [a.workspaceRoot, g] as const),
|
|
209
|
+
]),
|
|
200
210
|
)
|
|
201
211
|
}
|
|
202
212
|
const facts: SaveFacts = {}
|
|
@@ -214,6 +224,7 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
214
224
|
// The root an output symlink's target must stay inside, outputs.workspaceFiles or not.
|
|
215
225
|
workspaceRoot: a.workspaceRoot,
|
|
216
226
|
...(wsOutputFiles.length > 0 ? { workspaceOutputFiles: wsOutputFiles } : {}),
|
|
227
|
+
...(wsTree.emptyDirs.length > 0 ? { workspaceOutputDirs: wsTree.emptyDirs } : {}),
|
|
217
228
|
inputComponents: a.captured.map((c) => ({ entryHash: a.hash, ...c })),
|
|
218
229
|
// No `exitCode`: the save only runs under `effectiveExitCode === 0`, and
|
|
219
230
|
// the contract no longer accepts one — so the invariant is enforced by
|
|
@@ -250,13 +261,13 @@ export async function saveMiss(a: SaveMissArgs): Promise<{ landed: Promise<SaveF
|
|
|
250
261
|
// list gets no snapshot: recording here was that same refusal (item
|
|
251
262
|
// 637 deleted the fallback and nothing reddened), and the next hit
|
|
252
263
|
// walks, as it did.
|
|
253
|
-
const savedDirPrefixes =
|
|
264
|
+
const savedDirPrefixes = outputDirPrefixes(splitNegations(a.outputs).positive)
|
|
254
265
|
if (savedDirPrefixes !== null && a.outputDirSnapshots !== undefined) {
|
|
255
266
|
a.outputDirSnapshots.push({
|
|
256
267
|
hash: a.hash,
|
|
257
268
|
projectDir: node.projectDir,
|
|
258
269
|
prefixes: savedDirPrefixes,
|
|
259
|
-
holds: entryHolds(node),
|
|
270
|
+
holds: entryHolds(node, outputWalkTakes(node.projectDir, a.outputs, a.nestedProjectDirs)),
|
|
260
271
|
})
|
|
261
272
|
}
|
|
262
273
|
}
|
|
@@ -380,3 +391,32 @@ function realpathOrNull(p: string): string | null {
|
|
|
380
391
|
return null
|
|
381
392
|
}
|
|
382
393
|
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The clause a "matched no files" warning gains when a glob holds an
|
|
397
|
+
* unescaped `[` and, read as a literal path, names something on disk:
|
|
398
|
+
* `app/[slug]/page.tsx` is a character class (caching.md) that matches
|
|
399
|
+
* no route, so the key stops following the file and an edit replays the
|
|
400
|
+
* old output. Only on the already-empty path; one stat per such glob, on
|
|
401
|
+
* its literal prefix up to the first segment holding another glob token.
|
|
402
|
+
*/
|
|
403
|
+
export function bracketLiteralHint(
|
|
404
|
+
globs: readonly (readonly [base: string, glob: string])[],
|
|
405
|
+
): string {
|
|
406
|
+
let hint = ''
|
|
407
|
+
for (const [base, glob] of globs) {
|
|
408
|
+
if (glob.startsWith('!') || !/(^|[^\\])\[/.test(glob)) continue
|
|
409
|
+
const segments: string[] = []
|
|
410
|
+
for (const seg of glob.split('/')) {
|
|
411
|
+
if (/(^|[^\\])[*?{}]/.test(seg)) break
|
|
412
|
+
segments.push(seg)
|
|
413
|
+
}
|
|
414
|
+
const prefix = segments.join('/')
|
|
415
|
+
if (!/(^|[^\\])\[/.test(prefix)) continue
|
|
416
|
+
const literal = prefix.replace(/\\(.)/g, '$1')
|
|
417
|
+
if (!existsSync(path.join(base, literal))) continue
|
|
418
|
+
const escaped = glob.replace(/(^|[^\\])(?=[[\]])/g, '$1\\')
|
|
419
|
+
hint += ` — [ starts a character class and ${literal} exists: write ${escaped} to name it`
|
|
420
|
+
}
|
|
421
|
+
return hint
|
|
422
|
+
}
|
|
@@ -84,12 +84,12 @@ export interface RunOptions {
|
|
|
84
84
|
*/
|
|
85
85
|
cacheFlags?: CacheFlags
|
|
86
86
|
/**
|
|
87
|
-
* The
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* saying it is not.
|
|
87
|
+
* The setting that asked for a remote axis by name (`'--cache-policy'` for
|
|
88
|
+
* `--cache-policy=remote:rw`), named in the warning. With no cache plugin
|
|
89
|
+
* supplying a remote layer the axes are inert, and a CI job that believes
|
|
90
|
+
* it is filling a shared cache deserves one line saying it is not.
|
|
91
91
|
*/
|
|
92
|
-
remoteRequested?:
|
|
92
|
+
remoteRequested?: string
|
|
93
93
|
/** This run's cache scope, above `VX_CACHE_SCOPE`, a plugin's and the branch's (an embedder's). */
|
|
94
94
|
cacheScope?: string
|
|
95
95
|
/**
|
|
@@ -205,7 +205,9 @@ export interface RunOptions {
|
|
|
205
205
|
* never-started ones `aborted`. The seam for an embedder that owns its
|
|
206
206
|
* process's lifetime instead of `handleSignals` — the watch loop aborts
|
|
207
207
|
* its in-flight cycle on SIGINT/SIGTERM this way, so a cancellation
|
|
208
|
-
* never orphans a cycle's children.
|
|
208
|
+
* never orphans a cycle's children. A reason naming a signal
|
|
209
|
+
* (`'SIGINT'`, `'SIGTERM'`, `'SIGHUP'`) is a stop as vx's own handler
|
|
210
|
+
* makes one: the run records no history, only what finished.
|
|
209
211
|
*/
|
|
210
212
|
signal?: AbortSignal
|
|
211
213
|
/**
|