@vzn/vx 0.0.636 → 0.0.638

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 (151) hide show
  1. package/README.md +39 -11
  2. package/docs/caching.md +246 -213
  3. package/docs/cli.md +791 -705
  4. package/docs/execution.md +68 -76
  5. package/docs/features.md +47 -42
  6. package/docs/patterns.md +26 -25
  7. package/docs/schema.md +392 -205
  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 +4 -4
  16. package/schemas/show.json +5 -3
  17. package/schemas/why.json +15 -1
  18. package/skills/vx/SKILL.md +28 -13
  19. package/src/bin.ts +30 -9
  20. package/src/cache/archive.ts +11 -0
  21. package/src/cache/cache.ts +124 -53
  22. package/src/cache/chained-cache.ts +4 -0
  23. package/src/cache/git-inputs.ts +277 -63
  24. package/src/cache/index.ts +15 -1
  25. package/src/cache/inputs.ts +102 -36
  26. package/src/cache/key-fold.ts +8 -7
  27. package/src/cache/layer.ts +15 -18
  28. package/src/cache/layered-cache.ts +20 -102
  29. package/src/cache/plugin-cache.ts +248 -0
  30. package/src/cache/policy.ts +32 -27
  31. package/src/cache/run-history.ts +104 -14
  32. package/src/cache/run-store.ts +77 -0
  33. package/src/cache/schema.ts +58 -31
  34. package/src/cache/scoped-cache.ts +237 -0
  35. package/src/cli/cache.ts +173 -34
  36. package/src/cli/completions.ts +11 -7
  37. package/src/cli/core-alias.ts +1 -1
  38. package/src/cli/foreign-flags.ts +13 -12
  39. package/src/cli/help.ts +110 -70
  40. package/src/cli/index.ts +67 -24
  41. package/src/cli/info.ts +43 -27
  42. package/src/cli/init.ts +115 -407
  43. package/src/cli/last.ts +157 -9
  44. package/src/cli/lock.ts +356 -122
  45. package/src/cli/plan-format.ts +7 -5
  46. package/src/cli/plugin-commands.ts +81 -14
  47. package/src/cli/plugin-templates.ts +11 -11
  48. package/src/cli/run.ts +116 -160
  49. package/src/cli/select.ts +50 -47
  50. package/src/cli/show.ts +83 -59
  51. package/src/cli/task-verb.ts +6 -13
  52. package/src/cli/upgrade.ts +415 -149
  53. package/src/cli/watch-cycle.ts +9 -2
  54. package/src/cli/watch-filter.ts +39 -61
  55. package/src/cli/watch-fs.ts +29 -16
  56. package/src/cli/watch-judge.ts +18 -4
  57. package/src/cli/watch-set.ts +16 -42
  58. package/src/cli/watch.ts +227 -230
  59. package/src/cli/why.ts +38 -18
  60. package/src/cli/workspace-config.ts +33 -70
  61. package/src/config.ts +169 -70
  62. package/src/exec/env.ts +1 -31
  63. package/src/exec/executor.ts +1 -5
  64. package/src/exec/kill-tree.ts +2 -1
  65. package/src/exec/sandbox-runtime.ts +12 -1
  66. package/src/exec/sandbox-violations.ts +9 -1
  67. package/src/graph/dependency-spec.ts +1 -1
  68. package/src/graph/index.ts +1 -0
  69. package/src/graph/priorities.ts +1 -1
  70. package/src/graph/scheduler.ts +27 -7
  71. package/src/graph/task-graph.ts +76 -24
  72. package/src/handoff.ts +169 -0
  73. package/src/index.ts +35 -58
  74. package/src/orchestrator/affected-tasks.ts +15 -8
  75. package/src/orchestrator/command-windows.ts +44 -0
  76. package/src/orchestrator/deferred-outputs.ts +12 -10
  77. package/src/orchestrator/doctor.ts +98 -110
  78. package/src/orchestrator/download-policy.ts +3 -3
  79. package/src/orchestrator/events.ts +19 -3
  80. package/src/orchestrator/excluded-keys.ts +0 -2
  81. package/src/orchestrator/execute-task.ts +147 -69
  82. package/src/orchestrator/failure-mode.ts +19 -10
  83. package/src/orchestrator/fingerprint-watch.ts +18 -15
  84. package/src/orchestrator/forecast.ts +10 -2
  85. package/src/orchestrator/framed-output.ts +9 -5
  86. package/src/orchestrator/graph-stage.ts +5 -5
  87. package/src/orchestrator/history.ts +126 -30
  88. package/src/orchestrator/hit-restore.ts +15 -3
  89. package/src/orchestrator/index.ts +20 -5
  90. package/src/orchestrator/local-shortcircuit.ts +2 -1
  91. package/src/orchestrator/lockfile-claim.ts +79 -20
  92. package/src/orchestrator/logger.ts +80 -41
  93. package/src/orchestrator/metrics.ts +78 -9
  94. package/src/orchestrator/miss-save.ts +3 -0
  95. package/src/orchestrator/options.ts +42 -29
  96. package/src/orchestrator/output-host.ts +72 -0
  97. package/src/orchestrator/plan.ts +6 -8
  98. package/src/orchestrator/plugin-host.ts +100 -66
  99. package/src/orchestrator/plugin.ts +173 -67
  100. package/src/orchestrator/prepare.ts +163 -78
  101. package/src/orchestrator/projects.ts +105 -99
  102. package/src/orchestrator/remote-prefetch.ts +11 -6
  103. package/src/orchestrator/run-context.ts +25 -6
  104. package/src/orchestrator/run-failures.ts +17 -8
  105. package/src/orchestrator/run-lock.ts +1 -1
  106. package/src/orchestrator/run-records.ts +1 -1
  107. package/src/orchestrator/run-report.ts +9 -9
  108. package/src/orchestrator/run-settings.ts +48 -0
  109. package/src/orchestrator/run.ts +244 -119
  110. package/src/orchestrator/sandbox-fix.ts +39 -20
  111. package/src/orchestrator/sandbox-request.ts +84 -13
  112. package/src/orchestrator/signals.ts +1 -1
  113. package/src/orchestrator/stable-keys.ts +13 -11
  114. package/src/orchestrator/status-line.ts +49 -7
  115. package/src/orchestrator/summary.ts +115 -106
  116. package/src/orchestrator/task-hash.ts +12 -83
  117. package/src/orchestrator/telemetry.ts +4 -4
  118. package/src/settings.ts +428 -0
  119. package/src/{orchestrator → util}/colors.ts +5 -0
  120. package/src/util/config-frame.ts +31 -2
  121. package/src/util/errors.ts +33 -1
  122. package/src/util/essential-env.ts +31 -0
  123. package/src/util/hide-env.ts +107 -0
  124. package/src/util/index.ts +16 -3
  125. package/src/util/paths.ts +10 -0
  126. package/src/util/refusal-notice.ts +111 -0
  127. package/src/util/report.ts +31 -0
  128. package/src/util/settle.ts +49 -30
  129. package/src/util/size.ts +23 -4
  130. package/src/util/timing.ts +3 -2
  131. package/src/util/ui.ts +186 -0
  132. package/src/util/ulid.ts +1 -1
  133. package/src/util/verbs.ts +0 -15
  134. package/src/workspace/affected.ts +84 -232
  135. package/src/workspace/config-eval.ts +15 -20
  136. package/src/workspace/config-imports.ts +78 -3
  137. package/src/workspace/config-schema.ts +209 -268
  138. package/src/workspace/dependency-rules.ts +65 -0
  139. package/src/workspace/filter.ts +2 -2
  140. package/src/workspace/fingerprint.ts +11 -121
  141. package/src/workspace/index.ts +20 -27
  142. package/src/workspace/load-reads.ts +4 -4
  143. package/src/workspace/lockfile.ts +96 -140
  144. package/src/workspace/migration.ts +307 -215
  145. package/src/workspace/package-graph.ts +30 -216
  146. package/src/workspace/project-loader.ts +80 -285
  147. package/src/workspace/repo-id.ts +2 -1
  148. package/src/workspace/workspace.ts +363 -831
  149. package/src/cache/config-evals.ts +0 -130
  150. package/src/workspace/config-cache.ts +0 -661
  151. package/src/workspace/migrate-scripts.ts +0 -1121
package/src/cli/cache.ts CHANGED
@@ -1,7 +1,17 @@
1
- import { existsSync } from 'node:fs'
2
- import { Cache } from '../cache/index.js'
1
+ import { existsSync, readdirSync, rmSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import path from 'node:path'
4
+ import { Cache, vxCacheEntries } from '../cache/index.js'
3
5
  import { flagHint, formatValue, refuse, refusedWord, seeHelp } from './help.js'
4
- import { errorCode, nearest, parseDuration, parseSize } from '../util/index.js'
6
+ import {
7
+ errorCode,
8
+ nearest,
9
+ parseDuration,
10
+ parseSize,
11
+ relPosix,
12
+ treeBytes,
13
+ ui,
14
+ } from '../util/index.js'
5
15
  import { acquireRunLock } from '../orchestrator/index.js'
6
16
  import { findWorkspaceRoot } from '../workspace/index.js'
7
17
  import {
@@ -19,8 +29,8 @@ export { parseDuration, parseSize } from '../util/index.js'
19
29
  /**
20
30
  * `vx cache stats` is a habit from the other runners, and vx's answer is a
21
31
  * different VERB rather than a missing one — saying so where the user typed
22
- * it beats sending them to a help screen that lists only `prune` (walked the
23
- * first-run path, 2026-09-20). A typo of `prune` itself gets the same
32
+ * it beats sending them to a help screen that lists only `clean` (walked the
33
+ * first-run path, 2026-09-20). A typo of `clean` itself gets the same
24
34
  * nearest-neighbour hint a task, flag or project name gets.
25
35
  */
26
36
  const CACHE_ELSEWHERE: Record<string, string> = {
@@ -32,34 +42,39 @@ const CACHE_ELSEWHERE: Record<string, string> = {
32
42
  ls: '`vx info` reports the cache entry count and size, and `vx last --list` the recent runs',
33
43
  dir: '`vx info` prints the cache directory this workspace uses',
34
44
  path: '`vx info` prints the cache directory this workspace uses',
35
- clean: '`vx cache prune` is the eviction verb (`--older-than`, `--max-size`)',
36
- clear: '`vx cache prune` is the eviction verb (`--older-than`, `--max-size`)',
37
- rm: '`vx cache prune` is the eviction verb (`--older-than`, `--max-size`)',
38
- delete: '`vx cache prune` is the eviction verb (`--older-than`, `--max-size`)',
39
- evict: '`vx cache prune` is the eviction verb (`--older-than`, `--max-size`)',
40
- gc: '`vx cache prune` is the eviction verb (`--older-than`, `--max-size`)',
41
- purge: '`vx cache prune` is the eviction verb (`--older-than`, `--max-size`)',
45
+ clear:
46
+ '`vx cache clean` with no bound clears the whole cache; `--older-than` / `--max-size` trim it',
47
+ rm: '`vx cache clean` with no bound clears the whole cache; `--older-than` / `--max-size` trim it',
48
+ delete:
49
+ '`vx cache clean` with no bound clears the whole cache; `--older-than` / `--max-size` trim it',
50
+ reset:
51
+ '`vx cache clean` with no bound clears the whole cache; `--older-than` / `--max-size` trim it',
52
+ wipe: '`vx cache clean` with no bound clears the whole cache; `--older-than` / `--max-size` trim it',
53
+ evict: '`vx cache clean` is the eviction verb (`--older-than`, `--max-size`)',
54
+ gc: '`vx cache clean` is the eviction verb (`--older-than`, `--max-size`)',
55
+ purge:
56
+ '`vx cache clean` with no bound clears the whole cache; `--older-than` / `--max-size` trim it',
42
57
  }
43
58
 
44
59
  function cacheSubHint(sub: string): string {
45
60
  const elsewhere = CACHE_ELSEWHERE[sub]
46
61
  if (elsewhere !== undefined) return ` — ${elsewhere}`
47
- const best = nearest(sub, ['prune'])
48
- // `prune` is the one subcommand: name it when nothing is near (M-58).
49
- return best === undefined ? '. The subcommand is prune' : `. Did you mean ${best}?`
62
+ const best = nearest(sub, ['clean'])
63
+ // `clean` is the one subcommand: name it when nothing is near (M-58).
64
+ return best === undefined ? '. The subcommand is clean' : `. Did you mean ${best}?`
50
65
  }
51
66
 
52
67
  export async function cacheCmd(args: readonly string[]): Promise<number> {
53
68
  const [sub, ...rest] = args
54
69
  switch (sub) {
55
- case 'prune':
56
- return await pruneCmd(rest)
70
+ case 'clean':
71
+ return await cleanCmd(rest)
57
72
  case undefined:
58
- // A bare `vx cache prune` refuses too: the hint names a whole command.
73
+ // A bare `vx cache clean` refuses too: the hint names a whole command.
59
74
  return refuse(
60
75
  'cache',
61
76
  args,
62
- `missing subcommand. Try \`vx cache prune --older-than 30d\` or \`vx cache prune --max-size 1G\`${seeHelp('cache')}`,
77
+ `missing subcommand. Try \`vx cache clean --older-than 30d\` to trim the cache, or \`vx cache clean\` to clear it${seeHelp('cache')}`,
63
78
  'VX_E_USAGE',
64
79
  )
65
80
  default:
@@ -79,6 +94,8 @@ interface PruneArgs {
79
94
  format?: 'pretty' | 'json'
80
95
  /** `--cache-dir`: prune the cache a run with the same flag uses. */
81
96
  cacheDir?: string
97
+ /** `--global`: clear every repository's store under `~/.vx`. */
98
+ global?: boolean
82
99
  error?: string
83
100
  }
84
101
 
@@ -104,7 +121,7 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
104
121
  // the deliberate way to do it.
105
122
  if (ms === 0) {
106
123
  return {
107
- error: `--older-than 0 would evict every entry — to clear the cache, delete the cache store \`vx info\` names`,
124
+ error: `--older-than 0 would evict every entry — \`vx cache clean\` with no bound clears the whole cache`,
108
125
  }
109
126
  }
110
127
  out.olderThanMs = Date.now() - ms
@@ -118,7 +135,7 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
118
135
  return { error: `--max-size must be a size like 500M or 1G (got ${v})${seeHelp('cache')}` }
119
136
  if (bytes === 0) {
120
137
  return {
121
- error: `--max-size 0 would evict every entry — to clear the cache, delete the cache store \`vx info\` names`,
138
+ error: `--max-size 0 would evict every entry — \`vx cache clean\` with no bound clears the whole cache`,
122
139
  }
123
140
  }
124
141
  // A bare number is bytes to `parseSize` (a computed size needs that),
@@ -134,6 +151,8 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
134
151
  out.maxBytes = bytes
135
152
  } else if (a === '--dry-run') {
136
153
  out.dryRun = true
154
+ } else if (a === '--global') {
155
+ out.global = true
137
156
  } else if (a === '--format' || a?.startsWith('--format=')) {
138
157
  const v = formatValue(a === '--format' ? args[++i] : a.slice('--format='.length), 'cache')
139
158
  if (typeof v === 'object') return v
@@ -142,31 +161,38 @@ export function parsePruneArgs(args: readonly string[]): PruneArgs {
142
161
  const cd = parseCacheDirFlag(args, i)
143
162
  if (cd === null)
144
163
  return {
145
- error: `${refusedWord(a ?? '')}: ${a}${flagHint('cache prune', a ?? '')}${seeHelp('cache')}`,
164
+ error: `${refusedWord(a ?? '')}: ${a}${flagHint('cache clean', a ?? '')}${seeHelp('cache')}`,
146
165
  }
147
166
  if ('error' in cd) return { error: `${cd.error}${seeHelp('cache')}` }
148
167
  out.cacheDir = cd.cacheDir
149
168
  i = cd.next
150
169
  }
151
170
  }
152
- if (out.olderThanMs === undefined && out.maxBytes === undefined) {
153
- return { error: `needs --older-than <duration> or --max-size <size>${seeHelp('cache')}` }
171
+ if (out.global === true && (out.olderThanMs !== undefined || out.maxBytes !== undefined)) {
172
+ return {
173
+ error: `--global clears every repository's store and takes no --older-than or --max-size${seeHelp('cache')}`,
174
+ }
175
+ }
176
+ if (out.global === true && out.cacheDir !== undefined) {
177
+ return {
178
+ error: `--global clears the stores under ~/.vx and takes no --cache-dir${seeHelp('cache')}`,
179
+ }
154
180
  }
155
181
  return out
156
182
  }
157
183
 
158
- async function pruneCmd(args: readonly string[]): Promise<number> {
184
+ async function cleanCmd(args: readonly string[]): Promise<number> {
159
185
  const parsed = parsePruneArgs(args)
160
- if (parsed.error) return refuse('cache prune', args, parsed.error, 'VX_E_USAGE')
186
+ if (parsed.error) return refuse('cache clean', args, parsed.error, 'VX_E_USAGE')
161
187
  const cwd = process.cwd()
162
188
  let root: string
163
189
  try {
164
190
  root = await findWorkspaceRoot(cwd)
165
191
  } catch (err) {
166
- return refuse('cache prune', args, (err as Error).message, errorCode(err))
192
+ return refuse('cache clean', args, (err as Error).message, errorCode(err))
167
193
  }
168
194
  // Honor `--cache-dir`, `defineWorkspace({ cacheDir: '...' })` and a
169
- // `config` plugin's edit of it — `vx run` and `vx cache prune` must
195
+ // `config` plugin's edit of it — `vx run` and `vx cache clean` must
170
196
  // operate on the same directory or prune silently no-ops against the
171
197
  // wrong path.
172
198
  // A dry run only reads: it never resets the index (item 896).
@@ -181,6 +207,9 @@ async function pruneCmd(args: readonly string[]): Promise<number> {
181
207
  dir = ws.cacheDir
182
208
  storeRoot = await ws.storeRoot
183
209
  }
210
+ if (parsed.olderThanMs === undefined && parsed.maxBytes === undefined) {
211
+ return clear(args, root, dir, storeRoot, parsed)
212
+ }
184
213
  // A workspace that never ran has nothing to prune, and opening the cache
185
214
  // to find that out made it: `.vx/cache` with a database and a
186
215
  // `.gitignore`, where item 900 had held the dry run to making nothing
@@ -233,11 +262,121 @@ function printPruned(
233
262
  )
234
263
  return
235
264
  }
236
- const orphans =
237
- result.orphans > 0
238
- ? `, ${dry ? 'would reap' : 'reaped'} ${result.orphans} orphaned artifact${result.orphans === 1 ? '' : 's'} (${formatBytes(result.orphanBytes)})`
239
- : ''
240
- process.stdout.write(
241
- `${dry ? 'Would prune' : 'Pruned'} ${result.evicted} entr${result.evicted === 1 ? 'y' : 'ies'} (${formatBytes(result.bytesFreed)}${dry ? '' : ' freed'})${orphans}\n`,
265
+ const plural = (n: number, one: string, many: string): string => `${n} ${n === 1 ? one : many}`
266
+ const facts = [
267
+ ...(result.evicted > 0 ? [plural(result.evicted, 'entry', 'entries')] : []),
268
+ ...(result.orphans > 0
269
+ ? [plural(result.orphans, 'orphaned artifact', 'orphaned artifacts')]
270
+ : []),
271
+ ]
272
+ ui.print(
273
+ 'out',
274
+ freedStatus(dry, result.bytesFreed + result.orphanBytes, facts.length > 0),
275
+ ...(facts.length > 0 ? [ui.text(facts.join(' · '))] : []),
276
+ )
277
+ }
278
+
279
+ /** `✓ vx cache clean freed 1.3 GB`, `› … would free 1.3 GB (dry run)`, or nothing to free. */
280
+ function freedStatus(dry: boolean, bytes: number, any: boolean): ui.Block {
281
+ if (!any) return ui.status('info', 'vx cache clean', `nothing to free${dry ? ' (dry run)' : ''}`)
282
+ return dry
283
+ ? ui.status('info', 'vx cache clean', `would free ${formatBytes(bytes)} (dry run)`)
284
+ : ui.status('ok', 'vx cache clean', `freed ${formatBytes(bytes)}`)
285
+ }
286
+
287
+ /** `vx cache clean --format json` with no bound (schemas/cache.json `clear`). */
288
+ export interface ClearResult {
289
+ dryRun: boolean
290
+ /** Workspace-relative inside the workspace, else absolute. */
291
+ cleared: string[]
292
+ bytesFreed: number
293
+ }
294
+
295
+ /**
296
+ * `vx cache clean` with no bound: delete the cache directory (its index,
297
+ * run history and any artifacts kept beside it) and the store its index
298
+ * names (`~/.vx/<id>/cache`, shared by the repository's checkouts), as
299
+ * `nx reset` does. `--global` takes every store under `~/.vx` too. The
300
+ * workspace config, the lock and `.vx/.gitignore` sit outside them all.
301
+ * A directory the user named (`cache.dir`) may be shared (`build/`,
302
+ * `~/.cache`): there only what vx wrote goes (item 968).
303
+ */
304
+ async function clear(
305
+ args: readonly string[],
306
+ root: string,
307
+ dir: string,
308
+ storeRoot: string | null,
309
+ parsed: PruneArgs,
310
+ ): Promise<number> {
311
+ // The store the index recorded wins over today's (a changed HOME), as a
312
+ // run reads it; a directory with no index is not a vx cache to delete.
313
+ const targets: string[] = []
314
+ if (existsSync(path.join(dir, 'cache.db'))) {
315
+ const cache = new Cache(dir, undefined, undefined, undefined, 'preview', storeRoot)
316
+ const store = cache.storeDir
317
+ cache.close()
318
+ targets.push(dir)
319
+ // The index is a file in the checkout (committed, or restored from a CI
320
+ // cache): the store it names is deleted only when it is one.
321
+ if (store !== undefined && isStore(store)) targets.push(store)
322
+ } else if (storeRoot !== null) targets.push(storeRoot)
323
+ if (parsed.global === true) {
324
+ const vxHome = path.join(process.env['HOME'] || homedir(), '.vx')
325
+ const ids = existsSync(vxHome) ? readdirSync(vxHome) : []
326
+ for (const id of ids) targets.push(path.join(vxHome, id, 'cache'))
327
+ }
328
+ // `cache.dir: '.vx'` (or the root) would take the workspace config and
329
+ // the lock with it.
330
+ const dotVx = path.join(root, '.vx')
331
+ const doomed = [...new Set(targets.map((t) => path.resolve(t)))].filter((t) => existsSync(t))
332
+ const unsafe = doomed.find((t) => t === dotVx || !path.relative(t, dotVx).startsWith('..'))
333
+ if (unsafe !== undefined) {
334
+ return refuse(
335
+ 'cache clean',
336
+ args,
337
+ `cache directory ${unsafe} holds the workspace config and lock; set cache.dir to a directory of its own`,
338
+ 'VX_E_CONFIG',
339
+ )
340
+ }
341
+ const dry = parsed.dryRun === true
342
+ // `.vx/cache` and the stores are vx's own; any other directory is the
343
+ // user's, cleared entry by entry and removed only if that empties it.
344
+ const named = path.resolve(dir) === path.join(dotVx, 'cache') ? undefined : path.resolve(dir)
345
+ const entries = (t: string): string[] =>
346
+ t === named ? vxCacheEntries(t).map((e) => path.join(t, e)) : [t]
347
+ const bytes = doomed.reduce((n, t) => n + entries(t).reduce((m, e) => m + treeBytes(e), 0), 0)
348
+ if (!dry && doomed.length > 0) {
349
+ // A clear beside a run deletes what the run is restoring or saving; it
350
+ // waits for the run's lock, as a trim does.
351
+ const release = await acquireRunLock(root, { log: warnToStderr })
352
+ try {
353
+ for (const t of doomed) {
354
+ for (const e of entries(t)) rmSync(e, { recursive: true, force: true })
355
+ if (t === named && readdirSync(t).length === 0) rmSync(t, { recursive: true })
356
+ }
357
+ } finally {
358
+ await release()
359
+ }
360
+ }
361
+ const shown = doomed.map((t) => (path.relative(root, t).startsWith('..') ? t : relPosix(root, t)))
362
+ if (parsed.format === 'json') {
363
+ const result: ClearResult = { dryRun: dry, cleared: shown, bytesFreed: bytes }
364
+ process.stdout.write(`${JSON.stringify(result)}\n`)
365
+ } else
366
+ ui.print(
367
+ 'out',
368
+ freedStatus(dry, bytes, shown.length > 0),
369
+ ...(shown.length > 0 ? [ui.text(...shown)] : []),
370
+ )
371
+ return 0
372
+ }
373
+
374
+ /** A store's shape: `<home>/.vx/<id>/cache` holding its `store.db`. */
375
+ function isStore(stored: string): boolean {
376
+ const dir = path.resolve(stored)
377
+ return (
378
+ path.basename(dir) === 'cache' &&
379
+ path.basename(path.dirname(path.dirname(dir))) === '.vx' &&
380
+ existsSync(path.join(dir, 'store.db'))
242
381
  )
243
382
  }
@@ -6,13 +6,13 @@
6
6
  // Tab is the wrong price).
7
7
 
8
8
  import { acceptedFlags, CORE_VERBS, flagHint, seeHelp } from './help.js'
9
- import { nearest } from '../util/index.js'
9
+ import { nearest, sayNotice } from '../util/index.js'
10
10
 
11
11
  /** A flag whose value is one of a fixed set; the tests hold each to its parser. */
12
12
  export const FLAG_VALUES: Readonly<Record<string, readonly string[]>> = {
13
13
  '--format': ['pretty', 'json'],
14
14
  '--output-logs': ['full', 'errors-only', 'hash-only', 'none'],
15
- '--download': ['all', 'toplevel', 'none'],
15
+ '--cache-download': ['all', 'toplevel', 'none'],
16
16
  }
17
17
 
18
18
  export type CompletionShell = 'bash' | 'zsh' | 'fish'
@@ -30,7 +30,7 @@ export function verbFlags(verb: string): string[] {
30
30
 
31
31
  /** The words that follow `vx <verb>` besides flags: subcommands and fixed choices. */
32
32
  const VERB_WORDS: Readonly<Record<string, readonly string[]>> = {
33
- cache: ['prune'],
33
+ cache: ['clean'],
34
34
  completions: [...SHELLS],
35
35
  }
36
36
 
@@ -139,8 +139,10 @@ export async function completionsCmd(
139
139
  if (args.length !== 1 || !(SHELLS as readonly string[]).includes(shell ?? '')) {
140
140
  const flag = args.find((a) => a.startsWith('-'))
141
141
  if (flag !== undefined) {
142
- process.stderr.write(
143
- `vx completions: unknown flag: ${flag}${flagHint('completions', flag)}${seeHelp('completions')}\n`,
142
+ sayNotice(
143
+ 'error',
144
+ 'vx completions',
145
+ `unknown flag: ${flag}${flagHint('completions', flag)}${seeHelp('completions')}`,
144
146
  )
145
147
  return 1
146
148
  }
@@ -148,8 +150,10 @@ export async function completionsCmd(
148
150
  // One edit: shell names are three or four letters, so two edits took
149
151
  // `tcsh` to `bash`, a different shell.
150
152
  const best = one ? nearest(shell, SHELLS, 1) : undefined
151
- process.stderr.write(
152
- `vx completions: expected one shell — bash, zsh or fish${one ? ` (got ${shell})` : ''}${best === undefined ? '' : `. Did you mean ${best}?`}${seeHelp('completions')}\n`,
153
+ sayNotice(
154
+ 'error',
155
+ 'vx completions',
156
+ `expected one shell — bash, zsh or fish${one ? ` (got ${shell})` : ''}${best === undefined ? '' : `. Did you mean ${best}?`}${seeHelp('completions')}`,
153
157
  )
154
158
  return 1
155
159
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * One core per process. A plugin package, a `vx.workspace.ts`, a
2
+ * One core per process. A plugin package, a `.vx/workspace.ts`, a
3
3
  * `vx.config.ts` all `import … from '@vzn/vx'`, and that specifier
4
4
  * resolves through node_modules — to a SECOND copy of core when the host
5
5
  * is the compiled binary (whose own copy lives under the bunfs root) or
@@ -159,14 +159,15 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
159
159
  value: false,
160
160
  outcome: 'same',
161
161
  // Turbo 2.11's own help: "Equivalent to `--cache=local:r,remote:r`".
162
- vx: "`--no-cache` reads nothing either; Turbo's (reads, no writes) is `--cache local:r,remote:r`",
162
+ vx: "`--no-cache` reads nothing either; Turbo's (reads, no writes) is `--cache-policy local:r,remote:r`",
163
163
  },
164
164
  {
165
165
  runner: 'turbo',
166
166
  names: ['--cache'],
167
167
  value: true,
168
- outcome: 'same',
169
- vx: '`--cache local:rw,remote:r`',
168
+ outcome: 'alias',
169
+ vx: '`--cache-policy`, the same spec (`local:rw,remote:r`)',
170
+ to: (v) => ['--cache-policy', v ?? ''],
170
171
  },
171
172
  {
172
173
  runner: 'turbo',
@@ -256,7 +257,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
256
257
  value: true,
257
258
  shown: '`--experimental-otel-*`',
258
259
  outcome: 'refuse',
259
- vx: 'telemetry is a plugin: `otel()` from @vzn/vx-otel in vx.workspace.ts',
260
+ vx: 'telemetry is a plugin: `otel()` from @vzn/vx-otel in .vx/workspace.ts',
260
261
  },
261
262
  {
262
263
  runner: 'nx',
@@ -297,7 +298,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
297
298
  value: false,
298
299
  bool: true,
299
300
  outcome: 'refuse',
300
- vx: 'use `--cache local:,remote:rw`',
301
+ vx: 'use `--cache-policy local:,remote:rw`',
301
302
  },
302
303
  {
303
304
  runner: 'turbo',
@@ -305,7 +306,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
305
306
  value: false,
306
307
  bool: true,
307
308
  outcome: 'refuse',
308
- vx: 'use `--cache local:rw,remote:r`',
309
+ vx: 'use `--cache-policy local:rw,remote:r`',
309
310
  },
310
311
  {
311
312
  runner: 'turbo',
@@ -396,7 +397,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
396
397
  names: ['--token', '--team', '--api'],
397
398
  value: true,
398
399
  outcome: 'refuse',
399
- vx: 'a remote cache is a plugin: `turboCache()` from @vzn/vx-migrate in vx.workspace.ts reads TURBO_TOKEN / TURBO_TEAM / TURBO_API',
400
+ vx: 'a remote cache is a plugin: `turboCache()` from @vzn/vx-migrate in .vx/workspace.ts reads TURBO_TOKEN / TURBO_TEAM / TURBO_API',
400
401
  },
401
402
  {
402
403
  runner: 'turbo',
@@ -532,8 +533,8 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
532
533
  value: false,
533
534
  bool: true,
534
535
  outcome: 'alias',
535
- vx: '`--cache local:rw,remote:`',
536
- to: () => ['--cache', 'local:rw,remote:'],
536
+ vx: '`--cache-policy local:rw,remote:`',
537
+ to: () => ['--cache-policy', 'local:rw,remote:'],
537
538
  },
538
539
  {
539
540
  runner: 'nx',
@@ -575,7 +576,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
575
576
  names: ['--runner'],
576
577
  value: true,
577
578
  outcome: 'refuse',
578
- vx: 'a remote cache is a plugin: `nxCache()` from @vzn/vx-migrate in vx.workspace.ts',
579
+ vx: 'a remote cache is a plugin: `nxCache()` from @vzn/vx-migrate in .vx/workspace.ts',
579
580
  },
580
581
  {
581
582
  runner: 'nx',
@@ -656,7 +657,7 @@ export const FOREIGN_FLAGS: readonly ForeignFlag[] = [
656
657
 
657
658
  /** Nx's verbs, as its users type them: `vx run` takes their flags. */
658
659
  const TURBO_LOGIN =
659
- 'vx has no login: a remote cache is a plugin, `turboCache()` from @vzn/vx-migrate in vx.workspace.ts, which reads TURBO_TOKEN / TURBO_TEAM / TURBO_API'
660
+ 'vx has no login: a remote cache is a plugin, `turboCache()` from @vzn/vx-migrate in .vx/workspace.ts, which reads TURBO_TOKEN / TURBO_TEAM / TURBO_API'
660
661
 
661
662
  export const FOREIGN_VERBS: Readonly<Record<string, string>> = {
662
663
  'run-many':
@@ -668,7 +669,7 @@ export const FOREIGN_VERBS: Readonly<Record<string, string>> = {
668
669
  query:
669
670
  '`turbo query` has no vx form: `vx run <task> --dry=json` prints the planned graph as JSON',
670
671
  reset:
671
- '`nx reset` is `vx cache prune --older-than <age>` here, or delete the cache store `vx info` names to drop it all',
672
+ '`nx reset` is `vx cache clean` here; `--older-than <age>` trims the cache instead of clearing it',
672
673
  daemon: 'vx has no daemon: there is nothing to start or stop',
673
674
  login: TURBO_LOGIN,
674
675
  logout: TURBO_LOGIN,