@vzn/vx 0.0.278 → 0.0.280

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx",
3
- "version": "0.0.278",
3
+ "version": "0.0.280",
4
4
  "description": "A fast, correct task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,10 +17,10 @@
17
17
  "node": ">=18"
18
18
  },
19
19
  "optionalDependencies": {
20
- "@vzn/vx-linux-x64": "0.0.278",
21
- "@vzn/vx-linux-arm64": "0.0.278",
22
- "@vzn/vx-darwin-x64": "0.0.278",
23
- "@vzn/vx-darwin-arm64": "0.0.278"
20
+ "@vzn/vx-linux-x64": "0.0.280",
21
+ "@vzn/vx-linux-arm64": "0.0.280",
22
+ "@vzn/vx-darwin-x64": "0.0.280",
23
+ "@vzn/vx-darwin-arm64": "0.0.280"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/bin.ts CHANGED
@@ -1,26 +1,25 @@
1
1
  #!/usr/bin/env -S bun --no-env-file --no-install
2
- import { registerCoreAlias, run } from './cli/index.js'
3
- import {
4
- fsRefusalHint,
5
- isFsRefusal,
6
- isOutOfFds,
7
- isUserError,
8
- OUT_OF_FDS_HINT,
9
- } from './util/index.js'
10
-
11
- // Every `import … from '@vzn/vx'` this process evaluates — a plugin
12
- // package, a workspace or project config — resolves to THIS core, not to
13
- // a second copy from node_modules (core-alias.ts says why and what it
14
- // costs). Registered before any verb runs; the façade loads on first use.
15
- registerCoreAlias(() => import('./index.js') as Promise<Record<string, unknown>>)
16
-
17
2
  // Wrapped in an explicit async main so `bun build --compile` accepts
18
3
  // the file. The compile target doesn't allow top-level await.
19
4
  let settled = false
20
5
 
21
6
  async function main(): Promise<void> {
7
+ const argv = process.argv.slice(2)
22
8
  try {
23
- const code = await run(process.argv.slice(2))
9
+ // `vx --version` needs one leaf module; the dispatcher and the util
10
+ // barrel it pulled in were 20 ms of its 28 (2026-10-01).
11
+ if (argv.length === 1 && (argv[0] === '--version' || argv[0] === 'version')) {
12
+ process.stdout.write(`vx ${(await import('./version.js')).VERSION}\n`)
13
+ settled = true
14
+ return
15
+ }
16
+ const { registerCoreAlias, run } = await import('./cli/index.js')
17
+ // Every `import … from '@vzn/vx'` this process evaluates — a plugin
18
+ // package, a workspace or project config — resolves to THIS core, not to
19
+ // a second copy from node_modules (core-alias.ts says why and what it
20
+ // costs). Registered before any verb runs; the façade loads on first use.
21
+ registerCoreAlias(() => import('./index.js') as Promise<Record<string, unknown>>)
22
+ const code = await run(argv)
24
23
  // NOTHING calls `process.exit` here, and that is the fix rather than a
25
24
  // simplification. Bun drops what a pipe has not yet taken when
26
25
  // `process.exit` follows a large write: 300 KB written then exit
@@ -34,6 +33,8 @@ async function main(): Promise<void> {
34
33
  // several hundred spawns of this binary would show at once.
35
34
  process.exitCode = code
36
35
  } catch (err) {
36
+ const { fsRefusalHint, isFsRefusal, isOutOfFds, isUserError, OUT_OF_FDS_HINT } =
37
+ await import('./util/index.js')
37
38
  // UserError (workspace not found, cycle, config invalid, ...) —
38
39
  // print the message only; the stack is noise the user can't act
39
40
  // on. Everything else gets the full stack so internal bugs are
package/src/cli/help.ts CHANGED
@@ -61,7 +61,7 @@ export function helpText(pluginCommands: readonly string[] = []): string {
61
61
  ' foo, @scope/*, ./packages/foo, foo..., ...foo,',
62
62
  ' foo^..., !foo, [<since>]',
63
63
  ' --affected[=<base>] Sugar for --filter "...[<base>]". Default base =',
64
- ' origin/HEAD (falls back to HEAD~1).',
64
+ ' affectedBase, else origin/HEAD, else HEAD~1.',
65
65
  " pkg#task Run a specific project's task directly.",
66
66
  '',
67
67
  'Execution (for run):',
@@ -216,6 +216,9 @@ export function flagHint(verb: string, arg: string): string {
216
216
  if (!arg.startsWith('-')) return ''
217
217
  const name = arg.replace(/=.*$/, '')
218
218
  const flags = acceptedFlags(verb)
219
+ // `--json` is gh's and Nx's spelling; a verb with `--format` reads it
220
+ // there, and no edit budget reaches `--format json` from `--json`.
221
+ if (name === '--json' && flags.includes('--format')) return ' (did you mean --format json?)'
219
222
  // A flag cut short at a word (`--dry` for prune's `--dry-run`, which
220
223
  // `vx run` spells `--dry`) is too many edits away for either budget.
221
224
  const cut = flags.filter((f) => f.startsWith(`${name}-`))
package/src/cli/init.ts CHANGED
@@ -180,7 +180,9 @@ async function adopt(
180
180
  : `wrote ${name}.`
181
181
  const cacheLine =
182
182
  remote === undefined
183
- ? ''
183
+ ? runner === 'nx' && usesNxCloud(root)
184
+ ? 'nx.json connects Nx Cloud, whose cache vx cannot share: runs cache on this machine (nxCache() serves a self-hosted Nx cache).\n'
185
+ : ''
184
186
  : kept && !readFileSync(path.join(root, existing), 'utf8').includes(`${cache}(`)
185
187
  ? `${remote}: add ${cache}() from @vzn/vx-migrate to its plugins and vx shares that remote cache.\n`
186
188
  : kept
@@ -195,6 +197,31 @@ async function adopt(
195
197
 
196
198
  const REMOTE_CACHE = { turbo: 'turboCache', nx: 'nxCache' } as const
197
199
 
200
+ /**
201
+ * nx.json names an Nx Cloud workspace (`nxCloudId`, `nxCloudAccessToken`,
202
+ * or the legacy `nx-cloud` runner). Its wire is Nx's own, so a user who
203
+ * expected their remote cache under vx heard nothing and ran cold.
204
+ */
205
+ function usesNxCloud(root: string): boolean {
206
+ let json: {
207
+ nxCloudId?: unknown
208
+ nxCloudAccessToken?: unknown
209
+ tasksRunnerOptions?: { default?: { runner?: unknown } }
210
+ } | null
211
+ try {
212
+ json = Bun.JSONC.parse(readFileSync(path.join(root, 'nx.json'), 'utf8')) as typeof json
213
+ } catch {
214
+ return false
215
+ }
216
+ const set = (v: unknown) => typeof v === 'string' && v !== ''
217
+ const runner = json?.tasksRunnerOptions?.default?.runner
218
+ return (
219
+ set(json?.nxCloudId) ||
220
+ set(json?.nxCloudAccessToken) ||
221
+ (typeof runner === 'string' && /(^|\/)nx-cloud$/.test(runner))
222
+ )
223
+ }
224
+
198
225
  /** The variable each runner's self-hosted remote cache is set by, as its plugin reads it. */
199
226
  const REMOTE_CACHE_ENV = { turbo: 'TURBO_TOKEN', nx: 'NX_SELF_HOSTED_REMOTE_CACHE_SERVER' } as const
200
227
 
@@ -202,7 +229,7 @@ const CI_FILES = ['.gitlab-ci.yml', '.circleci/config.yml']
202
229
 
203
230
  /**
204
231
  * Where the repo shows a remote cache its runner uses, or undefined: turbo.json's
205
- * enabled `remoteCache`, or a CI file that sets the runner's cache variable (a
232
+ * enabled `remoteCache`, a `turbo link`ed `.turbo/config.json`, or a CI file that sets the runner's cache variable (a
206
233
  * local repo holds no token; CI does).
207
234
  */
208
235
  function remoteCacheSignal(root: string, runner: 'turbo' | 'nx'): string | undefined {
@@ -222,6 +249,21 @@ function remoteCacheSignal(root: string, runner: 'turbo' | 'nx'): string | undef
222
249
  return `${f} names a remoteCache`
223
250
  }
224
251
  }
252
+ // `turbo link` writes the team here (gitignored, on the machine that
253
+ // linked); turboCache() reads it, so a linked repo has a remote cache.
254
+ let linked: unknown
255
+ try {
256
+ linked = JSON.parse(readFileSync(path.join(root, '.turbo', 'config.json'), 'utf8'))
257
+ } catch {}
258
+ if (
259
+ typeof linked === 'object' &&
260
+ linked !== null &&
261
+ Object.entries(linked).some(
262
+ ([k, v]) => /^(teamid|teamslug|token)$/i.test(k) && typeof v === 'string' && v !== '',
263
+ )
264
+ ) {
265
+ return '.turbo/config.json links a team (turbo link)'
266
+ }
225
267
  }
226
268
  const variable = REMOTE_CACHE_ENV[runner]
227
269
  // A line that sets it (`TURBO_TOKEN: …`, `TURBO_TOKEN=…`), not one that
package/src/cli/run.ts CHANGED
@@ -17,6 +17,7 @@ import type { ContinueMode } from '../graph/index.js'
17
17
  import { type CachePolicy, FULL_CACHE_POLICY, parseCachePolicy } from '../cache/index.js'
18
18
  import { findCwdSelection, pickTask, resolveFilters } from './select.js'
19
19
  import { nxTargetHint, taskNamesHere } from './task-verb.js'
20
+ import { loadCliWorkspace } from './workspace-config.js'
20
21
  import { MAX_TIMEOUT_MS, isUserError, parseDecimalInt, machineParallelism } from '../util/index.js'
21
22
  import { formatGraphDot, formatPlanJson, formatPlanText } from './plan-format.js'
22
23
 
@@ -464,6 +465,9 @@ export async function resolveRunOptions(
464
465
  if (parsed.affected !== undefined) {
465
466
  const root = await findWorkspaceRoot(cwd)
466
467
  let base = parsed.affected
468
+ // The workspace's `affectedBase` — or a plugin's `config` stage, from
469
+ // nx.json's `defaultBase` or `TURBO_SCM_BASE` — comes before the guess.
470
+ if (base === '') base = (await loadCliWorkspace(root)).workspaceConfig?.affectedBase ?? ''
467
471
  if (base === '') {
468
472
  try {
469
473
  base = await defaultAffectedBase(root)
package/src/config.ts CHANGED
@@ -22,6 +22,12 @@ export interface WorkspaceConfig {
22
22
  * folded into a cache key.
23
23
  */
24
24
  cacheRetention?: { olderThan?: string; maxSize?: string }
25
+ /**
26
+ * The git ref a bare `--affected` compares with (`origin/develop`).
27
+ * `--affected=<base>` overrides it. Omitted → `origin/HEAD`, else
28
+ * `HEAD~1`. Never folded into a cache key.
29
+ */
30
+ affectedBase?: string
25
31
  /**
26
32
  * Plugins registered for this workspace, consulted in this order once
27
33
  * per `vx run`: each fills any of the pipeline stages and capabilities
@@ -35,6 +35,7 @@ const WORKSPACE_FIELDS = new Set([
35
35
  'cacheDir',
36
36
  'timeout',
37
37
  'cacheRetention',
38
+ 'affectedBase',
38
39
  'plugins',
39
40
  ])
40
41
 
@@ -105,6 +106,12 @@ export function validateWorkspace(config: WorkspaceConfig, configPath: string):
105
106
  assertTimeoutInRange(config.timeout, `${configPath}: \`timeout\``)
106
107
  }
107
108
  if (config.cacheRetention !== undefined) validateRetention(config.cacheRetention, configPath)
109
+ if (config.affectedBase !== undefined) {
110
+ // A leading '-' would reach git as an option, not a ref.
111
+ if (typeof config.affectedBase !== 'string' || !/^[^-\s]\S*$/.test(config.affectedBase)) {
112
+ throw new UserError(`${configPath}: \`affectedBase\` must be a git ref like 'origin/main'`)
113
+ }
114
+ }
108
115
  if (config.plugins !== undefined) {
109
116
  if (!Array.isArray(config.plugins)) {
110
117
  throw new UserError(`${configPath}: \`plugins\` must be an array of plugin objects`)
@@ -768,7 +775,7 @@ const FOREIGN_FIELDS: ReadonlyMap<ReadonlySet<string>, Readonly<Record<string, s
768
775
  'a cache plugin in `plugins` (`turboCache()` or `nxCache()` from `@vzn/vx-migrate`)',
769
776
  parallel: '`concurrency`',
770
777
  cacheDirectory: '`cacheDir`',
771
- defaultBase: 'the base in `--affected=<base>` (default `origin/HEAD`)',
778
+ defaultBase: '`affectedBase`',
772
779
  tasksRunnerOptions: 'a cache plugin in `plugins` (`nxCache()` from `@vzn/vx-migrate`)',
773
780
  globalPassThroughEnv: '`exec.env.passThrough` on the tasks it passes to',
774
781
  },