@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.
- package/README.md +29 -22
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +112 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +18 -2
- package/src/bin.ts +53 -12
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +268 -53
- package/src/cache/git-inputs.ts +219 -87
- package/src/cache/index.ts +3 -2
- package/src/cache/inputs.ts +460 -78
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +49 -9
- package/src/cache/layered-cache.ts +123 -58
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +19 -3
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +90 -14
- package/src/cache/run-history.ts +70 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +25 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +58 -19
- package/src/cli/index.ts +79 -33
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +186 -31
- package/src/cli/last.ts +439 -45
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +70 -14
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +109 -34
- package/src/cli/select.ts +143 -46
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +151 -49
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +154 -10
- package/src/exec/index.ts +6 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +3 -1
- package/src/exec/runner.ts +253 -54
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +211 -24
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +270 -60
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +167 -61
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +90 -30
- package/src/index.ts +30 -13
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +4 -1
- package/src/orchestrator/execute-task.ts +130 -38
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +1 -1
- package/src/orchestrator/hit-restore.ts +217 -119
- package/src/orchestrator/index.ts +4 -3
- package/src/orchestrator/local-floor.ts +119 -25
- package/src/orchestrator/local-shortcircuit.ts +28 -4
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +42 -8
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +84 -29
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +185 -17
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +214 -42
- package/src/orchestrator/plugin.ts +113 -37
- package/src/orchestrator/prepare.ts +168 -30
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +149 -13
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -29
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +331 -88
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +70 -20
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +90 -26
- package/src/orchestrator/task-hash.ts +78 -13
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +7 -1
- package/src/orchestrator/telemetry-host.ts +22 -2
- package/src/orchestrator/telemetry.ts +53 -21
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +31 -9
- package/src/report/framed-output.ts +141 -50
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +170 -179
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +81 -53
- package/src/report/summary.ts +114 -32
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +7 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +4 -2
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +21 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +91 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +61 -3
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +17 -11
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -1
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +69 -18
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +226 -37
- package/src/workspace/repo-id.ts +1 -1
- 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 {
|
|
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
|
-
/**
|
|
59
|
-
remoteRequested?:
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
263
|
-
return {
|
|
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 (
|
|
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
|
|
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 =
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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(
|
|
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:
|
|
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
|
-
/**
|
|
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
|
|
737
|
+
return envFlagOn(v)
|
|
663
738
|
}
|