@vzn/vx 0.0.16 → 0.0.21

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 (138) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +135 -124
  3. package/index.ts +10 -0
  4. package/launcher.mjs +10 -11
  5. package/package.json +11 -7
  6. package/src/bin.ts +14 -4
  7. package/src/cache/archive.ts +659 -0
  8. package/src/cache/cache.ts +769 -1101
  9. package/src/cache/chained-cache.ts +194 -0
  10. package/src/cache/config-evals.ts +89 -0
  11. package/src/cache/file-hashes.ts +262 -0
  12. package/src/cache/git-inputs.ts +637 -0
  13. package/src/cache/index.ts +24 -5
  14. package/src/cache/inputs.ts +373 -456
  15. package/src/cache/layer.ts +604 -0
  16. package/src/cache/layered-cache.ts +96 -43
  17. package/src/cache/output-index.ts +224 -0
  18. package/src/cache/policy.ts +84 -0
  19. package/src/cache/run-history.ts +147 -0
  20. package/src/cache/tar-stream.ts +358 -0
  21. package/src/cache/zstd.ts +160 -0
  22. package/src/cli/cache.ts +75 -21
  23. package/src/cli/completions.ts +104 -0
  24. package/src/cli/core-alias.ts +25 -0
  25. package/src/cli/help.ts +195 -128
  26. package/src/cli/index.ts +116 -46
  27. package/src/cli/info.ts +122 -63
  28. package/src/cli/init.ts +75 -0
  29. package/src/cli/last.ts +172 -0
  30. package/src/cli/lock.ts +2 -1
  31. package/src/cli/plan-format.ts +72 -6
  32. package/src/cli/plugin-commands.ts +103 -0
  33. package/src/cli/run.ts +245 -271
  34. package/src/cli/select.ts +332 -0
  35. package/src/cli/show.ts +144 -69
  36. package/src/cli/upgrade.ts +9 -7
  37. package/src/cli/watch.ts +858 -110
  38. package/src/cli/why.ts +223 -0
  39. package/src/cli/workspace-config.ts +126 -0
  40. package/src/config.ts +187 -150
  41. package/src/exec/env.ts +18 -1
  42. package/src/exec/executor.ts +224 -0
  43. package/src/exec/index.ts +24 -2
  44. package/src/exec/local-executor.ts +41 -0
  45. package/src/exec/runner.ts +166 -40
  46. package/src/exec/sandbox-binds.ts +182 -0
  47. package/src/exec/sandbox-paths.ts +50 -0
  48. package/src/exec/sandbox-runtime.ts +642 -260
  49. package/src/exec/sandbox-violations.ts +280 -0
  50. package/src/graph/index.ts +4 -10
  51. package/src/graph/scheduler.ts +232 -203
  52. package/src/graph/task-graph.ts +222 -8
  53. package/src/index.ts +146 -153
  54. package/src/orchestrator/admission.ts +137 -0
  55. package/src/orchestrator/deferred-outputs.ts +147 -0
  56. package/src/orchestrator/doctor.ts +255 -0
  57. package/src/orchestrator/download-policy.ts +171 -0
  58. package/src/orchestrator/events.ts +72 -8
  59. package/src/orchestrator/execute-task.ts +374 -549
  60. package/src/orchestrator/failure-mode.ts +202 -0
  61. package/src/orchestrator/framed-output.ts +126 -69
  62. package/src/orchestrator/history.ts +126 -59
  63. package/src/orchestrator/hit-restore.ts +221 -0
  64. package/src/orchestrator/index.ts +77 -93
  65. package/src/orchestrator/local-shortcircuit.ts +22 -0
  66. package/src/orchestrator/lockfile-claim.ts +281 -0
  67. package/src/orchestrator/logger.ts +208 -35
  68. package/src/orchestrator/metrics.ts +102 -1388
  69. package/src/orchestrator/miss-save.ts +169 -0
  70. package/src/orchestrator/options.ts +55 -34
  71. package/src/orchestrator/persistent.ts +79 -0
  72. package/src/orchestrator/placement.ts +182 -0
  73. package/src/orchestrator/plan.ts +144 -1
  74. package/src/orchestrator/plugin-host.ts +357 -120
  75. package/src/orchestrator/plugin.ts +298 -58
  76. package/src/orchestrator/prepare.ts +202 -106
  77. package/src/orchestrator/projects.ts +259 -0
  78. package/src/orchestrator/remote-prefetch.ts +12 -8
  79. package/src/orchestrator/run-artifacts.ts +63 -16
  80. package/src/orchestrator/run-context.ts +167 -29
  81. package/src/orchestrator/run-records.ts +149 -0
  82. package/src/orchestrator/run-report.ts +74 -47
  83. package/src/orchestrator/run.ts +534 -417
  84. package/src/orchestrator/sandbox-request.ts +243 -0
  85. package/src/orchestrator/save-lane.ts +47 -0
  86. package/src/orchestrator/signals.ts +111 -0
  87. package/src/orchestrator/stable-keys.ts +129 -31
  88. package/src/orchestrator/status-line.ts +49 -9
  89. package/src/orchestrator/summary.ts +90 -26
  90. package/src/orchestrator/tally.ts +78 -35
  91. package/src/orchestrator/task-hash.ts +111 -15
  92. package/src/orchestrator/task-log-buffer.ts +288 -0
  93. package/src/orchestrator/telemetry-host.ts +34 -6
  94. package/src/orchestrator/telemetry.ts +182 -36
  95. package/src/orchestrator/upstream.ts +72 -31
  96. package/src/util/cgroup.ts +160 -0
  97. package/src/util/edit-distance.ts +70 -0
  98. package/src/util/errors.ts +15 -1
  99. package/src/util/index.ts +15 -2
  100. package/src/util/num.ts +37 -0
  101. package/src/util/paths.ts +64 -0
  102. package/src/util/settle.ts +67 -0
  103. package/src/util/size.ts +5 -1
  104. package/src/util/tail.ts +66 -0
  105. package/src/util/timing.ts +63 -0
  106. package/src/util/verbs.ts +39 -0
  107. package/src/workspace/affected.ts +250 -23
  108. package/src/workspace/config-cache.ts +313 -0
  109. package/src/workspace/config-eval.ts +217 -0
  110. package/src/workspace/config-imports.ts +162 -0
  111. package/src/workspace/config-schema.ts +837 -0
  112. package/src/workspace/filter.ts +65 -10
  113. package/src/workspace/fingerprint.ts +53 -4
  114. package/src/workspace/index.ts +42 -3
  115. package/src/workspace/lockfile.ts +25 -9
  116. package/src/workspace/migrate-scripts.ts +134 -0
  117. package/src/workspace/migration.ts +314 -0
  118. package/src/workspace/nested-dirs.ts +21 -7
  119. package/src/workspace/package-graph.ts +114 -41
  120. package/src/workspace/project-loader.ts +224 -450
  121. package/src/workspace/workspace.ts +298 -72
  122. package/src/cache/cas-backend.ts +0 -97
  123. package/src/cache/digest.ts +0 -47
  124. package/src/cache/tar.ts +0 -343
  125. package/src/cli/backend.ts +0 -23
  126. package/src/cli/mcp-rpc.ts +0 -292
  127. package/src/cli/mcp.ts +0 -92
  128. package/src/cli/migrate-nx.ts +0 -450
  129. package/src/cli/migrate-turbo.ts +0 -364
  130. package/src/cli/migrate.ts +0 -219
  131. package/src/orchestrator/devframe-surface.ts +0 -74
  132. package/src/orchestrator/predict.ts +0 -106
  133. package/src/orchestrator/protocol.ts +0 -156
  134. package/src/orchestrator/resources.ts +0 -59
  135. package/src/orchestrator/run-state.ts +0 -145
  136. package/src/orchestrator/verify.ts +0 -164
  137. package/src/orchestrator/wire-render.ts +0 -92
  138. package/src/orchestrator/wire.ts +0 -212
package/src/cli/cache.ts CHANGED
@@ -1,6 +1,8 @@
1
- import { Cache } from '../cache/index.js'
2
- import { parseSize } from '../util/index.js'
3
- import { findWorkspaceRoot, loadWorkspaceConfig, resolveCacheDir } from '../workspace/index.js'
1
+ import { Cache, noteSchemaReset } from '../cache/index.js'
2
+ import { seeHelp } from './help.js'
3
+ import { parseDecimalInt, parseSize } from '../util/index.js'
4
+ import { findWorkspaceRoot } from '../workspace/index.js'
5
+ import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
4
6
  import { formatBytes } from './format.js'
5
7
 
6
8
  // parseSize moved to `util` (the orchestrator's resource resolver needs it
@@ -16,7 +18,7 @@ export async function cacheCmd(args: readonly string[]): Promise<number> {
16
18
  process.stderr.write('vx cache: missing subcommand. Try `vx cache prune`.\n')
17
19
  return 1
18
20
  default:
19
- process.stderr.write(`vx cache: unknown subcommand: ${sub}\n`)
21
+ process.stderr.write(`vx cache: unknown subcommand: ${sub}${seeHelp('cache')}\n`)
20
22
  return 1
21
23
  }
22
24
  }
@@ -24,6 +26,9 @@ export async function cacheCmd(args: readonly string[]): Promise<number> {
24
26
  interface PruneArgs {
25
27
  olderThanMs?: number
26
28
  maxBytes?: number
29
+ dryRun?: boolean
30
+ /** `--cache-dir`: prune the cache a run with the same flag uses. */
31
+ cacheDir?: string
27
32
  error?: string
28
33
  }
29
34
 
@@ -31,20 +36,54 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
31
36
  const out: PruneArgs = {}
32
37
  for (let i = 0; i < args.length; i++) {
33
38
  const a = args[i]
34
- if (a === '--older-than') {
35
- const v = args[++i]
36
- if (v === undefined) return { error: `${a} requires a value (e.g. 30d, 24h, 60m)` }
39
+ if (a === '--older-than' || a?.startsWith('--older-than=')) {
40
+ const v = a === '--older-than' ? args[++i] : a.slice('--older-than='.length)
41
+ if (v === undefined || v === '') {
42
+ return { error: `--older-than requires a value (e.g. 30d, 24h, 60m)` }
43
+ }
37
44
  const ms = parseDuration(v)
38
45
  if (ms === null) return { error: `invalid duration: ${v}` }
46
+ // `0d` evicts everything ever cached. That is far more often a
47
+ // computed-to-zero retention than an intent, and no other flag
48
+ // combination expresses "wipe the cache" — so refuse it and name
49
+ // the deliberate way to do it.
50
+ if (ms === 0) {
51
+ return {
52
+ error: `--older-than 0 would evict every entry — delete the cache directory instead`,
53
+ }
54
+ }
39
55
  out.olderThanMs = Date.now() - ms
40
- } else if (a === '--max-size') {
41
- const v = args[++i]
42
- if (v === undefined) return { error: `${a} requires a value (e.g. 500M, 1G)` }
56
+ } else if (a === '--max-size' || a?.startsWith('--max-size=')) {
57
+ const v = a === '--max-size' ? args[++i] : a.slice('--max-size='.length)
58
+ if (v === undefined || v === '') {
59
+ return { error: `--max-size requires a value (e.g. 500M, 1G)` }
60
+ }
43
61
  const bytes = parseSize(v)
44
62
  if (bytes === null) return { error: `invalid size: ${v}` }
63
+ if (bytes === 0) {
64
+ return {
65
+ error: `--max-size 0 would evict every entry — delete the cache directory instead`,
66
+ }
67
+ }
68
+ // A bare number is bytes to `parseSize` (a computed size needs that),
69
+ // but nobody caps a cache at 10 bytes: `--max-size 10`
70
+ // meant 10G or 10M and would evict everything, the same class of
71
+ // destructive typo the zero bound above refuses. An explicit `10B`
72
+ // still passes.
73
+ if (/^\d+$/.test(v)) {
74
+ return {
75
+ error: `--max-size ${v} would read as ${v} bytes and evict nearly everything — give a unit (e.g. ${v}M, ${v}G)`,
76
+ }
77
+ }
45
78
  out.maxBytes = bytes
79
+ } else if (a === '--dry-run') {
80
+ out.dryRun = true
46
81
  } else {
47
- return { error: `unknown argument: ${a}` }
82
+ const cd = parseCacheDirFlag(args, i)
83
+ if (cd === null) return { error: `unknown argument: ${a}${seeHelp('cache')}` }
84
+ if ('error' in cd) return { error: cd.error }
85
+ out.cacheDir = cd.cacheDir
86
+ i = cd.next
48
87
  }
49
88
  }
50
89
  if (out.olderThanMs === undefined && out.maxBytes === undefined) {
@@ -67,18 +106,25 @@ async function pruneCmd(args: readonly string[]): Promise<number> {
67
106
  process.stderr.write(`vx cache prune: ${(err as Error).message}\n`)
68
107
  return 1
69
108
  }
70
- // Honor `defineWorkspace({ cacheDir: '...' })` — `vx run` and
71
- // `vx cache prune` must operate on the same directory or prune
72
- // silently no-ops against the wrong path.
73
- const workspaceConfig = await loadWorkspaceConfig(root)
74
- const cache = new Cache(resolveCacheDir(root, workspaceConfig))
109
+ // Honor `--cache-dir`, `defineWorkspace({ cacheDir: '...' })` and a
110
+ // `config` plugin's edit of it — `vx run` and `vx cache prune` must
111
+ // operate on the same directory or prune silently no-ops against the
112
+ // wrong path.
113
+ const cache = new Cache(await cliCacheDir(root, parsed.cacheDir))
114
+ noteSchemaReset(cache, warnToStderr)
75
115
  try {
76
- const opts: { olderThanMs?: number; maxBytes?: number } = {}
116
+ const opts: { olderThanMs?: number; maxBytes?: number; dryRun?: boolean } = {}
77
117
  if (parsed.olderThanMs !== undefined) opts.olderThanMs = parsed.olderThanMs
78
118
  if (parsed.maxBytes !== undefined) opts.maxBytes = parsed.maxBytes
119
+ if (parsed.dryRun) opts.dryRun = true
79
120
  const result = await cache.prune(opts)
121
+ const dry = parsed.dryRun === true
122
+ const orphans =
123
+ result.orphans > 0
124
+ ? `, ${dry ? 'would reap' : 'reaped'} ${result.orphans} orphaned artifact${result.orphans === 1 ? '' : 's'} (${formatBytes(result.orphanBytes)})`
125
+ : ''
80
126
  process.stdout.write(
81
- `Pruned ${result.evicted} entr${result.evicted === 1 ? 'y' : 'ies'} (${formatBytes(result.bytesFreed)} freed)\n`,
127
+ `${dry ? 'Would prune' : 'Pruned'} ${result.evicted} entr${result.evicted === 1 ? 'y' : 'ies'} (${formatBytes(result.bytesFreed)}${dry ? '' : ' freed'})${orphans}\n`,
82
128
  )
83
129
  } finally {
84
130
  cache.close()
@@ -86,11 +132,19 @@ async function pruneCmd(args: readonly string[]): Promise<number> {
86
132
  return 0
87
133
  }
88
134
 
135
+ /**
136
+ * Parse `<n><unit>` where unit is s/m/h/d, case-insensitively (`parseSize`
137
+ * has always been case-insensitive; this matched only lowercase, so `30D`
138
+ * was rejected while `1GB` and `1gb` both worked).
139
+ */
89
140
  export function parseDuration(input: string): number | null {
90
- const m = input.match(/^(\d+)([smhd])$/)
141
+ const m = input.match(/^(\d+)([smhd])$/i)
91
142
  if (!m) return null
92
- const n = Number(m[1])
93
- const unit = m[2]
143
+ const n = parseDecimalInt(m[1]!)
144
+ if (n === null) return null
145
+ // Lowercase before the switch: with the /i flag an uppercase `M` would
146
+ // otherwise fall through to the days branch.
147
+ const unit = m[2]!.toLowerCase()
94
148
  const mult = unit === 's' ? 1000 : unit === 'm' ? 60_000 : unit === 'h' ? 3_600_000 : 86_400_000
95
149
  return n * mult
96
150
  }
@@ -0,0 +1,104 @@
1
+ // `vx completions <shell>` — a bash / zsh / fish script over the verb table
2
+ // and each verb's help cut, so the verbs and every flag of each complete
3
+ // from the one text `vx <verb> --help` prints. Plugin verbs of the
4
+ // workspace around the cwd are included at generation time; task and
5
+ // project names are not (a completion that evaluates configs on every
6
+ // Tab is the wrong price).
7
+
8
+ import { CORE_VERBS, seeHelp, verbHelpText } from './help.js'
9
+
10
+ export type CompletionShell = 'bash' | 'zsh' | 'fish'
11
+ const SHELLS: readonly CompletionShell[] = ['bash', 'zsh', 'fish']
12
+
13
+ /** Every `--flag` the verb's help cut names, plus `--help`; deduped, in order of appearance. */
14
+ export function verbFlags(verb: string): string[] {
15
+ const flags = new Set<string>(['--help'])
16
+ for (const f of verbHelpText(verb).match(/--[a-zA-Z][a-zA-Z-]*/g) ?? []) flags.add(f)
17
+ return [...flags]
18
+ }
19
+
20
+ /** The words that follow `vx <verb>` besides flags: subcommands and fixed choices. */
21
+ const VERB_WORDS: Readonly<Record<string, readonly string[]>> = {
22
+ cache: ['prune'],
23
+ completions: [...SHELLS],
24
+ }
25
+
26
+ export function completionScript(shell: CompletionShell, verbs: readonly string[]): string {
27
+ const table = verbs.map((v) => ({
28
+ verb: v,
29
+ // A plugin verb owns its own arguments; only `--help` is known here.
30
+ words: [
31
+ ...(VERB_WORDS[v] ?? []),
32
+ ...((CORE_VERBS as readonly string[]).includes(v) ? verbFlags(v) : ['--help']),
33
+ ],
34
+ }))
35
+ switch (shell) {
36
+ case 'bash':
37
+ return [
38
+ '# vx completions — generated by `vx completions bash`',
39
+ '_vx() {',
40
+ ' local cur="${COMP_WORDS[COMP_CWORD]}"',
41
+ ' local verb="${COMP_WORDS[1]}"',
42
+ ' if [ "$COMP_CWORD" -eq 1 ]; then',
43
+ ` COMPREPLY=($(compgen -W "${verbs.join(' ')}" -- "$cur"))`,
44
+ ' return',
45
+ ' fi',
46
+ ' case "$verb" in',
47
+ ...table.map(
48
+ (t) => ` ${t.verb}) COMPREPLY=($(compgen -W "${t.words.join(' ')}" -- "$cur")) ;;`,
49
+ ),
50
+ ' *) COMPREPLY=() ;;',
51
+ ' esac',
52
+ '}',
53
+ 'complete -F _vx vx',
54
+ '',
55
+ ].join('\n')
56
+ case 'zsh':
57
+ return [
58
+ '#compdef vx',
59
+ '# vx completions — generated by `vx completions zsh`',
60
+ '_vx() {',
61
+ ' if (( CURRENT == 2 )); then',
62
+ ` compadd -- ${verbs.join(' ')}`,
63
+ ' return',
64
+ ' fi',
65
+ ' case "${words[2]}" in',
66
+ ...table.map((t) => ` ${t.verb}) compadd -- ${t.words.join(' ')} ;;`),
67
+ ' esac',
68
+ '}',
69
+ 'compdef _vx vx',
70
+ '',
71
+ ].join('\n')
72
+ case 'fish':
73
+ return [
74
+ '# vx completions — generated by `vx completions fish`',
75
+ 'complete -c vx -f',
76
+ ...verbs.map((v) => `complete -c vx -n __fish_use_subcommand -a ${v}`),
77
+ ...table.flatMap((t) =>
78
+ t.words.map((w) =>
79
+ w.startsWith('--')
80
+ ? `complete -c vx -n "__fish_seen_subcommand_from ${t.verb}" -l ${w.slice(2)}`
81
+ : `complete -c vx -n "__fish_seen_subcommand_from ${t.verb}" -a ${w}`,
82
+ ),
83
+ ),
84
+ '',
85
+ ].join('\n')
86
+ }
87
+ }
88
+
89
+ export async function completionsCmd(
90
+ args: readonly string[],
91
+ pluginVerbs: readonly string[],
92
+ ): Promise<number> {
93
+ const shell = args[0]
94
+ if (args.length !== 1 || !(SHELLS as readonly string[]).includes(shell ?? '')) {
95
+ process.stderr.write(
96
+ `vx completions: expected one shell — bash, zsh or fish${shell !== undefined && args.length === 1 ? ` (got ${shell})` : ''}${seeHelp('completions')}\n`,
97
+ )
98
+ return 1
99
+ }
100
+ const verbs = [...CORE_VERBS.filter((v) => v !== 'help' && v !== 'version'), 'help', 'version']
101
+ for (const v of pluginVerbs) if (!verbs.includes(v)) verbs.push(v)
102
+ process.stdout.write(completionScript(shell as CompletionShell, verbs))
103
+ return 0
104
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * One core per process. A plugin package, a `vx.workspace.ts`, a
3
+ * `vx.config.ts` all `import … from '@vzn/vx'`, and that specifier
4
+ * resolves through node_modules — to a SECOND copy of core when the host
5
+ * is the compiled binary (whose own copy lives under the bunfs root) or
6
+ * an install with its own nested `@vzn/vx`. The second copy is core's
7
+ * whole source transpiled again, per process: ~20 ms per plugin package
8
+ * in the binary (measured 2026-09-10, `@vzn/vx-otel`: 20–25 ms → 2–3 ms),
9
+ * and two module states — two `definePlugin` memos, two of everything
10
+ * keyed by identity rather than by registry symbol.
11
+ *
12
+ * The alias is a Bun virtual module for the exact specifier `@vzn/vx`,
13
+ * served from the host's own façade, loaded lazily on the first import
14
+ * so a verb that never loads a plugin never loads the façade. The
15
+ * source form (`bun src/bin.ts`) resolves the specifier to the same file
16
+ * anyway; the alias makes that a guarantee rather than a layout fact.
17
+ */
18
+ export function registerCoreAlias(load: () => Promise<Record<string, unknown>>): void {
19
+ Bun.plugin({
20
+ name: 'vx-core-alias',
21
+ setup(build) {
22
+ build.module('@vzn/vx', async () => ({ exports: await load(), loader: 'object' }))
23
+ },
24
+ })
25
+ }
package/src/cli/help.ts CHANGED
@@ -1,129 +1,196 @@
1
- export function printHelp(): void {
2
- process.stdout.write(
3
- [
4
- 'vx — open, extensible monorepo task runner',
5
- '',
6
- 'Usage:',
7
- ' vx run [OPTIONS] [TASK | PKG#TASK] [-- forwarded-args...]',
8
- ' vx watch [OPTIONS] TASK [-- forwarded-args...]',
9
- ' vx cache prune [--older-than <duration>] [--max-size <bytes>]',
10
- ' vx lock [--check]',
11
- ' vx migrate [--from turbo|nx] [--dry] [--force]',
12
- ' vx upgrade [tag]',
13
- ' vx show [PROJECT[#TASK]] [--format pretty|json]',
14
- ' vx info',
15
- ' vx mcp [--stdio]',
16
- ' vx help',
17
- ' vx version',
18
- '',
19
- 'Selection (for run):',
20
- ' (default) Run task in the project containing cwd.',
21
- ' --all Run task in every project that declares it.',
22
- ' --filter <pat> pnpm-style filter (repeatable). Examples:',
23
- ' foo, @scope/*, ./packages/foo, foo..., ...foo,',
24
- ' foo^..., !foo, [<since>]',
25
- ' --affected[=<base>] Sugar for --filter "[<base>]". Default base =',
26
- ' origin/HEAD (falls back to HEAD~1).',
27
- " pkg#task Run a specific project's task directly.",
28
- '',
29
- 'Execution (for run):',
30
- ' --concurrency <n> Max parallel tasks (default: CPU count).',
31
- ' --excludeDependencies[=names] Skip dependsOn edges. No value = all; comma list = specific names.',
32
- ' --no-cache Disable caching entirely (no reads, no writes, outputs left alone).',
33
- ' --force Re-execute everything (skip reads) but still refresh the cache (writes on).',
34
- ' --cache <spec> Per-layer read/write control. Comma list of <layer>:<flags>',
35
- ' (layer = local|remote, flags ⊆ rw). A named layer is set EXACTLY',
36
- ' to its flags; unnamed layers keep their value. Examples:',
37
- ' local:rw,remote:r (remote read-only), remote: (remote off),',
38
- ' local:r (local read-only).',
39
- ' --cache-dir <path> Cache directory override (cwd-relative). Beats the workspace',
40
- ' cacheDir field + the .vx/cache default.',
41
- ' --retry <n> Re-run a failed task up to <n> more times (default for tasks',
42
- " without their own exec.retries; explicit config wins). Doesn't",
43
- ' affect cache keys.',
44
- ' --timeout <ms> Default per-task timeout for tasks without their own exec.timeout',
45
- ' (above VX_TASK_TIMEOUT env + workspace timeout). Kills + fails',
46
- ' a runaway task. Per-task exec.timeout always wins.',
47
- ' --memory <size> Memory budget (e.g. 8GB) that exec.resources.memory reservations',
48
- ' pack against. Default: total system RAM — pass this in',
49
- ' cgroup-limited containers (the default reads the HOST RAM).',
50
- ' Reservations are per-task config (exec.resources), not flags.',
51
- ' --verify[=<what>] Prove cache correctness. determinism (default): re-run each',
52
- ' executed cacheable task + content-compare outputs. inputs: sandbox',
53
- ' with the declared-input baseline + flag reads of undeclared inputs.',
54
- ' fingerprint: ship output-tree fingerprints for the cross-machine',
55
- ' diff on a connected serve (~1× exec — no re-run). all: everything.',
56
- ' An unsafe task fails the run with the exact paths.',
57
- ' ~2× exec (determinism); CI-shaped. inputs/all need the OS sandbox.',
58
- ' --verify-allow <pkg#task,…> Exempt known-nondeterministic tasks from failing --verify.',
59
- ' --frozen Load configs from vx-lock.json (CI; pair with vx lock --check).',
60
- ' --output-logs <mode> full | errors-only | none — overrides the flow default',
61
- ' (focused without --all/--filter/--affected, broad with, full in CI).',
62
- ' --verbosity <n> 0=quiet, 1=verbose summary, 2+=debug (reserved).',
63
- '',
64
- 'Planning (for run — skips execution):',
65
- ' --dry[=text|json] Print the task graph + cache hit/miss prediction.',
66
- ' --graph[=<path>] Emit Graphviz DOT (stdout if no path).',
67
- '',
68
- 'Artifacts (for run):',
69
- ' --summarize[=<path>] Write per-run JSON to <cacheDir>/runs/<run_id>.json.',
70
- ' --profile[=<path>] Write Chrome-trace JSON (default profile.json).',
71
- '',
72
- 'Extensions (plugins):',
73
- ' vx core runs tasks in-process and nothing else. A dashboard, remote',
74
- ' cache, distributed execution, and telemetry export are provided by',
75
- ' PLUGINS declared in vx.workspace.ts — first-party or your own. Core',
76
- ' depends on none of them; a plain `vx run` never requires a plugin.',
77
- ' See the plugin guide: https://vznjs.github.io/vx/guides/plugins/',
78
- '',
79
- 'Argument forwarding (for run):',
80
- " Anything after `--` is forwarded (shell-quoted) to the task's exec",
81
- ' command. The forwarded args are folded into the cache key.',
82
- '',
83
- 'Watch mode:',
84
- ' vx watch <task> Initial run, then re-run on filesystem changes.',
85
- ' Uses the same flags as `vx run` except --dry /',
86
- ' --graph / --summarize / --profile. Press Ctrl+C',
87
- ' to stop. Cache means most re-runs are near-zero',
88
- ' cost (cache hits restore outputs + replay logs).',
89
- '',
90
- 'Cache management:',
91
- ' vx cache prune --older-than 30d Evict entries last accessed > 30 days ago.',
92
- ' vx cache prune --max-size 1G Keep total cache under 1 GB (LRU eviction).',
93
- '',
94
- ' Duration units: s, m, h, d. Size units: K, M, G, T (powers of 1024).',
95
- '',
96
- 'Introspection:',
97
- ' vx show List every project: name, dir, declared task count.',
98
- " vx show <project> Print the project's LIVE resolved config (fresh",
99
- ' evaluation, not the lock), one block per task.',
100
- " vx show <pkg>#<task> Print a single task's resolved config.",
101
- ' --format <fmt> pretty (default) | json.',
102
- ' vx info Workspace doctor: vx/bun/git versions, project +',
103
- ' task counts, cache dir/entries/size, recent runs,',
104
- ' lock status.',
105
- ' vx stats Deprecated: alias of vx info.',
106
- ' vx mcp Model Context Protocol server (stdio) — exposes inspector',
107
- ' RPCs as MCP tools for AI agents (Claude Code, Cursor, …).',
108
- '',
109
- 'Migration:',
110
- ' vx migrate Generate vx.config.ts per package from turbo.json or an',
111
- ' Nx project graph (.nx/workspace-data/project-graph.json).',
112
- ' Unmappable settings become TODO(vx-migrate) comments;',
113
- ' turbo global fields become a shared vx-preset.ts.',
114
- ' --dry Print the generated files instead of writing them.',
115
- ' --force Overwrite existing vx.config.* / vx-preset.ts files.',
116
- '',
117
- 'Config lock:',
118
- ' vx lock Evaluate every vx.config in the current environment and',
119
- ' freeze the resolved objects into vx-lock.json. Plain runs',
120
- ' always evaluate live; only `vx run --frozen` consumes the',
121
- ' lock (CI reproducibility, frozen-env semantics).',
122
- ' vx lock --check Audit the lock: file-changed hash report PLUS a full',
123
- ' re-evaluation compared against the frozen objects. Exits 1',
124
- ' on drift (e.g. configs that read env vars). Run it before',
125
- ' `vx run --frozen` in CI.',
126
- '',
127
- ].join('\n'),
128
- )
1
+ export function printHelp(pluginCommands: readonly string[] = [], verb?: string): void {
2
+ process.stdout.write(verb === undefined ? helpText(pluginCommands) : verbHelpText(verb))
129
3
  }
4
+
5
+ /**
6
+ * `vx <verb> --help`: the reference cut to one verb — the title, the Usage
7
+ * lines that name it, and every section that is `(for <verb>)` or lists a
8
+ * `vx <verb>` form — read from the same text, so nothing can drift. A verb
9
+ * the reference does not know gets the whole thing.
10
+ */
11
+ export function verbHelpText(verb: string): string {
12
+ const blocks = helpText().split('\n\n')
13
+ const title = blocks[0]!
14
+ const names = (line: string): boolean =>
15
+ new RegExp(`^\\s*vx ${verb}( |$)`).test(line) ||
16
+ line.includes(`(for ${verb})`) ||
17
+ line.includes(`(for ${verb} `)
18
+ const kept: string[] = []
19
+ for (const block of blocks.slice(1)) {
20
+ const lines = block.split('\n')
21
+ if (lines[0] === 'Usage:') {
22
+ const own = lines.slice(1).filter(names)
23
+ if (own.length > 0) kept.push(['Usage:', ...own].join('\n'))
24
+ continue
25
+ }
26
+ if (lines.some(names)) kept.push(block)
27
+ }
28
+ if (kept.length === 0) return helpText()
29
+ return [title, ...kept, 'Full reference: vx help', ''].join('\n\n')
30
+ }
31
+
32
+ export function helpText(pluginCommands: readonly string[] = []): string {
33
+ return [
34
+ 'vx — open, extensible monorepo task runner',
35
+ '',
36
+ 'Usage:',
37
+ ' vx run [OPTIONS] [TASK | PKG#TASK] [-- forwarded-args...]',
38
+ ' vx watch [OPTIONS] TASK [-- forwarded-args...]',
39
+ ' vx cache prune [--older-than <duration>] [--max-size <size>] [--dry-run] [--cache-dir <path>]',
40
+ ' vx lock [--check]',
41
+ ' vx init [--dry] [--force] [--mjs]',
42
+ ' vx upgrade [tag]',
43
+ ' vx show [PROJECT[#TASK] | TASK] [--format pretty|json]',
44
+ ' vx info [--format pretty|json] [--cache-dir <path>]',
45
+ ' vx why [TASK | PKG#TASK] [--run <runId>] [--format pretty|json] [--cache-dir <path>]',
46
+ ' vx last [RUNID] [--list[=N]] [--format pretty|json] [--cache-dir <path>]',
47
+ ' vx completions bash|zsh|fish',
48
+ ' vx help',
49
+ ' vx version',
50
+ '',
51
+ 'Selection (for run):',
52
+ ' (default) Run task in the project containing cwd.',
53
+ ' --all Run task in every project that declares it.',
54
+ ' --filter <pat> pnpm-style filter (repeatable). Examples:',
55
+ ' foo, @scope/*, ./packages/foo, foo..., ...foo,',
56
+ ' foo^..., !foo, [<since>]',
57
+ ' --affected[=<base>] Sugar for --filter "[<base>]". Default base =',
58
+ ' origin/HEAD (falls back to HEAD~1).',
59
+ " pkg#task Run a specific project's task directly.",
60
+ '',
61
+ 'Execution (for run):',
62
+ ' --concurrency <n> Max parallel tasks (default: the cores this process may use); `50%` = half of them.',
63
+ ' --exclude-dependencies[=names] Skip dependsOn edges. No value = all; comma list = specific names.',
64
+ ' --no-cache Disable caching entirely (no reads, no writes, outputs left alone).',
65
+ ' --force Re-execute everything (skip reads) but still refresh the cache (writes on).',
66
+ ' --cache <spec> Per-layer read/write control. Comma list of <layer>:<flags>',
67
+ ' (layer = local|remote, flags ⊆ rw). A named layer is set EXACTLY',
68
+ ' to its flags; unnamed layers keep their value. Examples:',
69
+ ' local:rw,remote:r (remote read-only), remote: (remote off),',
70
+ ' local:r (local read-only).',
71
+ ' --cache-dir <path> Cache directory override (cwd-relative). Beats the workspace',
72
+ ' cacheDir field + the .vx/cache default.',
73
+ ' --retry <n> Re-run a failed task up to <n> more times (default for tasks',
74
+ " without their own exec.retries; explicit config wins). Doesn't",
75
+ ' affect cache keys.',
76
+ ' --timeout <ms> Default per-task timeout for tasks without their own exec.timeout',
77
+ ' (above VX_TASK_TIMEOUT env + workspace timeout). Kills + fails',
78
+ ' a runaway task. Per-task exec.timeout always wins.',
79
+ ' --frozen Load configs from vx-lock.json (CI; pair with vx lock --check).',
80
+ ' --output-logs <mode> full | errors-only | hash-only | none — overrides the flow default',
81
+ ' --download <mode> all (default) | toplevel | none — where remote outputs land',
82
+ ' (focused without --all/--filter/--affected, broad with, full in CI).',
83
+ ' --verbosity <n> 0 (default) or 1+: print a per-task summary table after the run.',
84
+ '',
85
+ 'Planning (for run — skips execution):',
86
+ ' --dry[=text|json] Print the task graph + cache hit/miss prediction.',
87
+ ' --graph[=<path>] Emit Graphviz DOT (stdout if no path).',
88
+ '',
89
+ 'Artifacts (for run):',
90
+ ' --summarize[=<path>] Write per-run JSON to <cacheDir>/runs/<run_id>.json.',
91
+ ' --profile[=<path>] Write Chrome-trace JSON (default profile.json).',
92
+ '',
93
+ 'Extensions (plugins):',
94
+ ' vx core runs tasks in-process and nothing else. A dashboard, remote',
95
+ ' cache, distributed execution, and telemetry export are provided by',
96
+ ' PLUGINS declared in vx.workspace.ts — first-party or your own. Core',
97
+ ' depends on none of them; a plain `vx run` never requires a plugin.',
98
+ ' See the plugin guide: https://vznjs.github.io/vx/guides/plugins/',
99
+ '',
100
+ 'Argument forwarding (for run):',
101
+ " Anything after `--` is forwarded (shell-quoted) to the task's exec",
102
+ ' command. The forwarded args are folded into the cache key.',
103
+ '',
104
+ 'Watch mode:',
105
+ ' vx watch <task> Initial run, then re-run on filesystem changes.',
106
+ ' Uses the same flags as `vx run` except --dry /',
107
+ ' --graph / --summarize / --profile. Press Ctrl+C',
108
+ ' to stop. Cache means most re-runs are near-zero',
109
+ ' cost (cache hits restore outputs + replay logs).',
110
+ '',
111
+ 'Cache management:',
112
+ ' vx cache prune --older-than 30d Evict entries last accessed > 30 days ago.',
113
+ ' vx cache prune --max-size 1G Keep total cache under 1 GB (LRU eviction).',
114
+ ' --dry-run Say what the policy would reap; delete nothing.',
115
+ ' --cache-dir <path> The cache a run with the same flag uses (why, last, info too).',
116
+ '',
117
+ ' Duration units: s, m, h, d. Size units: K, M, G, T (powers of 1024).',
118
+ '',
119
+ 'Introspection:',
120
+ ' vx show List every project: name, dir, declared task count.',
121
+ " vx show <project> Print the project's LIVE resolved config (fresh",
122
+ ' evaluation, not the lock), one block per task.',
123
+ " vx show <pkg>#<task> Print a single task's resolved config.",
124
+ ' vx show <task> That task in every project declaring it.',
125
+ ' --format <fmt> pretty (default) | json.',
126
+ ' vx info Workspace doctor: vx/bun/git versions, project +',
127
+ ' task counts, cache dir/entries/size, recent runs,',
128
+ ' lock status.',
129
+ ' --format <fmt> pretty (default) | json.',
130
+ ' vx stats Deprecated: alias of vx info.',
131
+ " vx why <task> Why did this task re-run? Compares the task's latest",
132
+ ' run (or --run <id>) against its previous run: names the',
133
+ ' exact changed cache-key components (files / env / runtime',
134
+ ' / upstream) from the persisted input fingerprints.',
135
+ ' --format <fmt> pretty (default) | json.',
136
+ " vx last [runId] Replay a recorded run's summary from the local history —",
137
+ ' header (command, when, branch, counts) plus the per-task',
138
+ ' table, failures first. No re-execution, no cache probe.',
139
+ ' --list[=N] List the N most recent runs (default 10) with run ids.',
140
+ ' --format <fmt> pretty (default) | json.',
141
+ '',
142
+ 'Migration:',
143
+ ' vx init Generate vx.workspace.ts + one vx.config.ts per package',
144
+ ' from package.json scripts. Unmappable settings become',
145
+ ' TODO(vx-migrate) comments.',
146
+ ' --dry Print the generated files instead of writing them.',
147
+ ' --force Overwrite existing vx.config.* files.',
148
+ ' From turbo.json or an Nx project graph: `bunx @vzn/vx-migrate` (its own package).',
149
+ '',
150
+ 'Shell completions:',
151
+ ' vx completions <shell> Print a completion script for bash, zsh or fish: the',
152
+ ' verbs (plugin verbs of this workspace included) and',
153
+ ' every flag of each. `eval "$(vx completions bash)"`,',
154
+ ' or write it where the shell loads completions from.',
155
+ '',
156
+ 'Config lock:',
157
+ ' vx lock Evaluate every vx.config in the current environment and',
158
+ ' freeze the resolved objects into vx-lock.json. Plain runs',
159
+ ' always evaluate live; only `vx run --frozen` consumes the',
160
+ ' lock (CI reproducibility, frozen-env semantics).',
161
+ ' vx lock --check Audit the lock: file-changed hash report PLUS a full',
162
+ ' re-evaluation compared against the frozen objects. Exits 1',
163
+ ' on drift (e.g. configs that read env vars). Run it before',
164
+ ' `vx run --frozen` in CI.',
165
+ '',
166
+ ...(pluginCommands.length > 0 ? ['Plugin commands:', ...pluginCommands, ''] : []),
167
+ ].join('\n')
168
+ }
169
+
170
+ /**
171
+ * Every argument error ends with this. `vx <verb> --help` prints the
172
+ * reference (2026-09-04); before that there was nowhere to send a user who
173
+ * got a flag wrong, so the errors just said no.
174
+ */
175
+ export function seeHelp(verb: string): string {
176
+ return ` (see \`vx ${verb} --help\`)`
177
+ }
178
+
179
+ /**
180
+ * The documented `--flags` of one verb, read from the help text's
181
+ * sections headed `… (for <verb>):` so there is no second list to drift —
182
+ * and no hint that names another verb's flag.
183
+ */
184
+ export function documentedFlags(verb: string): string[] {
185
+ const flags = new Set<string>()
186
+ let inVerb = false
187
+ for (const line of helpText().split('\n')) {
188
+ if (/^[A-Z][A-Za-z ]*(?: \(for [a-z]+\))?:$/.test(line))
189
+ inVerb = line.endsWith(`(for ${verb}):`)
190
+ if (!inVerb) continue
191
+ for (const f of line.match(/--[a-zA-Z][a-zA-Z-]*/g) ?? []) flags.add(f)
192
+ }
193
+ return [...flags]
194
+ }
195
+
196
+ export { CORE_VERBS } from '../util/index.js'