@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
|
@@ -13,11 +13,19 @@ import { beginRun, mark, nearest, UserError } from '../util/index.js'
|
|
|
13
13
|
import {
|
|
14
14
|
Cache,
|
|
15
15
|
type CacheLayer,
|
|
16
|
+
applyCacheFlags,
|
|
16
17
|
type CachePolicy,
|
|
17
18
|
FULL_CACHE_POLICY,
|
|
18
|
-
|
|
19
|
+
parseCachePolicy,
|
|
19
20
|
GitFilesCache,
|
|
20
21
|
LayeredCache,
|
|
22
|
+
absentPluginCache,
|
|
23
|
+
pluginCache,
|
|
24
|
+
putRunEntry,
|
|
25
|
+
writesRunStore,
|
|
26
|
+
type PluginCache,
|
|
27
|
+
type PluginCacheRemote,
|
|
28
|
+
ScopedCache,
|
|
21
29
|
applyGitEnumeration,
|
|
22
30
|
gitPathspecs,
|
|
23
31
|
MAX_SCOPED_PATHSPECS,
|
|
@@ -27,15 +35,11 @@ import {
|
|
|
27
35
|
import {
|
|
28
36
|
buildPackageGraph,
|
|
29
37
|
computeNestedProjectDirs,
|
|
30
|
-
computeWorkspaceFingerprints,
|
|
31
38
|
findWorkspaceRoot,
|
|
32
39
|
cacheScopeEnvError,
|
|
33
40
|
isCacheScope,
|
|
34
41
|
type LoadReads,
|
|
35
|
-
|
|
36
|
-
FROZEN_WITHOUT_LOCK,
|
|
37
|
-
type Lockfile,
|
|
38
|
-
readLockfile,
|
|
42
|
+
readClaimedFiles,
|
|
39
43
|
resolveCacheDir,
|
|
40
44
|
resolveStoreRoot,
|
|
41
45
|
type ProjectEntry,
|
|
@@ -44,6 +48,7 @@ import {
|
|
|
44
48
|
buildTaskGraph,
|
|
45
49
|
excludeDependencies,
|
|
46
50
|
expandRequested,
|
|
51
|
+
type GraphRules,
|
|
47
52
|
isGroupTask,
|
|
48
53
|
type TaskNode,
|
|
49
54
|
undeclaredDepsError,
|
|
@@ -64,6 +69,8 @@ import {
|
|
|
64
69
|
isDefaultBuild,
|
|
65
70
|
loadProjects,
|
|
66
71
|
loadWorkspacePlugins,
|
|
72
|
+
lockOfDiscovery,
|
|
73
|
+
lockReader,
|
|
67
74
|
type LoadedProjects,
|
|
68
75
|
} from './projects.js'
|
|
69
76
|
import { keptByAffected } from './affected-tasks.js'
|
|
@@ -71,10 +78,12 @@ import { applyGraphStage } from './graph-stage.js'
|
|
|
71
78
|
import { keyExcludedDependencies } from './excluded-keys.js'
|
|
72
79
|
import { FingerprintWatch } from './fingerprint-watch.js'
|
|
73
80
|
import type { VxPlugin } from './plugin.js'
|
|
81
|
+
import { currentBranch } from './run-context.js'
|
|
74
82
|
import { createHashCache, type HashCache } from './task-hash.js'
|
|
75
83
|
import type { Logger } from './logger.js'
|
|
76
84
|
import type { RunOptions } from './options.js'
|
|
77
85
|
import { refuseFiltersThroughGroups } from './upstream.js'
|
|
86
|
+
import { runRules } from '../settings.js'
|
|
78
87
|
|
|
79
88
|
export interface PreparedRun {
|
|
80
89
|
workspaceRoot: string
|
|
@@ -97,7 +106,7 @@ export interface PreparedRun {
|
|
|
97
106
|
* would be saved still cleans its outputs before executing.
|
|
98
107
|
*/
|
|
99
108
|
hasRemoteLayer: boolean
|
|
100
|
-
/** `--cache` with the workspace's `cacheScope` applied: what the layers were built under. */
|
|
109
|
+
/** `--cache-policy` with the workspace's `cacheScope` applied: what the layers were built under. */
|
|
101
110
|
cachePolicy: CachePolicy
|
|
102
111
|
/**
|
|
103
112
|
* Scheduling priorities from plugins' `schedule` stage (task id → weight,
|
|
@@ -107,6 +116,19 @@ export interface PreparedRun {
|
|
|
107
116
|
* nothing-changed run never dispatches. Runs the stage once.
|
|
108
117
|
*/
|
|
109
118
|
priorities: () => Promise<ReadonlyMap<string, number>>
|
|
119
|
+
/**
|
|
120
|
+
* The run's branch (`run.branch`, else git's, else the CI provider's):
|
|
121
|
+
* what its history records and the timings a forecast reads first.
|
|
122
|
+
* Learned on first ask, once per run.
|
|
123
|
+
*/
|
|
124
|
+
branch: () => string | null
|
|
125
|
+
/** Each plugin's `ctx.cache`: the run's local store and remotes, under its policy and scope. */
|
|
126
|
+
cacheFor: (plugin: string) => PluginCache
|
|
127
|
+
/**
|
|
128
|
+
* Keep `bytes` of this run under `name` (run-store.ts); undefined when no
|
|
129
|
+
* remote takes the run's writes, and so core keeps nothing.
|
|
130
|
+
*/
|
|
131
|
+
keepRunEntry: ((name: string, bytes: Uint8Array) => Promise<boolean>) | undefined
|
|
110
132
|
nodes: Map<string, TaskNode>
|
|
111
133
|
/**
|
|
112
134
|
* The tasks `--exclude-dependencies` took out of the schedule: keyed as a
|
|
@@ -135,8 +157,7 @@ export interface PreparedRun {
|
|
|
135
157
|
* mistaken for a workspace vx was never set up in.
|
|
136
158
|
*/
|
|
137
159
|
anyProjectConfig: boolean
|
|
138
|
-
|
|
139
|
-
/** Whether a task has since rewritten what that fingerprint folded (fingerprint-watch.ts). */
|
|
160
|
+
/** The claimed root files, and whether a task has since rewritten one (fingerprint-watch.ts). */
|
|
140
161
|
fingerprintWatch: FingerprintWatch
|
|
141
162
|
nestedDirsByProject: Map<string, string[]>
|
|
142
163
|
/**
|
|
@@ -149,11 +170,9 @@ export interface PreparedRun {
|
|
|
149
170
|
/** Every project discovery found, in or out of scope — header scope bar. */
|
|
150
171
|
workspaceProjectCount: number
|
|
151
172
|
/**
|
|
152
|
-
* Per-run memo for derived hashes —
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
* the same project's package.json (and the same task config
|
|
156
|
-
* object) aren't re-hashed on every task in that project.
|
|
173
|
+
* Per-run memo for derived hashes — task-config hash keyed by config
|
|
174
|
+
* object identity, resolved file lists. Shared across every task's
|
|
175
|
+
* `computeTaskHash` call so the same config object isn't re-hashed.
|
|
157
176
|
*/
|
|
158
177
|
hashCache: HashCache
|
|
159
178
|
/**
|
|
@@ -188,22 +207,19 @@ export interface PreparedRun {
|
|
|
188
207
|
export async function prepareRun(options: RunOptions, log: Logger): Promise<PreparedRun> {
|
|
189
208
|
beginRun()
|
|
190
209
|
mark('startup')
|
|
191
|
-
//
|
|
192
|
-
//
|
|
210
|
+
// A claimed root file is read once. A watch cycle is a new run and reads
|
|
211
|
+
// them afresh.
|
|
193
212
|
const reads: LoadReads = new Map()
|
|
194
|
-
const workspaceRoot = await findWorkspaceRoot(options.cwd
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
(
|
|
203
|
-
|
|
204
|
-
return read
|
|
205
|
-
}))
|
|
206
|
-
if (options.frozen === true && options.staged === undefined) void readLock().catch(() => {})
|
|
213
|
+
const workspaceRoot = await findWorkspaceRoot(options.cwd)
|
|
214
|
+
// The lock is where a run's projects come from, so the read (1.1 MB at
|
|
215
|
+
// 1,000 projects, ~10 ms to read and parse) starts here and overlaps the
|
|
216
|
+
// workspace config's evaluation (X-181). A missing or malformed lock
|
|
217
|
+
// refuses at discovery, after any earlier refusal.
|
|
218
|
+
const reused = options.discovered?.root === workspaceRoot ? options.discovered : undefined
|
|
219
|
+
const readLock =
|
|
220
|
+
(reused !== undefined ? lockOfDiscovery(reused.projects) : undefined) ??
|
|
221
|
+
lockReader(workspaceRoot)
|
|
222
|
+
if (reused === undefined) void readLock().catch(() => {})
|
|
207
223
|
// An UNSCOPED run (no explicit scope, at least one bare task name)
|
|
208
224
|
// enumerates the whole tree whatever the configs say, so git starts
|
|
209
225
|
// HERE — the walk needs only the root — and overlaps the workspace
|
|
@@ -221,18 +237,21 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
221
237
|
// whole-tree enumeration a scoped run would otherwise scope later, here or
|
|
222
238
|
// in the CLI's selection pass; the run reuses it instead of walking the
|
|
223
239
|
// tree twice (G-75).
|
|
224
|
-
const reused = options.discovered?.root === workspaceRoot ? options.discovered : undefined
|
|
225
240
|
const git =
|
|
226
241
|
(reused !== undefined ? gitOfDiscovery(reused.projects) : undefined) ??
|
|
227
242
|
lazyGitEnumeration(workspaceRoot)
|
|
228
243
|
if (wholeTree) void git.start()
|
|
229
|
-
const workspace = await loadWorkspace(workspaceRoot, reads)
|
|
230
244
|
const { workspaceConfig, plugins } = await loadWorkspacePlugins(workspaceRoot, (m) =>
|
|
231
245
|
log.status(m),
|
|
232
246
|
)
|
|
233
247
|
mark('workspace config')
|
|
248
|
+
let learnedBranch: string | null | undefined
|
|
249
|
+
const branch = (): string | null =>
|
|
250
|
+
learnedBranch === undefined
|
|
251
|
+
? (learnedBranch = currentBranch(workspaceRoot, workspaceConfig, options.branch))
|
|
252
|
+
: learnedBranch
|
|
234
253
|
// `--cache-dir <path>` (RunOptions.cacheDir) overrides the workspace
|
|
235
|
-
// `
|
|
254
|
+
// `cache.dir` + the `.vx/cache` default; resolved relative to cwd.
|
|
236
255
|
// A cache dir named anywhere holds everything; the default shares its
|
|
237
256
|
// entries through the user's store.
|
|
238
257
|
const cacheDir = options.cacheDir
|
|
@@ -244,7 +263,14 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
244
263
|
const projectMetas =
|
|
245
264
|
reused !== undefined
|
|
246
265
|
? reused.projects
|
|
247
|
-
: await discoverProjects(
|
|
266
|
+
: await discoverProjects(
|
|
267
|
+
workspaceRoot,
|
|
268
|
+
readLock,
|
|
269
|
+
plugins,
|
|
270
|
+
cacheDir,
|
|
271
|
+
(m) => log.status(m),
|
|
272
|
+
git,
|
|
273
|
+
)
|
|
248
274
|
mark('discover projects')
|
|
249
275
|
|
|
250
276
|
// SCOPED config loading: configs are programs, and evaluating 1090
|
|
@@ -264,7 +290,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
264
290
|
// anchors alone are the scope and nothing else needs its config
|
|
265
291
|
// evaluated.
|
|
266
292
|
// `vx run //#lint` is Turbo's `turbo run //#lint`: the root project's
|
|
267
|
-
// task, the
|
|
293
|
+
// task, under the name it has when something names it (D-39).
|
|
268
294
|
const root = path.resolve(workspaceRoot)
|
|
269
295
|
const rootName = projectMetas.find((m) => path.resolve(m.dir) === root)?.name
|
|
270
296
|
const tasks =
|
|
@@ -282,20 +308,26 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
282
308
|
options.projects !== undefined ? [...options.projects, ...anchored] : hasBare ? 'all' : anchored
|
|
283
309
|
// Its own mark: the package graph is the bulk of what used to be charged
|
|
284
310
|
// to `open cache` (measured on a 1,000-project tree, 2026-09-20 — the
|
|
285
|
-
// graph 3-8 ms against the cache open's ~1 ms and the
|
|
311
|
+
// graph 3-8 ms against the cache open's ~1 ms and the root files' ~0.3),
|
|
286
312
|
// so a reader profiling that stage was sent to the wrong code.
|
|
287
313
|
mark('package graph')
|
|
288
314
|
|
|
289
|
-
// The local cache opens BEFORE the configs load: it is also where their
|
|
290
|
-
// cached evaluations live.
|
|
291
315
|
// `--cache-scope`, then `VX_CACHE_SCOPE`, win over the workspace (and `github()`'s guess):
|
|
292
316
|
// a CI job that knows its trust level says so without editing config.
|
|
293
317
|
const envScope = process.env['VX_CACHE_SCOPE'] || undefined
|
|
294
318
|
if (envScope !== undefined && !isCacheScope(envScope)) throw cacheScopeEnvError()
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
319
|
+
const workspacePolicy = workspaceCachePolicy(workspaceConfig)
|
|
320
|
+
const cacheScope = options.cacheScope ?? envScope ?? workspaceConfig?.cache?.scope
|
|
321
|
+
const asked =
|
|
322
|
+
options.cache ?? applyCacheFlags(workspacePolicy ?? FULL_CACHE_POLICY, options.cacheFlags)
|
|
323
|
+
// A laptop reads the shared cache and does not write it unless a policy
|
|
324
|
+
// names the remote (owner, 2026-10-03; Security F1, 2026-10-14): a key
|
|
325
|
+
// holds only declared inputs, so a laptop's write could be replayed on CI.
|
|
326
|
+
// A workspace policy naming the remote asks to write it, as `--cache-policy` does.
|
|
327
|
+
const readOnly =
|
|
328
|
+
options.remoteReadOnlyByDefault === true &&
|
|
329
|
+
!(options.cache === undefined && namesRemote(workspaceConfig?.cache?.policy))
|
|
330
|
+
const policy: CachePolicy = readOnly ? { ...asked, remoteWrite: false } : asked
|
|
299
331
|
const localCache = new Cache(
|
|
300
332
|
cacheDir,
|
|
301
333
|
{ read: policy.localRead, write: policy.localWrite },
|
|
@@ -305,29 +337,20 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
305
337
|
await storeRoot,
|
|
306
338
|
)
|
|
307
339
|
localCache.assertWritable()
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
// plugin claims (`VxPlugin.fingerprint`).
|
|
340
|
+
// The root files the plugins claim (`VxPlugin.fingerprint`), whose bytes
|
|
341
|
+
// their claimants fold: read now so a task that rewrites one is caught.
|
|
311
342
|
const fingerprintsAt = Date.now()
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
new Set(fingerprintClaims(plugins).keys()),
|
|
315
|
-
reads,
|
|
316
|
-
)
|
|
317
|
-
const workspaceFingerprint = fingerprints.unclaimed
|
|
343
|
+
const claimed = [...fingerprintClaims(plugins).keys()]
|
|
344
|
+
const claimedBytes = await readClaimedFiles(workspaceRoot, claimed, reads)
|
|
318
345
|
mark('open cache')
|
|
319
346
|
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
// their locked values. Default (local) runs ALWAYS evaluate live:
|
|
324
|
-
// a byte hash can't see a config's import closure (shared presets),
|
|
325
|
-
// so consuming the lock by default would silently serve stale
|
|
326
|
-
// freezes. `vx lock --check` is the full re-evaluation audit.
|
|
347
|
+
// Configs load FROM .vx/projects.lock.json with no evaluation and no check of
|
|
348
|
+
// the configs (`vx lock --check` re-evaluates them; a run does not):
|
|
349
|
+
// only `vx lock` evaluates (owner, 2026-10-14).
|
|
327
350
|
// See docs/design/config-lock-2026-06.md.
|
|
328
351
|
// Read once, and only when a config is to be read from it: the CLI's
|
|
329
352
|
// selection pass may have staged every config (`options.staged`), and
|
|
330
|
-
// its load refused a
|
|
353
|
+
// its load refused a run without a lock. A 1,000-project lock is
|
|
331
354
|
// 1.1 MB of JSON (I-27); a run with nothing staged started its read
|
|
332
355
|
// beside discovery, above.
|
|
333
356
|
const staged = options.staged
|
|
@@ -336,8 +359,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
336
359
|
projectMetas.every(
|
|
337
360
|
(m) => typeof m.configPath !== 'string' || m.configPath === '' || staged.has(m.name),
|
|
338
361
|
)
|
|
339
|
-
if (
|
|
340
|
-
const lock = options.frozen === true ? readLock : null
|
|
362
|
+
if (!allStaged) await readLock()
|
|
341
363
|
|
|
342
364
|
const loadArgs = {
|
|
343
365
|
workspaceRoot,
|
|
@@ -345,8 +367,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
345
367
|
plugins,
|
|
346
368
|
projectMetas,
|
|
347
369
|
packageGraph,
|
|
348
|
-
|
|
349
|
-
evalCache: { store: localCache, workspaceRoot, workspaceFingerprint: fingerprints.all },
|
|
370
|
+
configs: readLock,
|
|
350
371
|
warn: (m: string) => log.status(m),
|
|
351
372
|
}
|
|
352
373
|
let loaded: LoadedProjects
|
|
@@ -358,8 +379,8 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
358
379
|
...(options.staged !== undefined ? { staged: options.staged } : {}),
|
|
359
380
|
})
|
|
360
381
|
} catch (err) {
|
|
361
|
-
// The cache opened before the configs loaded
|
|
362
|
-
//
|
|
382
|
+
// The cache opened before the configs loaded; a config error must not
|
|
383
|
+
// leak the handle.
|
|
363
384
|
localCache.close()
|
|
364
385
|
throw err
|
|
365
386
|
}
|
|
@@ -443,6 +464,11 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
443
464
|
// winning prevents double-wrapping when the workspace also declares a
|
|
444
465
|
// cache plugin.
|
|
445
466
|
let cache: CacheLayer
|
|
467
|
+
const remotes: PluginCacheRemote[] = []
|
|
468
|
+
if (options.remoteCache) {
|
|
469
|
+
const { remoteCache } = options
|
|
470
|
+
remotes.push({ remote: remoteCache, read: policy.remoteRead, write: policy.remoteWrite })
|
|
471
|
+
}
|
|
446
472
|
try {
|
|
447
473
|
cache = options.remoteCache
|
|
448
474
|
? new LayeredCache(localCache, options.remoteCache, {
|
|
@@ -453,8 +479,10 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
453
479
|
workspaceRoot,
|
|
454
480
|
cacheDir,
|
|
455
481
|
warn: (m) => log.status(m),
|
|
482
|
+
scope: cacheScope,
|
|
456
483
|
localCache,
|
|
457
484
|
policy,
|
|
485
|
+
remotes,
|
|
458
486
|
})
|
|
459
487
|
} catch (err) {
|
|
460
488
|
// A cache plugin that throws or returns something off-contract is
|
|
@@ -465,6 +493,8 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
465
493
|
localCache.close()
|
|
466
494
|
throw err
|
|
467
495
|
}
|
|
496
|
+
// One scoping rule above every layer, local and remote alike.
|
|
497
|
+
if (cacheScope !== undefined) cache = new ScopedCache(cache, cacheScope)
|
|
468
498
|
// Ask the LAYER, don't infer. Identity against `localCache` answers a
|
|
469
499
|
// DIFFERENT question — "did the plugin hand back something other than the
|
|
470
500
|
// handle I passed in?" — which an ordinary pass-through decorator (a
|
|
@@ -480,9 +510,14 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
480
510
|
// returns.
|
|
481
511
|
try {
|
|
482
512
|
const hashCache = createHashCache()
|
|
483
|
-
const fingerprintWatch = new FingerprintWatch(
|
|
513
|
+
const fingerprintWatch = new FingerprintWatch(
|
|
514
|
+
workspaceRoot,
|
|
515
|
+
claimed,
|
|
516
|
+
claimedBytes,
|
|
517
|
+
fingerprintsAt,
|
|
518
|
+
)
|
|
484
519
|
|
|
485
|
-
// Empty-cases bookkeeping. We still construct the cache +
|
|
520
|
+
// Empty-cases bookkeeping. We still construct the cache + watch
|
|
486
521
|
// so the caller's try/finally pattern can close it uniformly.
|
|
487
522
|
const emptyRun = (reason?: 'none-affected'): PreparedRun => ({
|
|
488
523
|
workspaceRoot,
|
|
@@ -493,7 +528,10 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
493
528
|
localCache,
|
|
494
529
|
hasRemoteLayer,
|
|
495
530
|
cachePolicy: policy,
|
|
531
|
+
keepRunEntry: undefined,
|
|
496
532
|
priorities: () => Promise.resolve(new Map()),
|
|
533
|
+
branch,
|
|
534
|
+
cacheFor: () => absentPluginCache,
|
|
497
535
|
nodes: new Map(),
|
|
498
536
|
keyOnly: new Map(),
|
|
499
537
|
unresolvedTasks,
|
|
@@ -501,7 +539,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
501
539
|
projects,
|
|
502
540
|
hintProjects,
|
|
503
541
|
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
504
|
-
workspaceFingerprint,
|
|
505
542
|
fingerprintWatch,
|
|
506
543
|
nestedDirsByProject,
|
|
507
544
|
gitFilesCache: new GitFilesCache(),
|
|
@@ -521,6 +558,10 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
521
558
|
// the whole workspace: a `^name` nothing loaded declares may be declared
|
|
522
559
|
// by a config the scope left out, so the builder hands it back instead of
|
|
523
560
|
// refusing it, and the rest decide.
|
|
561
|
+
// A run never checks the graph rules: `vx lock` and `vx lock --check`
|
|
562
|
+
// do (owner, 2026-10-14). `exclusiveOutputs: false` still asks the walk
|
|
563
|
+
// for its addition marks.
|
|
564
|
+
const graphRules: GraphRules = { ...runRules(workspaceConfig), locked: true }
|
|
524
565
|
const unproven: Array<[taskId: string, name: string]> = []
|
|
525
566
|
const graphOf = (req: typeof requested) =>
|
|
526
567
|
buildTaskGraph({
|
|
@@ -528,7 +569,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
528
569
|
packageGraph,
|
|
529
570
|
requested: req,
|
|
530
571
|
workspaceRoot,
|
|
531
|
-
rules:
|
|
572
|
+
rules: graphRules,
|
|
532
573
|
...(partialLoad
|
|
533
574
|
? { undeclaredDeps: (id: string, name: string) => void unproven.push([id, name]) }
|
|
534
575
|
: {}),
|
|
@@ -541,7 +582,7 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
541
582
|
cacheDir,
|
|
542
583
|
projectMetas,
|
|
543
584
|
warn: (m) => log.status(m),
|
|
544
|
-
rules:
|
|
585
|
+
rules: graphRules,
|
|
545
586
|
})
|
|
546
587
|
// `--affected` selects over the FINAL graph: an edge or an input a
|
|
547
588
|
// `graph` hook adds moves a key, so the hooks run first, over every
|
|
@@ -681,7 +722,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
681
722
|
dropped,
|
|
682
723
|
cache,
|
|
683
724
|
workspaceRoot,
|
|
684
|
-
workspaceFingerprint,
|
|
685
725
|
forwardArgs: options.forwardArgs,
|
|
686
726
|
nestedDirsByProject,
|
|
687
727
|
gitFilesCache,
|
|
@@ -689,15 +729,35 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
689
729
|
})
|
|
690
730
|
}
|
|
691
731
|
}
|
|
732
|
+
const pluginCaches = new Map<string, PluginCache>()
|
|
733
|
+
const cacheFor = (plugin: string): PluginCache => {
|
|
734
|
+
let own = pluginCaches.get(plugin)
|
|
735
|
+
if (own === undefined) {
|
|
736
|
+
own = pluginCache(plugin, {
|
|
737
|
+
db: localCache.dbHandle(),
|
|
738
|
+
policy,
|
|
739
|
+
remotes,
|
|
740
|
+
scope: cacheScope,
|
|
741
|
+
})
|
|
742
|
+
pluginCaches.set(plugin, own)
|
|
743
|
+
}
|
|
744
|
+
return own
|
|
745
|
+
}
|
|
692
746
|
let scheduled: Promise<ReadonlyMap<string, number>> | undefined
|
|
693
747
|
const priorities = (): Promise<ReadonlyMap<string, number>> =>
|
|
694
748
|
(scheduled ??= hasHook(plugins, 'schedule')
|
|
695
|
-
? applyScheduleHooks(
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
749
|
+
? applyScheduleHooks(
|
|
750
|
+
plugins,
|
|
751
|
+
nodes,
|
|
752
|
+
{
|
|
753
|
+
workspaceRoot,
|
|
754
|
+
cacheDir,
|
|
755
|
+
warn: (m) => log.status(m),
|
|
756
|
+
localCache,
|
|
757
|
+
branch: branch(),
|
|
758
|
+
},
|
|
759
|
+
cacheFor,
|
|
760
|
+
)
|
|
701
761
|
: Promise.resolve(new Map()))
|
|
702
762
|
|
|
703
763
|
return {
|
|
@@ -710,6 +770,11 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
710
770
|
hasRemoteLayer,
|
|
711
771
|
cachePolicy: policy,
|
|
712
772
|
priorities,
|
|
773
|
+
branch,
|
|
774
|
+
cacheFor,
|
|
775
|
+
keepRunEntry: writesRunStore(remotes)
|
|
776
|
+
? (name, bytes) => putRunEntry(remotes, cacheScope, name, bytes)
|
|
777
|
+
: undefined,
|
|
713
778
|
nodes,
|
|
714
779
|
keyOnly,
|
|
715
780
|
unresolvedTasks,
|
|
@@ -717,7 +782,6 @@ export async function prepareRun(options: RunOptions, log: Logger): Promise<Prep
|
|
|
717
782
|
projects,
|
|
718
783
|
hintProjects,
|
|
719
784
|
anyProjectConfig: projectsWithConfigs.length > 0,
|
|
720
|
-
workspaceFingerprint,
|
|
721
785
|
fingerprintWatch,
|
|
722
786
|
nestedDirsByProject,
|
|
723
787
|
gitFilesCache,
|
|
@@ -771,7 +835,7 @@ function undeclaredIn(
|
|
|
771
835
|
|
|
772
836
|
/**
|
|
773
837
|
* Refuses the first `^name` in `unproven` that no project in the whole
|
|
774
|
-
* workspace declares. `load`
|
|
838
|
+
* workspace declares. `load` reads the locked configs a scoped run left out;
|
|
775
839
|
* it is reached only when a `^name` found no holder and nothing loaded
|
|
776
840
|
* declares it, so a run that names only declared tasks never pays for it.
|
|
777
841
|
* The rest failing to load leaves the names unjudged: an out-of-scope
|
|
@@ -815,3 +879,24 @@ function pruneToClosure(nodes: Map<string, TaskNode>, roots: ReadonlySet<string>
|
|
|
815
879
|
else if (!roots.has(id)) n.requested = false
|
|
816
880
|
}
|
|
817
881
|
}
|
|
882
|
+
|
|
883
|
+
const namesRemote = (spec: string | undefined): boolean =>
|
|
884
|
+
spec !== undefined && /remote:[rw]/.test(spec)
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* The workspace's `cache.policy`, parsed: the schema checks only its shape,
|
|
888
|
+
* since the grammar lives in the cache module the workspace module may not
|
|
889
|
+
* import. Undefined when it names none.
|
|
890
|
+
*/
|
|
891
|
+
function workspaceCachePolicy(config: WorkspaceConfig | null | undefined): CachePolicy | undefined {
|
|
892
|
+
const spec = config?.cache?.policy
|
|
893
|
+
if (spec === undefined) return undefined
|
|
894
|
+
try {
|
|
895
|
+
return parseCachePolicy(spec)
|
|
896
|
+
} catch (err) {
|
|
897
|
+
throw new UserError(
|
|
898
|
+
`.vx/workspace: \`cache.policy\`: ${err instanceof Error ? err.message : String(err)}`,
|
|
899
|
+
'VX_E_CONFIG',
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
}
|