@vzn/vx 0.0.645 → 0.0.648

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.
Files changed (186) hide show
  1. package/README.md +39 -50
  2. package/launcher.cjs +9 -2
  3. package/launcher.sh +2 -2
  4. package/package.json +5 -5
  5. package/schemas/cache.json +5 -5
  6. package/schemas/error.json +1 -1
  7. package/schemas/info.json +16 -2
  8. package/schemas/last.json +127 -5
  9. package/schemas/plan.json +28 -2
  10. package/schemas/show.json +10 -1
  11. package/skills/vx/SKILL.md +19 -3
  12. package/src/bin.ts +54 -13
  13. package/src/cache/archive.ts +324 -23
  14. package/src/cache/cache.ts +330 -65
  15. package/src/cache/git-inputs.ts +274 -97
  16. package/src/cache/index.ts +7 -2
  17. package/src/cache/inputs.ts +515 -83
  18. package/src/cache/key-fold.ts +15 -6
  19. package/src/cache/layer.ts +74 -11
  20. package/src/cache/layered-cache.ts +154 -59
  21. package/src/cache/linked-inputs.ts +1 -1
  22. package/src/cache/output-index.ts +44 -14
  23. package/src/cache/plugin-cache.ts +49 -23
  24. package/src/cache/remote-hits.ts +72 -0
  25. package/src/cache/remote-lane.ts +100 -15
  26. package/src/cache/run-history.ts +77 -23
  27. package/src/cache/run-store.ts +11 -10
  28. package/src/cache/schema.ts +42 -6
  29. package/src/cache/tar-stream.ts +5 -0
  30. package/src/cache/umask.ts +1 -2
  31. package/src/cli/cache.ts +58 -27
  32. package/src/cli/completions.ts +12 -5
  33. package/src/cli/core-alias.ts +5 -2
  34. package/src/cli/docs.ts +42 -16
  35. package/src/cli/help.ts +62 -25
  36. package/src/cli/index.ts +79 -45
  37. package/src/cli/info.ts +12 -6
  38. package/src/cli/init.ts +222 -34
  39. package/src/cli/last.ts +443 -47
  40. package/src/cli/lock.ts +197 -52
  41. package/src/cli/plan-format.ts +91 -49
  42. package/src/cli/plugin-commands.ts +160 -11
  43. package/src/cli/plugin-templates.ts +13 -13
  44. package/src/cli/registry.ts +209 -0
  45. package/src/cli/run.ts +111 -41
  46. package/src/cli/select.ts +148 -49
  47. package/src/cli/show.ts +59 -16
  48. package/src/cli/task-verb.ts +16 -6
  49. package/src/cli/upgrade.ts +155 -54
  50. package/src/cli/watch-cycle.ts +10 -5
  51. package/src/cli/watch-filter.ts +71 -3
  52. package/src/cli/watch-fs.ts +22 -6
  53. package/src/cli/watch-judge.ts +48 -6
  54. package/src/cli/watch.ts +163 -42
  55. package/src/cli/why.ts +211 -73
  56. package/src/cli/workspace-config.ts +54 -15
  57. package/src/config.ts +13 -1
  58. package/src/exec/env.ts +7 -2
  59. package/src/exec/executor.ts +262 -15
  60. package/src/exec/index.ts +8 -2
  61. package/src/exec/kill-tree.ts +15 -0
  62. package/src/exec/local-executor.ts +1 -1
  63. package/src/exec/proc-sample.ts +70 -6
  64. package/src/exec/runner.ts +256 -53
  65. package/src/exec/sandbox-binds.ts +7 -3
  66. package/src/exec/sandbox-paths.ts +1 -1
  67. package/src/exec/sandbox-runtime.ts +222 -28
  68. package/src/exec/sandbox-violations.ts +52 -20
  69. package/src/graph/critical-path.ts +79 -0
  70. package/src/graph/default-build.ts +19 -0
  71. package/src/graph/definition.ts +38 -0
  72. package/src/graph/folds.ts +38 -5
  73. package/src/graph/index.ts +4 -1
  74. package/src/graph/priorities.ts +74 -8
  75. package/src/graph/scheduler.ts +326 -65
  76. package/src/graph/task-graph.ts +228 -35
  77. package/src/handoff.ts +97 -9
  78. package/src/history/failure-mode.ts +6 -3
  79. package/src/history/history.ts +268 -50
  80. package/src/history/index.ts +2 -0
  81. package/src/history/metrics.ts +142 -45
  82. package/src/index.ts +42 -10
  83. package/src/orchestrator/admission.ts +10 -6
  84. package/src/orchestrator/affected-tasks.ts +11 -6
  85. package/src/orchestrator/deferred-outputs.ts +2 -0
  86. package/src/orchestrator/doctor.ts +134 -42
  87. package/src/orchestrator/execute-args.ts +6 -0
  88. package/src/orchestrator/execute-task.ts +155 -42
  89. package/src/orchestrator/fingerprint-watch.ts +1 -1
  90. package/src/orchestrator/graph-memo.ts +140 -0
  91. package/src/orchestrator/graph-stage.ts +18 -1
  92. package/src/orchestrator/held-servers.ts +86 -0
  93. package/src/orchestrator/hit-restore.ts +227 -122
  94. package/src/orchestrator/index.ts +20 -3
  95. package/src/orchestrator/local-floor.ts +198 -0
  96. package/src/orchestrator/local-shortcircuit.ts +51 -8
  97. package/src/orchestrator/lockfile-claim.ts +96 -16
  98. package/src/orchestrator/miss-save.ts +52 -12
  99. package/src/orchestrator/options.ts +8 -6
  100. package/src/orchestrator/output-host.ts +43 -14
  101. package/src/orchestrator/output-log.ts +90 -10
  102. package/src/orchestrator/persistent-task.ts +97 -28
  103. package/src/orchestrator/persistent.ts +16 -4
  104. package/src/orchestrator/placement.ts +257 -14
  105. package/src/orchestrator/plan.ts +64 -7
  106. package/src/orchestrator/plugin-host.ts +230 -40
  107. package/src/orchestrator/plugin.ts +150 -37
  108. package/src/orchestrator/prepare.ts +178 -32
  109. package/src/orchestrator/projects.ts +65 -40
  110. package/src/orchestrator/remote-prefetch.ts +119 -25
  111. package/src/orchestrator/run-artifacts.ts +95 -32
  112. package/src/orchestrator/run-context.ts +39 -41
  113. package/src/orchestrator/run-failures.ts +65 -23
  114. package/src/orchestrator/run-id.ts +6 -3
  115. package/src/orchestrator/run-keys.ts +300 -0
  116. package/src/orchestrator/run-live.ts +294 -0
  117. package/src/orchestrator/run-lock.ts +13 -7
  118. package/src/orchestrator/run-records.ts +35 -23
  119. package/src/orchestrator/run-report.ts +3 -3
  120. package/src/orchestrator/run-steps.ts +58 -22
  121. package/src/orchestrator/run-stop.ts +22 -5
  122. package/src/orchestrator/run.ts +409 -79
  123. package/src/orchestrator/sandbox-fix.ts +7 -5
  124. package/src/orchestrator/sandbox-request.ts +78 -22
  125. package/src/orchestrator/shell-verdict.ts +5 -5
  126. package/src/orchestrator/signals.ts +26 -1
  127. package/src/orchestrator/stable-keys.ts +92 -8
  128. package/src/orchestrator/steps.ts +2 -2
  129. package/src/orchestrator/task-artifacts.ts +206 -0
  130. package/src/orchestrator/task-hash.ts +89 -14
  131. package/src/orchestrator/task-log-buffer.ts +72 -36
  132. package/src/orchestrator/task-process.ts +22 -0
  133. package/src/orchestrator/telemetry-host.ts +24 -3
  134. package/src/orchestrator/telemetry.ts +113 -38
  135. package/src/orchestrator/upstream.ts +15 -0
  136. package/src/report/events.ts +32 -13
  137. package/src/report/forecast.ts +36 -35
  138. package/src/report/framed-output.ts +173 -46
  139. package/src/report/index.ts +1 -0
  140. package/src/report/logger.ts +217 -186
  141. package/src/report/path-links.ts +12 -4
  142. package/src/report/plain-output.ts +13 -5
  143. package/src/report/status-line.ts +94 -46
  144. package/src/report/summary.ts +174 -38
  145. package/src/report/terminal-signals.ts +13 -2
  146. package/src/settings.ts +14 -6
  147. package/src/util/bun-version.ts +4 -1
  148. package/src/util/colors.ts +20 -3
  149. package/src/util/config-frame.ts +41 -4
  150. package/src/{graph → util}/dependency-spec.ts +22 -13
  151. package/src/util/edit-distance.ts +13 -1
  152. package/src/util/env-flag.ts +9 -0
  153. package/src/util/errors.ts +59 -3
  154. package/src/util/essential-env.ts +57 -0
  155. package/src/util/git-ignored.ts +1 -1
  156. package/src/util/hash.ts +35 -0
  157. package/src/util/hide-env.ts +45 -5
  158. package/src/util/index.ts +22 -5
  159. package/src/util/num.ts +8 -0
  160. package/src/util/paths.ts +121 -21
  161. package/src/util/report.ts +6 -1
  162. package/src/util/secret-mask.ts +62 -1
  163. package/src/util/secrets.ts +7 -2
  164. package/src/util/size.ts +11 -3
  165. package/src/util/tail.ts +6 -1
  166. package/src/util/timing.ts +13 -1
  167. package/src/util/ui.ts +65 -4
  168. package/src/util/which.ts +1 -1
  169. package/src/util/write-file.ts +67 -0
  170. package/src/workspace/affected.ts +710 -107
  171. package/src/workspace/bracket-literal.ts +84 -0
  172. package/src/workspace/canonical-dir.ts +37 -0
  173. package/src/workspace/config-bytecode.ts +282 -0
  174. package/src/workspace/config-eval.ts +29 -8
  175. package/src/workspace/config-imports.ts +199 -58
  176. package/src/workspace/config-schema.ts +242 -39
  177. package/src/workspace/filter.ts +37 -8
  178. package/src/workspace/index.ts +11 -2
  179. package/src/workspace/json-data.ts +3 -4
  180. package/src/workspace/lockfile.ts +57 -8
  181. package/src/workspace/migration.ts +72 -49
  182. package/src/workspace/package-graph.ts +5 -4
  183. package/src/workspace/project-loader.ts +314 -47
  184. package/src/workspace/repo-id.ts +1 -1
  185. package/src/workspace/workspace.ts +176 -33
  186. package/src/cli/foreign-flags.ts +0 -727
package/src/cli/run.ts CHANGED
@@ -1,7 +1,8 @@
1
+ import { statSync } from 'node:fs'
2
+ import path from 'node:path'
1
3
  import { isatty } from 'node:tty'
2
4
  import { findWorkspaceRoot } from '../workspace/index.js'
3
- import { translateForeign } from './foreign-flags.js'
4
- import { flagHint, refuse, seeHelp } from './help.js'
5
+ import { anchoredSpecError, flagHint, refuse, seeHelp } from './help.js'
5
6
  import {
6
7
  planRun,
7
8
  run as runOrchestrator,
@@ -19,7 +20,14 @@ import {
19
20
  } from '../cache/index.js'
20
21
  import { affectedFilterFor, findCwdSelection, pickTask, resolveFilters } from './select.js'
21
22
  import { nxTargetHint, taskNamesHere } from './task-verb.js'
22
- import { parseDecimalInt, sayNotice, sayLine } from '../util/index.js'
23
+ import {
24
+ detectColors,
25
+ envFlagOn,
26
+ hasControl,
27
+ parseDecimalInt,
28
+ sayNotice,
29
+ sayLine,
30
+ } from '../util/index.js'
23
31
  import { kindHint, parseSettingValue, type Setting, SETTINGS, settingRefusal } from '../settings.js'
24
32
  import { formatPlanJson, formatPlanText } from './plan-format.js'
25
33
 
@@ -56,15 +64,15 @@ export interface RunArgs {
56
64
  * workspace's `cache.policy` and applies these on top.
57
65
  */
58
66
  cacheFlags?: CacheFlags
59
- /** The policy named a remote axis (`remote:r`, `:w`, `:rw`): the run says so when no plugin serves one. */
60
- remoteRequested?: boolean
67
+ /** Which setting named a remote axis (`remote:r`, `:w`, `:rw`): the run says so when no plugin serves one. */
68
+ remoteRequested?: '--cache-policy' | 'VX_CACHE_POLICY'
61
69
  /** `--cache-dir <path>`: override the cache directory (cwd-relative). */
62
70
  cacheDir: string | undefined
63
71
  /** `vx watch --poll` (`watch.poll`). */
64
72
  poll?: true
65
73
  forwardArgs: string[]
66
74
  verbosity: number
67
- dry: 'text' | 'json' | undefined
75
+ dry: 'pretty' | 'json' | undefined
68
76
  profile: string | undefined
69
77
  /**
70
78
  * `--affected[=<base>]`. When undefined the flag wasn't passed.
@@ -116,7 +124,7 @@ const SETTING_OPTIONS = [
116
124
 
117
125
  /** `verb` is the one being parsed for: `vx watch` reads `vx run`'s flags, and
118
126
  * its flag hints named `vx run` (M-58). The caller appends the help pointer. */
119
- export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' = 'run'): RunArgs {
127
+ export function parseRunArgs(args: readonly string[], verb: 'run' | 'watch' = 'run'): RunArgs {
120
128
  const out: RunArgs = {
121
129
  tasks: [],
122
130
  filters: [],
@@ -132,10 +140,6 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
132
140
  affected: undefined,
133
141
  }
134
142
 
135
- // A Turbo or Nx spelling becomes vx's, or a refusal naming vx's.
136
- const translated = translateForeign(rawArgs)
137
- if ('error' in translated) return { ...out, error: translated.error }
138
- const args = translated
139
143
  const sepIdx = args.indexOf('--')
140
144
  const before = sepIdx === -1 ? args : args.slice(0, sepIdx)
141
145
  out.forwardArgs = sepIdx === -1 ? [] : args.slice(sepIdx + 1)
@@ -158,7 +162,7 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
158
162
  if (v === undefined || v === '' || (a === '--filter' && v.startsWith('--')))
159
163
  return {
160
164
  ...out,
161
- error: `--filter requires a value (a project name, glob or path, e.g. --filter app)`,
165
+ error: `--filter requires a value: a project name, glob or path, e.g. --filter app`,
162
166
  }
163
167
  out.filters.push(v)
164
168
  } else if (a === '--all') {
@@ -178,19 +182,26 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
178
182
  // half the callers mean. Reject and name both explicit forms; the
179
183
  // sibling value flags (--retries=, --timeout=, --cache-dir=)
180
184
  // reject an empty `=` value the same way.
181
- if (raw === '') {
185
+ const names = raw
186
+ .split(',')
187
+ .map((s) => s.trim())
188
+ .filter((s) => s.length > 0)
189
+ // `=,` named nothing and ran every edge, as `=` would have.
190
+ if (names.length === 0) {
182
191
  return {
183
192
  ...out,
184
193
  error: `--exclude-dependencies= needs a value — pass bare --exclude-dependencies to drop every dependsOn edge, or omit the flag to keep them`,
185
194
  }
186
195
  }
187
- out.excludeDependencies = raw
188
- .split(',')
189
- .map((s) => s.trim())
190
- .filter((s) => s.length > 0)
196
+ // Repeated, the last list replaced the earlier ones, so their
197
+ // edges ran; `--filter` adds up the same way.
198
+ if (out.excludeDependencies !== 'all')
199
+ out.excludeDependencies = [...new Set([...out.excludeDependencies, ...names])]
191
200
  } else if (a === '--cache-dir' || a?.startsWith('--cache-dir=')) {
192
201
  const v = a === '--cache-dir' ? before[++i] : a.slice('--cache-dir='.length)
193
- if (v === undefined || v === '') return { ...out, error: `--cache-dir requires a path` }
202
+ // Whitespace named a directory in spaces; `VX_CACHE_DIR` and `cache.dir` refuse it.
203
+ if (v === undefined || v.trim() === '')
204
+ return { ...out, error: `--cache-dir requires a path` }
194
205
  // Unlike every other value flag, a cache dir is an arbitrary
195
206
  // string — nothing about its shape rejects a swallowed flag. An
196
207
  // unquoted empty shell var (`--cache-dir $EMPTY --force`) would
@@ -210,7 +221,7 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
210
221
  if (v === undefined)
211
222
  return {
212
223
  ...out,
213
- error: `${name} requires a value (a spec like local:r, local:rw, remote:, or local:,remote:rw)`,
224
+ error: `${name} requires a value: a spec like local:r, local:rw, remote:, or local:,remote:rw`,
214
225
  }
215
226
  // A spec with no segment at all applies NOTHING, so `--cache-policy=`
216
227
  // (an empty shell var, or someone reaching for "no cache") left
@@ -225,7 +236,7 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
225
236
  }
226
237
  try {
227
238
  cachePolicy = parseCachePolicy(v, cachePolicy)
228
- if (/remote:[rw]/.test(v)) out.remoteRequested = true
239
+ if (/remote:[rw]/.test(v)) out.remoteRequested = '--cache-policy'
229
240
  } catch (err) {
230
241
  cacheSpecError = `--cache-policy: ${err instanceof Error ? err.message : String(err)}`
231
242
  }
@@ -246,7 +257,7 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
246
257
  } else if (a?.startsWith('--continue=')) {
247
258
  const v = a.slice('--continue='.length)
248
259
  if (v === '') {
249
- return { ...out, error: `--continue= requires a mode (never, deps-ok, or always)` }
260
+ return { ...out, error: `--continue= requires a mode: never, deps-ok, or always` }
250
261
  }
251
262
  if (v !== 'never' && v !== 'deps-ok' && v !== 'always') {
252
263
  return { ...out, error: `--continue must be never, deps-ok, or always (got ${v})` }
@@ -254,18 +265,31 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
254
265
  out.continueMode = v
255
266
  } else if (a === '--verbosity' || a?.startsWith('--verbosity=')) {
256
267
  const v = a === '--verbosity' ? before[++i] : a.slice('--verbosity='.length)
257
- if (v === undefined)
258
- return { ...out, error: `--verbosity requires a value (a non-negative integer)` }
268
+ if (v === undefined || v === '')
269
+ return { ...out, error: `--verbosity requires a value: a non-negative integer` }
259
270
  const n = parseDecimalInt(v)
260
271
  if (n === null)
261
272
  return { ...out, error: `--verbosity must be a non-negative integer (got ${v})` }
262
273
  out.verbosity = n
263
274
  } else if (a === '--dry') {
264
- out.dry = 'text'
275
+ // As `--continue`'s mode: `--dry json` read `json` as a task, and the
276
+ // run said no project declares it.
277
+ const next = before[i + 1]
278
+ if (next === 'pretty' || next === 'json') {
279
+ return { ...out, error: `--dry takes its format with '=': --dry=${next}` }
280
+ }
281
+ out.dry = 'pretty'
265
282
  } else if (a?.startsWith('--dry=')) {
266
283
  const fmt = a.slice('--dry='.length)
267
- if (fmt !== 'text' && fmt !== 'json') {
268
- return { ...out, error: `--dry must be text or json (got ${fmt})` }
284
+ if (fmt === '') {
285
+ return {
286
+ ...out,
287
+ error: `--dry= requires a format: pretty or json; bare --dry prints pretty`,
288
+ }
289
+ }
290
+ // The values every --format takes.
291
+ if (fmt !== 'pretty' && fmt !== 'json') {
292
+ return { ...out, error: `--dry must be pretty or json (got ${fmt})` }
269
293
  }
270
294
  out.dry = fmt
271
295
  } else if (a === '--profile') {
@@ -280,19 +304,36 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
280
304
  out.affected = ''
281
305
  } else if (a?.startsWith('--affected=')) {
282
306
  out.affected = a.slice('--affected='.length)
283
- } else if (verb === 'watch' && a === '--poll') {
307
+ } else if (a === '--poll') {
308
+ // The edit-distance hint offered vx run `--all`, a different flag.
309
+ if (verb === 'run')
310
+ return { ...out, error: `unknown flag: --poll (a vx watch flag: vx watch --poll <task>)` }
284
311
  out.poll = true
285
312
  } else if (a !== undefined && settingFlagOf(a) !== undefined) {
286
313
  const { s: set, name } = settingFlagOf(a)!
287
314
  const v = a === name ? before[++i] : a.slice(name.length + 1)
288
315
  if (v === undefined || v === '')
289
- return { ...out, error: `${name} requires a value (${kindHint(set.kind)})` }
316
+ return { ...out, error: `${name} requires a value: ${kindHint(set.kind)}` }
317
+ // A string's shape rejects nothing, so the space form would swallow
318
+ // the next flag as the value, as `--cache-dir` would above.
319
+ if (set.kind.type === 'string' && a === name && v.startsWith('-'))
320
+ return {
321
+ ...out,
322
+ error: `${name} requires a value (got flag ${v}; a value that starts with - needs ${name}=${v})`,
323
+ }
290
324
  const value = parseSettingValue(set.kind, v)
291
325
  if (value === null) return { ...out, error: settingRefusal(set.path, name, v) }
292
326
  out.settings[set.path] = value
293
327
  } else if (a !== undefined && a.startsWith('-')) {
294
328
  return { ...out, error: `unknown flag: ${a}${flagHint(verb, a)}` }
295
329
  } else if (a !== undefined) {
330
+ // A config may not declare such a name (`taskNameProblem`); a pasted
331
+ // newline was refused as an unknown task, printed raw.
332
+ if (hasControl(a))
333
+ return {
334
+ ...out,
335
+ error: `task ${JSON.stringify(a)} holds a control character, which no task name may hold — retype it`,
336
+ }
296
337
  out.tasks.push(a)
297
338
  }
298
339
  }
@@ -316,7 +357,7 @@ export function parseRunArgs(rawArgs: readonly string[], verb: 'run' | 'watch' =
316
357
  error: `VX_CACHE_POLICY: ${err instanceof Error ? err.message : String(err)}`,
317
358
  }
318
359
  }
319
- if (/remote:[rw]/.test(envSpec)) out.remoteRequested = true
360
+ if (/remote:[rw]/.test(envSpec)) out.remoteRequested = 'VX_CACHE_POLICY'
320
361
  }
321
362
  // --no-cache (all off) beats --force (reads off, writes kept), both
322
363
  // layered on top of the spec; with none named, on the workspace's.
@@ -405,12 +446,22 @@ export async function resolveRunOptions(
405
446
  scopeOut?: { affectedBase?: string },
406
447
  ): Promise<RunOptions | { error: string; code?: string } | { nothingSelected: string }> {
407
448
  for (const t of tasks) {
408
- const idx = t.indexOf('#')
409
- if (idx >= 0) {
410
- const project = t.slice(0, idx)
411
- const task = t.slice(idx + 1)
412
- if (!project || !task) {
413
- return { error: `invalid pkg#task: ${t}` }
449
+ // A blank name (`vx run --all ''`) otherwise reached the orchestrator,
450
+ // which refuses it in the embedder API's words.
451
+ if (t.trim() === '')
452
+ return { error: `empty task name ${JSON.stringify(t)} (vx show lists the tasks)` }
453
+ // A bare name runs in the cwd project: `--all` is every project.
454
+ const error = anchoredSpecError(
455
+ t,
456
+ (task) => `vx ${verb} ${task} --all runs it in every project`,
457
+ )
458
+ if (error !== undefined) return { error }
459
+ // No config can declare a padded name (taskNameProblem): `'build '` was
460
+ // reported as "no projects declare task(s): build", its space unseen.
461
+ const task = t.slice(t.indexOf('#') + 1)
462
+ if (task.trim() !== task) {
463
+ return {
464
+ error: `task name ${JSON.stringify(task)} has surrounding whitespace (vx show lists the tasks)`,
414
465
  }
415
466
  }
416
467
  }
@@ -441,6 +492,7 @@ export async function resolveRunOptions(
441
492
  filterStrings,
442
493
  parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {},
443
494
  affectedFilter,
495
+ verb,
444
496
  )
445
497
  : undefined
446
498
  if (resolved !== undefined && 'error' in resolved) return resolved
@@ -469,7 +521,10 @@ export async function resolveRunOptions(
469
521
  } else if (parsed.all) {
470
522
  projects = undefined
471
523
  } else {
472
- const cwdProject = await findCwdSelection(cwd)
524
+ const cwdProject = await findCwdSelection(
525
+ cwd,
526
+ parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {},
527
+ )
473
528
  if (!cwdProject) {
474
529
  const nx = await nxTargetHint(tasks, cwd)
475
530
  if (nx !== null) return { error: nx }
@@ -487,7 +542,7 @@ export async function resolveRunOptions(
487
542
  ...(parsed.cacheFlags !== undefined
488
543
  ? { cacheFlags: parsed.cacheFlags }
489
544
  : { cache: parsed.cache }),
490
- ...(parsed.remoteRequested ? { remoteRequested: true } : {}),
545
+ ...(parsed.remoteRequested ? { remoteRequested: parsed.remoteRequested } : {}),
491
546
  // A laptop reads the shared cache and never writes it (owner,
492
547
  // 2026-10-03); naming the remote in `--cache-policy` is asking to.
493
548
  ...(!parsed.remoteRequested && !onCI(process.env['CI'])
@@ -532,6 +587,10 @@ export async function runCmd(args: readonly string[]): Promise<number> {
532
587
  }
533
588
 
534
589
  const cwd = process.cwd()
590
+ if (parsed.profile !== undefined) {
591
+ const problem = profileProblem(cwd, parsed.profile)
592
+ if (problem !== undefined) return refuse('run', args, problem, 'VX_E_USAGE')
593
+ }
535
594
  let tasks = [...parsed.tasks]
536
595
  let picked: string | undefined
537
596
 
@@ -645,7 +704,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
645
704
  if (parsed.dry === 'json') {
646
705
  process.stdout.write(formatPlanJson(plan, scopeOut.affectedBase))
647
706
  } else {
648
- process.stdout.write(formatPlanText(plan))
707
+ process.stdout.write(formatPlanText(plan, detectColors(process.stdout)))
649
708
  }
650
709
  return 0
651
710
  }
@@ -662,7 +721,18 @@ export async function runCmd(args: readonly string[]): Promise<number> {
662
721
  return summary.ok ? 0 : 1
663
722
  }
664
723
 
665
- /** CI=0 and CI=false are "not CI": several vendors set CI=true. */
666
- function onCI(v: string | undefined): boolean {
667
- return v !== undefined && v !== '' && v !== '0' && v !== 'false'
724
+ /**
725
+ * Why `--profile` cannot write `target`, or undefined. Asked before the
726
+ * run: a directory named as the file died with EISDIR only after it. A
727
+ * missing parent is no fault: the write makes it (item 993).
728
+ */
729
+ function profileProblem(cwd: string, target: string): string | undefined {
730
+ if (statSync(path.resolve(cwd, target), { throwIfNoEntry: false })?.isDirectory() === true)
731
+ return `--profile: ${target} is a directory — name a file, e.g. --profile=${path.join(target, 'profile.json')}`
732
+ return undefined
733
+ }
734
+
735
+ /** Whether `CI` names a CI run, as detectCi reads it. */
736
+ export function onCI(v: string | undefined): boolean {
737
+ return envFlagOn(v)
668
738
  }