@vzn/vx 0.0.646 → 0.0.649

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 (184) hide show
  1. package/README.md +29 -22
  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 +112 -5
  9. package/schemas/plan.json +28 -2
  10. package/schemas/show.json +10 -1
  11. package/skills/vx/SKILL.md +18 -2
  12. package/src/bin.ts +53 -12
  13. package/src/cache/archive.ts +324 -23
  14. package/src/cache/cache.ts +268 -53
  15. package/src/cache/git-inputs.ts +219 -87
  16. package/src/cache/index.ts +3 -2
  17. package/src/cache/inputs.ts +460 -78
  18. package/src/cache/key-fold.ts +15 -6
  19. package/src/cache/layer.ts +49 -9
  20. package/src/cache/layered-cache.ts +123 -58
  21. package/src/cache/linked-inputs.ts +1 -1
  22. package/src/cache/output-index.ts +19 -3
  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 +90 -14
  26. package/src/cache/run-history.ts +70 -23
  27. package/src/cache/run-store.ts +11 -10
  28. package/src/cache/schema.ts +25 -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/docs.ts +42 -16
  34. package/src/cli/help.ts +58 -19
  35. package/src/cli/index.ts +79 -33
  36. package/src/cli/info.ts +12 -6
  37. package/src/cli/init.ts +186 -31
  38. package/src/cli/last.ts +439 -45
  39. package/src/cli/lock.ts +197 -52
  40. package/src/cli/plan-format.ts +91 -49
  41. package/src/cli/plugin-commands.ts +70 -14
  42. package/src/cli/plugin-templates.ts +13 -13
  43. package/src/cli/registry.ts +209 -0
  44. package/src/cli/run.ts +109 -34
  45. package/src/cli/select.ts +143 -46
  46. package/src/cli/show.ts +59 -16
  47. package/src/cli/task-verb.ts +16 -6
  48. package/src/cli/upgrade.ts +151 -49
  49. package/src/cli/watch-cycle.ts +10 -5
  50. package/src/cli/watch-filter.ts +71 -3
  51. package/src/cli/watch-fs.ts +22 -6
  52. package/src/cli/watch-judge.ts +48 -6
  53. package/src/cli/watch.ts +163 -42
  54. package/src/cli/why.ts +211 -73
  55. package/src/cli/workspace-config.ts +54 -15
  56. package/src/config.ts +13 -1
  57. package/src/exec/env.ts +7 -2
  58. package/src/exec/executor.ts +154 -10
  59. package/src/exec/index.ts +6 -2
  60. package/src/exec/kill-tree.ts +15 -0
  61. package/src/exec/local-executor.ts +1 -1
  62. package/src/exec/proc-sample.ts +3 -1
  63. package/src/exec/runner.ts +253 -54
  64. package/src/exec/sandbox-binds.ts +7 -3
  65. package/src/exec/sandbox-paths.ts +1 -1
  66. package/src/exec/sandbox-runtime.ts +211 -24
  67. package/src/exec/sandbox-violations.ts +52 -20
  68. package/src/graph/critical-path.ts +79 -0
  69. package/src/graph/default-build.ts +19 -0
  70. package/src/graph/definition.ts +38 -0
  71. package/src/graph/folds.ts +38 -5
  72. package/src/graph/index.ts +4 -1
  73. package/src/graph/priorities.ts +74 -8
  74. package/src/graph/scheduler.ts +270 -60
  75. package/src/graph/task-graph.ts +228 -35
  76. package/src/handoff.ts +97 -9
  77. package/src/history/failure-mode.ts +6 -3
  78. package/src/history/history.ts +167 -61
  79. package/src/history/index.ts +2 -0
  80. package/src/history/metrics.ts +90 -30
  81. package/src/index.ts +30 -13
  82. package/src/orchestrator/admission.ts +10 -6
  83. package/src/orchestrator/affected-tasks.ts +11 -6
  84. package/src/orchestrator/deferred-outputs.ts +2 -0
  85. package/src/orchestrator/doctor.ts +134 -42
  86. package/src/orchestrator/execute-args.ts +4 -1
  87. package/src/orchestrator/execute-task.ts +130 -38
  88. package/src/orchestrator/fingerprint-watch.ts +1 -1
  89. package/src/orchestrator/graph-memo.ts +140 -0
  90. package/src/orchestrator/graph-stage.ts +18 -1
  91. package/src/orchestrator/held-servers.ts +1 -1
  92. package/src/orchestrator/hit-restore.ts +217 -119
  93. package/src/orchestrator/index.ts +4 -3
  94. package/src/orchestrator/local-floor.ts +119 -25
  95. package/src/orchestrator/local-shortcircuit.ts +28 -4
  96. package/src/orchestrator/lockfile-claim.ts +96 -16
  97. package/src/orchestrator/miss-save.ts +42 -8
  98. package/src/orchestrator/options.ts +8 -6
  99. package/src/orchestrator/output-host.ts +43 -14
  100. package/src/orchestrator/output-log.ts +90 -10
  101. package/src/orchestrator/persistent-task.ts +84 -29
  102. package/src/orchestrator/persistent.ts +16 -4
  103. package/src/orchestrator/placement.ts +185 -17
  104. package/src/orchestrator/plan.ts +64 -7
  105. package/src/orchestrator/plugin-host.ts +214 -42
  106. package/src/orchestrator/plugin.ts +113 -37
  107. package/src/orchestrator/prepare.ts +168 -30
  108. package/src/orchestrator/projects.ts +65 -40
  109. package/src/orchestrator/remote-prefetch.ts +119 -25
  110. package/src/orchestrator/run-artifacts.ts +95 -32
  111. package/src/orchestrator/run-context.ts +39 -41
  112. package/src/orchestrator/run-failures.ts +65 -23
  113. package/src/orchestrator/run-id.ts +6 -3
  114. package/src/orchestrator/run-keys.ts +149 -13
  115. package/src/orchestrator/run-live.ts +294 -0
  116. package/src/orchestrator/run-lock.ts +13 -7
  117. package/src/orchestrator/run-records.ts +35 -29
  118. package/src/orchestrator/run-report.ts +3 -3
  119. package/src/orchestrator/run-steps.ts +58 -22
  120. package/src/orchestrator/run-stop.ts +22 -5
  121. package/src/orchestrator/run.ts +331 -88
  122. package/src/orchestrator/sandbox-fix.ts +7 -5
  123. package/src/orchestrator/sandbox-request.ts +70 -20
  124. package/src/orchestrator/shell-verdict.ts +5 -5
  125. package/src/orchestrator/signals.ts +26 -1
  126. package/src/orchestrator/stable-keys.ts +92 -8
  127. package/src/orchestrator/steps.ts +2 -2
  128. package/src/orchestrator/task-artifacts.ts +90 -26
  129. package/src/orchestrator/task-hash.ts +78 -13
  130. package/src/orchestrator/task-log-buffer.ts +72 -36
  131. package/src/orchestrator/task-process.ts +7 -1
  132. package/src/orchestrator/telemetry-host.ts +22 -2
  133. package/src/orchestrator/telemetry.ts +53 -21
  134. package/src/orchestrator/upstream.ts +15 -0
  135. package/src/report/events.ts +32 -13
  136. package/src/report/forecast.ts +31 -9
  137. package/src/report/framed-output.ts +141 -50
  138. package/src/report/index.ts +1 -0
  139. package/src/report/logger.ts +170 -179
  140. package/src/report/path-links.ts +12 -4
  141. package/src/report/plain-output.ts +13 -5
  142. package/src/report/status-line.ts +81 -53
  143. package/src/report/summary.ts +114 -32
  144. package/src/report/terminal-signals.ts +13 -2
  145. package/src/settings.ts +7 -6
  146. package/src/util/bun-version.ts +4 -1
  147. package/src/util/colors.ts +4 -2
  148. package/src/util/config-frame.ts +41 -4
  149. package/src/{graph → util}/dependency-spec.ts +22 -13
  150. package/src/util/edit-distance.ts +13 -1
  151. package/src/util/env-flag.ts +9 -0
  152. package/src/util/errors.ts +59 -3
  153. package/src/util/essential-env.ts +57 -0
  154. package/src/util/git-ignored.ts +1 -1
  155. package/src/util/hash.ts +35 -0
  156. package/src/util/hide-env.ts +45 -5
  157. package/src/util/index.ts +21 -5
  158. package/src/util/num.ts +8 -0
  159. package/src/util/paths.ts +91 -21
  160. package/src/util/report.ts +6 -1
  161. package/src/util/secret-mask.ts +62 -1
  162. package/src/util/secrets.ts +7 -2
  163. package/src/util/size.ts +11 -3
  164. package/src/util/tail.ts +6 -1
  165. package/src/util/timing.ts +13 -1
  166. package/src/util/ui.ts +61 -3
  167. package/src/util/which.ts +1 -1
  168. package/src/util/write-file.ts +67 -0
  169. package/src/workspace/affected.ts +710 -107
  170. package/src/workspace/bracket-literal.ts +84 -0
  171. package/src/workspace/canonical-dir.ts +37 -0
  172. package/src/workspace/config-bytecode.ts +17 -11
  173. package/src/workspace/config-eval.ts +29 -8
  174. package/src/workspace/config-imports.ts +199 -58
  175. package/src/workspace/config-schema.ts +242 -39
  176. package/src/workspace/filter.ts +37 -8
  177. package/src/workspace/index.ts +11 -1
  178. package/src/workspace/json-data.ts +3 -4
  179. package/src/workspace/lockfile.ts +57 -8
  180. package/src/workspace/migration.ts +69 -18
  181. package/src/workspace/package-graph.ts +5 -4
  182. package/src/workspace/project-loader.ts +226 -37
  183. package/src/workspace/repo-id.ts +1 -1
  184. package/src/workspace/workspace.ts +176 -33
@@ -0,0 +1,209 @@
1
+ // The npm registry a package's releases are read from, as the user
2
+ // configured it for their package manager: many companies block
3
+ // registry.npmjs.org and serve a mirror. npm's order first: a scoped
4
+ // `@scope:registry` from any `.npmrc`, then `registry` from the
5
+ // environment, the `.npmrc` files from the cwd up, and the user's. Then
6
+ // bunfig's `install.scopes` / `install.registry`. npmjs.org only when
7
+ // nothing names one. The credential npm or Bun would send to that host
8
+ // goes with each request under it, never to another host.
9
+
10
+ import { readFileSync } from 'node:fs'
11
+ import os from 'node:os'
12
+ import path from 'node:path'
13
+ import { UserError } from '../util/index.js'
14
+
15
+ const NPMJS = 'https://registry.npmjs.org/'
16
+
17
+ export interface Registry {
18
+ /** Base URL, ending in `/`. */
19
+ url: string
20
+ /** The `Authorization` value for a URL under the registry, if one is configured. */
21
+ auth(url: string): string | undefined
22
+ }
23
+
24
+ type Env = Record<string, string | undefined>
25
+
26
+ /** `npm_config_<key>`, either case, as npm reads it. */
27
+ function npmEnv(env: Env, key: string): string | undefined {
28
+ return env[`npm_config_${key}`] ?? env[`NPM_CONFIG_${key.toUpperCase()}`]
29
+ }
30
+
31
+ /** `${VAR}` from `env`, as npm expands it; an unset one is empty. */
32
+ const expandNpm = (value: string, env: Env): string =>
33
+ value.replace(/\$\{([^}?]+)\??\}/g, (_, name: string) => env[name] ?? '')
34
+
35
+ /** `$VAR` and `${VAR}` from `env`, as Bun expands bunfig. */
36
+ const expandBun = (value: string, env: Env): string =>
37
+ expandNpm(value, env).replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name: string) => env[name] ?? '')
38
+
39
+ function readText(file: string): string | undefined {
40
+ try {
41
+ return readFileSync(file, 'utf8')
42
+ } catch {
43
+ return undefined
44
+ }
45
+ }
46
+
47
+ /** An `.npmrc`'s `key=value` lines. */
48
+ function parseNpmrc(text: string, env: Env): Map<string, string> {
49
+ const out = new Map<string, string>()
50
+ for (const raw of text.split(/\r?\n/)) {
51
+ const line = raw.trim()
52
+ if (line === '' || line.startsWith('#') || line.startsWith(';')) continue
53
+ const eq = line.indexOf('=')
54
+ if (eq < 0) continue
55
+ let value = line.slice(eq + 1).trim()
56
+ if (/^(["']).*\1$/.test(value)) value = value.slice(1, -1)
57
+ out.set(expandNpm(line.slice(0, eq).trim(), env), expandNpm(value, env))
58
+ }
59
+ return out
60
+ }
61
+
62
+ const withSlash = (url: string): string => (url.endsWith('/') ? url : `${url}/`)
63
+
64
+ /**
65
+ * `value` when it is an http(s) URL. A hand-written one is a boundary: a
66
+ * scheme left out (`registry=npm.corp.example`) crashed the request with
67
+ * a TypeError stack.
68
+ */
69
+ function httpUrl(value: string, where: string): string {
70
+ let u: URL | undefined
71
+ try {
72
+ u = new URL(value)
73
+ } catch {}
74
+ if (u?.protocol !== 'https:' && u?.protocol !== 'http:') {
75
+ throw new UserError(`registry "${value}" in ${where} is not an http(s) URL`)
76
+ }
77
+ return value
78
+ }
79
+
80
+ /** npm's "nerf dart": `//host[:port]/path/`, the key its credentials sit under. */
81
+ const nerfDart = (url: string): string => {
82
+ const u = new URL(url)
83
+ return `//${u.host}${withSlash(u.pathname)}`
84
+ }
85
+
86
+ interface Bunfig {
87
+ registry?: unknown
88
+ scopes?: Record<string, unknown>
89
+ }
90
+
91
+ /** A bunfig registry: a URL (credentials allowed in it), or `{ url, token }` / `{ url, username, password }`. */
92
+ function bunRegistry(
93
+ value: unknown,
94
+ env: Env,
95
+ file: string,
96
+ ): { url: string; auth?: string } | undefined {
97
+ const entry =
98
+ typeof value === 'string'
99
+ ? { url: value }
100
+ : typeof value === 'object' && value !== null
101
+ ? (value as { url?: unknown; token?: unknown; username?: unknown; password?: unknown })
102
+ : undefined
103
+ if (entry === undefined || typeof entry.url !== 'string') return undefined
104
+ const u = new URL(httpUrl(expandBun(entry.url, env), file))
105
+ let user = decodeURIComponent(u.username)
106
+ let pass = decodeURIComponent(u.password)
107
+ u.username = ''
108
+ u.password = ''
109
+ if (typeof entry.username === 'string') user = expandBun(entry.username, env)
110
+ if (typeof entry.password === 'string') pass = expandBun(entry.password, env)
111
+ const auth =
112
+ typeof entry.token === 'string'
113
+ ? `Bearer ${expandBun(entry.token, env)}`
114
+ : user !== ''
115
+ ? `Basic ${Buffer.from(`${user}:${pass}`).toString('base64')}`
116
+ : undefined
117
+ return { url: withSlash(u.href), ...(auth === undefined ? {} : { auth }) }
118
+ }
119
+
120
+ /**
121
+ * The registry `scope` (`@vzn`) resolves to from `cwd`, `env` and `home`,
122
+ * with the credentials configured for it.
123
+ */
124
+ export function resolveRegistry(
125
+ scope: string,
126
+ cwd: string = process.cwd(),
127
+ env: Env = process.env,
128
+ home: string = os.homedir(),
129
+ ): Registry {
130
+ const userrc = npmEnv(env, 'userconfig') ?? path.join(home, '.npmrc')
131
+ const rcs: { file: string; keys: Map<string, string> }[] = []
132
+ // A cwd under home reaches the user's file as an ancestor; it is read once, last.
133
+ const files = ancestors(cwd)
134
+ .map((d) => path.join(d, '.npmrc'))
135
+ .filter((f) => f !== userrc)
136
+ for (const file of [...files, userrc]) {
137
+ const text = readText(file)
138
+ if (text !== undefined) rcs.push({ file, keys: parseNpmrc(text, env) })
139
+ }
140
+ const rcOf = (key: string) => rcs.find((rc) => rc.keys.has(key))
141
+ const fromRc = (key: string): string | undefined => rcOf(key)?.keys.get(key)
142
+
143
+ const scoped = rcOf(`${scope}:registry`)
144
+ const envUrl = npmEnv(env, 'registry')
145
+ const npmUrl = scoped?.keys.get(`${scope}:registry`) ?? envUrl ?? fromRc('registry')
146
+ if (npmUrl !== undefined) {
147
+ const where =
148
+ scoped !== undefined
149
+ ? scoped.file
150
+ : envUrl !== undefined
151
+ ? env['npm_config_registry'] !== undefined
152
+ ? 'npm_config_registry'
153
+ : 'NPM_CONFIG_REGISTRY'
154
+ : rcOf('registry')!.file
155
+ const url = withSlash(httpUrl(npmUrl, where))
156
+ return {
157
+ url,
158
+ auth: (target) => {
159
+ // npm's lookup: the target's own path, then each parent, under its host.
160
+ const dart = nerfDart(target)
161
+ for (
162
+ let at = dart;
163
+ at.length > 2;
164
+ at = at.slice(0, at.lastIndexOf('/', at.length - 2) + 1)
165
+ ) {
166
+ const token = fromRc(`${at}:_authToken`)
167
+ if (token !== undefined) return `Bearer ${token}`
168
+ const basic = fromRc(`${at}:_auth`)
169
+ if (basic !== undefined) return `Basic ${basic}`
170
+ }
171
+ return undefined
172
+ },
173
+ }
174
+ }
175
+
176
+ const bunfigs = [
177
+ ...ancestors(cwd).map((d) => path.join(d, 'bunfig.toml')),
178
+ path.join(env['XDG_CONFIG_HOME'] ?? home, '.bunfig.toml'),
179
+ ]
180
+ for (const file of bunfigs) {
181
+ const text = readText(file)
182
+ if (text === undefined) continue
183
+ let install: Bunfig | undefined
184
+ try {
185
+ install = (Bun.TOML.parse(text) as { install?: Bunfig }).install
186
+ } catch (err) {
187
+ throw new UserError(
188
+ `${file}: not valid TOML (${err instanceof Error ? err.message : String(err)})`,
189
+ )
190
+ }
191
+ const found =
192
+ bunRegistry(install?.scopes?.[scope] ?? install?.scopes?.[scope.slice(1)], env, file) ??
193
+ bunRegistry(install?.registry, env, file)
194
+ if (found === undefined) continue
195
+ return {
196
+ url: found.url,
197
+ auth: (target) => (withSlash(target).startsWith(found.url) ? found.auth : undefined),
198
+ }
199
+ }
200
+ return { url: NPMJS, auth: () => undefined }
201
+ }
202
+
203
+ function ancestors(cwd: string): string[] {
204
+ const out: string[] = []
205
+ for (let dir = path.resolve(cwd); ; dir = path.dirname(dir)) {
206
+ out.push(dir)
207
+ if (path.dirname(dir) === dir) return out
208
+ }
209
+ }
package/src/cli/run.ts CHANGED
@@ -1,6 +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 { flagHint, refuse, seeHelp } from './help.js'
5
+ import { anchoredSpecError, flagHint, refuse, seeHelp } from './help.js'
4
6
  import {
5
7
  planRun,
6
8
  run as runOrchestrator,
@@ -18,7 +20,14 @@ import {
18
20
  } from '../cache/index.js'
19
21
  import { affectedFilterFor, findCwdSelection, pickTask, resolveFilters } from './select.js'
20
22
  import { nxTargetHint, taskNamesHere } from './task-verb.js'
21
- 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'
22
31
  import { kindHint, parseSettingValue, type Setting, SETTINGS, settingRefusal } from '../settings.js'
23
32
  import { formatPlanJson, formatPlanText } from './plan-format.js'
24
33
 
@@ -55,15 +64,15 @@ export interface RunArgs {
55
64
  * workspace's `cache.policy` and applies these on top.
56
65
  */
57
66
  cacheFlags?: CacheFlags
58
- /** The policy named a remote axis (`remote:r`, `:w`, `:rw`): the run says so when no plugin serves one. */
59
- 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'
60
69
  /** `--cache-dir <path>`: override the cache directory (cwd-relative). */
61
70
  cacheDir: string | undefined
62
71
  /** `vx watch --poll` (`watch.poll`). */
63
72
  poll?: true
64
73
  forwardArgs: string[]
65
74
  verbosity: number
66
- dry: 'text' | 'json' | undefined
75
+ dry: 'pretty' | 'json' | undefined
67
76
  profile: string | undefined
68
77
  /**
69
78
  * `--affected[=<base>]`. When undefined the flag wasn't passed.
@@ -153,7 +162,7 @@ export function parseRunArgs(args: readonly string[], verb: 'run' | 'watch' = 'r
153
162
  if (v === undefined || v === '' || (a === '--filter' && v.startsWith('--')))
154
163
  return {
155
164
  ...out,
156
- 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`,
157
166
  }
158
167
  out.filters.push(v)
159
168
  } else if (a === '--all') {
@@ -173,19 +182,26 @@ export function parseRunArgs(args: readonly string[], verb: 'run' | 'watch' = 'r
173
182
  // half the callers mean. Reject and name both explicit forms; the
174
183
  // sibling value flags (--retries=, --timeout=, --cache-dir=)
175
184
  // reject an empty `=` value the same way.
176
- 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) {
177
191
  return {
178
192
  ...out,
179
193
  error: `--exclude-dependencies= needs a value — pass bare --exclude-dependencies to drop every dependsOn edge, or omit the flag to keep them`,
180
194
  }
181
195
  }
182
- out.excludeDependencies = raw
183
- .split(',')
184
- .map((s) => s.trim())
185
- .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])]
186
200
  } else if (a === '--cache-dir' || a?.startsWith('--cache-dir=')) {
187
201
  const v = a === '--cache-dir' ? before[++i] : a.slice('--cache-dir='.length)
188
- 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` }
189
205
  // Unlike every other value flag, a cache dir is an arbitrary
190
206
  // string — nothing about its shape rejects a swallowed flag. An
191
207
  // unquoted empty shell var (`--cache-dir $EMPTY --force`) would
@@ -205,7 +221,7 @@ export function parseRunArgs(args: readonly string[], verb: 'run' | 'watch' = 'r
205
221
  if (v === undefined)
206
222
  return {
207
223
  ...out,
208
- 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`,
209
225
  }
210
226
  // A spec with no segment at all applies NOTHING, so `--cache-policy=`
211
227
  // (an empty shell var, or someone reaching for "no cache") left
@@ -220,7 +236,7 @@ export function parseRunArgs(args: readonly string[], verb: 'run' | 'watch' = 'r
220
236
  }
221
237
  try {
222
238
  cachePolicy = parseCachePolicy(v, cachePolicy)
223
- if (/remote:[rw]/.test(v)) out.remoteRequested = true
239
+ if (/remote:[rw]/.test(v)) out.remoteRequested = '--cache-policy'
224
240
  } catch (err) {
225
241
  cacheSpecError = `--cache-policy: ${err instanceof Error ? err.message : String(err)}`
226
242
  }
@@ -241,7 +257,7 @@ export function parseRunArgs(args: readonly string[], verb: 'run' | 'watch' = 'r
241
257
  } else if (a?.startsWith('--continue=')) {
242
258
  const v = a.slice('--continue='.length)
243
259
  if (v === '') {
244
- 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` }
245
261
  }
246
262
  if (v !== 'never' && v !== 'deps-ok' && v !== 'always') {
247
263
  return { ...out, error: `--continue must be never, deps-ok, or always (got ${v})` }
@@ -249,18 +265,31 @@ export function parseRunArgs(args: readonly string[], verb: 'run' | 'watch' = 'r
249
265
  out.continueMode = v
250
266
  } else if (a === '--verbosity' || a?.startsWith('--verbosity=')) {
251
267
  const v = a === '--verbosity' ? before[++i] : a.slice('--verbosity='.length)
252
- if (v === undefined)
253
- 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` }
254
270
  const n = parseDecimalInt(v)
255
271
  if (n === null)
256
272
  return { ...out, error: `--verbosity must be a non-negative integer (got ${v})` }
257
273
  out.verbosity = n
258
274
  } else if (a === '--dry') {
259
- 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'
260
282
  } else if (a?.startsWith('--dry=')) {
261
283
  const fmt = a.slice('--dry='.length)
262
- if (fmt !== 'text' && fmt !== 'json') {
263
- 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})` }
264
293
  }
265
294
  out.dry = fmt
266
295
  } else if (a === '--profile') {
@@ -275,19 +304,36 @@ export function parseRunArgs(args: readonly string[], verb: 'run' | 'watch' = 'r
275
304
  out.affected = ''
276
305
  } else if (a?.startsWith('--affected=')) {
277
306
  out.affected = a.slice('--affected='.length)
278
- } 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>)` }
279
311
  out.poll = true
280
312
  } else if (a !== undefined && settingFlagOf(a) !== undefined) {
281
313
  const { s: set, name } = settingFlagOf(a)!
282
314
  const v = a === name ? before[++i] : a.slice(name.length + 1)
283
315
  if (v === undefined || v === '')
284
- 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
+ }
285
324
  const value = parseSettingValue(set.kind, v)
286
325
  if (value === null) return { ...out, error: settingRefusal(set.path, name, v) }
287
326
  out.settings[set.path] = value
288
327
  } else if (a !== undefined && a.startsWith('-')) {
289
328
  return { ...out, error: `unknown flag: ${a}${flagHint(verb, a)}` }
290
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
+ }
291
337
  out.tasks.push(a)
292
338
  }
293
339
  }
@@ -311,7 +357,7 @@ export function parseRunArgs(args: readonly string[], verb: 'run' | 'watch' = 'r
311
357
  error: `VX_CACHE_POLICY: ${err instanceof Error ? err.message : String(err)}`,
312
358
  }
313
359
  }
314
- if (/remote:[rw]/.test(envSpec)) out.remoteRequested = true
360
+ if (/remote:[rw]/.test(envSpec)) out.remoteRequested = 'VX_CACHE_POLICY'
315
361
  }
316
362
  // --no-cache (all off) beats --force (reads off, writes kept), both
317
363
  // layered on top of the spec; with none named, on the workspace's.
@@ -400,12 +446,22 @@ export async function resolveRunOptions(
400
446
  scopeOut?: { affectedBase?: string },
401
447
  ): Promise<RunOptions | { error: string; code?: string } | { nothingSelected: string }> {
402
448
  for (const t of tasks) {
403
- const idx = t.indexOf('#')
404
- if (idx >= 0) {
405
- const project = t.slice(0, idx)
406
- const task = t.slice(idx + 1)
407
- if (!project || !task) {
408
- 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)`,
409
465
  }
410
466
  }
411
467
  }
@@ -436,6 +492,7 @@ export async function resolveRunOptions(
436
492
  filterStrings,
437
493
  parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {},
438
494
  affectedFilter,
495
+ verb,
439
496
  )
440
497
  : undefined
441
498
  if (resolved !== undefined && 'error' in resolved) return resolved
@@ -464,7 +521,10 @@ export async function resolveRunOptions(
464
521
  } else if (parsed.all) {
465
522
  projects = undefined
466
523
  } else {
467
- const cwdProject = await findCwdSelection(cwd)
524
+ const cwdProject = await findCwdSelection(
525
+ cwd,
526
+ parsed.cacheDir !== undefined ? { cacheDir: parsed.cacheDir } : {},
527
+ )
468
528
  if (!cwdProject) {
469
529
  const nx = await nxTargetHint(tasks, cwd)
470
530
  if (nx !== null) return { error: nx }
@@ -482,7 +542,7 @@ export async function resolveRunOptions(
482
542
  ...(parsed.cacheFlags !== undefined
483
543
  ? { cacheFlags: parsed.cacheFlags }
484
544
  : { cache: parsed.cache }),
485
- ...(parsed.remoteRequested ? { remoteRequested: true } : {}),
545
+ ...(parsed.remoteRequested ? { remoteRequested: parsed.remoteRequested } : {}),
486
546
  // A laptop reads the shared cache and never writes it (owner,
487
547
  // 2026-10-03); naming the remote in `--cache-policy` is asking to.
488
548
  ...(!parsed.remoteRequested && !onCI(process.env['CI'])
@@ -527,6 +587,10 @@ export async function runCmd(args: readonly string[]): Promise<number> {
527
587
  }
528
588
 
529
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
+ }
530
594
  let tasks = [...parsed.tasks]
531
595
  let picked: string | undefined
532
596
 
@@ -640,7 +704,7 @@ export async function runCmd(args: readonly string[]): Promise<number> {
640
704
  if (parsed.dry === 'json') {
641
705
  process.stdout.write(formatPlanJson(plan, scopeOut.affectedBase))
642
706
  } else {
643
- process.stdout.write(formatPlanText(plan))
707
+ process.stdout.write(formatPlanText(plan, detectColors(process.stdout)))
644
708
  }
645
709
  return 0
646
710
  }
@@ -657,7 +721,18 @@ export async function runCmd(args: readonly string[]): Promise<number> {
657
721
  return summary.ok ? 0 : 1
658
722
  }
659
723
 
660
- /** CI=0 and CI=false are "not CI": several vendors set CI=true. */
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. */
661
736
  export function onCI(v: string | undefined): boolean {
662
- return v !== undefined && v !== '' && v !== '0' && v !== 'false'
737
+ return envFlagOn(v)
663
738
  }