@vzn/vx 0.0.636 → 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 +39 -11
- package/docs/caching.md +213 -207
- package/docs/cli.md +699 -655
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +26 -25
- package/docs/schema.md +376 -198
- 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 +5 -3
- 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 -63
- 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 +103 -149
- 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 +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 +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +24 -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 +158 -63
- 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 +42 -29
- 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 +113 -72
- 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 +121 -95
- 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 +209 -268
- 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 +20 -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,85 +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, and
|
|
46
|
-
* `vx run --cache-scope` beats both. Never
|
|
47
|
-
* 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.
|
|
48
56
|
*/
|
|
49
|
-
|
|
57
|
+
timeout?: Duration
|
|
58
|
+
/** Re-runs of a failed task, for tasks without their own `exec.retries`. Omitted → 0. */
|
|
59
|
+
retries?: number
|
|
50
60
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* any depth, `critical-path` the head of the longest chain of tasks,
|
|
54
|
-
* `direct-dependents` the one the most tasks wait on directly (Nx's
|
|
55
|
-
* rule), `ready-order` the one that became ready first (Turbo's). A
|
|
56
|
-
* `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.
|
|
57
63
|
*/
|
|
58
|
-
|
|
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
|
|
59
71
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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.
|
|
63
76
|
*/
|
|
64
|
-
|
|
77
|
+
rules?: WorkspaceRules
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `WorkspaceConfig.cache`: where the cache lives and what a run does with it. */
|
|
81
|
+
export interface CacheSettings {
|
|
65
82
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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.
|
|
69
86
|
*/
|
|
70
|
-
|
|
87
|
+
dir?: string
|
|
71
88
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* `
|
|
77
|
-
*
|
|
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`.
|
|
78
127
|
*/
|
|
79
|
-
|
|
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
|
|
80
135
|
}
|
|
81
136
|
|
|
82
|
-
/** `WorkspaceConfig.rules`. */
|
|
137
|
+
/** `WorkspaceConfig.run.rules`. */
|
|
83
138
|
export interface WorkspaceRules {
|
|
84
139
|
/**
|
|
85
140
|
* Refuse two tasks whose declared outputs overlap even when a `dependsOn`
|
|
@@ -94,6 +149,27 @@ export interface WorkspaceRules {
|
|
|
94
149
|
* waits for its producer (`caching.md` § Local restore tier).
|
|
95
150
|
*/
|
|
96
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[]
|
|
97
173
|
}
|
|
98
174
|
|
|
99
175
|
/**
|
|
@@ -110,7 +186,7 @@ export const PLUGIN_PACKAGE: unique symbol = Symbol.for('vx.plugin.package')
|
|
|
110
186
|
/**
|
|
111
187
|
* Structural plugin shape — any subset of the pipeline stages
|
|
112
188
|
* (`config`/`project`/`graph`/`key`/`schedule`), run-level capabilities
|
|
113
|
-
* (`cache`/`executor`/`telemetry`) and CLI `commands`, plus optional
|
|
189
|
+
* (`cache`/`executor`/`telemetry`/`output`) and CLI `commands`, plus optional
|
|
114
190
|
* `setup`/`teardown`. The fully-typed `VxPlugin` lives in
|
|
115
191
|
* `src/orchestrator/plugin.ts` and is what users import; this is a
|
|
116
192
|
* re-declaration with opaque function types so `config.ts` stays a leaf
|
|
@@ -131,6 +207,7 @@ export interface Plugin {
|
|
|
131
207
|
cache?(ctx: unknown): unknown
|
|
132
208
|
executor?(ctx: unknown): unknown
|
|
133
209
|
telemetry?(ctx: unknown): unknown
|
|
210
|
+
output?(ctx: unknown): unknown
|
|
134
211
|
setup?(ctx: unknown): void | Promise<void>
|
|
135
212
|
teardown?(): void | Promise<void>
|
|
136
213
|
readonly fingerprint?: unknown
|
|
@@ -156,6 +233,7 @@ export const PLUGIN_HOOKS = [
|
|
|
156
233
|
'executor',
|
|
157
234
|
'cache',
|
|
158
235
|
'telemetry',
|
|
236
|
+
'output',
|
|
159
237
|
'setup',
|
|
160
238
|
'commands',
|
|
161
239
|
'teardown',
|
|
@@ -175,6 +253,13 @@ const _pluginHooksMatch: [HooksNotListed, HooksNotOnPlugin] extends [never, neve
|
|
|
175
253
|
void _pluginHooksMatch
|
|
176
254
|
|
|
177
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
|
|
178
263
|
/**
|
|
179
264
|
* Labels for selection: `--filter tag:<name>` picks the projects that
|
|
180
265
|
* carry one. Not in any cache key: a tag changes no task's behaviour.
|
|
@@ -387,7 +472,8 @@ export interface ExecConfig {
|
|
|
387
472
|
/** Environment exposed to the child process. */
|
|
388
473
|
env?: ExecEnv
|
|
389
474
|
/**
|
|
390
|
-
* Upper bound
|
|
475
|
+
* Upper bound before vx SIGTERMs the child (`'10m'`), beating
|
|
476
|
+
* `run.timeout` and its flag and variable. A normal task that
|
|
391
477
|
* runs longer is killed and reported `failed` (timed out) — never
|
|
392
478
|
* cached. Omitted → no limit.
|
|
393
479
|
*
|
|
@@ -398,7 +484,7 @@ export interface ExecConfig {
|
|
|
398
484
|
* on spawn (no `readyWhen`) becomes ready before the timer can fire,
|
|
399
485
|
* so the timeout is a no-op for it.
|
|
400
486
|
*/
|
|
401
|
-
timeout?:
|
|
487
|
+
timeout?: Duration
|
|
402
488
|
/**
|
|
403
489
|
* Max ADDITIONAL attempts after a failed attempt. `retries: 2` means
|
|
404
490
|
* up to 3 executions total. 0 / omitted → no retries (today's
|
|
@@ -408,8 +494,8 @@ export interface ExecConfig {
|
|
|
408
494
|
* a failed attempt's partial outputs can't leak into the next. The
|
|
409
495
|
* final outcome (and the cached artifact) is the last attempt's.
|
|
410
496
|
* Not allowed with `persistent` — a persistent task has no exit to
|
|
411
|
-
* retry.
|
|
412
|
-
*
|
|
497
|
+
* retry. Beats `run.retries` and its flag and variable, including
|
|
498
|
+
* `retries: 0`.
|
|
413
499
|
*/
|
|
414
500
|
retries?: number
|
|
415
501
|
/**
|
|
@@ -511,7 +597,8 @@ export interface InputStability {
|
|
|
511
597
|
* withholds the entry, whatever the file holds after, since an edit
|
|
512
598
|
* undone mid-run cannot be told from no edit. `'mutable'`: a file
|
|
513
599
|
* rewritten to the bytes the key folded counts as unchanged, for a task
|
|
514
|
-
* that rewrites its own input identically (tsdown's `package.json`)
|
|
600
|
+
* that rewrites its own input identically (tsdown's `package.json`),
|
|
601
|
+
* unless another task that may write it ran meanwhile.
|
|
515
602
|
* `'unchecked'`: no check.
|
|
516
603
|
*/
|
|
517
604
|
strategy?: 'immutable' | 'mutable' | 'unchecked'
|
|
@@ -543,6 +630,14 @@ export interface CacheOutputs {
|
|
|
543
630
|
* and before a restore, so the second would delete the first's).
|
|
544
631
|
*/
|
|
545
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
|
|
546
641
|
}
|
|
547
642
|
|
|
548
643
|
export interface CacheInputs {
|
|
@@ -604,8 +699,8 @@ export interface CacheInputs {
|
|
|
604
699
|
* trimmed (stdout + stderr) output is folded into the cache key.
|
|
605
700
|
* Deduped per (projectDir, command) within a run. A non-zero exit
|
|
606
701
|
* fails the run. Modeled on `env`: the COMMANDS are frozen in the
|
|
607
|
-
* lock, the OUTPUT is resolved live every run
|
|
608
|
-
*
|
|
702
|
+
* lock, the OUTPUT is resolved live every run. Use for
|
|
703
|
+
* project-specific runtime probes.
|
|
609
704
|
*/
|
|
610
705
|
runtime?: readonly string[]
|
|
611
706
|
/**
|
|
@@ -698,11 +793,15 @@ export function defineProject<const T extends ProjectConfig>(
|
|
|
698
793
|
export function defineWorkspace<T extends WorkspaceConfig>(
|
|
699
794
|
config: T &
|
|
700
795
|
Known<T, WorkspaceConfig> & {
|
|
701
|
-
|
|
702
|
-
At<T, '
|
|
703
|
-
|
|
704
|
-
>
|
|
705
|
-
|
|
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>
|
|
706
805
|
},
|
|
707
806
|
): T {
|
|
708
807
|
return config
|
package/src/exec/env.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { ESSENTIAL_ENV } from '../util/index.js'
|
|
3
4
|
|
|
4
5
|
// Build the environment exposed to a task.
|
|
5
6
|
//
|
|
@@ -9,37 +10,6 @@ import path from 'node:path'
|
|
|
9
10
|
// 3. define: explicit name=value pairs from the task config.
|
|
10
11
|
// 4. binPaths prepended to PATH (after all the above resolve PATH).
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
* Exported so the schema doc's enumeration of it can be PINNED against the
|
|
14
|
-
* real list: a hand-copied allowlist is how the two drift, and a reader
|
|
15
|
-
* asking "what does my build script actually see?" is asking a
|
|
16
|
-
* security-shaped question that a stale list answers wrongly.
|
|
17
|
-
*/
|
|
18
|
-
export const ESSENTIAL_ENV: readonly string[] = [
|
|
19
|
-
'PATH',
|
|
20
|
-
'HOME',
|
|
21
|
-
'SHELL',
|
|
22
|
-
'USER',
|
|
23
|
-
'LOGNAME',
|
|
24
|
-
'TMPDIR',
|
|
25
|
-
'TEMP',
|
|
26
|
-
'TMP',
|
|
27
|
-
'LANG',
|
|
28
|
-
'LC_ALL',
|
|
29
|
-
'LC_CTYPE',
|
|
30
|
-
'TERM',
|
|
31
|
-
'COLORTERM',
|
|
32
|
-
'FORCE_COLOR',
|
|
33
|
-
'NO_COLOR',
|
|
34
|
-
'CI',
|
|
35
|
-
'NODE_OPTIONS',
|
|
36
|
-
// Where corepack and pnpm keep their own binaries, as HOME is: a machine
|
|
37
|
-
// path, never task output. Without them a corepack shim re-downloads its
|
|
38
|
-
// manager (offline or sandboxed: fails) and pnpm loses its global dir.
|
|
39
|
-
'COREPACK_HOME',
|
|
40
|
-
'PNPM_HOME',
|
|
41
|
-
]
|
|
42
|
-
|
|
43
13
|
/**
|
|
44
14
|
* The two variables vx sets on every task it spawns, over the layers
|
|
45
15
|
* above: the workspace root the run is in and the `project#task` running.
|