@vzn/vx 0.0.635 → 0.0.637
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -19
- package/docs/caching.md +222 -210
- package/docs/cli.md +699 -664
- package/docs/execution.md +67 -75
- package/docs/features.md +44 -40
- package/docs/patterns.md +31 -32
- package/docs/schema.md +418 -202
- package/docs/security.md +76 -39
- package/launcher.sh +43 -0
- package/package.json +5 -5
- package/schemas/cache.json +41 -19
- package/schemas/info.json +23 -11
- package/schemas/last.json +48 -2
- package/schemas/lock.json +1 -1
- package/schemas/plan.json +2 -2
- package/schemas/show.json +13 -4
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +16 -12
- package/src/bin.ts +17 -2
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +105 -53
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +7 -1
- package/src/cache/inputs.ts +125 -35
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +8 -17
- package/src/cache/layered-cache.ts +16 -102
- package/src/cache/policy.ts +25 -29
- package/src/cache/run-history.ts +36 -14
- package/src/cache/schema.ts +21 -23
- package/src/cache/scoped-cache.ts +224 -0
- package/src/cli/cache.ts +150 -28
- package/src/cli/completions.ts +2 -2
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +94 -61
- package/src/cli/index.ts +42 -13
- package/src/cli/info.ts +38 -23
- package/src/cli/init.ts +115 -406
- package/src/cli/last.ts +145 -4
- package/src/cli/lock.ts +333 -111
- package/src/cli/plan-format.ts +2 -2
- package/src/cli/plugin-commands.ts +5 -9
- package/src/cli/plugin-templates.ts +10 -10
- package/src/cli/run.ts +123 -144
- package/src/cli/select.ts +33 -39
- package/src/cli/show.ts +17 -19
- package/src/cli/task-verb.ts +2 -9
- package/src/cli/upgrade.ts +402 -145
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +33 -60
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +12 -3
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +188 -215
- package/src/cli/why.ts +28 -17
- package/src/cli/workspace-config.ts +31 -68
- package/src/config.ts +191 -71
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +19 -7
- package/src/graph/task-graph.ts +44 -20
- package/src/handoff.ts +169 -0
- package/src/index.ts +25 -57
- package/src/orchestrator/affected-tasks.ts +7 -5
- package/src/orchestrator/command-windows.ts +44 -0
- package/src/orchestrator/deferred-outputs.ts +12 -10
- package/src/orchestrator/doctor.ts +98 -110
- package/src/orchestrator/download-policy.ts +3 -3
- package/src/orchestrator/events.ts +19 -3
- package/src/orchestrator/excluded-keys.ts +0 -2
- package/src/orchestrator/execute-task.ts +192 -70
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +8 -3
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +70 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +18 -4
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +71 -39
- package/src/orchestrator/metrics.ts +71 -6
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +43 -28
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +87 -63
- package/src/orchestrator/plugin.ts +94 -60
- package/src/orchestrator/prepare.ts +114 -73
- package/src/orchestrator/projects.ts +102 -99
- package/src/orchestrator/remote-prefetch.ts +2 -1
- package/src/orchestrator/run-context.ts +25 -6
- package/src/orchestrator/run-failures.ts +17 -8
- package/src/orchestrator/run-lock.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +127 -88
- package/src/orchestrator/sandbox-fix.ts +39 -20
- package/src/orchestrator/sandbox-request.ts +84 -13
- package/src/orchestrator/signals.ts +1 -1
- package/src/orchestrator/stable-keys.ts +13 -11
- package/src/orchestrator/status-line.ts +48 -6
- package/src/orchestrator/summary.ts +46 -62
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +3 -3
- package/src/settings.ts +417 -0
- package/src/util/errors.ts +1 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +8 -2
- package/src/util/paths.ts +10 -0
- package/src/util/settle.ts +49 -30
- package/src/util/size.ts +23 -4
- package/src/util/timing.ts +3 -2
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +77 -222
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +251 -241
- package/src/workspace/dependency-rules.ts +57 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +22 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +242 -146
- package/src/workspace/package-graph.ts +30 -216
- package/src/workspace/project-loader.ts +80 -285
- package/src/workspace/repo-id.ts +2 -1
- package/src/workspace/workspace.ts +362 -831
- package/src/cache/config-evals.ts +0 -130
- package/src/workspace/config-cache.ts +0 -661
- package/src/workspace/migrate-scripts.ts +0 -1121
package/src/cli/run.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { isatty } from 'node:tty'
|
|
4
|
-
import { findWorkspaceRoot } from '../workspace/index.js'
|
|
4
|
+
import { cacheScopeError, findWorkspaceRoot, isCacheScope } from '../workspace/index.js'
|
|
5
5
|
import { translateForeign } from './foreign-flags.js'
|
|
6
6
|
import { flagHint, formatValue, refuse, seeHelp } from './help.js'
|
|
7
7
|
import {
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
} from '../orchestrator/index.js'
|
|
15
15
|
import type { ContinueMode, TaskOutcome } from '../graph/index.js'
|
|
16
16
|
import {
|
|
17
|
+
applyCacheFlags,
|
|
18
|
+
type CacheFlags,
|
|
17
19
|
type CachePolicy,
|
|
18
20
|
FULL_CACHE_POLICY,
|
|
19
21
|
gitRefusal,
|
|
@@ -21,7 +23,8 @@ import {
|
|
|
21
23
|
} from '../cache/index.js'
|
|
22
24
|
import { affectedFilterFor, findCwdSelection, pickTask, resolveFilters } from './select.js'
|
|
23
25
|
import { nxTargetHint, taskNamesHere } from './task-verb.js'
|
|
24
|
-
import {
|
|
26
|
+
import { parseDecimalInt } from '../util/index.js'
|
|
27
|
+
import { kindHint, parseSettingValue, type Setting, SETTINGS, settingRefusal } from '../settings.js'
|
|
25
28
|
import { formatGraphDot, formatPlanJson, formatPlanText } from './plan-format.js'
|
|
26
29
|
|
|
27
30
|
export interface RunArgs {
|
|
@@ -42,30 +45,29 @@ export interface RunArgs {
|
|
|
42
45
|
* `[...names]` → drop only these specific dep names.
|
|
43
46
|
*/
|
|
44
47
|
excludeDependencies: 'all' | string[]
|
|
45
|
-
concurrency: number | undefined
|
|
46
48
|
/**
|
|
47
|
-
*
|
|
49
|
+
* The settings a flag set, by path (`run.timeout` → ms): each flag
|
|
50
|
+
* src/settings.ts names for the verb, parsed by its kind.
|
|
51
|
+
*/
|
|
52
|
+
settings: Record<string, unknown>
|
|
53
|
+
/**
|
|
54
|
+
* Resolved 4-axis cache policy after applying `--cache-policy` / `--no-cache`
|
|
48
55
|
* / `--force` in precedence order. Defaults to all-on.
|
|
49
56
|
*/
|
|
50
57
|
cache: CachePolicy
|
|
51
|
-
/**
|
|
58
|
+
/**
|
|
59
|
+
* Neither `--cache-policy` nor `VX_CACHE_POLICY` named a spec: the run starts from the
|
|
60
|
+
* workspace's `cache.policy` and applies these on top.
|
|
61
|
+
*/
|
|
62
|
+
cacheFlags?: CacheFlags
|
|
63
|
+
/** The policy named a remote axis (`remote:r`, `:w`, `:rw`): the run says so when no plugin serves one. */
|
|
52
64
|
remoteRequested?: boolean
|
|
53
65
|
/** `--cache-dir <path>`: override the cache directory (cwd-relative). */
|
|
54
66
|
cacheDir: string | undefined
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/
|
|
60
|
-
retries: number | undefined
|
|
61
|
-
/**
|
|
62
|
-
* `--timeout <ms>` / `--timeout=<ms>`: run-level default task timeout for
|
|
63
|
-
* tasks without their own `exec.timeout`. Sits above the `VX_TASK_TIMEOUT`
|
|
64
|
-
* env and workspace `timeout` defaults. Undefined when not passed.
|
|
65
|
-
*/
|
|
66
|
-
timeout: number | undefined
|
|
67
|
-
outputLogs?: 'full' | 'errors-only' | 'none' | 'hash-only'
|
|
68
|
-
download?: 'all' | 'toplevel' | 'none'
|
|
67
|
+
/** `--cache-scope <name>`: this run's `cache.scope`, above `VX_CACHE_SCOPE` and the workspace's. */
|
|
68
|
+
cacheScope?: string
|
|
69
|
+
/** `vx watch --poll` (`watch.poll`). */
|
|
70
|
+
poll?: true
|
|
69
71
|
forwardArgs: string[]
|
|
70
72
|
verbosity: number
|
|
71
73
|
dry: 'text' | 'json' | undefined
|
|
@@ -98,25 +100,42 @@ export interface RunArgs {
|
|
|
98
100
|
// quoted literal, so the doc-drift test does not count it as an accepted flag.
|
|
99
101
|
const RETIRED_EXCLUDE_DEPENDENCIES = /^--excludeDependencies(=|$)/
|
|
100
102
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
/** `name` when `a` is it, bare or with `=`. */
|
|
104
|
+
function flagOf(a: string | undefined, name: string): string | undefined {
|
|
105
|
+
return a === name || a?.startsWith(`${name}=`) ? name : undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The settings parsed by their kind alone; the cache flags, `--continue`
|
|
109
|
+
// and `--cache-dir` keep their own branches above.
|
|
110
|
+
const GENERIC_FLAGS = SETTINGS.filter(
|
|
111
|
+
(s) =>
|
|
112
|
+
s.flag?.verb === 'run' &&
|
|
113
|
+
!['run.continue', 'cache.dir', 'cache.policy', 'cache.scope'].includes(s.path),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
/** The setting a flag names. */
|
|
117
|
+
function settingFlagOf(a: string): { s: Setting; name: string } | undefined {
|
|
118
|
+
for (const s of GENERIC_FLAGS) {
|
|
119
|
+
const name = s.flag!.name
|
|
120
|
+
if (flagOf(a, name) !== undefined) return { s, name }
|
|
115
121
|
}
|
|
116
|
-
|
|
117
|
-
return n === null || n < 1 ? null : n
|
|
122
|
+
return undefined
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
/** Each setting a flag sets, and the RunOptions field it lands in. */
|
|
126
|
+
const SETTING_OPTIONS = [
|
|
127
|
+
['run.concurrency', 'concurrency'],
|
|
128
|
+
['run.schedule', 'schedule'],
|
|
129
|
+
['run.branch', 'branch'],
|
|
130
|
+
['run.timeout', 'timeout'],
|
|
131
|
+
['run.retries', 'retries'],
|
|
132
|
+
['run.outputLogs', 'outputLogs'],
|
|
133
|
+
['run.killGrace', 'killGrace'],
|
|
134
|
+
['run.teardownTimeout', 'teardownTimeout'],
|
|
135
|
+
['run.readyNotice', 'readyNotice'],
|
|
136
|
+
['cache.download', 'download'],
|
|
137
|
+
] as const satisfies readonly (readonly [string, keyof RunOptions])[]
|
|
138
|
+
|
|
120
139
|
/** `verb` is the one being parsed for: `vx watch` reads `vx run`'s flags, and
|
|
121
140
|
* its flag hints named `vx run` (M-58). The caller appends the help pointer. */
|
|
122
141
|
export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' = 'run'): RunArgs {
|
|
@@ -125,12 +144,9 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
125
144
|
filters: [],
|
|
126
145
|
all: false,
|
|
127
146
|
excludeDependencies: [],
|
|
128
|
-
|
|
147
|
+
settings: {},
|
|
129
148
|
cache: { ...FULL_CACHE_POLICY },
|
|
130
149
|
cacheDir: undefined,
|
|
131
|
-
frozen: false,
|
|
132
|
-
retries: undefined,
|
|
133
|
-
timeout: undefined,
|
|
134
150
|
forwardArgs: [],
|
|
135
151
|
verbosity: 0,
|
|
136
152
|
dry: undefined,
|
|
@@ -153,11 +169,12 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
153
169
|
out.forwardArgs = sepIdx === -1 ? [] : args.slice(sepIdx + 1)
|
|
154
170
|
|
|
155
171
|
// Cache policy is resolved AFTER the loop in precedence order:
|
|
156
|
-
// start all-true → apply each `--cache` spec → `--no-cache` forces
|
|
172
|
+
// start all-true → apply each `--cache-policy` spec → `--no-cache` forces
|
|
157
173
|
// all false → `--force` forces both reads false. So `--no-cache`
|
|
158
|
-
// beats `--force`, and `--cache` is the base both may override.
|
|
174
|
+
// beats `--force`, and `--cache-policy` is the base both may override.
|
|
159
175
|
let cachePolicy: CachePolicy = { ...FULL_CACHE_POLICY }
|
|
160
176
|
let cacheSpecError: string | undefined
|
|
177
|
+
let cacheFlag = false
|
|
161
178
|
let noCacheFlag = false
|
|
162
179
|
let forceFlag = false
|
|
163
180
|
|
|
@@ -172,20 +189,6 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
172
189
|
error: `--filter requires a value (a project name, glob or path, e.g. --filter app)`,
|
|
173
190
|
}
|
|
174
191
|
out.filters.push(v)
|
|
175
|
-
} else if (a === '--concurrency' || a?.startsWith('--concurrency=')) {
|
|
176
|
-
const v = a === '--concurrency' ? before[++i] : a.slice('--concurrency='.length)
|
|
177
|
-
if (v === undefined)
|
|
178
|
-
return {
|
|
179
|
-
...out,
|
|
180
|
-
error: `--concurrency requires a value (a positive integer, or a share of the cores such as 50%)`,
|
|
181
|
-
}
|
|
182
|
-
const n = parseConcurrency(v)
|
|
183
|
-
if (n === null)
|
|
184
|
-
return {
|
|
185
|
-
...out,
|
|
186
|
-
error: `--concurrency must be a positive integer, or a share of the cores such as 50% (got ${v})`,
|
|
187
|
-
}
|
|
188
|
-
out.concurrency = n
|
|
189
192
|
} else if (a === '--all') {
|
|
190
193
|
out.all = true
|
|
191
194
|
} else if (RETIRED_EXCLUDE_DEPENDENCIES.test(a ?? '')) {
|
|
@@ -201,7 +204,7 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
201
204
|
// bare flag) and "drop none" (an empty list) are equally defensible
|
|
202
205
|
// readings, and silently picking either does the opposite of what
|
|
203
206
|
// half the callers mean. Reject and name both explicit forms; the
|
|
204
|
-
// sibling value flags (--
|
|
207
|
+
// sibling value flags (--retries=, --timeout=, --cache-dir=)
|
|
205
208
|
// reject an empty `=` value the same way.
|
|
206
209
|
if (raw === '') {
|
|
207
210
|
return {
|
|
@@ -213,60 +216,6 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
213
216
|
.split(',')
|
|
214
217
|
.map((s) => s.trim())
|
|
215
218
|
.filter((s) => s.length > 0)
|
|
216
|
-
} else if (a === '--frozen') {
|
|
217
|
-
out.frozen = true
|
|
218
|
-
} else if (a === '--retry' || a?.startsWith('--retry=')) {
|
|
219
|
-
const v = a === '--retry' ? before[++i] : a.slice('--retry='.length)
|
|
220
|
-
if (v === undefined)
|
|
221
|
-
return { ...out, error: `--retry requires a value (a non-negative integer)` }
|
|
222
|
-
const n = parseDecimalInt(v)
|
|
223
|
-
if (n === null) {
|
|
224
|
-
return { ...out, error: `--retry must be a non-negative integer (got ${v})` }
|
|
225
|
-
}
|
|
226
|
-
out.retries = n
|
|
227
|
-
} else if (a === '--timeout' || a?.startsWith('--timeout=')) {
|
|
228
|
-
const v = a === '--timeout' ? before[++i] : a.slice('--timeout='.length)
|
|
229
|
-
if (v === undefined)
|
|
230
|
-
return { ...out, error: `--timeout requires a value (a positive integer, in ms)` }
|
|
231
|
-
const n = parseDecimalInt(v)
|
|
232
|
-
if (n !== null && n > MAX_TIMEOUT_MS) {
|
|
233
|
-
// Not "effectively no limit" — setTimeout reduces a delay past 2^31-1
|
|
234
|
-
// to 1 ms, so this would kill every task the moment it spawned.
|
|
235
|
-
return {
|
|
236
|
-
...out,
|
|
237
|
-
error:
|
|
238
|
-
`--timeout ${n} exceeds the maximum timer delay (${MAX_TIMEOUT_MS} ms, ~24.8 days); ` +
|
|
239
|
-
`larger values are silently reduced to 1 ms. Omit --timeout for no limit.`,
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (n === null || n <= 0) {
|
|
243
|
-
return { ...out, error: `--timeout must be a positive integer in ms (got ${v})` }
|
|
244
|
-
}
|
|
245
|
-
out.timeout = n
|
|
246
|
-
} else if (a === '--output-logs' || a?.startsWith('--output-logs=')) {
|
|
247
|
-
const v = a === '--output-logs' ? before[++i] : a.slice('--output-logs='.length)
|
|
248
|
-
if (v === undefined || v === '') {
|
|
249
|
-
return {
|
|
250
|
-
...out,
|
|
251
|
-
error: `--output-logs requires a value (full, errors-only, hash-only, or none)`,
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (v !== 'full' && v !== 'errors-only' && v !== 'none' && v !== 'hash-only') {
|
|
255
|
-
return {
|
|
256
|
-
...out,
|
|
257
|
-
error: `--output-logs must be full, errors-only, hash-only, or none (got ${v})`,
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
out.outputLogs = v
|
|
261
|
-
} else if (a === '--download' || a?.startsWith('--download=')) {
|
|
262
|
-
const v = a === '--download' ? before[++i] : a.slice('--download='.length)
|
|
263
|
-
if (v === undefined || v === '') {
|
|
264
|
-
return { ...out, error: `--download requires a value (all, toplevel, or none)` }
|
|
265
|
-
}
|
|
266
|
-
if (v !== 'all' && v !== 'toplevel' && v !== 'none') {
|
|
267
|
-
return { ...out, error: `--download must be all, toplevel, or none (got ${v})` }
|
|
268
|
-
}
|
|
269
|
-
out.download = v
|
|
270
219
|
} else if (a === '--cache-dir' || a?.startsWith('--cache-dir=')) {
|
|
271
220
|
const v = a === '--cache-dir' ? before[++i] : a.slice('--cache-dir='.length)
|
|
272
221
|
if (v === undefined || v === '') return { ...out, error: `--cache-dir requires a path` }
|
|
@@ -282,14 +231,21 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
282
231
|
}
|
|
283
232
|
}
|
|
284
233
|
out.cacheDir = v
|
|
285
|
-
} else if (a === '--cache' || a?.startsWith('--cache=')) {
|
|
286
|
-
const v = a === '--cache' ? before[++i] : a.slice('--cache='.length)
|
|
234
|
+
} else if (a === '--cache-scope' || a?.startsWith('--cache-scope=')) {
|
|
235
|
+
const v = a === '--cache-scope' ? before[++i] : a.slice('--cache-scope='.length)
|
|
236
|
+
if (v === undefined || !isCacheScope(v))
|
|
237
|
+
return { ...out, error: cacheScopeError('--cache-scope') }
|
|
238
|
+
out.cacheScope = v
|
|
239
|
+
} else if (flagOf(a, '--cache-policy')) {
|
|
240
|
+
cacheFlag = true
|
|
241
|
+
const name = flagOf(a, '--cache-policy')!
|
|
242
|
+
const v = a === name ? before[++i] : a!.slice(name.length + 1)
|
|
287
243
|
if (v === undefined)
|
|
288
244
|
return {
|
|
289
245
|
...out,
|
|
290
|
-
error:
|
|
246
|
+
error: `${name} requires a value (a spec like local:r, local:rw, remote:, or local:,remote:rw)`,
|
|
291
247
|
}
|
|
292
|
-
// A spec with no segment at all applies NOTHING, so `--cache=`
|
|
248
|
+
// A spec with no segment at all applies NOTHING, so `--cache-policy=`
|
|
293
249
|
// (an empty shell var, or someone reaching for "no cache") left
|
|
294
250
|
// all four axes ON — the opposite of the intent. Reject it; an
|
|
295
251
|
// empty flag list for a NAMED layer (`local:`) stays valid and
|
|
@@ -297,14 +253,14 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
297
253
|
if (!v.split(',').some((seg) => seg.trim().length > 0)) {
|
|
298
254
|
return {
|
|
299
255
|
...out,
|
|
300
|
-
error:
|
|
256
|
+
error: `${name} needs a spec like local:r, local:rw, remote:, or local:,remote:rw — pass --no-cache to disable every axis`,
|
|
301
257
|
}
|
|
302
258
|
}
|
|
303
259
|
try {
|
|
304
260
|
cachePolicy = parseCachePolicy(v, cachePolicy)
|
|
305
261
|
if (/remote:[rw]/.test(v)) out.remoteRequested = true
|
|
306
262
|
} catch (err) {
|
|
307
|
-
cacheSpecError = err instanceof Error ? err.message : String(err)
|
|
263
|
+
cacheSpecError = `--cache-policy: ${err instanceof Error ? err.message : String(err)}`
|
|
308
264
|
}
|
|
309
265
|
} else if (a === '--no-cache') {
|
|
310
266
|
noCacheFlag = true
|
|
@@ -397,6 +353,16 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
397
353
|
return { ...out, error: `--report must be markdown (got ${fmt})` }
|
|
398
354
|
}
|
|
399
355
|
out.report = fmt
|
|
356
|
+
} else if (verb === 'watch' && a === '--poll') {
|
|
357
|
+
out.poll = true
|
|
358
|
+
} else if (a !== undefined && settingFlagOf(a) !== undefined) {
|
|
359
|
+
const { s: set, name } = settingFlagOf(a)!
|
|
360
|
+
const v = a === name ? before[++i] : a.slice(name.length + 1)
|
|
361
|
+
if (v === undefined || v === '')
|
|
362
|
+
return { ...out, error: `${name} requires a value (${kindHint(set.kind)})` }
|
|
363
|
+
const value = parseSettingValue(set.kind, v)
|
|
364
|
+
if (value === null) return { ...out, error: settingRefusal(set.path, name, v) }
|
|
365
|
+
out.settings[set.path] = value
|
|
400
366
|
} else if (a !== undefined && a.startsWith('-')) {
|
|
401
367
|
return { ...out, error: `unknown flag: ${a}${flagHint(verb, a)}` }
|
|
402
368
|
} else if (a !== undefined) {
|
|
@@ -407,15 +373,32 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
407
373
|
if (cacheSpecError !== undefined) {
|
|
408
374
|
return { ...out, error: cacheSpecError }
|
|
409
375
|
}
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
if (
|
|
413
|
-
|
|
376
|
+
// `VX_CACHE_POLICY` is the spec a run takes when no flag names one.
|
|
377
|
+
const envSpec = process.env['VX_CACHE_POLICY'] || undefined
|
|
378
|
+
if (!cacheFlag && envSpec !== undefined) {
|
|
379
|
+
if (!envSpec.split(',').some((seg) => seg.trim().length > 0))
|
|
380
|
+
return {
|
|
381
|
+
...out,
|
|
382
|
+
error: `VX_CACHE_POLICY needs a spec like local:r, local:rw, remote:, or local:,remote:rw`,
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
cachePolicy = parseCachePolicy(envSpec, cachePolicy)
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return {
|
|
388
|
+
...out,
|
|
389
|
+
error: `VX_CACHE_POLICY: ${err instanceof Error ? err.message : String(err)}`,
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (/remote:[rw]/.test(envSpec)) out.remoteRequested = true
|
|
414
393
|
}
|
|
415
|
-
|
|
416
|
-
|
|
394
|
+
// --no-cache (all off) beats --force (reads off, writes kept), both
|
|
395
|
+
// layered on top of the spec; with none named, on the workspace's.
|
|
396
|
+
const flags: CacheFlags = {
|
|
397
|
+
...(noCacheFlag ? { noCache: true } : {}),
|
|
398
|
+
...(forceFlag ? { force: true } : {}),
|
|
417
399
|
}
|
|
418
|
-
out.cache = cachePolicy
|
|
400
|
+
out.cache = applyCacheFlags(cachePolicy, flags)
|
|
401
|
+
if (!cacheFlag && envSpec === undefined) out.cacheFlags = flags
|
|
419
402
|
|
|
420
403
|
if (out.dry !== undefined && out.graph !== undefined) {
|
|
421
404
|
return { ...out, error: '--dry and --graph are mutually exclusive' }
|
|
@@ -471,9 +454,6 @@ export function detectFlow(
|
|
|
471
454
|
}
|
|
472
455
|
|
|
473
456
|
/** `CI=0` / `CI=false` are not CI, as the output flow reads them. */
|
|
474
|
-
function onCI(v: string | undefined): boolean {
|
|
475
|
-
return v !== undefined && v !== '' && v !== '0' && v !== 'false'
|
|
476
|
-
}
|
|
477
457
|
|
|
478
458
|
/** The filters a run's scope comes from: `--filter`s, with `--affected` as its `...[<base>]`. */
|
|
479
459
|
async function scopeFilters(
|
|
@@ -561,10 +541,7 @@ export async function resolveRunOptions(
|
|
|
561
541
|
? await resolveFilters(
|
|
562
542
|
cwd,
|
|
563
543
|
filterStrings,
|
|
564
|
-
{
|
|
565
|
-
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
566
|
-
...(parsed.frozen ? { frozen: true } : {}),
|
|
567
|
-
},
|
|
544
|
+
parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {},
|
|
568
545
|
affectedFilter,
|
|
569
546
|
)
|
|
570
547
|
: undefined
|
|
@@ -609,18 +586,18 @@ export async function resolveRunOptions(
|
|
|
609
586
|
const opts: RunOptions = {
|
|
610
587
|
cwd,
|
|
611
588
|
tasks: [...tasks],
|
|
612
|
-
|
|
589
|
+
...(parsed.cacheFlags !== undefined
|
|
590
|
+
? { cacheFlags: parsed.cacheFlags }
|
|
591
|
+
: { cache: parsed.cache }),
|
|
613
592
|
...(parsed.remoteRequested ? { remoteRequested: true } : {}),
|
|
593
|
+
...(parsed.cacheScope !== undefined ? { cacheScope: parsed.cacheScope } : {}),
|
|
614
594
|
// A laptop reads the shared cache and never writes it (owner,
|
|
615
|
-
// 2026-10-03); naming the remote in `--cache` is asking to.
|
|
595
|
+
// 2026-10-03); naming the remote in `--cache-policy` is asking to.
|
|
616
596
|
...(!parsed.remoteRequested && !onCI(process.env['CI'])
|
|
617
|
-
? {
|
|
597
|
+
? { remoteReadOnlyByDefault: true }
|
|
618
598
|
: {}),
|
|
619
599
|
flow: detectFlow(parsed),
|
|
620
|
-
...(parsed.frozen ? { frozen: true } : {}),
|
|
621
|
-
...(parsed.outputLogs !== undefined ? { outputLogs: parsed.outputLogs } : {}),
|
|
622
600
|
...(parsed.verbosity > 0 ? { summaryTable: true } : {}),
|
|
623
|
-
...(parsed.download !== undefined ? { download: parsed.download } : {}),
|
|
624
601
|
...(parsed.continueMode !== undefined ? { continueMode: parsed.continueMode } : {}),
|
|
625
602
|
forwardArgs: parsed.forwardArgs,
|
|
626
603
|
// isatty, not process.stdin: touching the stream opens it, and a config
|
|
@@ -638,10 +615,10 @@ export async function resolveRunOptions(
|
|
|
638
615
|
if (outright !== undefined) opts.selectedOutright = outright
|
|
639
616
|
if (staged !== undefined) opts.staged = staged
|
|
640
617
|
if (discovered !== undefined) opts.discovered = discovered
|
|
641
|
-
if (parsed.retries !== undefined) opts.retries = parsed.retries
|
|
642
|
-
if (parsed.timeout !== undefined) opts.timeout = parsed.timeout
|
|
643
618
|
if (parsed.cacheDir !== undefined) opts.cacheDir = parsed.cacheDir
|
|
644
|
-
|
|
619
|
+
for (const [path, field] of SETTING_OPTIONS) {
|
|
620
|
+
if (parsed.settings[path] !== undefined) Object.assign(opts, { [field]: parsed.settings[path] })
|
|
621
|
+
}
|
|
645
622
|
if (parsed.summarize !== undefined) opts.summarize = parsed.summarize
|
|
646
623
|
if (parsed.format === 'json') opts.json = true
|
|
647
624
|
if (parsed.profile !== undefined) opts.profile = parsed.profile
|
|
@@ -691,10 +668,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
691
668
|
process.stderr.write(`${refusal.message}\n`)
|
|
692
669
|
return 1
|
|
693
670
|
}
|
|
694
|
-
const load = {
|
|
695
|
-
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
696
|
-
...(parsed.frozen ? { frozen: true } : {}),
|
|
697
|
-
}
|
|
671
|
+
const load = parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}
|
|
698
672
|
// `--filter` / `--affected` scope the menu, as they scope a bare task:
|
|
699
673
|
// the menu listed every project and the anchored pick ran outside it.
|
|
700
674
|
const scope = await scopeFilters(parsed, cwd)
|
|
@@ -843,6 +817,11 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
843
817
|
}
|
|
844
818
|
}
|
|
845
819
|
}
|
|
846
|
-
if (summary.json !== undefined) process.stdout.write(`${JSON.stringify(summary.json
|
|
820
|
+
if (summary.json !== undefined) process.stdout.write(`${JSON.stringify(summary.json)}\n`)
|
|
847
821
|
return summary.ok ? 0 : 1
|
|
848
822
|
}
|
|
823
|
+
|
|
824
|
+
/** CI=0 and CI=false are "not CI": several vendors set CI=true. */
|
|
825
|
+
function onCI(v: string | undefined): boolean {
|
|
826
|
+
return v !== undefined && v !== '' && v !== '0' && v !== 'false'
|
|
827
|
+
}
|
package/src/cli/select.ts
CHANGED
|
@@ -14,11 +14,9 @@ import {
|
|
|
14
14
|
buildPackageGraph,
|
|
15
15
|
defaultAffectedBase,
|
|
16
16
|
findWorkspaceRoot,
|
|
17
|
-
type LoadReads,
|
|
18
17
|
type FingerprintClaims,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
loadWorkspace,
|
|
18
|
+
lockKey,
|
|
19
|
+
NO_LOCK,
|
|
22
20
|
parseFilter,
|
|
23
21
|
type ProjectMeta,
|
|
24
22
|
readLockfile,
|
|
@@ -49,15 +47,15 @@ import {
|
|
|
49
47
|
loadCliProjects,
|
|
50
48
|
loadCliWorkspace,
|
|
51
49
|
} from './workspace-config.js'
|
|
50
|
+
import { resolveSetting } from '../settings.js'
|
|
52
51
|
|
|
53
52
|
/**
|
|
54
53
|
* The projects whose tasks declare a `cache.inputs.workspaceFiles` glob
|
|
55
54
|
* matching a changed path, one inside another project included (item
|
|
56
55
|
* 954): `--affected` selects them, since the glob is that task's input. Through the run
|
|
57
56
|
* path's staged load, so a glob a `project` plugin gave a config-less
|
|
58
|
-
* package counts;
|
|
59
|
-
*
|
|
60
|
-
* do load, one by one — a broken out-of-scope config does not fail a
|
|
57
|
+
* package counts; read from the lock as a run does. A load that fails drops to the lock's
|
|
58
|
+
* entries, one by one — a project the lock does not hold does not fail a
|
|
61
59
|
* scoped run, and must not fail its selection either.
|
|
62
60
|
*/
|
|
63
61
|
export async function workspaceGlobOwners(
|
|
@@ -85,26 +83,23 @@ export async function workspaceGlobOwners(
|
|
|
85
83
|
} catch {
|
|
86
84
|
// Fall through to the per-file sweep.
|
|
87
85
|
}
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (load.frozen === true && (await readLockfile(root)) === null) {
|
|
94
|
-
throw new UserError(FROZEN_WITHOUT_LOCK)
|
|
95
|
-
}
|
|
86
|
+
// No lock is refused here, before the tolerant sweep below could answer
|
|
87
|
+
// "nothing affected" and exit 0 without ever reaching the run's own
|
|
88
|
+
// refusal. The sweep reads each entry from the lock: it never evaluates.
|
|
89
|
+
const lock = await readLockfile(root)
|
|
90
|
+
if (lock === null) throw new UserError(NO_LOCK)
|
|
96
91
|
const owners: string[] = []
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
92
|
+
for (const meta of projects) {
|
|
93
|
+
if (meta.configPath === null || meta.configPath === '') continue
|
|
94
|
+
const dir = lockKey(root, meta.dir)
|
|
95
|
+
if (!Object.hasOwn(lock.projects, dir)) continue
|
|
96
|
+
try {
|
|
97
|
+
if (declaresMatch(lock.projects[dir]!.config)) owners.push(meta.name)
|
|
98
|
+
} catch {
|
|
99
|
+
// A hand-edited entry that will not read cannot be shown to declare a
|
|
100
|
+
// matching glob; surfacing it here would fail a run the load tolerates.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
108
103
|
return owners
|
|
109
104
|
}
|
|
110
105
|
|
|
@@ -133,9 +128,7 @@ async function workspaceFingerprintClaims(
|
|
|
133
128
|
}
|
|
134
129
|
|
|
135
130
|
async function loadWorkspaceProjects(cwd: string): Promise<ProjectMeta[]> {
|
|
136
|
-
|
|
137
|
-
const root = await findWorkspaceRoot(cwd, reads)
|
|
138
|
-
return await discoverCliProjects(await loadWorkspace(root, reads))
|
|
131
|
+
return await discoverCliProjects(await findWorkspaceRoot(cwd))
|
|
139
132
|
}
|
|
140
133
|
|
|
141
134
|
export async function findCwdProject(cwd: string): Promise<string | null> {
|
|
@@ -150,9 +143,8 @@ export async function findCwdProject(cwd: string): Promise<string | null> {
|
|
|
150
143
|
export async function findCwdSelection(
|
|
151
144
|
cwd: string,
|
|
152
145
|
): Promise<{ name: string; discovered: { root: string; projects: ProjectMeta[] } } | null> {
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const projects = await discoverCliProjects(await loadWorkspace(root, reads))
|
|
146
|
+
const root = await findWorkspaceRoot(cwd)
|
|
147
|
+
const projects = await discoverCliProjects(root)
|
|
156
148
|
const abs = path.resolve(cwd)
|
|
157
149
|
const within = (dirOf: (p: ProjectMeta) => string): string | null => {
|
|
158
150
|
let best: string | null = null
|
|
@@ -251,14 +243,17 @@ function refusalOf(err: unknown): Refusal {
|
|
|
251
243
|
|
|
252
244
|
/**
|
|
253
245
|
* The filter `--affected[=<base>]` stands for: `...[<base>]`, the changed
|
|
254
|
-
* projects and their dependents. An empty base is
|
|
255
|
-
*
|
|
246
|
+
* projects and their dependents. An empty base is `run.affectedBase`
|
|
247
|
+
* (its variable, then the workspace's, which a plugin's `config` stage may set from nx.json's
|
|
256
248
|
* `defaultBase` or `TURBO_SCM_BASE`), then the guess.
|
|
257
249
|
*/
|
|
258
250
|
export async function affectedFilterFor(cwd: string, affected: string): Promise<string | Refusal> {
|
|
259
251
|
const root = await findWorkspaceRoot(cwd)
|
|
260
252
|
let base = affected
|
|
261
|
-
if (base === '')
|
|
253
|
+
if (base === '') {
|
|
254
|
+
const ws = (await loadCliWorkspace(root)).workspaceConfig
|
|
255
|
+
base = (resolveSetting('run.affectedBase', ws).value as string | undefined) ?? ''
|
|
256
|
+
}
|
|
262
257
|
if (base === '') {
|
|
263
258
|
try {
|
|
264
259
|
base = await defaultAffectedBase(root)
|
|
@@ -339,7 +334,6 @@ export async function resolveFilters(
|
|
|
339
334
|
workspaceGlobOwners: (changed: readonly string[], nested: readonly string[]) =>
|
|
340
335
|
workspaceGlobOwners(root, projects, changed, load, stagedOnce, nested),
|
|
341
336
|
fingerprintClaims: () => workspaceFingerprintClaims(root, projects, load),
|
|
342
|
-
taskEdges: async () => edges ?? taskEdgesFrom(await stagedOnce()),
|
|
343
337
|
...(git !== undefined ? { untracked: async () => (await git.start()).untracked } : {}),
|
|
344
338
|
}
|
|
345
339
|
let names: Set<string>
|
|
@@ -524,7 +518,7 @@ export async function pickTask(
|
|
|
524
518
|
}
|
|
525
519
|
if (entries.length === 0) {
|
|
526
520
|
process.stderr.write(
|
|
527
|
-
'vx run: no tasks declared in any project; declare one under `tasks` in a vx.config, or run `vx init` to write them from package.json scripts
|
|
521
|
+
'vx run: no tasks declared in any project; declare one under `tasks` in a vx.config, or run `vx init` to write them from package.json scripts, then `vx lock`\n',
|
|
528
522
|
)
|
|
529
523
|
return null
|
|
530
524
|
}
|
|
@@ -604,7 +598,7 @@ function didYouMeanProject(
|
|
|
604
598
|
projects: Iterable<{ name: string }>,
|
|
605
599
|
): string {
|
|
606
600
|
const names = [...projects].map((p) => p.name)
|
|
607
|
-
// A scoped name typed without its scope (`vx-
|
|
601
|
+
// A scoped name typed without its scope (`vx-otel` for `@vzn/vx-otel`) is
|
|
608
602
|
// many edits from the whole name and none from the part after the `/`.
|
|
609
603
|
const byBare = new Map<string, string[]>()
|
|
610
604
|
for (const n of names) {
|
|
@@ -615,7 +609,7 @@ function didYouMeanProject(
|
|
|
615
609
|
const typed = pattern.replace(/^!|\.\.\.$|^\.\.\.|\^/g, '')
|
|
616
610
|
// Two edits from any two-letter name: `--filter //` hinted `ui` (X-13).
|
|
617
611
|
if (typed === '//') {
|
|
618
|
-
return
|
|
612
|
+
return '. `//` is the workspace root, which is no project here: add a vx.config there and run `vx lock`'
|
|
619
613
|
}
|
|
620
614
|
const best = nearest(typed, names)
|
|
621
615
|
if (best !== undefined) return `. Did you mean ${best}?`
|