@vzn/vx 0.0.635 → 0.0.637
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 +47 -19
- package/docs/caching.md +222 -210
- package/docs/cli.md +699 -664
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +31 -32
- package/docs/schema.md +418 -202
- 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 +2 -2
- package/schemas/show.json +13 -4
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -61
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +123 -144
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- package/src/config.ts +191 -71
- 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 +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +25 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- 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 +192 -70
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +43 -28
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +114 -73
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- 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-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +127 -88
- 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 +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -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/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +251 -241
- package/src/workspace/dependency-rules.ts +57 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +22 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +242 -146
- 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 +362 -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/cli/why.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
// `vx why [pkg#task | task]` — answer "why did this task re-run?" from the
|
|
2
|
-
// terminal. The
|
|
3
|
-
// the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// no config evaluation, no re-hash.
|
|
2
|
+
// terminal. The runs table names the run-over-run hash change, and
|
|
3
|
+
// `entry_inputs` (the input-fingerprint moat) names the exact cache-key
|
|
4
|
+
// components that differ. Read-only over cache.db plus a stat of each
|
|
5
|
+
// moved input file — no config evaluation, no re-hash.
|
|
7
6
|
|
|
8
|
-
import path from 'node:path'
|
|
9
7
|
import { Cache } from '../cache/index.js'
|
|
10
8
|
import { flagHint, formatValue, seeHelp } from './help.js'
|
|
11
9
|
import { splitTaskId } from '../graph/index.js'
|
|
12
10
|
import {
|
|
13
11
|
cacheKeyDiff,
|
|
14
12
|
explainCacheKeyQuery as explainCacheKey,
|
|
13
|
+
inputWriters,
|
|
15
14
|
latestRunId,
|
|
16
15
|
rootCauses,
|
|
17
16
|
whyDidThisRerunQuery as whyDidThisRerun,
|
|
@@ -20,7 +19,7 @@ import {
|
|
|
20
19
|
import { MASKED, nearMatches, printable, UserError } from '../util/index.js'
|
|
21
20
|
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
22
21
|
import { findCwdProject } from './select.js'
|
|
23
|
-
import { cliCacheDir, parseCacheDirFlag } from './workspace-config.js'
|
|
22
|
+
import { cliCacheDir, discoverCliProjects, parseCacheDirFlag } from './workspace-config.js'
|
|
24
23
|
|
|
25
24
|
interface WhyArgs {
|
|
26
25
|
target?: string
|
|
@@ -43,9 +42,6 @@ export const WHAT_TO_DO: Readonly<Record<string, string>> = {
|
|
|
43
42
|
'ws-runtime':
|
|
44
43
|
'the workspace command printed something else; drop timestamps and paths from its output',
|
|
45
44
|
forward: 'the arguments after -- differ; the same arguments share one entry',
|
|
46
|
-
package: "the project's package.json changed (a dependency, version or script)",
|
|
47
|
-
workspace:
|
|
48
|
-
'a lockfile or root manifest changed, which re-keys every task; a lockfile plugin keys each project on its own dependencies',
|
|
49
45
|
config:
|
|
50
46
|
"the task's evaluated config changed (its vx.config or a file it imports); `vx show <that task>` prints it as it is now",
|
|
51
47
|
upstream: "a dependency's key moved; `vx why <that task>` says why",
|
|
@@ -174,14 +170,14 @@ async function resolveTarget(
|
|
|
174
170
|
|
|
175
171
|
const fmtWhen = (ms: number): string => new Date(ms).toISOString()
|
|
176
172
|
|
|
177
|
-
/** `//#task`, Turbo's root package, as the root project's id:
|
|
173
|
+
/** `//#task`, Turbo's root package, as the root project's id: the name discovery gives it (D-39). */
|
|
178
174
|
async function rootSpelled(root: string, target: string): Promise<string> {
|
|
179
175
|
if (!target.startsWith('//#')) return target
|
|
180
|
-
const name = await
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return
|
|
176
|
+
const name = await discoverCliProjects(root).then(
|
|
177
|
+
(metas) => metas.find((m) => m.dir === root)?.name,
|
|
178
|
+
() => undefined,
|
|
179
|
+
)
|
|
180
|
+
return name === undefined ? target : `${name}#${target.slice(3)}`
|
|
185
181
|
}
|
|
186
182
|
|
|
187
183
|
/** ` · up-to-date` or ` · restored` on a hit, as the run's summary said it; nothing otherwise. */
|
|
@@ -247,9 +243,10 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
247
243
|
// each moved dependency in this run down to the tasks whose own
|
|
248
244
|
// inputs moved, so one `vx why` names the file, not the next task.
|
|
249
245
|
const roots = rootCauses(db, runId, taskId, diff.entries)
|
|
246
|
+
const writers = inputWriters(db, root, taskId, diff.entries, roots)
|
|
250
247
|
|
|
251
248
|
if (parsed.format === 'json') {
|
|
252
|
-
process.stdout.write(`${JSON.stringify({ taskId, runId, why, diff, roots })}\n`)
|
|
249
|
+
process.stdout.write(`${JSON.stringify({ taskId, runId, why, diff, roots, writers })}\n`)
|
|
253
250
|
return 0
|
|
254
251
|
}
|
|
255
252
|
|
|
@@ -320,6 +317,12 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
320
317
|
}
|
|
321
318
|
if (roots.length > ROOTS_SHOWN) lines.push(` and ${roots.length - ROOTS_SHOWN} more`)
|
|
322
319
|
}
|
|
320
|
+
if (writers.length > 0) {
|
|
321
|
+
lines.push('', ' changed during a run:')
|
|
322
|
+
for (const w of writers) {
|
|
323
|
+
lines.push(` ${printable(w.file)} while ${w.writers.join(', ')} ran`)
|
|
324
|
+
}
|
|
325
|
+
}
|
|
323
326
|
const rootKinds = roots.flatMap((r) => r.entries.map((e) => e.kind))
|
|
324
327
|
const kinds = [...new Set([...diff.entries.map((e) => e.kind), ...rootKinds])].filter(
|
|
325
328
|
(k) => k in WHAT_TO_DO && !(k === 'upstream' && roots.length > 0),
|
|
@@ -341,6 +344,11 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
341
344
|
: WHAT_TO_DO[k]
|
|
342
345
|
lines.push(` ${k.padEnd(kindW)} ${todo}`)
|
|
343
346
|
}
|
|
347
|
+
if (writers.length > 0) {
|
|
348
|
+
// A task re-writing a file a key reads moves that key every run:
|
|
349
|
+
// the miss is right, and only moving the write ends it.
|
|
350
|
+
lines.push(` ${'written'.padEnd(kindW)} ${WRITTEN_TODO}`)
|
|
351
|
+
}
|
|
344
352
|
}
|
|
345
353
|
} else if (why.hashChanged === true) {
|
|
346
354
|
lines.push(` detail ${diff.note}`)
|
|
@@ -353,3 +361,6 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
|
|
|
353
361
|
}
|
|
354
362
|
|
|
355
363
|
const ROOTS_SHOWN = 5
|
|
364
|
+
|
|
365
|
+
const WRITTEN_TODO =
|
|
366
|
+
'a task that writes a file a key reads re-keys it every run; give that write its own task with the file as an output, or keep the file out of cache.inputs.files'
|
|
@@ -8,17 +8,18 @@ import { existsSync } from 'node:fs'
|
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
import type { WorkspaceConfig } from '../config.js'
|
|
10
10
|
import { UserError } from '../util/index.js'
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
discoverProjects,
|
|
13
|
+
loadProjects,
|
|
14
|
+
loadWorkspacePlugins,
|
|
15
|
+
lockOfDiscovery,
|
|
16
|
+
lockReader,
|
|
17
|
+
} from '../orchestrator/index.js'
|
|
13
18
|
import type { VxPlugin } from '../orchestrator/index.js'
|
|
14
19
|
import {
|
|
15
20
|
buildPackageGraph,
|
|
16
|
-
computeWorkspaceFingerprint,
|
|
17
|
-
FROZEN_WITHOUT_LOCK,
|
|
18
|
-
type Workspace,
|
|
19
21
|
type ProjectEntry,
|
|
20
22
|
type ProjectMeta,
|
|
21
|
-
readLockfile,
|
|
22
23
|
resolveCacheDir,
|
|
23
24
|
resolveStoreRoot,
|
|
24
25
|
} from '../workspace/index.js'
|
|
@@ -46,17 +47,18 @@ export async function loadCliWorkspace(workspaceRoot: string): Promise<CliWorksp
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
|
-
* The workspace's projects as a run discovers them:
|
|
50
|
-
* plugin `discover` stage, with the cache dir the run uses.
|
|
50
|
+
* The workspace's projects as a run discovers them: the lock's, then the
|
|
51
|
+
* plugin `discover` stage, with the cache dir the run uses. No lock is
|
|
52
|
+
* `NO_LOCK`.
|
|
51
53
|
*/
|
|
52
|
-
export async function discoverCliProjects(
|
|
53
|
-
const { plugins, cacheDir } = await loadCliWorkspace(
|
|
54
|
-
return await discoverProjects(
|
|
54
|
+
export async function discoverCliProjects(root: string): Promise<ProjectMeta[]> {
|
|
55
|
+
const { plugins, cacheDir } = await loadCliWorkspace(root)
|
|
56
|
+
return await discoverProjects(root, lockReader(root), plugins, cacheDir, warnToStderr)
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
/**
|
|
58
60
|
* `--cache-dir <path>` / `--cache-dir=<path>` for a verb that reads the
|
|
59
|
-
* cache a run wrote — `why`, `last`, `info`, `cache
|
|
61
|
+
* cache a run wrote — `why`, `last`, `info`, `cache clean`. Same rules as
|
|
60
62
|
* `vx run`'s: a value is required, and the space form refuses a value
|
|
61
63
|
* that looks like a flag (an unquoted empty shell variable would otherwise
|
|
62
64
|
* swallow the next flag as the path). Returns null when `args[i]` is not
|
|
@@ -104,16 +106,8 @@ export async function cliCacheDir(
|
|
|
104
106
|
|
|
105
107
|
/** The run flags a verb's staged load must honour to see what the run sees. */
|
|
106
108
|
export interface CliLoadOptions {
|
|
107
|
-
/** `--cache-dir`:
|
|
109
|
+
/** `--cache-dir`: the cache dir a plugin's `project` stage is told. */
|
|
108
110
|
cacheDir?: string
|
|
109
|
-
/** `--frozen`: configs come from `vx-lock.json`, not evaluation. */
|
|
110
|
-
frozen?: boolean
|
|
111
|
-
/**
|
|
112
|
-
* A reading verb (`vx show`) makes nothing on disk (item 900): with no
|
|
113
|
-
* cache directory yet, it evaluates without the cache rather than
|
|
114
|
-
* create one.
|
|
115
|
-
*/
|
|
116
|
-
noCreate?: boolean
|
|
117
111
|
/**
|
|
118
112
|
* Load what each seed's package closure reaches too, as a run does: a
|
|
119
113
|
* config-less package a configured one depends on gets the default
|
|
@@ -124,10 +118,9 @@ export interface CliLoadOptions {
|
|
|
124
118
|
|
|
125
119
|
/**
|
|
126
120
|
* The run path's project-config load (`loadProjects`) for a verb that only
|
|
127
|
-
* reads: the plugin `project` stage
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* only with `closure`, no lock (a verb reads live, as a default run does).
|
|
121
|
+
* reads: configs from the lock, the plugin `project` stage applied. `scope`
|
|
122
|
+
* is every project or a list of names; their closure only with `closure`.
|
|
123
|
+
* No lock is `NO_LOCK`, whatever the scope: only `vx lock` evaluates.
|
|
131
124
|
*/
|
|
132
125
|
export async function loadCliProjects(
|
|
133
126
|
workspaceRoot: string,
|
|
@@ -137,48 +130,18 @@ export async function loadCliProjects(
|
|
|
137
130
|
): Promise<Map<string, ProjectEntry>> {
|
|
138
131
|
const ws = await loadCliWorkspace(workspaceRoot)
|
|
139
132
|
const { plugins } = ws
|
|
140
|
-
// The cached evaluations live where the run's do: a verb given
|
|
141
|
-
// `--cache-dir` must not open (and create) the workspace's default one.
|
|
142
133
|
const cacheDir = opts.cacheDir ?? ws.cacheDir
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
opts.
|
|
150
|
-
?
|
|
151
|
-
:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'open',
|
|
157
|
-
opts.cacheDir === undefined ? await ws.storeRoot : null,
|
|
158
|
-
)
|
|
159
|
-
try {
|
|
160
|
-
const loaded = await loadProjects({
|
|
161
|
-
workspaceRoot,
|
|
162
|
-
cacheDir,
|
|
163
|
-
plugins,
|
|
164
|
-
projectMetas: metas,
|
|
165
|
-
seeds: scope,
|
|
166
|
-
...(opts.closure === true
|
|
167
|
-
? { closure: true as const, packageGraph: buildPackageGraph([...metas]) }
|
|
168
|
-
: { closure: false as const }),
|
|
169
|
-
lock: lock === null ? null : async () => lock,
|
|
170
|
-
evalCache:
|
|
171
|
-
cache === null
|
|
172
|
-
? undefined
|
|
173
|
-
: {
|
|
174
|
-
store: cache,
|
|
175
|
-
workspaceRoot,
|
|
176
|
-
workspaceFingerprint: await computeWorkspaceFingerprint(workspaceRoot),
|
|
177
|
-
},
|
|
178
|
-
warn: warnToStderr,
|
|
179
|
-
})
|
|
180
|
-
return loaded.projects
|
|
181
|
-
} finally {
|
|
182
|
-
cache?.close()
|
|
183
|
-
}
|
|
134
|
+
const loaded = await loadProjects({
|
|
135
|
+
workspaceRoot,
|
|
136
|
+
cacheDir,
|
|
137
|
+
plugins,
|
|
138
|
+
projectMetas: metas,
|
|
139
|
+
seeds: scope,
|
|
140
|
+
...(opts.closure === true
|
|
141
|
+
? { closure: true as const, packageGraph: buildPackageGraph([...metas]) }
|
|
142
|
+
: { closure: false as const }),
|
|
143
|
+
configs: lockOfDiscovery(metas) ?? lockReader(workspaceRoot),
|
|
144
|
+
warn: warnToStderr,
|
|
145
|
+
})
|
|
146
|
+
return loaded.projects
|
|
184
147
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,78 +1,140 @@
|
|
|
1
1
|
// Public schema for vx project and workspace configuration.
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* A duration with its unit: `'500ms'`, `'30s'`, `'5m'`, `'2h'`, `'1d'`. A
|
|
5
|
+
* bare number is refused: a unit in the value keeps the name free of one.
|
|
6
|
+
*/
|
|
7
|
+
export type Duration = `${number}${'ms' | 's' | 'm' | 'h' | 'd'}`
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The workspace: settings in groups (`run`, `cache`, `lock`, `watch`) and the
|
|
11
|
+
* plugins. Each setting's environment variable and flag derive from its
|
|
12
|
+
* path (`run.concurrency` → `VX_RUN_CONCURRENCY`, `vx run --concurrency`); a flag beats
|
|
13
|
+
* the variable, which beats this file. `docs/schema.md` lists them all.
|
|
14
|
+
* Nothing here is folded into a cache key.
|
|
15
|
+
*/
|
|
3
16
|
export interface WorkspaceConfig {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* cache and shares nothing; unset, the workspace keeps its index in
|
|
9
|
-
* `.vx/cache` and its entries in the user's shared store.
|
|
10
|
-
*/
|
|
11
|
-
cacheDir?: string
|
|
17
|
+
run?: RunSettings
|
|
18
|
+
cache?: CacheSettings
|
|
19
|
+
lock?: LockSettings
|
|
20
|
+
watch?: WatchSettings
|
|
12
21
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
22
|
+
* Plugins registered for this workspace, consulted in this order once
|
|
23
|
+
* per `vx run`: each fills any of the pipeline stages and capabilities
|
|
24
|
+
* below, and core's own executor and cache store sit at the tail of
|
|
25
|
+
* every list. See `docs/design/pipeline-2026-09.md`. An `undefined`,
|
|
26
|
+
* `null` or `false` entry is skipped, so a factory that declines
|
|
27
|
+
* (`github()` off Actions) or `cond && plugin()` needs no filter.
|
|
18
28
|
*/
|
|
19
|
-
|
|
29
|
+
plugins?: readonly (Plugin | undefined | null | false)[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `WorkspaceConfig.run`: how a run runs. */
|
|
33
|
+
export interface RunSettings {
|
|
34
|
+
/** Tasks at once: a positive integer, or a share of the cores (`'50%'`). Omitted → the cores this process may use. */
|
|
35
|
+
concurrency?: number | `${number}%`
|
|
20
36
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
37
|
+
* Which ready task starts first when there are more than workers, with no
|
|
38
|
+
* timings: `most-work` (the default) the one the most work waits on at
|
|
39
|
+
* any depth, `critical-path` the head of the longest chain of tasks,
|
|
40
|
+
* `direct-dependents` the one the most tasks wait on directly (Nx's
|
|
41
|
+
* rule), `ready-order` the one that became ready first (Turbo's). A
|
|
42
|
+
* `schedule` plugin's weights still come first; this breaks their ties.
|
|
27
43
|
*/
|
|
28
|
-
|
|
44
|
+
schedule?: 'most-work' | 'critical-path' | 'direct-dependents' | 'ready-order'
|
|
29
45
|
/**
|
|
30
46
|
* The git ref a bare `--affected` compares with (`origin/develop`).
|
|
31
47
|
* `--affected=<base>` overrides it. Omitted → `origin/HEAD`, then the
|
|
32
48
|
* first of `origin/main`, `origin/master`, `main`, `master` that is not
|
|
33
|
-
* HEAD, else `HEAD~1`.
|
|
49
|
+
* HEAD, else `HEAD~1`.
|
|
34
50
|
*/
|
|
35
51
|
affectedBase?: string
|
|
36
52
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* on CI (or with `--cache` naming the remote) `'trusted'`. Any other
|
|
41
|
-
* name (`'pr-123'`) is an untrusted scope: reads try the scope's keys,
|
|
42
|
-
* then the trusted ones, and writes go to the scope's only, so a PR never
|
|
43
|
-
* writes what the default branch reads. A client-side convention, not a
|
|
44
|
-
* security boundary: only a cache server that scopes writes by token
|
|
45
|
-
* can refuse a run that ignores it. `VX_CACHE_SCOPE` beats it. Never
|
|
46
|
-
* folded into a cache key.
|
|
53
|
+
* A task's time limit, for tasks without their own `exec.timeout` (which
|
|
54
|
+
* beats every run-level source). A runaway-process guard: a timed-out
|
|
55
|
+
* task fails and is never cached. Omitted → no limit.
|
|
47
56
|
*/
|
|
48
|
-
|
|
57
|
+
timeout?: Duration
|
|
58
|
+
/** Re-runs of a failed task, for tasks without their own `exec.retries`. Omitted → 0. */
|
|
59
|
+
retries?: number
|
|
49
60
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* any depth, `critical-path` the head of the longest chain of tasks,
|
|
53
|
-
* `direct-dependents` the one the most tasks wait on directly (Nx's
|
|
54
|
-
* rule), `ready-order` the one that became ready first (Turbo's). A
|
|
55
|
-
* `schedule` plugin's weights still come first; this breaks their ties.
|
|
61
|
+
* What a failed task takes down: `never` stops the run, `deps-ok` (the
|
|
62
|
+
* default) skips its dependents, `always` runs them too.
|
|
56
63
|
*/
|
|
57
|
-
|
|
64
|
+
continue?: 'never' | 'deps-ok' | 'always'
|
|
65
|
+
/** How long a task that ignores SIGTERM gets before SIGKILL. Omitted → `'2s'`. */
|
|
66
|
+
killGrace?: Duration
|
|
67
|
+
/** The bound on each plugin's end-of-run flush and teardown. Omitted → `'3s'`. */
|
|
68
|
+
teardownTimeout?: Duration
|
|
69
|
+
/** How long a persistent task may take to match `readyWhen` before vx says it waits. Omitted → `'10s'`. */
|
|
70
|
+
readyNotice?: Duration
|
|
58
71
|
/**
|
|
59
|
-
* Graph rules
|
|
60
|
-
*
|
|
61
|
-
*
|
|
72
|
+
* Graph rules checked by `vx lock` and `vx lock --check`, never by a
|
|
73
|
+
* run. Each is on
|
|
74
|
+
* unless set to `false`; turning one off allows a shape vx handles
|
|
75
|
+
* correctly but more slowly.
|
|
62
76
|
*/
|
|
63
77
|
rules?: WorkspaceRules
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `WorkspaceConfig.cache`: where the cache lives and what a run does with it. */
|
|
81
|
+
export interface CacheSettings {
|
|
64
82
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* every list. See `docs/design/pipeline-2026-09.md`. An `undefined`,
|
|
69
|
-
* `null` or `false` entry is skipped, so a factory that declines
|
|
70
|
-
* (`github()` off Actions) or `cond && plugin()` needs no filter.
|
|
83
|
+
* Cache directory, relative to the workspace root. Named, it holds the whole
|
|
84
|
+
* cache and shares nothing; unset, the workspace keeps its index in
|
|
85
|
+
* `.vx/cache` and its entries in the user's shared store.
|
|
71
86
|
*/
|
|
72
|
-
|
|
87
|
+
dir?: string
|
|
88
|
+
/**
|
|
89
|
+
* Evict from the local cache at the end of every run, whatever its
|
|
90
|
+
* policy (a remote hit is ingested under any), by the same policy
|
|
91
|
+
* `vx cache clean` takes as flags: entries not used for `olderThan`
|
|
92
|
+
* (`'30d'`, `'12h'`), then least-recently-used entries until the cache
|
|
93
|
+
* is under `maxSize` (`'10GB'`, `'500MB'`). Either or both. Omitted →
|
|
94
|
+
* nothing is evicted unless you run `vx cache clean`.
|
|
95
|
+
*/
|
|
96
|
+
retention?: { olderThan?: Duration; maxSize?: string }
|
|
97
|
+
/**
|
|
98
|
+
* Which layers a run reads and writes (`'local:rw,remote:r'`; a layer it
|
|
99
|
+
* leaves out stays on); `--no-cache` and `--force` still apply on top.
|
|
100
|
+
* Omitted → every layer reads and writes. The local layer is always read
|
|
101
|
+
* first, then each remote in declared order.
|
|
102
|
+
*/
|
|
103
|
+
policy?: string
|
|
104
|
+
/**
|
|
105
|
+
* Where this run's cache writes land. Omitted, a run reads and writes the
|
|
106
|
+
* default keys. A name (`'pr-123'`) reads the default key first, then the
|
|
107
|
+
* scope's, and writes only the scope's, so a PR never writes what the
|
|
108
|
+
* default branch reads. Whether a run writes at all is `policy`'s call.
|
|
109
|
+
* A client-side convention, not a security boundary: the boundary is a
|
|
110
|
+
* cache token the server limits.
|
|
111
|
+
*/
|
|
112
|
+
scope?: string
|
|
113
|
+
/** Which remote hits land on disk: `all` (the default), only the requested tasks' (`toplevel`), or `none`. */
|
|
114
|
+
download?: 'all' | 'toplevel' | 'none'
|
|
115
|
+
/** The `cache.inputStability` of every task that does not set its own. */
|
|
116
|
+
inputStability?: InputStability
|
|
117
|
+
/** The `cache.outputs.clean` of every task that does not set its own. */
|
|
118
|
+
outputs?: { clean?: boolean }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `WorkspaceConfig.lock`. */
|
|
122
|
+
export interface LockSettings {
|
|
123
|
+
/**
|
|
124
|
+
* `vx lock` writes each project's resolved config, pretty, to
|
|
125
|
+
* `vx.lock.json` beside its `vx.config`, for review; vx never reads it.
|
|
126
|
+
* On unless `false`.
|
|
127
|
+
*/
|
|
128
|
+
reviewFiles?: boolean
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** `WorkspaceConfig.watch`. */
|
|
132
|
+
export interface WatchSettings {
|
|
133
|
+
/** Poll for changes from the start instead of the OS watcher. */
|
|
134
|
+
poll?: boolean
|
|
73
135
|
}
|
|
74
136
|
|
|
75
|
-
/** `WorkspaceConfig.rules`. */
|
|
137
|
+
/** `WorkspaceConfig.run.rules`. */
|
|
76
138
|
export interface WorkspaceRules {
|
|
77
139
|
/**
|
|
78
140
|
* Refuse two tasks whose declared outputs overlap even when a `dependsOn`
|
|
@@ -87,6 +149,27 @@ export interface WorkspaceRules {
|
|
|
87
149
|
* waits for its producer (`caching.md` § Local restore tier).
|
|
88
150
|
*/
|
|
89
151
|
upfrontKeys?: boolean
|
|
152
|
+
/**
|
|
153
|
+
* Which workspace projects a project may depend on, checked by `vx lock`
|
|
154
|
+
* over each project's direct dependencies (its manifest's and its tasks'
|
|
155
|
+
* cross-project `dependsOn`). Every rule whose `from` selects a project
|
|
156
|
+
* applies to it; a project no rule selects is free.
|
|
157
|
+
*/
|
|
158
|
+
dependencies?: readonly DependencyRule[]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* One `run.rules.dependencies` entry. A selector is `tag:<tag>`, matching
|
|
163
|
+
* a project that carries the tag, or a project name; `*` matches any
|
|
164
|
+
* characters in either (`tag:scope:*`, `@acme/*`, `*`).
|
|
165
|
+
*/
|
|
166
|
+
export interface DependencyRule {
|
|
167
|
+
/** The projects this rule constrains. */
|
|
168
|
+
from: string
|
|
169
|
+
/** Given, each dependency must match one of these. */
|
|
170
|
+
allow?: readonly string[]
|
|
171
|
+
/** No dependency may match one of these; beats `allow`. */
|
|
172
|
+
deny?: readonly string[]
|
|
90
173
|
}
|
|
91
174
|
|
|
92
175
|
/**
|
|
@@ -103,7 +186,7 @@ export const PLUGIN_PACKAGE: unique symbol = Symbol.for('vx.plugin.package')
|
|
|
103
186
|
/**
|
|
104
187
|
* Structural plugin shape — any subset of the pipeline stages
|
|
105
188
|
* (`config`/`project`/`graph`/`key`/`schedule`), run-level capabilities
|
|
106
|
-
* (`cache`/`executor`/`telemetry`) and CLI `commands`, plus optional
|
|
189
|
+
* (`cache`/`executor`/`telemetry`/`output`) and CLI `commands`, plus optional
|
|
107
190
|
* `setup`/`teardown`. The fully-typed `VxPlugin` lives in
|
|
108
191
|
* `src/orchestrator/plugin.ts` and is what users import; this is a
|
|
109
192
|
* re-declaration with opaque function types so `config.ts` stays a leaf
|
|
@@ -124,6 +207,7 @@ export interface Plugin {
|
|
|
124
207
|
cache?(ctx: unknown): unknown
|
|
125
208
|
executor?(ctx: unknown): unknown
|
|
126
209
|
telemetry?(ctx: unknown): unknown
|
|
210
|
+
output?(ctx: unknown): unknown
|
|
127
211
|
setup?(ctx: unknown): void | Promise<void>
|
|
128
212
|
teardown?(): void | Promise<void>
|
|
129
213
|
readonly fingerprint?: unknown
|
|
@@ -149,6 +233,7 @@ export const PLUGIN_HOOKS = [
|
|
|
149
233
|
'executor',
|
|
150
234
|
'cache',
|
|
151
235
|
'telemetry',
|
|
236
|
+
'output',
|
|
152
237
|
'setup',
|
|
153
238
|
'commands',
|
|
154
239
|
'teardown',
|
|
@@ -168,6 +253,13 @@ const _pluginHooksMatch: [HooksNotListed, HooksNotOnPlugin] extends [never, neve
|
|
|
168
253
|
void _pluginHooksMatch
|
|
169
254
|
|
|
170
255
|
export interface ProjectConfig {
|
|
256
|
+
/**
|
|
257
|
+
* The project's name, as task specs, filters and `dependsOn` address it.
|
|
258
|
+
* Unset: the name a `discover` plugin gives (a lockfile plugin's
|
|
259
|
+
* `package.json#name`), else the directory's root-relative path (`//`
|
|
260
|
+
* for the root).
|
|
261
|
+
*/
|
|
262
|
+
name?: string
|
|
171
263
|
/**
|
|
172
264
|
* Labels for selection: `--filter tag:<name>` picks the projects that
|
|
173
265
|
* carry one. Not in any cache key: a tag changes no task's behaviour.
|
|
@@ -380,7 +472,8 @@ export interface ExecConfig {
|
|
|
380
472
|
/** Environment exposed to the child process. */
|
|
381
473
|
env?: ExecEnv
|
|
382
474
|
/**
|
|
383
|
-
* Upper bound
|
|
475
|
+
* Upper bound before vx SIGTERMs the child (`'10m'`), beating
|
|
476
|
+
* `run.timeout` and its flag and variable. A normal task that
|
|
384
477
|
* runs longer is killed and reported `failed` (timed out) — never
|
|
385
478
|
* cached. Omitted → no limit.
|
|
386
479
|
*
|
|
@@ -391,7 +484,7 @@ export interface ExecConfig {
|
|
|
391
484
|
* on spawn (no `readyWhen`) becomes ready before the timer can fire,
|
|
392
485
|
* so the timeout is a no-op for it.
|
|
393
486
|
*/
|
|
394
|
-
timeout?:
|
|
487
|
+
timeout?: Duration
|
|
395
488
|
/**
|
|
396
489
|
* Max ADDITIONAL attempts after a failed attempt. `retries: 2` means
|
|
397
490
|
* up to 3 executions total. 0 / omitted → no retries (today's
|
|
@@ -401,8 +494,8 @@ export interface ExecConfig {
|
|
|
401
494
|
* a failed attempt's partial outputs can't leak into the next. The
|
|
402
495
|
* final outcome (and the cached artifact) is the last attempt's.
|
|
403
496
|
* Not allowed with `persistent` — a persistent task has no exit to
|
|
404
|
-
* retry.
|
|
405
|
-
*
|
|
497
|
+
* retry. Beats `run.retries` and its flag and variable, including
|
|
498
|
+
* `retries: 0`.
|
|
406
499
|
*/
|
|
407
500
|
retries?: number
|
|
408
501
|
/**
|
|
@@ -491,15 +584,30 @@ export interface CacheConfig {
|
|
|
491
584
|
/** What this task produces. Captured for restore on a cache hit. */
|
|
492
585
|
outputs: CacheOutputs
|
|
493
586
|
/**
|
|
494
|
-
* What a write to an input while the command runs means.
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
587
|
+
* What a write to an input while the command runs means. Each field,
|
|
588
|
+
* highest first: its env var, this, the workspace's `inputStability`,
|
|
589
|
+
* the default (`'immutable'`, `'warn'`).
|
|
590
|
+
*/
|
|
591
|
+
inputStability?: InputStability
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export interface InputStability {
|
|
595
|
+
/**
|
|
596
|
+
* `'immutable'` (the default): any write to an input during the command
|
|
597
|
+
* withholds the entry, whatever the file holds after, since an edit
|
|
598
|
+
* undone mid-run cannot be told from no edit. `'mutable'`: a file
|
|
599
|
+
* rewritten to the bytes the key folded counts as unchanged, for a task
|
|
600
|
+
* that rewrites its own input identically (tsdown's `package.json`),
|
|
601
|
+
* unless another task that may write it ran meanwhile.
|
|
602
|
+
* `'unchecked'`: no check.
|
|
603
|
+
*/
|
|
604
|
+
strategy?: 'immutable' | 'mutable' | 'unchecked'
|
|
605
|
+
/**
|
|
606
|
+
* What a withheld entry does to the run. `'warn'` (the default): the
|
|
607
|
+
* result stands, vx says why, and the run goes on. `'error'`: the task
|
|
608
|
+
* fails, so the run fails.
|
|
501
609
|
*/
|
|
502
|
-
|
|
610
|
+
severity?: 'warn' | 'error'
|
|
503
611
|
}
|
|
504
612
|
|
|
505
613
|
export interface CacheOutputs {
|
|
@@ -522,6 +630,14 @@ export interface CacheOutputs {
|
|
|
522
630
|
* and before a restore, so the second would delete the first's).
|
|
523
631
|
*/
|
|
524
632
|
workspaceFiles?: readonly string[]
|
|
633
|
+
/**
|
|
634
|
+
* `false`: a miss leaves the declared outputs in place for the command
|
|
635
|
+
* (tsc's `.tsbuildinfo`) instead of removing them first. A run that
|
|
636
|
+
* leaves a file it found untouched saves nothing, so an entry holds only
|
|
637
|
+
* what a clean run would leave. A retry still starts clean. Omitted →
|
|
638
|
+
* the workspace's `cache.outputs.clean`, else `true`.
|
|
639
|
+
*/
|
|
640
|
+
clean?: boolean
|
|
525
641
|
}
|
|
526
642
|
|
|
527
643
|
export interface CacheInputs {
|
|
@@ -583,8 +699,8 @@ export interface CacheInputs {
|
|
|
583
699
|
* trimmed (stdout + stderr) output is folded into the cache key.
|
|
584
700
|
* Deduped per (projectDir, command) within a run. A non-zero exit
|
|
585
701
|
* fails the run. Modeled on `env`: the COMMANDS are frozen in the
|
|
586
|
-
* lock, the OUTPUT is resolved live every run
|
|
587
|
-
*
|
|
702
|
+
* lock, the OUTPUT is resolved live every run. Use for
|
|
703
|
+
* project-specific runtime probes.
|
|
588
704
|
*/
|
|
589
705
|
runtime?: readonly string[]
|
|
590
706
|
/**
|
|
@@ -677,11 +793,15 @@ export function defineProject<const T extends ProjectConfig>(
|
|
|
677
793
|
export function defineWorkspace<T extends WorkspaceConfig>(
|
|
678
794
|
config: T &
|
|
679
795
|
Known<T, WorkspaceConfig> & {
|
|
680
|
-
|
|
681
|
-
At<T, '
|
|
682
|
-
|
|
683
|
-
>
|
|
684
|
-
|
|
796
|
+
run?: Known<At<T, 'run'>, RunSettings> & {
|
|
797
|
+
rules?: Known<At<At<T, 'run'>, 'rules'>, WorkspaceRules>
|
|
798
|
+
}
|
|
799
|
+
cache?: Known<At<T, 'cache'>, CacheSettings> & {
|
|
800
|
+
retention?: Known<At<At<T, 'cache'>, 'retention'>, NonNullable<CacheSettings['retention']>>
|
|
801
|
+
inputStability?: Known<At<At<T, 'cache'>, 'inputStability'>, InputStability>
|
|
802
|
+
}
|
|
803
|
+
lock?: Known<At<T, 'lock'>, LockSettings>
|
|
804
|
+
watch?: Known<At<T, 'watch'>, WatchSettings>
|
|
685
805
|
},
|
|
686
806
|
): T {
|
|
687
807
|
return config
|