@vzn/vx 0.0.18 → 0.0.21
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 +99 -65
- package/package.json +10 -20
- package/src/bin.ts +12 -2
- package/src/cache/archive.ts +120 -18
- package/src/cache/cache.ts +313 -1324
- package/src/cache/chained-cache.ts +0 -8
- package/src/cache/config-evals.ts +89 -0
- package/src/cache/file-hashes.ts +262 -0
- package/src/cache/git-inputs.ts +637 -0
- package/src/cache/index.ts +16 -7
- package/src/cache/inputs.ts +246 -733
- package/src/cache/layer.ts +604 -0
- package/src/cache/layered-cache.ts +12 -17
- package/src/cache/output-index.ts +224 -0
- package/src/cache/policy.ts +84 -0
- package/src/cache/run-history.ts +147 -0
- package/src/cache/zstd.ts +160 -0
- package/src/cli/cache.ts +39 -11
- package/src/cli/completions.ts +104 -0
- package/src/cli/core-alias.ts +25 -0
- package/src/cli/help.ts +64 -36
- package/src/cli/index.ts +54 -15
- package/src/cli/info.ts +120 -91
- package/src/cli/init.ts +75 -0
- package/src/cli/last.ts +38 -6
- package/src/cli/lock.ts +2 -1
- package/src/cli/plan-format.ts +14 -3
- package/src/cli/plugin-commands.ts +15 -8
- package/src/cli/run.ts +84 -302
- package/src/cli/select.ts +332 -0
- package/src/cli/show.ts +144 -69
- package/src/cli/upgrade.ts +2 -1
- package/src/cli/watch.ts +647 -97
- package/src/cli/why.ts +44 -14
- package/src/cli/workspace-config.ts +126 -0
- package/src/config.ts +156 -136
- package/src/exec/env.ts +11 -0
- package/src/exec/executor.ts +11 -26
- package/src/exec/index.ts +6 -2
- package/src/{plugins/local-executor/index.ts → exec/local-executor.ts} +12 -16
- package/src/exec/runner.ts +57 -18
- package/src/exec/sandbox-binds.ts +182 -0
- package/src/exec/sandbox-paths.ts +50 -0
- package/src/exec/sandbox-runtime.ts +582 -370
- package/src/exec/sandbox-violations.ts +280 -0
- package/src/graph/index.ts +1 -9
- package/src/graph/scheduler.ts +158 -203
- package/src/graph/task-graph.ts +2 -2
- package/src/index.ts +97 -102
- package/src/orchestrator/admission.ts +137 -0
- package/src/orchestrator/deferred-outputs.ts +3 -0
- package/src/orchestrator/doctor.ts +255 -0
- package/src/orchestrator/events.ts +42 -8
- package/src/orchestrator/execute-task.ts +145 -554
- package/src/orchestrator/failure-mode.ts +169 -28
- package/src/orchestrator/framed-output.ts +41 -36
- package/src/orchestrator/history.ts +118 -98
- package/src/orchestrator/hit-restore.ts +221 -0
- package/src/orchestrator/index.ts +35 -4
- package/src/orchestrator/lockfile-claim.ts +281 -0
- package/src/orchestrator/logger.ts +13 -22
- package/src/orchestrator/metrics.ts +53 -20
- package/src/orchestrator/miss-save.ts +169 -0
- package/src/orchestrator/options.ts +34 -31
- package/src/orchestrator/persistent.ts +79 -0
- package/src/orchestrator/placement.ts +182 -0
- package/src/orchestrator/plugin-host.ts +195 -38
- package/src/orchestrator/plugin.ts +162 -9
- package/src/orchestrator/prepare.ts +77 -132
- package/src/orchestrator/projects.ts +259 -0
- package/src/orchestrator/remote-prefetch.ts +1 -1
- package/src/orchestrator/run-artifacts.ts +25 -3
- package/src/orchestrator/run-context.ts +1 -1
- package/src/orchestrator/run-records.ts +149 -0
- package/src/orchestrator/run-report.ts +2 -2
- package/src/orchestrator/run.ts +246 -597
- package/src/orchestrator/sandbox-request.ts +243 -0
- package/src/orchestrator/save-lane.ts +47 -0
- package/src/orchestrator/signals.ts +111 -0
- package/src/orchestrator/stable-keys.ts +60 -3
- package/src/orchestrator/status-line.ts +2 -1
- package/src/orchestrator/summary.ts +68 -25
- package/src/orchestrator/task-hash.ts +20 -18
- package/src/orchestrator/task-log-buffer.ts +8 -11
- package/src/orchestrator/telemetry-host.ts +11 -1
- package/src/orchestrator/telemetry.ts +3 -17
- package/src/util/cgroup.ts +160 -0
- package/src/util/edit-distance.ts +49 -0
- package/src/util/index.ts +11 -3
- package/src/util/paths.ts +23 -2
- package/src/util/settle.ts +19 -0
- package/src/util/verbs.ts +39 -0
- package/src/workspace/affected.ts +111 -11
- package/src/workspace/config-cache.ts +11 -2
- package/src/workspace/config-eval.ts +11 -4
- package/src/workspace/config-imports.ts +5 -4
- package/src/workspace/config-schema.ts +837 -0
- package/src/workspace/filter.ts +43 -6
- package/src/workspace/fingerprint.ts +37 -3
- package/src/workspace/index.ts +30 -3
- package/src/workspace/lockfile.ts +12 -7
- package/src/{cli → workspace}/migrate-scripts.ts +26 -19
- package/src/{cli/migrate.ts → workspace/migration.ts} +133 -124
- package/src/workspace/package-graph.ts +98 -28
- package/src/workspace/project-loader.ts +76 -746
- package/src/workspace/workspace.ts +98 -13
- 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-turbo.ts +0 -401
- 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/schedule-history/index.ts +0 -109
package/src/cli/run.ts
CHANGED
|
@@ -1,34 +1,21 @@
|
|
|
1
|
-
import readline from 'node:readline/promises'
|
|
2
1
|
import { appendFile } 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 { MAX_TIMEOUT_MS, parseDecimalInt, nearest, machineParallelism } from '../util/index.js'
|
|
32
19
|
import { formatGraphDot, formatPlanJson, formatPlanText } from './plan-format.js'
|
|
33
20
|
|
|
34
21
|
export interface RunArgs {
|
|
@@ -55,6 +42,8 @@ export interface RunArgs {
|
|
|
55
42
|
* / `--force` in precedence order. Defaults to all-on.
|
|
56
43
|
*/
|
|
57
44
|
cache: CachePolicy
|
|
45
|
+
/** `--cache` named a remote axis (`remote:r`, `:w`, `:rw`): the run says so when no plugin serves one. */
|
|
46
|
+
remoteRequested?: boolean
|
|
58
47
|
/** `--cache-dir <path>`: override the cache directory (cwd-relative). */
|
|
59
48
|
cacheDir: string | undefined
|
|
60
49
|
frozen: boolean
|
|
@@ -69,23 +58,6 @@ export interface RunArgs {
|
|
|
69
58
|
* env and workspace `timeout` defaults. Undefined when not passed.
|
|
70
59
|
*/
|
|
71
60
|
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
61
|
outputLogs?: 'full' | 'errors-only' | 'none' | 'hash-only'
|
|
90
62
|
download?: 'all' | 'toplevel' | 'none'
|
|
91
63
|
forwardArgs: string[]
|
|
@@ -113,6 +85,30 @@ export interface RunArgs {
|
|
|
113
85
|
error?: string
|
|
114
86
|
}
|
|
115
87
|
|
|
88
|
+
// The camelCase spelling the flag shipped with (retired 2026-09-10): two
|
|
89
|
+
// edits past the suggester's reach, so it is named outright. A regex, not a
|
|
90
|
+
// quoted literal, so the doc-drift test does not count it as an accepted flag.
|
|
91
|
+
const RETIRED_EXCLUDE_DEPENDENCIES = /^--excludeDependencies(=|$)/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `--concurrency <n>` is a positive decimal integer, or `<n>%` of the
|
|
95
|
+
* cores this process may use (`50%` on eight cores is 4, never below 1;
|
|
96
|
+
* over 100% is allowed for I/O-bound work; a container's CPU quota caps
|
|
97
|
+
* the count the way it caps the default). The percent form resolves
|
|
98
|
+
* here, at parse time, so the run sees one number and the summary says
|
|
99
|
+
* what it ran with.
|
|
100
|
+
*/
|
|
101
|
+
export function parseConcurrency(v: string, cpus = machineParallelism()): number | null {
|
|
102
|
+
const pct = /^(\d+)%$/.exec(v)
|
|
103
|
+
if (pct) {
|
|
104
|
+
const n = parseDecimalInt(pct[1]!)
|
|
105
|
+
if (n === null || n < 1) return null
|
|
106
|
+
return Math.max(1, Math.round((Math.max(1, cpus) * n) / 100))
|
|
107
|
+
}
|
|
108
|
+
const n = parseDecimalInt(v)
|
|
109
|
+
return n === null || n < 1 ? null : n
|
|
110
|
+
}
|
|
111
|
+
|
|
116
112
|
export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
117
113
|
const out: RunArgs = {
|
|
118
114
|
tasks: [],
|
|
@@ -125,9 +121,6 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
125
121
|
frozen: false,
|
|
126
122
|
retries: undefined,
|
|
127
123
|
timeout: undefined,
|
|
128
|
-
memory: undefined,
|
|
129
|
-
verify: undefined,
|
|
130
|
-
verifyAllow: [],
|
|
131
124
|
forwardArgs: [],
|
|
132
125
|
verbosity: 0,
|
|
133
126
|
dry: undefined,
|
|
@@ -162,25 +155,30 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
162
155
|
} else if (a === '--concurrency' || a?.startsWith('--concurrency=')) {
|
|
163
156
|
const v = a === '--concurrency' ? before[++i] : a.slice('--concurrency='.length)
|
|
164
157
|
if (v === undefined) return { ...out, error: `--concurrency requires a value` }
|
|
165
|
-
const n =
|
|
166
|
-
if (n === null
|
|
158
|
+
const n = parseConcurrency(v)
|
|
159
|
+
if (n === null) return { ...out, error: `invalid concurrency: ${v}` }
|
|
167
160
|
out.concurrency = n
|
|
168
161
|
} else if (a === '--all') {
|
|
169
162
|
out.all = true
|
|
170
|
-
} else if (a
|
|
163
|
+
} else if (RETIRED_EXCLUDE_DEPENDENCIES.test(a ?? '')) {
|
|
164
|
+
return {
|
|
165
|
+
...out,
|
|
166
|
+
error: `unknown flag: ${a} (the flag is --exclude-dependencies)${seeHelp('run')}`,
|
|
167
|
+
}
|
|
168
|
+
} else if (a === '--exclude-dependencies') {
|
|
171
169
|
out.excludeDependencies = 'all'
|
|
172
|
-
} else if (a?.startsWith('--
|
|
173
|
-
const raw = a.slice('--
|
|
170
|
+
} else if (a?.startsWith('--exclude-dependencies=')) {
|
|
171
|
+
const raw = a.slice('--exclude-dependencies='.length)
|
|
174
172
|
// An empty value is genuinely ambiguous — "drop every edge" (the
|
|
175
173
|
// bare flag) and "drop none" (an empty list) are equally defensible
|
|
176
174
|
// readings, and silently picking either does the opposite of what
|
|
177
175
|
// half the callers mean. Reject and name both explicit forms; the
|
|
178
|
-
// sibling value flags (--retry=, --timeout=, --
|
|
176
|
+
// sibling value flags (--retry=, --timeout=, --cache-dir=)
|
|
179
177
|
// reject an empty `=` value the same way.
|
|
180
178
|
if (raw === '') {
|
|
181
179
|
return {
|
|
182
180
|
...out,
|
|
183
|
-
error: `--
|
|
181
|
+
error: `--exclude-dependencies= needs a value — pass bare --exclude-dependencies to drop every dependsOn edge, or omit the flag to keep them`,
|
|
184
182
|
}
|
|
185
183
|
}
|
|
186
184
|
out.excludeDependencies = raw
|
|
@@ -215,42 +213,6 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
215
213
|
return { ...out, error: `--timeout must be a positive integer (ms), got: ${v}` }
|
|
216
214
|
}
|
|
217
215
|
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
216
|
} else if (a === '--output-logs' || a?.startsWith('--output-logs=')) {
|
|
255
217
|
const v = a === '--output-logs' ? before[++i] : a.slice('--output-logs='.length)
|
|
256
218
|
if (v !== 'full' && v !== 'errors-only' && v !== 'none' && v !== 'hash-only') {
|
|
@@ -291,6 +253,7 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
291
253
|
}
|
|
292
254
|
try {
|
|
293
255
|
cachePolicy = parseCachePolicy(v, cachePolicy)
|
|
256
|
+
if (/remote:[rw]/.test(v)) out.remoteRequested = true
|
|
294
257
|
} catch (err) {
|
|
295
258
|
cacheSpecError = err instanceof Error ? err.message : String(err)
|
|
296
259
|
}
|
|
@@ -299,7 +262,14 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
299
262
|
} else if (a === '--force') {
|
|
300
263
|
forceFlag = true
|
|
301
264
|
} else if (a === '--continue') {
|
|
302
|
-
// Bare --continue = 'always' (the Turbo convention).
|
|
265
|
+
// Bare --continue = 'always' (the Turbo convention). Its mode comes
|
|
266
|
+
// with '=' only: `--continue never` would read `never` as a TASK, and
|
|
267
|
+
// the run would then say "No projects declare task(s): never" — true,
|
|
268
|
+
// and no help at all.
|
|
269
|
+
const next = before[i + 1]
|
|
270
|
+
if (next === 'never' || next === 'deps-ok' || next === 'always') {
|
|
271
|
+
return { ...out, error: `--continue takes its mode with '=': --continue=${next}` }
|
|
272
|
+
}
|
|
303
273
|
out.continueMode = 'always'
|
|
304
274
|
} else if (a?.startsWith('--continue=')) {
|
|
305
275
|
const v = a.slice('--continue='.length)
|
|
@@ -367,7 +337,7 @@ export function parseRunArgs(args: readonly string[]): RunArgs {
|
|
|
367
337
|
}
|
|
368
338
|
out.report = fmt
|
|
369
339
|
} else if (a !== undefined && a.startsWith('-')) {
|
|
370
|
-
return { ...out, error: `unknown flag: ${a}${didYouMeanFlag(a)}` }
|
|
340
|
+
return { ...out, error: `unknown flag: ${a}${didYouMeanFlag(a)}${seeHelp('run')}` }
|
|
371
341
|
} else if (a !== undefined) {
|
|
372
342
|
out.tasks.push(a)
|
|
373
343
|
}
|
|
@@ -460,10 +430,14 @@ export async function resolveRunOptions(
|
|
|
460
430
|
const bareTasks = tasks.filter((t) => !t.includes('#'))
|
|
461
431
|
const anchoredTasks = tasks.filter((t) => t.includes('#'))
|
|
462
432
|
let projects: string[] | undefined
|
|
433
|
+
let staged: RunOptions['staged']
|
|
463
434
|
if (bareTasks.length === 0) {
|
|
464
435
|
projects = undefined
|
|
465
436
|
} else if (filterStrings.length > 0) {
|
|
466
|
-
const resolved = await resolveFilters(cwd, filterStrings
|
|
437
|
+
const resolved = await resolveFilters(cwd, filterStrings, {
|
|
438
|
+
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
439
|
+
...(parsed.frozen ? { frozen: true } : {}),
|
|
440
|
+
})
|
|
467
441
|
if ('error' in resolved) return { error: resolved.error }
|
|
468
442
|
if ('empty' in resolved) {
|
|
469
443
|
// "Nothing changed" is a clean exit for the BARE tasks the filter
|
|
@@ -476,6 +450,9 @@ export async function resolveRunOptions(
|
|
|
476
450
|
projects = []
|
|
477
451
|
} else {
|
|
478
452
|
projects = resolved.names
|
|
453
|
+
// The graph walk staged every config; the run reuses those entries
|
|
454
|
+
// instead of evaluating and staging them a second time.
|
|
455
|
+
staged = resolved.staged
|
|
479
456
|
}
|
|
480
457
|
} else if (parsed.all) {
|
|
481
458
|
projects = undefined
|
|
@@ -494,6 +471,7 @@ export async function resolveRunOptions(
|
|
|
494
471
|
cwd,
|
|
495
472
|
tasks: [...tasks],
|
|
496
473
|
cache: parsed.cache,
|
|
474
|
+
...(parsed.remoteRequested ? { remoteRequested: true } : {}),
|
|
497
475
|
flow: detectFlow(parsed),
|
|
498
476
|
...(parsed.frozen ? { frozen: true } : {}),
|
|
499
477
|
...(parsed.outputLogs !== undefined ? { outputLogs: parsed.outputLogs } : {}),
|
|
@@ -507,18 +485,10 @@ export async function resolveRunOptions(
|
|
|
507
485
|
opts.excludeDependencies = parsed.excludeDependencies
|
|
508
486
|
}
|
|
509
487
|
if (projects !== undefined) opts.projects = projects
|
|
488
|
+
if (staged !== undefined) opts.staged = staged
|
|
510
489
|
if (parsed.retries !== undefined) opts.retries = parsed.retries
|
|
511
490
|
if (parsed.timeout !== undefined) opts.timeout = parsed.timeout
|
|
512
|
-
if (parsed.memory !== undefined) opts.memory = parsed.memory
|
|
513
491
|
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
492
|
if (parsed.concurrency !== undefined) opts.concurrency = parsed.concurrency
|
|
523
493
|
if (parsed.summarize !== undefined) opts.summarize = parsed.summarize
|
|
524
494
|
if (parsed.profile !== undefined) opts.profile = parsed.profile
|
|
@@ -545,7 +515,14 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
545
515
|
process.stderr.write(`vx run: missing task name (stdin is not a TTY)\n`)
|
|
546
516
|
return 1
|
|
547
517
|
}
|
|
548
|
-
const picked = await pickTask(
|
|
518
|
+
const picked = await pickTask(
|
|
519
|
+
cwd,
|
|
520
|
+
{},
|
|
521
|
+
{
|
|
522
|
+
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
523
|
+
...(parsed.frozen ? { frozen: true } : {}),
|
|
524
|
+
},
|
|
525
|
+
)
|
|
549
526
|
if (!picked) return 1
|
|
550
527
|
tasks = [`${picked.project}#${picked.task}`]
|
|
551
528
|
}
|
|
@@ -630,182 +607,6 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
630
607
|
return result.ok ? 0 : 1
|
|
631
608
|
}
|
|
632
609
|
|
|
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
610
|
function printSummary(summary: RunResult): void {
|
|
810
611
|
const rows = summary.outcomes.map((o) => formatRow(o))
|
|
811
612
|
if (rows.length === 0) return
|
|
@@ -835,41 +636,22 @@ function printSummary(summary: RunResult): void {
|
|
|
835
636
|
}
|
|
836
637
|
|
|
837
638
|
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
|
-
}
|
|
639
|
+
return { task: o.taskId, status: outcomeLabel(o), duration: `${o.durationMs}ms` }
|
|
860
640
|
}
|
|
861
641
|
|
|
862
642
|
/** `(did you mean --concurrency?)` for a flag within two edits of a documented one. */
|
|
863
643
|
function didYouMeanFlag(flag: string): string {
|
|
864
644
|
const name = flag.replace(/=.*$/, '')
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
645
|
+
const flags = documentedFlags('run')
|
|
646
|
+
// The usual two edits; a third only between flags that share their
|
|
647
|
+
// first five characters, so `--retries` reaches `--retry` (the i/y)
|
|
648
|
+
// while `--zzz` does not reach `--all` — a plain three-edit budget did.
|
|
649
|
+
const best =
|
|
650
|
+
nearest(name, flags) ??
|
|
651
|
+
nearest(
|
|
652
|
+
name,
|
|
653
|
+
flags.filter((f) => f.slice(0, 5) === name.slice(0, 5)),
|
|
654
|
+
3,
|
|
655
|
+
)
|
|
656
|
+
return best === undefined ? '' : ` (did you mean ${best}?)`
|
|
875
657
|
}
|