@vzn/vx 0.0.636 → 0.0.637

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 (141) hide show
  1. package/README.md +39 -11
  2. package/docs/caching.md +213 -207
  3. package/docs/cli.md +699 -655
  4. package/docs/execution.md +67 -75
  5. package/docs/features.md +44 -40
  6. package/docs/patterns.md +26 -25
  7. package/docs/schema.md +376 -198
  8. package/docs/security.md +76 -39
  9. package/launcher.sh +43 -0
  10. package/package.json +5 -5
  11. package/schemas/cache.json +41 -19
  12. package/schemas/info.json +23 -11
  13. package/schemas/last.json +48 -2
  14. package/schemas/lock.json +1 -1
  15. package/schemas/plan.json +2 -2
  16. package/schemas/show.json +5 -3
  17. package/schemas/why.json +15 -1
  18. package/skills/vx/SKILL.md +16 -12
  19. package/src/bin.ts +17 -2
  20. package/src/cache/archive.ts +11 -0
  21. package/src/cache/cache.ts +105 -53
  22. package/src/cache/git-inputs.ts +277 -63
  23. package/src/cache/index.ts +7 -1
  24. package/src/cache/inputs.ts +125 -35
  25. package/src/cache/key-fold.ts +8 -7
  26. package/src/cache/layer.ts +8 -17
  27. package/src/cache/layered-cache.ts +16 -102
  28. package/src/cache/policy.ts +25 -29
  29. package/src/cache/run-history.ts +36 -14
  30. package/src/cache/schema.ts +21 -23
  31. package/src/cache/scoped-cache.ts +224 -0
  32. package/src/cli/cache.ts +150 -28
  33. package/src/cli/completions.ts +2 -2
  34. package/src/cli/core-alias.ts +1 -1
  35. package/src/cli/foreign-flags.ts +13 -12
  36. package/src/cli/help.ts +94 -63
  37. package/src/cli/index.ts +42 -13
  38. package/src/cli/info.ts +38 -23
  39. package/src/cli/init.ts +115 -406
  40. package/src/cli/last.ts +145 -4
  41. package/src/cli/lock.ts +333 -111
  42. package/src/cli/plan-format.ts +2 -2
  43. package/src/cli/plugin-commands.ts +5 -9
  44. package/src/cli/plugin-templates.ts +10 -10
  45. package/src/cli/run.ts +103 -149
  46. package/src/cli/select.ts +33 -39
  47. package/src/cli/show.ts +17 -19
  48. package/src/cli/task-verb.ts +2 -9
  49. package/src/cli/upgrade.ts +402 -145
  50. package/src/cli/watch-cycle.ts +9 -2
  51. package/src/cli/watch-filter.ts +33 -60
  52. package/src/cli/watch-fs.ts +29 -16
  53. package/src/cli/watch-judge.ts +12 -3
  54. package/src/cli/watch-set.ts +16 -42
  55. package/src/cli/watch.ts +188 -215
  56. package/src/cli/why.ts +28 -17
  57. package/src/cli/workspace-config.ts +31 -68
  58. package/src/config.ts +169 -70
  59. package/src/exec/env.ts +1 -31
  60. package/src/exec/executor.ts +1 -5
  61. package/src/exec/kill-tree.ts +2 -1
  62. package/src/exec/sandbox-runtime.ts +12 -1
  63. package/src/exec/sandbox-violations.ts +9 -1
  64. package/src/graph/dependency-spec.ts +1 -1
  65. package/src/graph/index.ts +1 -0
  66. package/src/graph/priorities.ts +1 -1
  67. package/src/graph/scheduler.ts +19 -7
  68. package/src/graph/task-graph.ts +44 -20
  69. package/src/handoff.ts +169 -0
  70. package/src/index.ts +24 -57
  71. package/src/orchestrator/affected-tasks.ts +7 -5
  72. package/src/orchestrator/command-windows.ts +44 -0
  73. package/src/orchestrator/deferred-outputs.ts +12 -10
  74. package/src/orchestrator/doctor.ts +98 -110
  75. package/src/orchestrator/download-policy.ts +3 -3
  76. package/src/orchestrator/events.ts +19 -3
  77. package/src/orchestrator/excluded-keys.ts +0 -2
  78. package/src/orchestrator/execute-task.ts +158 -63
  79. package/src/orchestrator/fingerprint-watch.ts +18 -15
  80. package/src/orchestrator/forecast.ts +10 -2
  81. package/src/orchestrator/framed-output.ts +8 -3
  82. package/src/orchestrator/graph-stage.ts +5 -5
  83. package/src/orchestrator/history.ts +70 -30
  84. package/src/orchestrator/hit-restore.ts +15 -3
  85. package/src/orchestrator/index.ts +18 -4
  86. package/src/orchestrator/local-shortcircuit.ts +2 -1
  87. package/src/orchestrator/lockfile-claim.ts +79 -20
  88. package/src/orchestrator/logger.ts +71 -39
  89. package/src/orchestrator/metrics.ts +71 -6
  90. package/src/orchestrator/miss-save.ts +3 -0
  91. package/src/orchestrator/options.ts +42 -29
  92. package/src/orchestrator/output-host.ts +72 -0
  93. package/src/orchestrator/plan.ts +6 -8
  94. package/src/orchestrator/plugin-host.ts +87 -63
  95. package/src/orchestrator/plugin.ts +94 -60
  96. package/src/orchestrator/prepare.ts +113 -72
  97. package/src/orchestrator/projects.ts +102 -99
  98. package/src/orchestrator/remote-prefetch.ts +2 -1
  99. package/src/orchestrator/run-context.ts +25 -6
  100. package/src/orchestrator/run-failures.ts +17 -8
  101. package/src/orchestrator/run-lock.ts +1 -1
  102. package/src/orchestrator/run-report.ts +9 -9
  103. package/src/orchestrator/run-settings.ts +48 -0
  104. package/src/orchestrator/run.ts +121 -95
  105. package/src/orchestrator/sandbox-fix.ts +39 -20
  106. package/src/orchestrator/sandbox-request.ts +84 -13
  107. package/src/orchestrator/signals.ts +1 -1
  108. package/src/orchestrator/stable-keys.ts +13 -11
  109. package/src/orchestrator/status-line.ts +48 -6
  110. package/src/orchestrator/summary.ts +46 -62
  111. package/src/orchestrator/task-hash.ts +12 -83
  112. package/src/orchestrator/telemetry.ts +3 -3
  113. package/src/settings.ts +417 -0
  114. package/src/util/errors.ts +1 -1
  115. package/src/util/essential-env.ts +31 -0
  116. package/src/util/hide-env.ts +107 -0
  117. package/src/util/index.ts +8 -2
  118. package/src/util/paths.ts +10 -0
  119. package/src/util/settle.ts +49 -30
  120. package/src/util/size.ts +23 -4
  121. package/src/util/timing.ts +3 -2
  122. package/src/util/ulid.ts +1 -1
  123. package/src/util/verbs.ts +0 -15
  124. package/src/workspace/affected.ts +77 -222
  125. package/src/workspace/config-eval.ts +15 -20
  126. package/src/workspace/config-imports.ts +78 -3
  127. package/src/workspace/config-schema.ts +209 -268
  128. package/src/workspace/dependency-rules.ts +57 -0
  129. package/src/workspace/filter.ts +2 -2
  130. package/src/workspace/fingerprint.ts +11 -121
  131. package/src/workspace/index.ts +20 -27
  132. package/src/workspace/load-reads.ts +4 -4
  133. package/src/workspace/lockfile.ts +96 -140
  134. package/src/workspace/migration.ts +242 -146
  135. package/src/workspace/package-graph.ts +30 -216
  136. package/src/workspace/project-loader.ts +80 -285
  137. package/src/workspace/repo-id.ts +2 -1
  138. package/src/workspace/workspace.ts +362 -831
  139. package/src/cache/config-evals.ts +0 -130
  140. package/src/workspace/config-cache.ts +0 -661
  141. package/src/workspace/migrate-scripts.ts +0 -1121
package/src/cli/select.ts CHANGED
@@ -14,11 +14,9 @@ import {
14
14
  buildPackageGraph,
15
15
  defaultAffectedBase,
16
16
  findWorkspaceRoot,
17
- type LoadReads,
18
17
  type FingerprintClaims,
19
- FROZEN_WITHOUT_LOCK,
20
- loadProjectConfig,
21
- loadWorkspace,
18
+ lockKey,
19
+ NO_LOCK,
22
20
  parseFilter,
23
21
  type ProjectMeta,
24
22
  readLockfile,
@@ -49,15 +47,15 @@ import {
49
47
  loadCliProjects,
50
48
  loadCliWorkspace,
51
49
  } from './workspace-config.js'
50
+ import { resolveSetting } from '../settings.js'
52
51
 
53
52
  /**
54
53
  * The projects whose tasks declare a `cache.inputs.workspaceFiles` glob
55
54
  * matching a changed path, one inside another project included (item
56
55
  * 954): `--affected` selects them, since the glob is that task's input. Through the run
57
56
  * path's staged load, so a glob a `project` plugin gave a config-less
58
- * package counts; evaluated live as a default run does, or read from the
59
- * lock as a `--frozen` run does. A load that fails drops to the config files that
60
- * do load, one by one — a broken out-of-scope config does not fail a
57
+ * package counts; read from the lock as a run does. A load that fails drops to the lock's
58
+ * entries, one by one — a project the lock does not hold does not fail a
61
59
  * scoped run, and must not fail its selection either.
62
60
  */
63
61
  export async function workspaceGlobOwners(
@@ -85,26 +83,23 @@ export async function workspaceGlobOwners(
85
83
  } catch {
86
84
  // Fall through to the per-file sweep.
87
85
  }
88
- // A frozen run with no lock is refused here, before the tolerant sweep
89
- // below could answer "nothing affected" and exit 0 without ever reaching
90
- // the run's own refusal. Asked only once the staged load failed: a
91
- // frozen load that succeeded read the lock, and a second read parsed it
92
- // again (1,000 projects: a 1.1 MB lock, ~10 ms).
93
- if (load.frozen === true && (await readLockfile(root)) === null) {
94
- throw new UserError(FROZEN_WITHOUT_LOCK)
95
- }
86
+ // No lock is refused here, before the tolerant sweep below could answer
87
+ // "nothing affected" and exit 0 without ever reaching the run's own
88
+ // refusal. The sweep reads each entry from the lock: it never evaluates.
89
+ const lock = await readLockfile(root)
90
+ if (lock === null) throw new UserError(NO_LOCK)
96
91
  const owners: string[] = []
97
- await Promise.all(
98
- projects.map(async (meta) => {
99
- if (meta.configPath === null || meta.configPath === '') return
100
- try {
101
- if (declaresMatch(await loadProjectConfig(meta.configPath))) owners.push(meta.name)
102
- } catch {
103
- // A config that will not load cannot be shown to declare a matching
104
- // glob; surfacing it here would fail a run the loader itself tolerates.
105
- }
106
- }),
107
- )
92
+ for (const meta of projects) {
93
+ if (meta.configPath === null || meta.configPath === '') continue
94
+ const dir = lockKey(root, meta.dir)
95
+ if (!Object.hasOwn(lock.projects, dir)) continue
96
+ try {
97
+ if (declaresMatch(lock.projects[dir]!.config)) owners.push(meta.name)
98
+ } catch {
99
+ // A hand-edited entry that will not read cannot be shown to declare a
100
+ // matching glob; surfacing it here would fail a run the load tolerates.
101
+ }
102
+ }
108
103
  return owners
109
104
  }
110
105
 
@@ -133,9 +128,7 @@ async function workspaceFingerprintClaims(
133
128
  }
134
129
 
135
130
  async function loadWorkspaceProjects(cwd: string): Promise<ProjectMeta[]> {
136
- const reads: LoadReads = new Map()
137
- const root = await findWorkspaceRoot(cwd, reads)
138
- return await discoverCliProjects(await loadWorkspace(root, reads))
131
+ return await discoverCliProjects(await findWorkspaceRoot(cwd))
139
132
  }
140
133
 
141
134
  export async function findCwdProject(cwd: string): Promise<string | null> {
@@ -150,9 +143,8 @@ export async function findCwdProject(cwd: string): Promise<string | null> {
150
143
  export async function findCwdSelection(
151
144
  cwd: string,
152
145
  ): Promise<{ name: string; discovered: { root: string; projects: ProjectMeta[] } } | null> {
153
- const reads: LoadReads = new Map()
154
- const root = await findWorkspaceRoot(cwd, reads)
155
- const projects = await discoverCliProjects(await loadWorkspace(root, reads))
146
+ const root = await findWorkspaceRoot(cwd)
147
+ const projects = await discoverCliProjects(root)
156
148
  const abs = path.resolve(cwd)
157
149
  const within = (dirOf: (p: ProjectMeta) => string): string | null => {
158
150
  let best: string | null = null
@@ -251,14 +243,17 @@ function refusalOf(err: unknown): Refusal {
251
243
 
252
244
  /**
253
245
  * The filter `--affected[=<base>]` stands for: `...[<base>]`, the changed
254
- * projects and their dependents. An empty base is the workspace's
255
- * `affectedBase` (or a plugin's `config` stage, from nx.json's
246
+ * projects and their dependents. An empty base is `run.affectedBase`
247
+ * (its variable, then the workspace's, which a plugin's `config` stage may set from nx.json's
256
248
  * `defaultBase` or `TURBO_SCM_BASE`), then the guess.
257
249
  */
258
250
  export async function affectedFilterFor(cwd: string, affected: string): Promise<string | Refusal> {
259
251
  const root = await findWorkspaceRoot(cwd)
260
252
  let base = affected
261
- if (base === '') base = (await loadCliWorkspace(root)).workspaceConfig?.affectedBase ?? ''
253
+ if (base === '') {
254
+ const ws = (await loadCliWorkspace(root)).workspaceConfig
255
+ base = (resolveSetting('run.affectedBase', ws).value as string | undefined) ?? ''
256
+ }
262
257
  if (base === '') {
263
258
  try {
264
259
  base = await defaultAffectedBase(root)
@@ -339,7 +334,6 @@ export async function resolveFilters(
339
334
  workspaceGlobOwners: (changed: readonly string[], nested: readonly string[]) =>
340
335
  workspaceGlobOwners(root, projects, changed, load, stagedOnce, nested),
341
336
  fingerprintClaims: () => workspaceFingerprintClaims(root, projects, load),
342
- taskEdges: async () => edges ?? taskEdgesFrom(await stagedOnce()),
343
337
  ...(git !== undefined ? { untracked: async () => (await git.start()).untracked } : {}),
344
338
  }
345
339
  let names: Set<string>
@@ -524,7 +518,7 @@ export async function pickTask(
524
518
  }
525
519
  if (entries.length === 0) {
526
520
  process.stderr.write(
527
- 'vx run: no tasks declared in any project; declare one under `tasks` in a vx.config, or run `vx init` to write them from package.json scripts\n',
521
+ 'vx run: no tasks declared in any project; declare one under `tasks` in a vx.config, or run `vx init` to write them from package.json scripts, then `vx lock`\n',
528
522
  )
529
523
  return null
530
524
  }
@@ -604,7 +598,7 @@ function didYouMeanProject(
604
598
  projects: Iterable<{ name: string }>,
605
599
  ): string {
606
600
  const names = [...projects].map((p) => p.name)
607
- // A scoped name typed without its scope (`vx-mcp` for `@vzn/vx-mcp`) is
601
+ // A scoped name typed without its scope (`vx-otel` for `@vzn/vx-otel`) is
608
602
  // many edits from the whole name and none from the part after the `/`.
609
603
  const byBare = new Map<string, string[]>()
610
604
  for (const n of names) {
@@ -615,7 +609,7 @@ function didYouMeanProject(
615
609
  const typed = pattern.replace(/^!|\.\.\.$|^\.\.\.|\^/g, '')
616
610
  // Two edits from any two-letter name: `--filter //` hinted `ui` (X-13).
617
611
  if (typed === '//') {
618
- return ". `//` is Turbo's root package, but the workspace root is no project here: give it a package.json name and a vx.config"
612
+ return '. `//` is the workspace root, which is no project here: add a vx.config there and run `vx lock`'
619
613
  }
620
614
  const best = nearest(typed, names)
621
615
  if (best !== undefined) return `. Did you mean ${best}?`
package/src/cli/show.ts CHANGED
@@ -1,10 +1,10 @@
1
- // `vx show [target]` — introspect the workspace's LIVE resolved configs:
2
- // what a run would see here, now. Configs load through the same path a
3
- // run uses (`loadProjects`): the plugin `config` and `project` stages
4
- // apply, so a package a plugin gives tasks to shows them. No target: one
5
- // line per project. `<project>`: every task's resolved config. `<pkg>#
6
- // <task>`: one task. `<task>`: that task in every project declaring it.
7
- // Deliberately NOT the lock — vx-lock.json is already the frozen JSON.
1
+ // `vx show [target]` — introspect the workspace's resolved configs: what a
2
+ // run would see here, now. Configs load through the same path a run uses
3
+ // (`loadProjects`): from the lock, with the plugin `config` and `project`
4
+ // stages applied, so a package a plugin gives tasks to shows them. No lock
5
+ // is `NO_LOCK`. No target: one line per project. `<project>`: every task's
6
+ // resolved config. `<pkg>#<task>`: one task. `<task>`: that task in every
7
+ // project declaring it.
8
8
 
9
9
  import path from 'node:path'
10
10
  import type { ProjectConfig, TaskConfig } from '../config.js'
@@ -23,11 +23,10 @@ import {
23
23
  type AffectedChanges,
24
24
  buildPackageGraph,
25
25
  findWorkspaceRoot,
26
- type LoadReads,
27
- loadWorkspace,
28
26
  type ProjectEntry,
29
27
  type ProjectMeta,
30
28
  } from '../workspace/index.js'
29
+ import { runRules } from '../settings.js'
31
30
 
32
31
  interface ShowArgs {
33
32
  target?: string
@@ -83,9 +82,8 @@ export function parseShowArgs(args: readonly string[]): ShowArgs {
83
82
  export async function showCmd(args: readonly string[]): Promise<number> {
84
83
  const parsed = parseShowArgs(args)
85
84
  if (parsed.error) return refuse('show', args, parsed.error, 'VX_E_USAGE')
86
- const reads: LoadReads = new Map()
87
- const root = await findWorkspaceRoot(process.cwd(), reads)
88
- const metas = await discoverCliProjects(await loadWorkspace(root, reads))
85
+ const root = await findWorkspaceRoot(process.cwd())
86
+ const metas = await discoverCliProjects(root)
89
87
  const byName = new Map(metas.map((m) => [m.name, m]))
90
88
 
91
89
  // `//#task` is Turbo's root package's task: the root project here (D-39),
@@ -149,7 +147,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
149
147
  affected = f
150
148
  raw.unshift(f)
151
149
  }
152
- const r = await resolveFilters(process.cwd(), raw, { noCreate: true }, affected)
150
+ const r = await resolveFilters(process.cwd(), raw, {}, affected)
153
151
  if ('error' in r) throw new UserError(`vx show: ${r.error}`, r.code ?? 'VX_E_USAGE')
154
152
  if ('empty' in r) process.stderr.write(`vx show: ${r.empty}\n`)
155
153
  selected = new Set('empty' in r ? [] : r.names)
@@ -158,7 +156,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
158
156
  }
159
157
  const inSelection = (name: string): boolean => selected === undefined || selected.has(name)
160
158
 
161
- const projects = await loadCliProjects(root, metas, scope, { noCreate: true, closure: true })
159
+ const projects = await loadCliProjects(root, metas, scope, { closure: true })
162
160
 
163
161
  if (parsed.target === undefined) {
164
162
  const shown = metas.filter((m) => inSelection(m.name))
@@ -200,7 +198,7 @@ export async function showCmd(args: readonly string[]): Promise<number> {
200
198
  plugins: ws.plugins,
201
199
  workspaceRoot: root,
202
200
  cacheDir: ws.cacheDir,
203
- rules: ws.workspaceConfig?.rules,
201
+ rules: runRules(ws.workspaceConfig),
204
202
  warn: warnToStderr,
205
203
  }),
206
204
  )
@@ -342,7 +340,7 @@ function renderProject(
342
340
  // Round-trip so the printed object is exactly the JSON form of the
343
341
  // resolved config (drops `undefined` fields).
344
342
  const shown = shownConfig(config)
345
- return `${JSON.stringify(JSON.parse(JSON.stringify({ name, dir, config: shown })), null, 2)}\n`
343
+ return `${JSON.stringify({ name, dir, config: shown })}\n`
346
344
  }
347
345
  const tags = config?.tags ?? []
348
346
  const tasks = Object.entries(config?.tasks ?? {})
@@ -365,7 +363,7 @@ function renderTask(
365
363
  ): string {
366
364
  if (format === 'json') {
367
365
  const obj = { name, dir, task: taskName, config: shownTask(task) }
368
- return `${JSON.stringify(JSON.parse(JSON.stringify(obj)), null, 2)}\n`
366
+ return `${JSON.stringify(obj)}\n`
369
367
  }
370
368
  return `${name} — ${dir}\n\n${taskBlock(taskName, task)}`
371
369
  }
@@ -383,7 +381,7 @@ function renderTaskAcross(
383
381
  task: taskName,
384
382
  config: shownTask(p.config.tasks![taskName]!),
385
383
  }))
386
- return `${JSON.stringify(JSON.parse(JSON.stringify(list)), null, 2)}\n`
384
+ return `${JSON.stringify(list)}\n`
387
385
  }
388
386
  return declaring
389
387
  .map(
@@ -406,7 +404,7 @@ function taskBlock(taskName: string, raw: TaskConfig): string {
406
404
  const exec = task.exec
407
405
  rows.push(['command', exec?.command ?? '(group)'])
408
406
  add('dependsOn', list(task.dependsOn))
409
- add('timeout', exec?.timeout === undefined ? undefined : `${exec.timeout}ms`)
407
+ add('timeout', exec?.timeout)
410
408
  add('retries', exec?.retries)
411
409
  add('env.passThrough', list(exec?.env?.passThrough))
412
410
  add(
@@ -5,7 +5,7 @@
5
5
  // name would change meaning the day a plugin declares that verb.
6
6
 
7
7
  import { isDefaultBuild, nxProjectTarget, projectNamed } from '../orchestrator/index.js'
8
- import { findWorkspaceRoot, loadWorkspace } from '../workspace/index.js'
8
+ import { findWorkspaceRoot } from '../workspace/index.js'
9
9
  import { nearest } from '../util/index.js'
10
10
  import { findCwdProject } from './select.js'
11
11
  import { discoverCliProjects, loadCliProjects } from './workspace-config.js'
@@ -79,14 +79,7 @@ async function workspaceProjects(
79
79
  ): Promise<Awaited<ReturnType<typeof loadCliProjects>> | null> {
80
80
  try {
81
81
  const root = await findWorkspaceRoot(cwd)
82
- return await loadCliProjects(
83
- root,
84
- await discoverCliProjects(await loadWorkspace(root)),
85
- 'all',
86
- {
87
- noCreate: true,
88
- },
89
- )
82
+ return await loadCliProjects(root, await discoverCliProjects(root))
90
83
  } catch {
91
84
  // No workspace, or a config that does not load: the caller's plain line stands.
92
85
  return null