@vzn/vx 0.0.636 → 0.0.638
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -11
- package/docs/caching.md +246 -213
- package/docs/cli.md +791 -705
- package/docs/execution.md +68 -76
- package/docs/features.md +47 -42
- package/docs/patterns.md +26 -25
- package/docs/schema.md +392 -205
- 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 +4 -4
- package/schemas/show.json +5 -3
- package/schemas/why.json +15 -1
- package/skills/vx/SKILL.md +28 -13
- package/src/bin.ts +30 -9
- package/src/cache/archive.ts +11 -0
- package/src/cache/cache.ts +124 -53
- package/src/cache/chained-cache.ts +4 -0
- package/src/cache/git-inputs.ts +277 -63
- package/src/cache/index.ts +15 -1
- package/src/cache/inputs.ts +102 -36
- package/src/cache/key-fold.ts +8 -7
- package/src/cache/layer.ts +15 -18
- package/src/cache/layered-cache.ts +20 -102
- package/src/cache/plugin-cache.ts +248 -0
- package/src/cache/policy.ts +32 -27
- package/src/cache/run-history.ts +104 -14
- package/src/cache/run-store.ts +77 -0
- package/src/cache/schema.ts +58 -31
- package/src/cache/scoped-cache.ts +237 -0
- package/src/cli/cache.ts +173 -34
- package/src/cli/completions.ts +11 -7
- package/src/cli/core-alias.ts +1 -1
- package/src/cli/foreign-flags.ts +13 -12
- package/src/cli/help.ts +110 -70
- package/src/cli/index.ts +67 -24
- package/src/cli/info.ts +43 -27
- package/src/cli/init.ts +115 -407
- package/src/cli/last.ts +157 -9
- package/src/cli/lock.ts +356 -122
- package/src/cli/plan-format.ts +7 -5
- package/src/cli/plugin-commands.ts +81 -14
- package/src/cli/plugin-templates.ts +11 -11
- package/src/cli/run.ts +116 -160
- package/src/cli/select.ts +50 -47
- package/src/cli/show.ts +83 -59
- package/src/cli/task-verb.ts +6 -13
- package/src/cli/upgrade.ts +415 -149
- package/src/cli/watch-cycle.ts +9 -2
- package/src/cli/watch-filter.ts +39 -61
- package/src/cli/watch-fs.ts +29 -16
- package/src/cli/watch-judge.ts +18 -4
- package/src/cli/watch-set.ts +16 -42
- package/src/cli/watch.ts +227 -230
- package/src/cli/why.ts +38 -18
- package/src/cli/workspace-config.ts +33 -70
- package/src/config.ts +169 -70
- package/src/exec/env.ts +1 -31
- package/src/exec/executor.ts +1 -5
- package/src/exec/kill-tree.ts +2 -1
- package/src/exec/sandbox-runtime.ts +12 -1
- package/src/exec/sandbox-violations.ts +9 -1
- package/src/graph/dependency-spec.ts +1 -1
- package/src/graph/index.ts +1 -0
- package/src/graph/priorities.ts +1 -1
- package/src/graph/scheduler.ts +27 -7
- package/src/graph/task-graph.ts +76 -24
- package/src/handoff.ts +169 -0
- package/src/index.ts +35 -58
- package/src/orchestrator/affected-tasks.ts +15 -8
- 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 +147 -69
- package/src/orchestrator/failure-mode.ts +19 -10
- package/src/orchestrator/fingerprint-watch.ts +18 -15
- package/src/orchestrator/forecast.ts +10 -2
- package/src/orchestrator/framed-output.ts +9 -5
- package/src/orchestrator/graph-stage.ts +5 -5
- package/src/orchestrator/history.ts +126 -30
- package/src/orchestrator/hit-restore.ts +15 -3
- package/src/orchestrator/index.ts +20 -5
- package/src/orchestrator/local-shortcircuit.ts +2 -1
- package/src/orchestrator/lockfile-claim.ts +79 -20
- package/src/orchestrator/logger.ts +80 -41
- package/src/orchestrator/metrics.ts +78 -9
- package/src/orchestrator/miss-save.ts +3 -0
- package/src/orchestrator/options.ts +42 -29
- package/src/orchestrator/output-host.ts +72 -0
- package/src/orchestrator/plan.ts +6 -8
- package/src/orchestrator/plugin-host.ts +100 -66
- package/src/orchestrator/plugin.ts +173 -67
- package/src/orchestrator/prepare.ts +163 -78
- package/src/orchestrator/projects.ts +105 -99
- package/src/orchestrator/remote-prefetch.ts +11 -6
- 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-records.ts +1 -1
- package/src/orchestrator/run-report.ts +9 -9
- package/src/orchestrator/run-settings.ts +48 -0
- package/src/orchestrator/run.ts +244 -119
- 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 +49 -7
- package/src/orchestrator/summary.ts +115 -106
- package/src/orchestrator/task-hash.ts +12 -83
- package/src/orchestrator/telemetry.ts +4 -4
- package/src/settings.ts +428 -0
- package/src/{orchestrator → util}/colors.ts +5 -0
- package/src/util/config-frame.ts +31 -2
- package/src/util/errors.ts +33 -1
- package/src/util/essential-env.ts +31 -0
- package/src/util/hide-env.ts +107 -0
- package/src/util/index.ts +16 -3
- package/src/util/paths.ts +10 -0
- package/src/util/refusal-notice.ts +111 -0
- package/src/util/report.ts +31 -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/ui.ts +186 -0
- package/src/util/ulid.ts +1 -1
- package/src/util/verbs.ts +0 -15
- package/src/workspace/affected.ts +84 -232
- package/src/workspace/config-eval.ts +15 -20
- package/src/workspace/config-imports.ts +78 -3
- package/src/workspace/config-schema.ts +209 -268
- package/src/workspace/dependency-rules.ts +65 -0
- package/src/workspace/filter.ts +2 -2
- package/src/workspace/fingerprint.ts +11 -121
- package/src/workspace/index.ts +20 -27
- package/src/workspace/load-reads.ts +4 -4
- package/src/workspace/lockfile.ts +96 -140
- package/src/workspace/migration.ts +307 -215
- 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 +363 -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
|
@@ -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, sayNotice, sayLine } 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,32 +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
|
-
/** `--cache-scope <name>`: this run's `
|
|
67
|
+
/** `--cache-scope <name>`: this run's `cache.scope`, above `VX_CACHE_SCOPE` and the workspace's. */
|
|
56
68
|
cacheScope?: string
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
* `--retry <n>` / `--retry=<n>`: run-level retry default for tasks
|
|
60
|
-
* without their own `exec.retries`. Undefined when not passed.
|
|
61
|
-
*/
|
|
62
|
-
retries: number | undefined
|
|
63
|
-
/**
|
|
64
|
-
* `--timeout <ms>` / `--timeout=<ms>`: run-level default task timeout for
|
|
65
|
-
* tasks without their own `exec.timeout`. Sits above the `VX_TASK_TIMEOUT`
|
|
66
|
-
* env and workspace `timeout` defaults. Undefined when not passed.
|
|
67
|
-
*/
|
|
68
|
-
timeout: number | undefined
|
|
69
|
-
outputLogs?: 'full' | 'errors-only' | 'none' | 'hash-only'
|
|
70
|
-
download?: 'all' | 'toplevel' | 'none'
|
|
69
|
+
/** `vx watch --poll` (`watch.poll`). */
|
|
70
|
+
poll?: true
|
|
71
71
|
forwardArgs: string[]
|
|
72
72
|
verbosity: number
|
|
73
73
|
dry: 'text' | 'json' | undefined
|
|
@@ -100,25 +100,42 @@ export interface RunArgs {
|
|
|
100
100
|
// quoted literal, so the doc-drift test does not count it as an accepted flag.
|
|
101
101
|
const RETIRED_EXCLUDE_DEPENDENCIES = /^--excludeDependencies(=|$)/
|
|
102
102
|
|
|
103
|
-
/**
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 }
|
|
117
121
|
}
|
|
118
|
-
|
|
119
|
-
return n === null || n < 1 ? null : n
|
|
122
|
+
return undefined
|
|
120
123
|
}
|
|
121
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
|
+
|
|
122
139
|
/** `verb` is the one being parsed for: `vx watch` reads `vx run`'s flags, and
|
|
123
140
|
* its flag hints named `vx run` (M-58). The caller appends the help pointer. */
|
|
124
141
|
export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' = 'run'): RunArgs {
|
|
@@ -127,12 +144,9 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
127
144
|
filters: [],
|
|
128
145
|
all: false,
|
|
129
146
|
excludeDependencies: [],
|
|
130
|
-
|
|
147
|
+
settings: {},
|
|
131
148
|
cache: { ...FULL_CACHE_POLICY },
|
|
132
149
|
cacheDir: undefined,
|
|
133
|
-
frozen: false,
|
|
134
|
-
retries: undefined,
|
|
135
|
-
timeout: undefined,
|
|
136
150
|
forwardArgs: [],
|
|
137
151
|
verbosity: 0,
|
|
138
152
|
dry: undefined,
|
|
@@ -155,9 +169,9 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
155
169
|
out.forwardArgs = sepIdx === -1 ? [] : args.slice(sepIdx + 1)
|
|
156
170
|
|
|
157
171
|
// Cache policy is resolved AFTER the loop in precedence order:
|
|
158
|
-
// start all-true → apply each `--cache` spec → `--no-cache` forces
|
|
172
|
+
// start all-true → apply each `--cache-policy` spec → `--no-cache` forces
|
|
159
173
|
// all false → `--force` forces both reads false. So `--no-cache`
|
|
160
|
-
// beats `--force`, and `--cache` is the base both may override.
|
|
174
|
+
// beats `--force`, and `--cache-policy` is the base both may override.
|
|
161
175
|
let cachePolicy: CachePolicy = { ...FULL_CACHE_POLICY }
|
|
162
176
|
let cacheSpecError: string | undefined
|
|
163
177
|
let cacheFlag = false
|
|
@@ -175,20 +189,6 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
175
189
|
error: `--filter requires a value (a project name, glob or path, e.g. --filter app)`,
|
|
176
190
|
}
|
|
177
191
|
out.filters.push(v)
|
|
178
|
-
} else if (a === '--concurrency' || a?.startsWith('--concurrency=')) {
|
|
179
|
-
const v = a === '--concurrency' ? before[++i] : a.slice('--concurrency='.length)
|
|
180
|
-
if (v === undefined)
|
|
181
|
-
return {
|
|
182
|
-
...out,
|
|
183
|
-
error: `--concurrency requires a value (a positive integer, or a share of the cores such as 50%)`,
|
|
184
|
-
}
|
|
185
|
-
const n = parseConcurrency(v)
|
|
186
|
-
if (n === null)
|
|
187
|
-
return {
|
|
188
|
-
...out,
|
|
189
|
-
error: `--concurrency must be a positive integer, or a share of the cores such as 50% (got ${v})`,
|
|
190
|
-
}
|
|
191
|
-
out.concurrency = n
|
|
192
192
|
} else if (a === '--all') {
|
|
193
193
|
out.all = true
|
|
194
194
|
} else if (RETIRED_EXCLUDE_DEPENDENCIES.test(a ?? '')) {
|
|
@@ -204,7 +204,7 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
204
204
|
// bare flag) and "drop none" (an empty list) are equally defensible
|
|
205
205
|
// readings, and silently picking either does the opposite of what
|
|
206
206
|
// half the callers mean. Reject and name both explicit forms; the
|
|
207
|
-
// sibling value flags (--
|
|
207
|
+
// sibling value flags (--retries=, --timeout=, --cache-dir=)
|
|
208
208
|
// reject an empty `=` value the same way.
|
|
209
209
|
if (raw === '') {
|
|
210
210
|
return {
|
|
@@ -216,60 +216,6 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
216
216
|
.split(',')
|
|
217
217
|
.map((s) => s.trim())
|
|
218
218
|
.filter((s) => s.length > 0)
|
|
219
|
-
} else if (a === '--frozen') {
|
|
220
|
-
out.frozen = true
|
|
221
|
-
} else if (a === '--retry' || a?.startsWith('--retry=')) {
|
|
222
|
-
const v = a === '--retry' ? before[++i] : a.slice('--retry='.length)
|
|
223
|
-
if (v === undefined)
|
|
224
|
-
return { ...out, error: `--retry requires a value (a non-negative integer)` }
|
|
225
|
-
const n = parseDecimalInt(v)
|
|
226
|
-
if (n === null) {
|
|
227
|
-
return { ...out, error: `--retry must be a non-negative integer (got ${v})` }
|
|
228
|
-
}
|
|
229
|
-
out.retries = n
|
|
230
|
-
} else if (a === '--timeout' || a?.startsWith('--timeout=')) {
|
|
231
|
-
const v = a === '--timeout' ? before[++i] : a.slice('--timeout='.length)
|
|
232
|
-
if (v === undefined)
|
|
233
|
-
return { ...out, error: `--timeout requires a value (a positive integer, in ms)` }
|
|
234
|
-
const n = parseDecimalInt(v)
|
|
235
|
-
if (n !== null && n > MAX_TIMEOUT_MS) {
|
|
236
|
-
// Not "effectively no limit" — setTimeout reduces a delay past 2^31-1
|
|
237
|
-
// to 1 ms, so this would kill every task the moment it spawned.
|
|
238
|
-
return {
|
|
239
|
-
...out,
|
|
240
|
-
error:
|
|
241
|
-
`--timeout ${n} exceeds the maximum timer delay (${MAX_TIMEOUT_MS} ms, ~24.8 days); ` +
|
|
242
|
-
`larger values are silently reduced to 1 ms. Omit --timeout for no limit.`,
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
if (n === null || n <= 0) {
|
|
246
|
-
return { ...out, error: `--timeout must be a positive integer in ms (got ${v})` }
|
|
247
|
-
}
|
|
248
|
-
out.timeout = n
|
|
249
|
-
} else if (a === '--output-logs' || a?.startsWith('--output-logs=')) {
|
|
250
|
-
const v = a === '--output-logs' ? before[++i] : a.slice('--output-logs='.length)
|
|
251
|
-
if (v === undefined || v === '') {
|
|
252
|
-
return {
|
|
253
|
-
...out,
|
|
254
|
-
error: `--output-logs requires a value (full, errors-only, hash-only, or none)`,
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (v !== 'full' && v !== 'errors-only' && v !== 'none' && v !== 'hash-only') {
|
|
258
|
-
return {
|
|
259
|
-
...out,
|
|
260
|
-
error: `--output-logs must be full, errors-only, hash-only, or none (got ${v})`,
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
out.outputLogs = v
|
|
264
|
-
} else if (a === '--download' || a?.startsWith('--download=')) {
|
|
265
|
-
const v = a === '--download' ? before[++i] : a.slice('--download='.length)
|
|
266
|
-
if (v === undefined || v === '') {
|
|
267
|
-
return { ...out, error: `--download requires a value (all, toplevel, or none)` }
|
|
268
|
-
}
|
|
269
|
-
if (v !== 'all' && v !== 'toplevel' && v !== 'none') {
|
|
270
|
-
return { ...out, error: `--download must be all, toplevel, or none (got ${v})` }
|
|
271
|
-
}
|
|
272
|
-
out.download = v
|
|
273
219
|
} else if (a === '--cache-dir' || a?.startsWith('--cache-dir=')) {
|
|
274
220
|
const v = a === '--cache-dir' ? before[++i] : a.slice('--cache-dir='.length)
|
|
275
221
|
if (v === undefined || v === '') return { ...out, error: `--cache-dir requires a path` }
|
|
@@ -290,15 +236,16 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
290
236
|
if (v === undefined || !isCacheScope(v))
|
|
291
237
|
return { ...out, error: cacheScopeError('--cache-scope') }
|
|
292
238
|
out.cacheScope = v
|
|
293
|
-
} else if (a
|
|
239
|
+
} else if (flagOf(a, '--cache-policy')) {
|
|
294
240
|
cacheFlag = true
|
|
295
|
-
const
|
|
241
|
+
const name = flagOf(a, '--cache-policy')!
|
|
242
|
+
const v = a === name ? before[++i] : a!.slice(name.length + 1)
|
|
296
243
|
if (v === undefined)
|
|
297
244
|
return {
|
|
298
245
|
...out,
|
|
299
|
-
error:
|
|
246
|
+
error: `${name} requires a value (a spec like local:r, local:rw, remote:, or local:,remote:rw)`,
|
|
300
247
|
}
|
|
301
|
-
// A spec with no segment at all applies NOTHING, so `--cache=`
|
|
248
|
+
// A spec with no segment at all applies NOTHING, so `--cache-policy=`
|
|
302
249
|
// (an empty shell var, or someone reaching for "no cache") left
|
|
303
250
|
// all four axes ON — the opposite of the intent. Reject it; an
|
|
304
251
|
// empty flag list for a NAMED layer (`local:`) stays valid and
|
|
@@ -306,14 +253,14 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
306
253
|
if (!v.split(',').some((seg) => seg.trim().length > 0)) {
|
|
307
254
|
return {
|
|
308
255
|
...out,
|
|
309
|
-
error:
|
|
256
|
+
error: `${name} needs a spec like local:r, local:rw, remote:, or local:,remote:rw — pass --no-cache to disable every axis`,
|
|
310
257
|
}
|
|
311
258
|
}
|
|
312
259
|
try {
|
|
313
260
|
cachePolicy = parseCachePolicy(v, cachePolicy)
|
|
314
261
|
if (/remote:[rw]/.test(v)) out.remoteRequested = true
|
|
315
262
|
} catch (err) {
|
|
316
|
-
cacheSpecError = err instanceof Error ? err.message : String(err)
|
|
263
|
+
cacheSpecError = `--cache-policy: ${err instanceof Error ? err.message : String(err)}`
|
|
317
264
|
}
|
|
318
265
|
} else if (a === '--no-cache') {
|
|
319
266
|
noCacheFlag = true
|
|
@@ -406,6 +353,16 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
406
353
|
return { ...out, error: `--report must be markdown (got ${fmt})` }
|
|
407
354
|
}
|
|
408
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
|
|
409
366
|
} else if (a !== undefined && a.startsWith('-')) {
|
|
410
367
|
return { ...out, error: `unknown flag: ${a}${flagHint(verb, a)}` }
|
|
411
368
|
} else if (a !== undefined) {
|
|
@@ -416,30 +373,32 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
|
|
|
416
373
|
if (cacheSpecError !== undefined) {
|
|
417
374
|
return { ...out, error: cacheSpecError }
|
|
418
375
|
}
|
|
419
|
-
// `
|
|
420
|
-
const envSpec = process.env['
|
|
376
|
+
// `VX_CACHE_POLICY` is the spec a run takes when no flag names one.
|
|
377
|
+
const envSpec = process.env['VX_CACHE_POLICY'] || undefined
|
|
421
378
|
if (!cacheFlag && envSpec !== undefined) {
|
|
422
379
|
if (!envSpec.split(',').some((seg) => seg.trim().length > 0))
|
|
423
380
|
return {
|
|
424
381
|
...out,
|
|
425
|
-
error: `
|
|
382
|
+
error: `VX_CACHE_POLICY needs a spec like local:r, local:rw, remote:, or local:,remote:rw`,
|
|
426
383
|
}
|
|
427
384
|
try {
|
|
428
385
|
cachePolicy = parseCachePolicy(envSpec, cachePolicy)
|
|
429
386
|
} catch (err) {
|
|
430
|
-
return {
|
|
387
|
+
return {
|
|
388
|
+
...out,
|
|
389
|
+
error: `VX_CACHE_POLICY: ${err instanceof Error ? err.message : String(err)}`,
|
|
390
|
+
}
|
|
431
391
|
}
|
|
432
392
|
if (/remote:[rw]/.test(envSpec)) out.remoteRequested = true
|
|
433
393
|
}
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (forceFlag) {
|
|
440
|
-
cachePolicy = { ...cachePolicy, localRead: false, remoteRead: false }
|
|
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 } : {}),
|
|
441
399
|
}
|
|
442
|
-
out.cache = cachePolicy
|
|
400
|
+
out.cache = applyCacheFlags(cachePolicy, flags)
|
|
401
|
+
if (!cacheFlag && envSpec === undefined) out.cacheFlags = flags
|
|
443
402
|
|
|
444
403
|
if (out.dry !== undefined && out.graph !== undefined) {
|
|
445
404
|
return { ...out, error: '--dry and --graph are mutually exclusive' }
|
|
@@ -495,9 +454,6 @@ export function detectFlow(
|
|
|
495
454
|
}
|
|
496
455
|
|
|
497
456
|
/** `CI=0` / `CI=false` are not CI, as the output flow reads them. */
|
|
498
|
-
function onCI(v: string | undefined): boolean {
|
|
499
|
-
return v !== undefined && v !== '' && v !== '0' && v !== 'false'
|
|
500
|
-
}
|
|
501
457
|
|
|
502
458
|
/** The filters a run's scope comes from: `--filter`s, with `--affected` as its `...[<base>]`. */
|
|
503
459
|
async function scopeFilters(
|
|
@@ -585,10 +541,7 @@ export async function resolveRunOptions(
|
|
|
585
541
|
? await resolveFilters(
|
|
586
542
|
cwd,
|
|
587
543
|
filterStrings,
|
|
588
|
-
{
|
|
589
|
-
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
590
|
-
...(parsed.frozen ? { frozen: true } : {}),
|
|
591
|
-
},
|
|
544
|
+
parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {},
|
|
592
545
|
affectedFilter,
|
|
593
546
|
)
|
|
594
547
|
: undefined
|
|
@@ -603,7 +556,7 @@ export async function resolveRunOptions(
|
|
|
603
556
|
// scope. An empty project list keeps the bare tasks unselected
|
|
604
557
|
// while the anchored ones still run.
|
|
605
558
|
if (anchoredTasks.length === 0) return { nothingSelected: resolved.empty }
|
|
606
|
-
|
|
559
|
+
sayNotice('info', 'vx run', `${resolved.empty} — running ${anchoredTasks.join(', ')} only`)
|
|
607
560
|
projects = []
|
|
608
561
|
} else {
|
|
609
562
|
projects = resolved.names
|
|
@@ -633,19 +586,18 @@ export async function resolveRunOptions(
|
|
|
633
586
|
const opts: RunOptions = {
|
|
634
587
|
cwd,
|
|
635
588
|
tasks: [...tasks],
|
|
636
|
-
|
|
589
|
+
...(parsed.cacheFlags !== undefined
|
|
590
|
+
? { cacheFlags: parsed.cacheFlags }
|
|
591
|
+
: { cache: parsed.cache }),
|
|
637
592
|
...(parsed.remoteRequested ? { remoteRequested: true } : {}),
|
|
638
593
|
...(parsed.cacheScope !== undefined ? { cacheScope: parsed.cacheScope } : {}),
|
|
639
594
|
// A laptop reads the shared cache and never writes it (owner,
|
|
640
|
-
// 2026-10-03); naming the remote in `--cache` is asking to.
|
|
595
|
+
// 2026-10-03); naming the remote in `--cache-policy` is asking to.
|
|
641
596
|
...(!parsed.remoteRequested && !onCI(process.env['CI'])
|
|
642
|
-
? {
|
|
597
|
+
? { remoteReadOnlyByDefault: true }
|
|
643
598
|
: {}),
|
|
644
599
|
flow: detectFlow(parsed),
|
|
645
|
-
...(parsed.frozen ? { frozen: true } : {}),
|
|
646
|
-
...(parsed.outputLogs !== undefined ? { outputLogs: parsed.outputLogs } : {}),
|
|
647
600
|
...(parsed.verbosity > 0 ? { summaryTable: true } : {}),
|
|
648
|
-
...(parsed.download !== undefined ? { download: parsed.download } : {}),
|
|
649
601
|
...(parsed.continueMode !== undefined ? { continueMode: parsed.continueMode } : {}),
|
|
650
602
|
forwardArgs: parsed.forwardArgs,
|
|
651
603
|
// isatty, not process.stdin: touching the stream opens it, and a config
|
|
@@ -663,10 +615,10 @@ export async function resolveRunOptions(
|
|
|
663
615
|
if (outright !== undefined) opts.selectedOutright = outright
|
|
664
616
|
if (staged !== undefined) opts.staged = staged
|
|
665
617
|
if (discovered !== undefined) opts.discovered = discovered
|
|
666
|
-
if (parsed.retries !== undefined) opts.retries = parsed.retries
|
|
667
|
-
if (parsed.timeout !== undefined) opts.timeout = parsed.timeout
|
|
668
618
|
if (parsed.cacheDir !== undefined) opts.cacheDir = parsed.cacheDir
|
|
669
|
-
|
|
619
|
+
for (const [path, field] of SETTING_OPTIONS) {
|
|
620
|
+
if (parsed.settings[path] !== undefined) Object.assign(opts, { [field]: parsed.settings[path] })
|
|
621
|
+
}
|
|
670
622
|
if (parsed.summarize !== undefined) opts.summarize = parsed.summarize
|
|
671
623
|
if (parsed.format === 'json') opts.json = true
|
|
672
624
|
if (parsed.profile !== undefined) opts.profile = parsed.profile
|
|
@@ -713,13 +665,10 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
713
665
|
// Every run needs git: refused here, not after the user has chosen.
|
|
714
666
|
const refusal = gitRefusal(await findWorkspaceRoot(cwd))
|
|
715
667
|
if (refusal !== undefined) {
|
|
716
|
-
|
|
668
|
+
sayLine('error', refusal.message, 'vx run')
|
|
717
669
|
return 1
|
|
718
670
|
}
|
|
719
|
-
const load = {
|
|
720
|
-
...(parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}),
|
|
721
|
-
...(parsed.frozen ? { frozen: true } : {}),
|
|
722
|
-
}
|
|
671
|
+
const load = parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {}
|
|
723
672
|
// `--filter` / `--affected` scope the menu, as they scope a bare task:
|
|
724
673
|
// the menu listed every project and the anchored pick ran outside it.
|
|
725
674
|
const scope = await scopeFilters(parsed, cwd)
|
|
@@ -730,7 +679,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
730
679
|
if ('error' in selected)
|
|
731
680
|
return refuse('run', args, selected.error, selected.code ?? 'VX_E_USAGE')
|
|
732
681
|
if ('empty' in selected) {
|
|
733
|
-
|
|
682
|
+
sayNotice('info', 'vx run', selected.empty)
|
|
734
683
|
return 0
|
|
735
684
|
}
|
|
736
685
|
only = new Set(selected.names)
|
|
@@ -746,7 +695,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
746
695
|
const resolved = await resolveRunOptions(parsed, cwd, tasks, 'run', scopeOut)
|
|
747
696
|
if ('error' in resolved) return refuse('run', args, resolved.error, resolved.code ?? 'VX_E_USAGE')
|
|
748
697
|
if ('nothingSelected' in resolved) {
|
|
749
|
-
|
|
698
|
+
sayNotice('info', 'vx run', resolved.nothingSelected)
|
|
750
699
|
return 0
|
|
751
700
|
}
|
|
752
701
|
const opts = resolved
|
|
@@ -766,8 +715,10 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
766
715
|
if (parsed.dry !== undefined || parsed.graph !== undefined) {
|
|
767
716
|
const plan = await planRun(opts)
|
|
768
717
|
if (plan.unresolvedTasks !== undefined && plan.unresolvedTasks.length > 0) {
|
|
769
|
-
|
|
770
|
-
|
|
718
|
+
sayNotice(
|
|
719
|
+
'error',
|
|
720
|
+
'vx run',
|
|
721
|
+
`no projects declare task(s): ${plan.unresolvedTasks.join(', ')}.${plan.unresolvedHint ?? ''}`,
|
|
771
722
|
)
|
|
772
723
|
return 1
|
|
773
724
|
}
|
|
@@ -775,14 +726,14 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
775
726
|
// The run's own line: a dry run that guessed from the scope alone
|
|
776
727
|
// said no affected project declared a task one did (X-141).
|
|
777
728
|
if (plan.noneAffected !== undefined) {
|
|
778
|
-
|
|
729
|
+
sayLine('info', plan.noneAffected, 'vx run')
|
|
779
730
|
return 0
|
|
780
731
|
}
|
|
781
732
|
if (opts.selectedByDiff === true) {
|
|
782
|
-
|
|
733
|
+
sayNotice('info', 'vx run', `no affected project declares task(s): ${tasks.join(', ')}`)
|
|
783
734
|
return 0
|
|
784
735
|
}
|
|
785
|
-
|
|
736
|
+
sayNotice('error', 'vx run', `no projects declare task(s): ${tasks.join(', ')}`)
|
|
786
737
|
return 1
|
|
787
738
|
}
|
|
788
739
|
if (parsed.graph !== undefined) {
|
|
@@ -798,7 +749,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
798
749
|
await Bun.write(target, out)
|
|
799
750
|
} catch (err) {
|
|
800
751
|
const message = err instanceof Error ? err.message : String(err)
|
|
801
|
-
|
|
752
|
+
sayNotice('error', 'vx run', `failed to write graph to ${target}: ${message}`)
|
|
802
753
|
return 1
|
|
803
754
|
}
|
|
804
755
|
}
|
|
@@ -864,10 +815,15 @@ export async function runCmd(args: readonly string[]): Promise<number> {
|
|
|
864
815
|
// Same contract as --summarize / --profile: the run already
|
|
865
816
|
// happened, so a write failure is reported, not fatal.
|
|
866
817
|
const message = err instanceof Error ? err.message : String(err)
|
|
867
|
-
|
|
818
|
+
sayNotice('warn', 'vx run', `failed to write report to ${target}: ${message}`)
|
|
868
819
|
}
|
|
869
820
|
}
|
|
870
821
|
}
|
|
871
|
-
if (summary.json !== undefined) process.stdout.write(`${JSON.stringify(summary.json
|
|
822
|
+
if (summary.json !== undefined) process.stdout.write(`${JSON.stringify(summary.json)}\n`)
|
|
872
823
|
return summary.ok ? 0 : 1
|
|
873
824
|
}
|
|
825
|
+
|
|
826
|
+
/** CI=0 and CI=false are "not CI": several vendors set CI=true. */
|
|
827
|
+
function onCI(v: string | undefined): boolean {
|
|
828
|
+
return v !== undefined && v !== '' && v !== '0' && v !== 'false'
|
|
829
|
+
}
|