@vzn/vx 0.0.18 → 0.0.22
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/LICENSE +1 -1
- package/README.md +107 -70
- package/package.json +10 -20
- package/src/bin.ts +64 -5
- package/src/cache/archive.ts +279 -28
- package/src/cache/cache.ts +952 -1584
- package/src/cache/chained-cache.ts +14 -10
- package/src/cache/config-evals.ts +112 -0
- package/src/cache/file-hashes.ts +290 -0
- package/src/cache/git-inputs.ts +966 -0
- package/src/cache/index.ts +23 -7
- package/src/cache/inputs.ts +553 -739
- package/src/cache/key-fold.ts +232 -0
- package/src/cache/layer.ts +674 -0
- package/src/cache/layered-cache.ts +211 -49
- package/src/cache/output-index.ts +323 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +152 -0
- package/src/cache/zstd.ts +183 -0
- package/src/cli/cache.ts +88 -30
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/format.ts +4 -15
- package/src/cli/help.ts +76 -38
- package/src/cli/index.ts +82 -17
- package/src/cli/info.ts +162 -91
- package/src/cli/init.ts +87 -0
- package/src/cli/last.ts +130 -23
- package/src/cli/lock.ts +36 -18
- package/src/cli/plan-format.ts +18 -7
- package/src/cli/plugin-commands.ts +34 -12
- package/src/cli/run.ts +138 -313
- package/src/cli/select.ts +342 -0
- package/src/cli/show.ts +151 -71
- package/src/cli/upgrade.ts +136 -15
- package/src/cli/watch.ts +1175 -116
- package/src/cli/why.ts +51 -29
- package/src/cli/workspace-config.ts +138 -0
- package/src/config.ts +183 -126
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +62 -31
- package/src/exec/index.ts +16 -2
- package/src/exec/kill-tree.ts +281 -0
- package/src/exec/local-executor.ts +41 -0
- package/src/exec/runner.ts +361 -77
- package/src/exec/sandbox-binds.ts +211 -0
- package/src/exec/sandbox-paths.ts +70 -0
- package/src/exec/sandbox-runtime.ts +1152 -415
- package/src/exec/sandbox-violations.ts +293 -0
- package/src/graph/dependency-spec.ts +14 -14
- package/src/graph/index.ts +6 -4
- package/src/graph/priorities.ts +130 -0
- package/src/graph/scheduler.ts +332 -329
- package/src/graph/task-graph.ts +552 -188
- package/src/index.ts +111 -102
- package/src/orchestrator/admission.ts +161 -0
- package/src/orchestrator/colors.ts +8 -3
- package/src/orchestrator/deferred-outputs.ts +25 -7
- package/src/orchestrator/doctor.ts +363 -0
- package/src/orchestrator/events.ts +111 -8
- package/src/orchestrator/excluded-keys.ts +155 -0
- package/src/orchestrator/execute-task.ts +450 -565
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/failure-recap.ts +121 -0
- package/src/orchestrator/fingerprint-watch.ts +89 -0
- package/src/orchestrator/framed-output.ts +121 -49
- package/src/orchestrator/history.ts +123 -104
- package/src/orchestrator/hit-restore.ts +285 -0
- package/src/orchestrator/index.ts +42 -5
- package/src/orchestrator/keyed-projects.ts +89 -0
- package/src/orchestrator/local-shortcircuit.ts +83 -23
- package/src/orchestrator/lockfile-claim.ts +401 -0
- package/src/orchestrator/logger.ts +128 -41
- package/src/orchestrator/metrics.ts +201 -72
- package/src/orchestrator/miss-save.ts +232 -0
- package/src/orchestrator/options.ts +63 -34
- package/src/orchestrator/persistent.ts +99 -0
- package/src/orchestrator/placement.ts +212 -0
- package/src/orchestrator/plan.ts +24 -11
- package/src/orchestrator/plugin-host.ts +200 -49
- package/src/orchestrator/plugin.ts +190 -14
- package/src/orchestrator/prepare.ts +168 -137
- package/src/orchestrator/projects.ts +270 -0
- package/src/orchestrator/remote-prefetch.ts +22 -2
- package/src/orchestrator/run-artifacts.ts +34 -3
- package/src/orchestrator/run-context.ts +20 -5
- package/src/orchestrator/run-lock.ts +349 -0
- package/src/orchestrator/run-records.ts +132 -0
- package/src/orchestrator/run-report.ts +6 -4
- package/src/orchestrator/run.ts +467 -611
- package/src/orchestrator/sandbox-request.ts +569 -0
- package/src/orchestrator/save-lane.ts +44 -0
- package/src/orchestrator/shell-verdict.ts +117 -0
- package/src/orchestrator/signals.ts +167 -0
- package/src/orchestrator/stable-keys.ts +259 -10
- package/src/orchestrator/status-line.ts +35 -2
- package/src/orchestrator/summary.ts +115 -25
- package/src/orchestrator/task-hash.ts +103 -21
- package/src/orchestrator/task-log-buffer.ts +15 -18
- package/src/orchestrator/telemetry-host.ts +27 -8
- package/src/orchestrator/telemetry.ts +71 -44
- package/src/orchestrator/upstream.ts +91 -45
- package/src/util/bun-version.ts +43 -0
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/errors.ts +86 -0
- package/src/util/hash.ts +10 -1
- package/src/util/index.ts +42 -5
- package/src/util/paths.ts +168 -5
- package/src/util/procfs.ts +20 -0
- package/src/util/settle.ts +23 -2
- package/src/util/size.ts +33 -0
- package/src/util/task-id.ts +20 -0
- package/src/util/timing.ts +11 -1
- package/src/util/verbs.ts +39 -0
- package/src/util/which.ts +48 -0
- package/src/workspace/affected.ts +368 -60
- package/src/workspace/config-cache.ts +271 -32
- package/src/workspace/config-eval.ts +60 -16
- package/src/workspace/config-imports.ts +131 -9
- package/src/workspace/config-schema.ts +1033 -0
- package/src/workspace/filter.ts +69 -17
- package/src/workspace/fingerprint.ts +59 -7
- package/src/workspace/index.ts +37 -3
- package/src/workspace/json-data.ts +90 -0
- package/src/workspace/load-reads.ts +32 -0
- package/src/workspace/lockfile.ts +41 -14
- package/src/workspace/migrate-scripts.ts +187 -0
- package/src/workspace/migration.ts +353 -0
- package/src/workspace/package-graph.ts +239 -31
- package/src/workspace/project-loader.ts +295 -815
- package/src/workspace/workspace.ts +266 -31
- package/plugins/local-cache/index.ts +0 -2
- package/plugins/local-executor/index.ts +0 -2
- package/plugins/schedule-history/index.ts +0 -2
- package/src/cache/cas-backend.ts +0 -115
- package/src/cache/digest.ts +0 -32
- package/src/cli/migrate-emit.ts +0 -21
- package/src/cli/migrate-nx.ts +0 -473
- package/src/cli/migrate-scripts.ts +0 -127
- package/src/cli/migrate-turbo.ts +0 -401
- package/src/cli/migrate.ts +0 -305
- package/src/cli/prune.ts +0 -291
- package/src/orchestrator/missing-plugin.ts +0 -9
- package/src/orchestrator/resources.ts +0 -45
- package/src/orchestrator/verify.ts +0 -174
- package/src/plugins/local-cache/index.ts +0 -11
- package/src/plugins/local-executor/index.ts +0 -45
- package/src/plugins/schedule-history/index.ts +0 -109
package/src/cli/run.ts
CHANGED
|
@@ -1,34 +1,27 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { appendFile } from 'node:fs/promises'
|
|
1
|
+
import { appendFile, mkdir } from 'node:fs/promises'
|
|
3
2
|
import path from 'node:path'
|
|
4
|
-
import { documentedFlags } from './help.js'
|
|
5
|
-
import {
|
|
6
|
-
affectedProjects,
|
|
7
|
-
applyFilters,
|
|
8
|
-
buildPackageGraph,
|
|
9
|
-
defaultAffectedBase,
|
|
10
|
-
findWorkspaceRoot,
|
|
11
|
-
listProjects,
|
|
12
|
-
loadProjectConfig,
|
|
13
|
-
loadWorkspace,
|
|
14
|
-
parseFilter,
|
|
15
|
-
readLockfile,
|
|
16
|
-
workspaceGlobsMatch,
|
|
17
|
-
type ProjectMeta,
|
|
18
|
-
} from '../workspace/index.js'
|
|
3
|
+
import { documentedFlags, seeHelp } from './help.js'
|
|
4
|
+
import { defaultAffectedBase, findWorkspaceRoot } from '../workspace/index.js'
|
|
19
5
|
import {
|
|
20
6
|
planRun,
|
|
21
7
|
formatRunReportMarkdown,
|
|
8
|
+
outcomeLabel,
|
|
22
9
|
projectOutcome,
|
|
23
10
|
run as runOrchestrator,
|
|
24
11
|
type OutcomeView,
|
|
25
12
|
type RunOptions,
|
|
26
13
|
type RunResult,
|
|
27
14
|
} from '../orchestrator/index.js'
|
|
28
|
-
import type { ProjectConfig } from '../config.js'
|
|
29
15
|
import type { ContinueMode } from '../graph/index.js'
|
|
30
16
|
import { type CachePolicy, FULL_CACHE_POLICY, parseCachePolicy } from '../cache/index.js'
|
|
31
|
-
import {
|
|
17
|
+
import { findCwdProject, pickTask, resolveFilters } from './select.js'
|
|
18
|
+
import {
|
|
19
|
+
MAX_TIMEOUT_MS,
|
|
20
|
+
isUserError,
|
|
21
|
+
parseDecimalInt,
|
|
22
|
+
nearest,
|
|
23
|
+
machineParallelism,
|
|
24
|
+
} from '../util/index.js'
|
|
32
25
|
import { formatGraphDot, formatPlanJson, formatPlanText } from './plan-format.js'
|
|
33
26
|
|
|
34
27
|
export interface RunArgs {
|
|
@@ -55,6 +48,8 @@ export interface RunArgs {
|
|
|
55
48
|
* / `--force` in precedence order. Defaults to all-on.
|
|
56
49
|
*/
|
|
57
50
|
cache: CachePolicy
|
|
51
|
+
/** `--cache` named a remote axis (`remote:r`, `:w`, `:rw`): the run says so when no plugin serves one. */
|
|
52
|
+
remoteRequested?: boolean
|
|
58
53
|
/** `--cache-dir <path>`: override the cache directory (cwd-relative). */
|
|
59
54
|
cacheDir: string | undefined
|
|
60
55
|
frozen: boolean
|
|
@@ -69,23 +64,6 @@ export interface RunArgs {
|
|
|
69
64
|
* env and workspace `timeout` defaults. Undefined when not passed.
|
|
70
65
|
*/
|
|
71
66
|
timeout: number | undefined
|
|
72
|
-
/**
|
|
73
|
-
* `--memory <size>` / `--memory=<size>`: memory budget (resolved to
|
|
74
|
-
* bytes) for `exec.resources.memory` reservations. Defaults to
|
|
75
|
-
* os.totalmem() when not passed — pass it in cgroup-limited containers.
|
|
76
|
-
*/
|
|
77
|
-
memory: number | undefined
|
|
78
|
-
/**
|
|
79
|
-
* `--verify[=determinism|inputs|fingerprint|all]`: cache-correctness
|
|
80
|
-
* verification. Undefined when not passed. `determinism` re-runs and
|
|
81
|
-
* content-compares outputs; `inputs` sandboxes with the declared-input
|
|
82
|
-
* baseline and flags undeclared reads; `fingerprint` ships output-tree
|
|
83
|
-
* fingerprints for the cross-machine diff (no re-run; the determinism
|
|
84
|
-
* modes set it too, for free). `allow` (from `--verify-allow=<pkg#task>,…`)
|
|
85
|
-
* exempts tasks from failing the run on a divergence.
|
|
86
|
-
*/
|
|
87
|
-
verify: { determinism: boolean; inputs: boolean; fingerprint: boolean } | undefined
|
|
88
|
-
verifyAllow: string[]
|
|
89
67
|
outputLogs?: 'full' | 'errors-only' | 'none' | 'hash-only'
|
|
90
68
|
download?: 'all' | 'toplevel' | 'none'
|
|
91
69
|
forwardArgs: string[]
|
|
@@ -113,6 +91,30 @@ export interface RunArgs {
|
|
|
113
91
|
error?: string
|
|
114
92
|
}
|
|
115
93
|
|
|
94
|
+
// The camelCase spelling the flag shipped with (retired 2026-09-10): two
|
|
95
|
+
// edits past the suggester's reach, so it is named outright. A regex, not a
|
|
96
|
+
// quoted literal, so the doc-drift test does not count it as an accepted flag.
|
|
97
|
+
const RETIRED_EXCLUDE_DEPENDENCIES = /^--excludeDependencies(=|$)/
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* `--concurrency <n>` is a positive decimal integer, or `<n>%` of the
|
|
101
|
+
* cores this process may use (`50%` on eight cores is 4, never below 1;
|
|
102
|
+
* over 100% is allowed for I/O-bound work; a container's CPU quota caps
|
|
103
|
+
* the count the way it caps the default). The percent form resolves
|
|
104
|
+
* here, at parse time, so the run sees one number and the summary says
|
|
105
|
+
* what it ran with.
|
|
106
|
+
*/
|
|
107
|
+
export function parseConcurrency(v: string, cpus = machineParallelism()): number | null {
|
|
108
|
+
const pct = /^(\d+)%$/.exec(v)
|
|
109
|
+
if (pct) {
|
|
110
|
+
const n = parseDecimalInt(pct[1]!)
|
|
111
|
+
if (n === null || n < 1) return null
|
|
112
|
+
return Math.max(1, Math.round((Math.max(1, cpus) * n) / 100))
|
|
113
|
+
}
|
|
114
|
+
const n = parseDecimalInt(v)
|
|
115
|
+
return n === null || n < 1 ? null : n
|
|
116
|
+
}
|
|
117
|
+
|
|
116
118
|
export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
117
119
|
const out: RunArgs = {
|
|
118
120
|
tasks: [],
|
|
@@ -125,9 +127,6 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
125
127
|
frozen: false,
|
|
126
128
|
retries: undefined,
|
|
127
129
|
timeout: undefined,
|
|
128
|
-
memory: undefined,
|
|
129
|
-
verify: undefined,
|
|
130
|
-
verifyAllow: [],
|
|
131
130
|
forwardArgs: [],
|
|
132
131
|
verbosity: 0,
|
|
133
132
|
dry: undefined,
|
|
@@ -162,25 +161,30 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
162
161
|
} else if (a === '--concurrency' || a?.startsWith('--concurrency=')) {
|
|
163
162
|
const v = a === '--concurrency' ? before[++i] : a.slice('--concurrency='.length)
|
|
164
163
|
if (v === undefined) return { ...out, error: `--concurrency requires a value` }
|
|
165
|
-
const n =
|
|
166
|
-
if (n === null
|
|
164
|
+
const n = parseConcurrency(v)
|
|
165
|
+
if (n === null) return { ...out, error: `invalid concurrency: ${v}` }
|
|
167
166
|
out.concurrency = n
|
|
168
167
|
} else if (a === '--all') {
|
|
169
168
|
out.all = true
|
|
170
|
-
} else if (a
|
|
169
|
+
} else if (RETIRED_EXCLUDE_DEPENDENCIES.test(a ?? '')) {
|
|
170
|
+
return {
|
|
171
|
+
...out,
|
|
172
|
+
error: `unknown flag: ${a} (the flag is --exclude-dependencies)${seeHelp('run')}`,
|
|
173
|
+
}
|
|
174
|
+
} else if (a === '--exclude-dependencies') {
|
|
171
175
|
out.excludeDependencies = 'all'
|
|
172
|
-
} else if (a?.startsWith('--
|
|
173
|
-
const raw = a.slice('--
|
|
176
|
+
} else if (a?.startsWith('--exclude-dependencies=')) {
|
|
177
|
+
const raw = a.slice('--exclude-dependencies='.length)
|
|
174
178
|
// An empty value is genuinely ambiguous — "drop every edge" (the
|
|
175
179
|
// bare flag) and "drop none" (an empty list) are equally defensible
|
|
176
180
|
// readings, and silently picking either does the opposite of what
|
|
177
181
|
// half the callers mean. Reject and name both explicit forms; the
|
|
178
|
-
// sibling value flags (--retry=, --timeout=, --
|
|
182
|
+
// sibling value flags (--retry=, --timeout=, --cache-dir=)
|
|
179
183
|
// reject an empty `=` value the same way.
|
|
180
184
|
if (raw === '') {
|
|
181
185
|
return {
|
|
182
186
|
...out,
|
|
183
|
-
error: `--
|
|
187
|
+
error: `--exclude-dependencies= needs a value — pass bare --exclude-dependencies to drop every dependsOn edge, or omit the flag to keep them`,
|
|
184
188
|
}
|
|
185
189
|
}
|
|
186
190
|
out.excludeDependencies = raw
|
|
@@ -215,42 +219,6 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
215
219
|
return { ...out, error: `--timeout must be a positive integer (ms), got: ${v}` }
|
|
216
220
|
}
|
|
217
221
|
out.timeout = n
|
|
218
|
-
} else if (a === '--memory' || a?.startsWith('--memory=')) {
|
|
219
|
-
const v = a === '--memory' ? before[++i] : a.slice('--memory='.length)
|
|
220
|
-
if (v === undefined || v === '') return { ...out, error: `--memory requires a value` }
|
|
221
|
-
const bytes = parseSize(v)
|
|
222
|
-
if (bytes === null || bytes <= 0) {
|
|
223
|
-
return { ...out, error: `--memory must be a size like 8GB or 512MB, got: ${v}` }
|
|
224
|
-
}
|
|
225
|
-
out.memory = bytes
|
|
226
|
-
} else if (a === '--verify' || a?.startsWith('--verify=')) {
|
|
227
|
-
const what = a === '--verify' ? 'determinism' : a.slice('--verify='.length)
|
|
228
|
-
if (what === 'determinism') {
|
|
229
|
-
out.verify = { determinism: true, inputs: false, fingerprint: true }
|
|
230
|
-
} else if (what === 'inputs') {
|
|
231
|
-
out.verify = { determinism: false, inputs: true, fingerprint: false }
|
|
232
|
-
} else if (what === 'fingerprint') {
|
|
233
|
-
out.verify = { determinism: false, inputs: false, fingerprint: true }
|
|
234
|
-
} else if (what === 'all') {
|
|
235
|
-
out.verify = { determinism: true, inputs: true, fingerprint: true }
|
|
236
|
-
} else {
|
|
237
|
-
return {
|
|
238
|
-
...out,
|
|
239
|
-
error: `--verify must be determinism | inputs | fingerprint | all (or bare --verify), got: ${what}`,
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
} else if (a === '--verify-allow' || a?.startsWith('--verify-allow=')) {
|
|
243
|
-
const v = a === '--verify-allow' ? before[++i] : a.slice('--verify-allow='.length)
|
|
244
|
-
if (v === undefined) return { ...out, error: `--verify-allow requires a value` }
|
|
245
|
-
// A flag-shaped space-form value is always a lost flag (task ids
|
|
246
|
-
// never start with `-`), never a value the user meant.
|
|
247
|
-
if (a === '--verify-allow' && v.startsWith('-')) {
|
|
248
|
-
return { ...out, error: `--verify-allow requires a value, got flag: ${v}` }
|
|
249
|
-
}
|
|
250
|
-
out.verifyAllow = v
|
|
251
|
-
.split(',')
|
|
252
|
-
.map((s) => s.trim())
|
|
253
|
-
.filter((s) => s.length > 0)
|
|
254
222
|
} else if (a === '--output-logs' || a?.startsWith('--output-logs=')) {
|
|
255
223
|
const v = a === '--output-logs' ? before[++i] : a.slice('--output-logs='.length)
|
|
256
224
|
if (v !== 'full' && v !== 'errors-only' && v !== 'none' && v !== 'hash-only') {
|
|
@@ -291,6 +259,7 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
291
259
|
}
|
|
292
260
|
try {
|
|
293
261
|
cachePolicy = parseCachePolicy(v, cachePolicy)
|
|
262
|
+
if (/remote:[rw]/.test(v)) out.remoteRequested = true
|
|
294
263
|
} catch (err) {
|
|
295
264
|
cacheSpecError = err instanceof Error ? err.message : String(err)
|
|
296
265
|
}
|
|
@@ -299,7 +268,14 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
299
268
|
} else if (a === '--force') {
|
|
300
269
|
forceFlag = true
|
|
301
270
|
} else if (a === '--continue') {
|
|
302
|
-
// Bare --continue = 'always' (the Turbo convention).
|
|
271
|
+
// Bare --continue = 'always' (the Turbo convention). Its mode comes
|
|
272
|
+
// with '=' only: `--continue never` would read `never` as a TASK, and
|
|
273
|
+
// the run would then say "No projects declare task(s): never" — true,
|
|
274
|
+
// and no help at all.
|
|
275
|
+
const next = before[i + 1]
|
|
276
|
+
if (next === 'never' || next === 'deps-ok' || next === 'always') {
|
|
277
|
+
return { ...out, error: `--continue takes its mode with '=': --continue=${next}` }
|
|
278
|
+
}
|
|
303
279
|
out.continueMode = 'always'
|
|
304
280
|
} else if (a?.startsWith('--continue=')) {
|
|
305
281
|
const v = a.slice('--continue='.length)
|
|
@@ -367,7 +343,7 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
367
343
|
}
|
|
368
344
|
out.report = fmt
|
|
369
345
|
} else if (a !== undefined && a.startsWith('-')) {
|
|
370
|
-
return { ...out, error: `unknown flag: ${a}${didYouMeanFlag(a)}` }
|
|
346
|
+
return { ...out, error: `unknown flag: ${a}${didYouMeanFlag(a)}${seeHelp('run')}` }
|
|
371
347
|
} else if (a !== undefined) {
|
|
372
348
|
out.tasks.push(a)
|
|
373
349
|
}
|
|
@@ -389,11 +365,21 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
389
365
|
if (out.dry !== undefined && out.graph !== undefined) {
|
|
390
366
|
return { ...out, error: '--dry and --graph are mutually exclusive' }
|
|
391
367
|
}
|
|
392
|
-
|
|
393
|
-
|
|
368
|
+
// Every flag that promises a file or a section about the run: the
|
|
369
|
+
// planning branch returns before any of them is written, and
|
|
370
|
+
// `--report-file="$GITHUB_STEP_SUMMARY" --dry` exited 0 having written
|
|
371
|
+
// nothing (item 992).
|
|
372
|
+
const needsRun =
|
|
373
|
+
out.summarize !== undefined ||
|
|
374
|
+
out.profile !== undefined ||
|
|
375
|
+
out.report !== undefined ||
|
|
376
|
+
out.reportFile !== undefined
|
|
377
|
+
const runOnly = '--summarize / --profile / --report / --report-file need a real run'
|
|
378
|
+
if (out.dry !== undefined && needsRun) {
|
|
379
|
+
return { ...out, error: `--dry skips execution; ${runOnly}` }
|
|
394
380
|
}
|
|
395
|
-
if (out.graph !== undefined &&
|
|
396
|
-
return { ...out, error:
|
|
381
|
+
if (out.graph !== undefined && needsRun) {
|
|
382
|
+
return { ...out, error: `--graph skips execution; ${runOnly}` }
|
|
397
383
|
}
|
|
398
384
|
return out
|
|
399
385
|
}
|
|
@@ -445,14 +431,28 @@ export async function resolveRunOptions(
|
|
|
445
431
|
}
|
|
446
432
|
}
|
|
447
433
|
|
|
448
|
-
// `--affected[=<base>]` is sugar for `--filter '[<base>]'
|
|
449
|
-
//
|
|
450
|
-
//
|
|
434
|
+
// `--affected[=<base>]` is sugar for `--filter '...[<base>]'`: the
|
|
435
|
+
// changed projects AND their dependents — what a CI gate must run, what
|
|
436
|
+
// the flag's name says, what the guides promised while the sugar was the
|
|
437
|
+
// changed-only `[<base>]` (item 287). `--filter '[<base>]'` stays the
|
|
438
|
+
// "only what I touched" form. Merging it into the filter list means the
|
|
439
|
+
// same code path handles plain filter use, --affected alone, and the combo.
|
|
440
|
+
// It goes FIRST: filters apply in argv order, and appended last it came
|
|
441
|
+
// after every `!` exclude, so `--affected --filter '!app'` still ran app
|
|
442
|
+
// (item 955). An include's place does not change a union.
|
|
451
443
|
const filterStrings = [...parsed.filters]
|
|
452
444
|
if (parsed.affected !== undefined) {
|
|
453
445
|
const root = await findWorkspaceRoot(cwd)
|
|
454
|
-
|
|
455
|
-
|
|
446
|
+
let base = parsed.affected
|
|
447
|
+
if (base === '') {
|
|
448
|
+
try {
|
|
449
|
+
base = await defaultAffectedBase(root)
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (!isUserError(err)) throw err
|
|
452
|
+
return { error: err.message }
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
filterStrings.unshift(`...[${base}]`)
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
// Project scope applies to bare task names only. Anchored entries
|
|
@@ -460,10 +460,14 @@ export async function resolveRunOptions(
|
|
|
460
460
|
const bareTasks = tasks.filter((t) => !t.includes('#'))
|
|
461
461
|
const anchoredTasks = tasks.filter((t) => t.includes('#'))
|
|
462
462
|
let projects: string[] | undefined
|
|
463
|
+
let staged: RunOptions['staged']
|
|
463
464
|
if (bareTasks.length === 0) {
|
|
464
465
|
projects = undefined
|
|
465
466
|
} else if (filterStrings.length > 0) {
|
|
466
|
-
const resolved = await resolveFilters(cwd, filterStrings
|
|
467
|
+
const resolved = await resolveFilters(cwd, filterStrings, {
|
|
468
|
+
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
469
|
+
...(parsed.frozen ? { frozen: true } : {}),
|
|
470
|
+
})
|
|
467
471
|
if ('error' in resolved) return { error: resolved.error }
|
|
468
472
|
if ('empty' in resolved) {
|
|
469
473
|
// "Nothing changed" is a clean exit for the BARE tasks the filter
|
|
@@ -476,6 +480,9 @@ export async function resolveRunOptions(
|
|
|
476
480
|
projects = []
|
|
477
481
|
} else {
|
|
478
482
|
projects = resolved.names
|
|
483
|
+
// The graph walk staged every config; the run reuses those entries
|
|
484
|
+
// instead of evaluating and staging them a second time.
|
|
485
|
+
staged = resolved.staged
|
|
479
486
|
}
|
|
480
487
|
} else if (parsed.all) {
|
|
481
488
|
projects = undefined
|
|
@@ -494,6 +501,7 @@ export async function resolveRunOptions(
|
|
|
494
501
|
cwd,
|
|
495
502
|
tasks: [...tasks],
|
|
496
503
|
cache: parsed.cache,
|
|
504
|
+
...(parsed.remoteRequested ? { remoteRequested: true } : {}),
|
|
497
505
|
flow: detectFlow(parsed),
|
|
498
506
|
...(parsed.frozen ? { frozen: true } : {}),
|
|
499
507
|
...(parsed.outputLogs !== undefined ? { outputLogs: parsed.outputLogs } : {}),
|
|
@@ -507,18 +515,10 @@ export async function resolveRunOptions(
|
|
|
507
515
|
opts.excludeDependencies = parsed.excludeDependencies
|
|
508
516
|
}
|
|
509
517
|
if (projects !== undefined) opts.projects = projects
|
|
518
|
+
if (staged !== undefined) opts.staged = staged
|
|
510
519
|
if (parsed.retries !== undefined) opts.retries = parsed.retries
|
|
511
520
|
if (parsed.timeout !== undefined) opts.timeout = parsed.timeout
|
|
512
|
-
if (parsed.memory !== undefined) opts.memory = parsed.memory
|
|
513
521
|
if (parsed.cacheDir !== undefined) opts.cacheDir = parsed.cacheDir
|
|
514
|
-
if (parsed.verify !== undefined) {
|
|
515
|
-
opts.verify = {
|
|
516
|
-
determinism: parsed.verify.determinism,
|
|
517
|
-
inputs: parsed.verify.inputs,
|
|
518
|
-
fingerprint: parsed.verify.fingerprint,
|
|
519
|
-
allow: new Set(parsed.verifyAllow),
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
522
|
if (parsed.concurrency !== undefined) opts.concurrency = parsed.concurrency
|
|
523
523
|
if (parsed.summarize !== undefined) opts.summarize = parsed.summarize
|
|
524
524
|
if (parsed.profile !== undefined) opts.profile = parsed.profile
|
|
@@ -545,7 +545,14 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
545
545
|
process.stderr.write(`vx run: missing task name (stdin is not a TTY)\n`)
|
|
546
546
|
return 1
|
|
547
547
|
}
|
|
548
|
-
const picked = await pickTask(
|
|
548
|
+
const picked = await pickTask(
|
|
549
|
+
cwd,
|
|
550
|
+
{},
|
|
551
|
+
{
|
|
552
|
+
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
553
|
+
...(parsed.frozen ? { frozen: true } : {}),
|
|
554
|
+
},
|
|
555
|
+
)
|
|
549
556
|
if (!picked) return 1
|
|
550
557
|
tasks = [`${picked.project}#${picked.task}`]
|
|
551
558
|
}
|
|
@@ -583,7 +590,17 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
583
590
|
if (parsed.graph === '') {
|
|
584
591
|
process.stdout.write(out)
|
|
585
592
|
} else {
|
|
586
|
-
|
|
593
|
+
// The graph is the command's one product, so a path it cannot write
|
|
594
|
+
// is exit 1, said in one line as the other output paths say theirs:
|
|
595
|
+
// a directory or an unreachable path printed a raw stack (item 993).
|
|
596
|
+
const target = path.resolve(cwd, parsed.graph)
|
|
597
|
+
try {
|
|
598
|
+
await Bun.write(target, out)
|
|
599
|
+
} catch (err) {
|
|
600
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
601
|
+
process.stderr.write(`vx run: failed to write graph to ${target}: ${message}\n`)
|
|
602
|
+
return 1
|
|
603
|
+
}
|
|
587
604
|
}
|
|
588
605
|
} else if (parsed.dry === 'json') {
|
|
589
606
|
process.stdout.write(formatPlanJson(plan))
|
|
@@ -618,6 +635,9 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
618
635
|
// same job also write to. Truncating it would silently destroy their
|
|
619
636
|
// content, and would make replacing the documented `>>` recipe a
|
|
620
637
|
// behaviour change rather than a drop-in.
|
|
638
|
+
// Its directory made first, as `--summarize`, `--profile` and
|
|
639
|
+
// `--graph` make theirs: `nr/r.md` failed alone of the four (item 993).
|
|
640
|
+
await mkdir(path.dirname(target), { recursive: true })
|
|
621
641
|
await appendFile(target, md)
|
|
622
642
|
} catch (err) {
|
|
623
643
|
// Same contract as --summarize / --profile: the run already
|
|
@@ -630,182 +650,6 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
630
650
|
return result.ok ? 0 : 1
|
|
631
651
|
}
|
|
632
652
|
|
|
633
|
-
/**
|
|
634
|
-
* Which projects declare a `cache.inputs.workspaceFiles` glob matching one of
|
|
635
|
-
* `orphans` (workspace-relative paths belonging to no project).
|
|
636
|
-
*
|
|
637
|
-
* Reached ONLY when a changed path belongs to no project, so a run whose every
|
|
638
|
-
* change is inside a project never gets here — the scoped-config-loading win
|
|
639
|
-
* is untouched for the common case.
|
|
640
|
-
*
|
|
641
|
-
* `vx-lock.json` FIRST when present: it holds the resolved configs, so CI —
|
|
642
|
-
* where `--affected` matters most and the lock is committed — answers with
|
|
643
|
-
* zero evaluation. Without a lock this evaluates configs, which is the honest
|
|
644
|
-
* cost of the question: nothing cheaper can know which globs a program
|
|
645
|
-
* declares. A config that fails to load is skipped rather than failing the
|
|
646
|
-
* run, matching the deliberate Turbo-like rule that a broken out-of-scope
|
|
647
|
-
* config does not fail a scoped run.
|
|
648
|
-
*/
|
|
649
|
-
async function workspaceGlobOwners(
|
|
650
|
-
root: string,
|
|
651
|
-
projects: readonly ProjectMeta[],
|
|
652
|
-
orphans: readonly string[],
|
|
653
|
-
): Promise<string[]> {
|
|
654
|
-
const lock = await readLockfile(root)
|
|
655
|
-
const owners: string[] = []
|
|
656
|
-
const declaresMatch = (config: ProjectConfig): boolean => {
|
|
657
|
-
for (const task of Object.values(config.tasks ?? {})) {
|
|
658
|
-
const globs = task.cache?.inputs?.workspaceFiles
|
|
659
|
-
if (globs === undefined) continue
|
|
660
|
-
if (orphans.some((rel) => workspaceGlobsMatch(globs, rel))) return true
|
|
661
|
-
}
|
|
662
|
-
return false
|
|
663
|
-
}
|
|
664
|
-
await Promise.all(
|
|
665
|
-
projects.map(async (meta) => {
|
|
666
|
-
const locked = lock?.projects[meta.name]
|
|
667
|
-
if (locked !== undefined) {
|
|
668
|
-
if (declaresMatch(locked.config)) owners.push(meta.name)
|
|
669
|
-
return
|
|
670
|
-
}
|
|
671
|
-
if (meta.configPath === null || meta.configPath === '') return
|
|
672
|
-
try {
|
|
673
|
-
if (declaresMatch(await loadProjectConfig(meta.configPath))) owners.push(meta.name)
|
|
674
|
-
} catch {
|
|
675
|
-
// A config that will not load cannot be shown to declare a matching
|
|
676
|
-
// glob; surfacing it here would fail a run the loader itself tolerates.
|
|
677
|
-
}
|
|
678
|
-
}),
|
|
679
|
-
)
|
|
680
|
-
return owners
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
async function loadWorkspaceProjects(cwd: string): Promise<ProjectMeta[]> {
|
|
684
|
-
const root = await findWorkspaceRoot(cwd)
|
|
685
|
-
const ws = await loadWorkspace(root)
|
|
686
|
-
return await listProjects(ws)
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
async function findCwdProject(cwd: string): Promise<string | null> {
|
|
690
|
-
const projects = await loadWorkspaceProjects(cwd)
|
|
691
|
-
const abs = path.resolve(cwd)
|
|
692
|
-
let best: ProjectMeta | null = null
|
|
693
|
-
for (const p of projects) {
|
|
694
|
-
if (abs === p.dir || abs.startsWith(p.dir + path.sep)) {
|
|
695
|
-
if (!best || p.dir.length > best.dir.length) best = p
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
return best?.name ?? null
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
type FilterResolution = { names: string[] } | { error: string } | { empty: string }
|
|
702
|
-
|
|
703
|
-
async function resolveFilters(cwd: string, raw: string[]): Promise<FilterResolution> {
|
|
704
|
-
const root = await findWorkspaceRoot(cwd)
|
|
705
|
-
const projects = await loadWorkspaceProjects(cwd)
|
|
706
|
-
const graph = buildPackageGraph(projects)
|
|
707
|
-
const parsed = raw.map((r) => parseFilter(r, root))
|
|
708
|
-
|
|
709
|
-
// Resolve every `[<since>]` filter against git before the pure
|
|
710
|
-
// applyFilters pass runs. One spawn per distinct ref — usually
|
|
711
|
-
// there's only one anyway.
|
|
712
|
-
const affectedByFilter = new Map<(typeof parsed)[number], Set<string>>()
|
|
713
|
-
for (const f of parsed) {
|
|
714
|
-
if (f.gitSince === undefined) continue
|
|
715
|
-
try {
|
|
716
|
-
const names = await affectedProjects({
|
|
717
|
-
workspaceRoot: root,
|
|
718
|
-
since: f.gitSince,
|
|
719
|
-
projects,
|
|
720
|
-
workspaceGlobOwners: (orphans) => workspaceGlobOwners(root, projects, orphans),
|
|
721
|
-
})
|
|
722
|
-
affectedByFilter.set(f, names)
|
|
723
|
-
} catch (err) {
|
|
724
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
725
|
-
return { error: msg }
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const selected = applyFilters({
|
|
730
|
-
filters: parsed,
|
|
731
|
-
projects,
|
|
732
|
-
graph,
|
|
733
|
-
affectedByFilter,
|
|
734
|
-
// A `[<since>]` selector matching nothing is the ordinary "nothing
|
|
735
|
-
// changed" outcome, reported below; only a name/path pattern that
|
|
736
|
-
// matched nothing is worth flagging as a probable typo.
|
|
737
|
-
onNoMatch: (f) => {
|
|
738
|
-
if (f.gitSince === undefined) {
|
|
739
|
-
process.stderr.write(`vx: filter "${f.raw}" matched no projects\n`)
|
|
740
|
-
}
|
|
741
|
-
},
|
|
742
|
-
})
|
|
743
|
-
if (selected.size === 0) {
|
|
744
|
-
// "Nothing changed" is a legitimate outcome, not a typo — a docs-only
|
|
745
|
-
// commit must not red `vx run … --affected=origin/main`. Only report an
|
|
746
|
-
// error when the user named something concrete that failed to resolve.
|
|
747
|
-
const includes = parsed.filter((f) => !f.negate)
|
|
748
|
-
if (includes.length > 0 && includes.every((f) => f.gitSince !== undefined)) {
|
|
749
|
-
const refs = includes.map((f) => f.gitSince).join(', ')
|
|
750
|
-
return { empty: `nothing affected since ${refs}` }
|
|
751
|
-
}
|
|
752
|
-
return { error: `no projects matched filter(s): ${raw.join(', ')}` }
|
|
753
|
-
}
|
|
754
|
-
return { names: [...selected].sort() }
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
interface PickedTask {
|
|
758
|
-
project: string
|
|
759
|
-
task: string
|
|
760
|
-
description?: string
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
export async function pickTask(
|
|
764
|
-
cwd: string,
|
|
765
|
-
io: { input?: NodeJS.ReadableStream; output?: NodeJS.WritableStream } = {},
|
|
766
|
-
): Promise<PickedTask | null> {
|
|
767
|
-
const projects = await loadWorkspaceProjects(cwd)
|
|
768
|
-
const entries: PickedTask[] = []
|
|
769
|
-
for (const meta of projects) {
|
|
770
|
-
if (!meta.configPath) continue
|
|
771
|
-
const config = await loadProjectConfig(meta.configPath)
|
|
772
|
-
const taskNames = Object.keys(config.tasks ?? {}).sort()
|
|
773
|
-
for (const t of taskNames) {
|
|
774
|
-
const desc = config.tasks?.[t]?.description
|
|
775
|
-
entries.push({ project: meta.name, task: t, ...(desc ? { description: desc } : {}) })
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
if (entries.length === 0) {
|
|
779
|
-
process.stderr.write(`vx run: no tasks declared in any project\n`)
|
|
780
|
-
return null
|
|
781
|
-
}
|
|
782
|
-
const out = io.output ?? process.stdout
|
|
783
|
-
const numW = String(entries.length).length
|
|
784
|
-
const idW = Math.max(...entries.map((e) => `${e.project}#${e.task}`.length))
|
|
785
|
-
out.write('Tasks:\n')
|
|
786
|
-
entries.forEach((e, i) => {
|
|
787
|
-
const n = String(i + 1).padStart(numW, ' ')
|
|
788
|
-
const id = `${e.project}#${e.task}`.padEnd(idW)
|
|
789
|
-
const desc = e.description ? ` ${e.description}` : ''
|
|
790
|
-
out.write(` ${n}. ${id}${desc}\n`)
|
|
791
|
-
})
|
|
792
|
-
const rl = readline.createInterface({
|
|
793
|
-
input: io.input ?? process.stdin,
|
|
794
|
-
output: io.output ?? process.stdout,
|
|
795
|
-
})
|
|
796
|
-
try {
|
|
797
|
-
const answer = (await rl.question(`Pick a task [1-${entries.length}]: `)).trim()
|
|
798
|
-
const n = Number(answer)
|
|
799
|
-
if (!Number.isInteger(n) || n < 1 || n > entries.length) {
|
|
800
|
-
process.stderr.write(`vx run: invalid selection: ${answer}\n`)
|
|
801
|
-
return null
|
|
802
|
-
}
|
|
803
|
-
return entries[n - 1] ?? null
|
|
804
|
-
} finally {
|
|
805
|
-
rl.close()
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
|
|
809
653
|
function printSummary(summary: RunResult): void {
|
|
810
654
|
const rows = summary.outcomes.map((o) => formatRow(o))
|
|
811
655
|
if (rows.length === 0) return
|
|
@@ -835,41 +679,22 @@ function printSummary(summary: RunResult): void {
|
|
|
835
679
|
}
|
|
836
680
|
|
|
837
681
|
function formatRow(o: OutcomeView): { task: string; status: string; duration: string } {
|
|
838
|
-
|
|
839
|
-
// executed / restored-local / restored-remote / up-to-date /
|
|
840
|
-
// failed / skipped.
|
|
841
|
-
const status =
|
|
842
|
-
o.status === 'cache-hit'
|
|
843
|
-
? o.restored === false
|
|
844
|
-
? 'up-to-date'
|
|
845
|
-
: 'restored-local'
|
|
846
|
-
: o.status === 'cache-hit-remote'
|
|
847
|
-
? o.restored === false
|
|
848
|
-
? 'up-to-date'
|
|
849
|
-
: 'restored-remote'
|
|
850
|
-
: o.status === 'success'
|
|
851
|
-
? 'executed'
|
|
852
|
-
: o.status === 'failed'
|
|
853
|
-
? `failed (exit ${o.exitCode})`
|
|
854
|
-
: o.status
|
|
855
|
-
return {
|
|
856
|
-
task: o.taskId,
|
|
857
|
-
status,
|
|
858
|
-
duration: `${o.durationMs}ms`,
|
|
859
|
-
}
|
|
682
|
+
return { task: o.taskId, status: outcomeLabel(o), duration: `${o.durationMs}ms` }
|
|
860
683
|
}
|
|
861
684
|
|
|
862
685
|
/** `(did you mean --concurrency?)` for a flag within two edits of a documented one. */
|
|
863
686
|
function didYouMeanFlag(flag: string): string {
|
|
864
687
|
const name = flag.replace(/=.*$/, '')
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
688
|
+
const flags = documentedFlags('run')
|
|
689
|
+
// The usual two edits; a third only between flags that share their
|
|
690
|
+
// first five characters, so `--retries` reaches `--retry` (the i/y)
|
|
691
|
+
// while `--zzz` does not reach `--all` — a plain three-edit budget did.
|
|
692
|
+
const best =
|
|
693
|
+
nearest(name, flags) ??
|
|
694
|
+
nearest(
|
|
695
|
+
name,
|
|
696
|
+
flags.filter((f) => f.slice(0, 5) === name.slice(0, 5)),
|
|
697
|
+
3,
|
|
698
|
+
)
|
|
699
|
+
return best === undefined ? '' : ` (did you mean ${best}?)`
|
|
875
700
|
}
|