@vzn/vx 0.0.116 → 0.0.118

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.116",
3
+ "version": "0.0.118",
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.116",
21
- "@vzn/vx-linux-arm64": "0.0.116",
22
- "@vzn/vx-darwin-x64": "0.0.116",
23
- "@vzn/vx-darwin-arm64": "0.0.116"
20
+ "@vzn/vx-linux-x64": "0.0.118",
21
+ "@vzn/vx-linux-arm64": "0.0.118",
22
+ "@vzn/vx-darwin-x64": "0.0.118",
23
+ "@vzn/vx-darwin-arm64": "0.0.118"
24
24
  },
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
@@ -0,0 +1,180 @@
1
+ // Which settled paths are changes: `vx watch`'s judgement, run one debounce
2
+ // window after events stop, on what the tree settled to. The loop in
3
+ // watch.ts owns when to judge and what to run; this owns what counts.
4
+
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import { xxh3 } from '../util/index.js'
8
+ import { gitIgnored } from './watch-filter.js'
9
+ import { modifiedBefore } from './watch-fs.js'
10
+
11
+ const ABSENT = -1n
12
+
13
+ function settledState(abs: string): bigint {
14
+ let st: fs.Stats
15
+ try {
16
+ st = fs.statSync(abs)
17
+ } catch {
18
+ return ABSENT
19
+ }
20
+ if (!st.isDirectory()) {
21
+ try {
22
+ return xxh3(fs.readFileSync(abs))
23
+ } catch {
24
+ return ABSENT
25
+ }
26
+ }
27
+ const entries: string[] = []
28
+ try {
29
+ for (const e of fs.readdirSync(abs, { withFileTypes: true })) {
30
+ let size = 0
31
+ if (e.isFile()) {
32
+ try {
33
+ size = fs.statSync(path.join(abs, e.name)).size
34
+ } catch {
35
+ size = -1
36
+ }
37
+ }
38
+ entries.push(`${e.name}\0${e.isDirectory() ? 'd' : size}`)
39
+ }
40
+ } catch {
41
+ return ABSENT
42
+ }
43
+ entries.sort()
44
+ return xxh3(Buffer.from(entries.join('\n')))
45
+ }
46
+
47
+ /** What the judgement reads from the loop, which owns it and may replace it. */
48
+ export interface JudgeContext {
49
+ workspaceRoot: string
50
+ /** The instant the watchers went live, on the mtime clock (`fsClockNow`). */
51
+ armedAt: number
52
+ /** Whether a persistent server the last cycle started is still running. */
53
+ held(): boolean
54
+ /** Projects whose uncached task may read a git-ignored file. */
55
+ uncached(): ReadonlySet<string>
56
+ }
57
+
58
+ export class ChangeJudge {
59
+ // Paths that fired since the last judgement, first label wins. The state
60
+ // check runs when the timer fires, on SETTLED state: per event it is
61
+ // wrong on Linux, where a shell redirect truncates the file (one event,
62
+ // empty) and then writes it (another, full), so consecutive events never
63
+ // agree and a self-write loops anyway (CI, 2026-09-04: 9 re-runs where
64
+ // macOS, which coalesces the two, saw 2). While a cycle runs, nothing is
65
+ // judged: the run's own writes are mid-flight (a `dist` deleted and not
66
+ // yet rebuilt is a state the tree will not keep), so the paths wait and
67
+ // are judged one window after the run ends, all together — an edit made
68
+ // meanwhile still differs from what the loop last saw and re-runs.
69
+ readonly pending = new Map<string, string>()
70
+ /** The last cycle's wall window; a write inside it is the run's own. */
71
+ lastCycle: { start: number; end: number } | undefined
72
+ // Declared outputs are ignored by PATH above. A task with no `cache`
73
+ // block declares none and still writes into its project, and the
74
+ // watcher sees the write: run 1 writes dist/x, the event re-runs, run 2
75
+ // writes the same bytes, the event re-runs — forever (the init
76
+ // walkthrough, 2026-09-04: every fresh workspace, since `init` emits no
77
+ // cache block). An undeclared write is caught by STATE, judged on what
78
+ // has settled (see `trigger`): a path whose settled state equals what
79
+ // this loop last saw for it is not a change. A file's state is its
80
+ // bytes; a directory's is its entries' names and sizes (a nested edit
81
+ // arrives as that path's own event); a path that is gone is one more
82
+ // state. A real edit changes the state; a first sighting passes through.
83
+ // So `rm -rf dist && tsc` — the shape of most build scripts — settles
84
+ // to the same `dist` it left and is one redundant cycle, not a loop
85
+ // (2026-09-10: 780 executions in two minutes from one edit, when a
86
+ // deletion and a directory each passed the gate unconditionally).
87
+ private readonly lastState = new Map<string, bigint>()
88
+ // A path that starts cycle after cycle from the run's own writes is a
89
+ // task rewriting a file with different bytes every run (a pid file, a
90
+ // timestamped log): the state gate cannot settle it, and nothing here
91
+ // can tell the third such write from a user's third save mid-run — so
92
+ // watch names it once, with the remedy, and keeps going. "The run's
93
+ // own write" is read off the path itself: its mtime falls inside the
94
+ // previous cycle's window. Not off which judgement started the cycle:
95
+ // macOS delivers a run's writes late, after the loop's own post-run
96
+ // judgement found nothing and broke out, so there every such cycle
97
+ // starts from the idle timer (CI, 2026-09-16: the storm ran, the
98
+ // notice never came).
99
+ private readonly streak = { abs: '', n: 0 }
100
+ private readonly noticed = new Set<string>()
101
+
102
+ constructor(private readonly ctx: JudgeContext) {}
103
+
104
+ // A path this loop has never judged is a change only if it moved since
105
+ // the watchers went live (its mtime or ctime, `modifiedBefore`). macOS delivers the initial run's own writes
106
+ // AFTER the arm (CI, 2026-09-11: `app dist; re-running...` with no edit
107
+ // made — FSEvents hands a stream what landed just before it started),
108
+ // and a first sighting used to pass unconditionally; the path's mtime
109
+ // says which side of the arm it belongs to. A path already gone is a
110
+ // change: a deletion has no date to read.
111
+ private sameState(abs: string): boolean {
112
+ const state = settledState(abs)
113
+ const prev = this.lastState.get(abs)
114
+ this.lastState.set(abs, state)
115
+ if (prev !== undefined) return prev === state
116
+ return state !== ABSENT && modifiedBefore(abs, this.ctx.armedAt)
117
+ }
118
+
119
+ private writtenDuringLastCycle(abs: string, openWhileHeld = false): boolean {
120
+ // A server the last cycle left running is still that cycle's: its
121
+ // writes land after the cycle ended, and with a closed window a dev
122
+ // server that rewrites a log in its project restarted itself forever
123
+ // with no word of it, 12 restarts in 8 s (item 948). The initial run's
124
+ // server is one too, from the arm on.
125
+ const open = openWhileHeld && this.ctx.held()
126
+ if (this.lastCycle === undefined && !open) return false
127
+ try {
128
+ const m = fs.statSync(abs).mtimeMs
129
+ const start = this.lastCycle?.start ?? this.ctx.armedAt
130
+ return m >= start && (open || m <= this.lastCycle!.end)
131
+ } catch {
132
+ return false
133
+ }
134
+ }
135
+
136
+ judge(): string | undefined {
137
+ const ignored = gitIgnored(this.ctx.workspaceRoot, [...this.pending.keys()])
138
+ let first: string | undefined
139
+ let firstAbs: string | undefined
140
+ // `sameState` before the `first` test, never after it: it is what
141
+ // RECORDS a path's settled state, and every path this judgement saw
142
+ // must be recorded even though only the first change names the cycle.
143
+ // Short-circuiting after the winner leaves the rest unjudged, and
144
+ // their next event is a first sighting stamped after the arm — so a
145
+ // batch edit (a `git checkout`) makes the same bytes written to any
146
+ // of the others a change.
147
+ // A git-ignored path keys nothing, but a task with no cache has no key:
148
+ // it reads what it likes, and its `.env.local` edit re-ran nothing
149
+ // (item 947). Under such a project an ignored path is judged when the
150
+ // user wrote it; one the last cycle wrote (the pid file an uncached
151
+ // task rewrites every run, the loop the filter exists for) stays out.
152
+ const editForUncached = (p: string): boolean =>
153
+ [...this.ctx.uncached()].some((dir) => p.startsWith(dir + path.sep)) &&
154
+ !this.writtenDuringLastCycle(p)
155
+ for (const [p, l] of this.pending) {
156
+ if (ignored.has(p) && !editForUncached(p)) continue
157
+ if (!this.sameState(p) && first === undefined) {
158
+ first = l
159
+ firstAbs = p
160
+ }
161
+ }
162
+ this.pending.clear()
163
+ const byServer = firstAbs !== undefined && this.ctx.held()
164
+ if (firstAbs === undefined || !this.writtenDuringLastCycle(firstAbs, true)) {
165
+ this.streak.n = 0
166
+ return first
167
+ }
168
+ this.streak.n = this.streak.abs === firstAbs ? this.streak.n + 1 : 1
169
+ this.streak.abs = firstAbs
170
+ if (this.streak.n >= 3 && !this.noticed.has(firstAbs)) {
171
+ this.noticed.add(firstAbs)
172
+ process.stdout.write(
173
+ byServer
174
+ ? `vx watch: ${first} has started 3 cycles in a row, written while a server the cycle before started was running — a persistent task rewrites it. Add it to .gitignore (a git-ignored path never starts a cycle); until then every write restarts the server.\n`
175
+ : `vx watch: ${first} has started 3 cycles in a row, written by the cycle before each — a task rewrites it every run. Declare it in cache.outputs (an output never starts a cycle) or add it to .gitignore (a git-ignored path never does); until then every run re-runs.\n`,
176
+ )
177
+ }
178
+ return first
179
+ }
180
+ }
package/src/cli/watch.ts CHANGED
@@ -14,7 +14,6 @@
14
14
 
15
15
  import fs from 'node:fs'
16
16
  import path from 'node:path'
17
- import { xxh3 } from '../util/index.js'
18
17
  import { parseRunArgs, resolveRunOptions, type RunArgs } from './run.js'
19
18
  import { seeHelp } from './help.js'
20
19
  import {
@@ -35,7 +34,6 @@ import {
35
34
  } from '../workspace/index.js'
36
35
  import { type CliLoadOptions, loadCliWorkspace } from './workspace-config.js'
37
36
  import {
38
- gitIgnored,
39
37
  isIgnoredWatchPath,
40
38
  isWorkspaceConfigFile,
41
39
  isWorkspaceFingerprintFile,
@@ -46,12 +44,12 @@ import {
46
44
  import {
47
45
  armWatcher,
48
46
  fsClockNow,
49
- modifiedBefore,
50
47
  pollWatcher,
51
48
  POLL_INTERVAL_MS,
52
49
  WATCH_PROBE_TIMEOUT_MS,
53
50
  type WatchHandle,
54
51
  } from './watch-fs.js'
52
+ import { ChangeJudge } from './watch-judge.js'
55
53
  import { memberEntries, sameMembers, sweepConfigs, watchedProjects } from './watch-set.js'
56
54
 
57
55
  /** One line for a watcher or re-read the OS refused; the loop goes on without it. */
@@ -336,170 +334,21 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
336
334
  let held = args.held
337
335
 
338
336
  // Reentrancy guard — never two orchestrator runs in flight. Events that
339
- // land while one is running wait in `pendingPaths` and are judged, on
337
+ // land while one is running wait in `changes.pending` and are judged, on
340
338
  // settled bytes, one debounce window after it ends.
341
339
  let running = false
342
340
  /** The cycle in flight, so the stop path can wait for its teardown before resolving. */
343
341
  let inFlight: Promise<void> = Promise.resolve()
344
342
  let debounceTimer: ReturnType<typeof setTimeout> | null = null
345
343
 
346
- // Declared outputs are ignored by PATH above. A task with no `cache`
347
- // block declares none and still writes into its project, and the
348
- // watcher sees the write: run 1 writes dist/x, the event re-runs, run 2
349
- // writes the same bytes, the event re-runs — forever (the init
350
- // walkthrough, 2026-09-04: every fresh workspace, since `init` emits no
351
- // cache block). An undeclared write is caught by STATE, judged on what
352
- // has settled (see `trigger`): a path whose settled state equals what
353
- // this loop last saw for it is not a change. A file's state is its
354
- // bytes; a directory's is its entries' names and sizes (a nested edit
355
- // arrives as that path's own event); a path that is gone is one more
356
- // state. A real edit changes the state; a first sighting passes through.
357
- // So `rm -rf dist && tsc` — the shape of most build scripts — settles
358
- // to the same `dist` it left and is one redundant cycle, not a loop
359
- // (2026-09-10: 780 executions in two minutes from one edit, when a
360
- // deletion and a directory each passed the gate unconditionally).
361
- const ABSENT = -1n
362
- const lastState = new Map<string, bigint>()
363
- const settledState = (abs: string): bigint => {
364
- let st: fs.Stats
365
- try {
366
- st = fs.statSync(abs)
367
- } catch {
368
- return ABSENT
369
- }
370
- if (!st.isDirectory()) {
371
- try {
372
- return xxh3(fs.readFileSync(abs))
373
- } catch {
374
- return ABSENT
375
- }
376
- }
377
- const entries: string[] = []
378
- try {
379
- for (const e of fs.readdirSync(abs, { withFileTypes: true })) {
380
- let size = 0
381
- if (e.isFile()) {
382
- try {
383
- size = fs.statSync(path.join(abs, e.name)).size
384
- } catch {
385
- size = -1
386
- }
387
- }
388
- entries.push(`${e.name}\0${e.isDirectory() ? 'd' : size}`)
389
- }
390
- } catch {
391
- return ABSENT
392
- }
393
- entries.sort()
394
- return xxh3(Buffer.from(entries.join('\n')))
395
- }
396
- // A path this loop has never judged is a change only if it moved since
397
- // the watchers went live (its mtime or ctime, `modifiedBefore`). macOS delivers the initial run's own writes
398
- // AFTER the arm (CI, 2026-09-11: `app dist; re-running...` with no edit
399
- // made — FSEvents hands a stream what landed just before it started),
400
- // and a first sighting used to pass unconditionally; the path's mtime
401
- // says which side of the arm it belongs to. A path already gone is a
402
- // change: a deletion has no date to read.
403
- const sameState = (abs: string): boolean => {
404
- const state = settledState(abs)
405
- const prev = lastState.get(abs)
406
- lastState.set(abs, state)
407
- if (prev !== undefined) return prev === state
408
- return state !== ABSENT && modifiedBefore(abs, armedAt)
409
- }
410
-
411
- // Paths that fired since the last judgement, first label wins. The state
412
- // check runs when the timer fires, on SETTLED state: per event it is
413
- // wrong on Linux, where a shell redirect truncates the file (one event,
414
- // empty) and then writes it (another, full), so consecutive events never
415
- // agree and a self-write loops anyway (CI, 2026-09-04: 9 re-runs where
416
- // macOS, which coalesces the two, saw 2). While a cycle runs, nothing is
417
- // judged: the run's own writes are mid-flight (a `dist` deleted and not
418
- // yet rebuilt is a state the tree will not keep), so the paths wait and
419
- // are judged one window after the run ends, all together — an edit made
420
- // meanwhile still differs from what the loop last saw and re-runs.
421
- const pendingPaths = new Map<string, string>()
422
- // A path that starts cycle after cycle from the run's own writes is a
423
- // task rewriting a file with different bytes every run (a pid file, a
424
- // timestamped log): the state gate cannot settle it, and nothing here
425
- // can tell the third such write from a user's third save mid-run — so
426
- // watch names it once, with the remedy, and keeps going. "The run's
427
- // own write" is read off the path itself: its mtime falls inside the
428
- // previous cycle's window. Not off which judgement started the cycle:
429
- // macOS delivers a run's writes late, after the loop's own post-run
430
- // judgement found nothing and broke out, so there every such cycle
431
- // starts from the idle timer (CI, 2026-09-16: the storm ran, the
432
- // notice never came).
433
- const streak = { abs: '', n: 0 }
434
- const noticed = new Set<string>()
435
- let lastCycle: { start: number; end: number } | undefined
436
- const writtenDuringLastCycle = (abs: string, openWhileHeld = false): boolean => {
437
- // A server the last cycle left running is still that cycle's: its
438
- // writes land after the cycle ended, and with a closed window a dev
439
- // server that rewrites a log in its project restarted itself forever
440
- // with no word of it, 12 restarts in 8 s (item 948). The initial run's
441
- // server is one too, from the arm on.
442
- const open = openWhileHeld && held !== undefined
443
- if (lastCycle === undefined && !open) return false
444
- try {
445
- const m = fs.statSync(abs).mtimeMs
446
- const start = lastCycle?.start ?? armedAt
447
- return m >= start && (open || m <= lastCycle!.end)
448
- } catch {
449
- return false
450
- }
451
- }
452
- const judge = (): string | undefined => {
453
- const ignored = gitIgnored(workspaceRoot, [...pendingPaths.keys()])
454
- let first: string | undefined
455
- let firstAbs: string | undefined
456
- // `sameState` before the `first` test, never after it: it is what
457
- // RECORDS a path's settled state, and every path this judgement saw
458
- // must be recorded even though only the first change names the cycle.
459
- // Short-circuiting after the winner leaves the rest unjudged, and
460
- // their next event is a first sighting stamped after the arm — so a
461
- // batch edit (a `git checkout`) makes the same bytes written to any
462
- // of the others a change.
463
- // A git-ignored path keys nothing, but a task with no cache has no key:
464
- // it reads what it likes, and its `.env.local` edit re-ran nothing
465
- // (item 947). Under such a project an ignored path is judged when the
466
- // user wrote it; one the last cycle wrote (the pid file an uncached
467
- // task rewrites every run, the loop the filter exists for) stays out.
468
- const editForUncached = (p: string): boolean =>
469
- [...uncached].some((dir) => p.startsWith(dir + path.sep)) && !writtenDuringLastCycle(p)
470
- for (const [p, l] of pendingPaths) {
471
- if (ignored.has(p) && !editForUncached(p)) continue
472
- if (!sameState(p) && first === undefined) {
473
- first = l
474
- firstAbs = p
475
- }
476
- }
477
- pendingPaths.clear()
478
- const byServer = firstAbs !== undefined && held !== undefined
479
- if (firstAbs === undefined || !writtenDuringLastCycle(firstAbs, true)) {
480
- streak.n = 0
481
- return first
482
- }
483
- streak.n = streak.abs === firstAbs ? streak.n + 1 : 1
484
- streak.abs = firstAbs
485
- if (streak.n >= 3 && !noticed.has(firstAbs)) {
486
- noticed.add(firstAbs)
487
- process.stdout.write(
488
- byServer
489
- ? `vx watch: ${first} has started 3 cycles in a row, written while a server the cycle before started was running — a persistent task rewrites it. Add it to .gitignore (a git-ignored path never starts a cycle); until then every write restarts the server.\n`
490
- : `vx watch: ${first} has started 3 cycles in a row, written by the cycle before each — a task rewrites it every run. Declare it in cache.outputs (an output never starts a cycle) or add it to .gitignore (a git-ignored path never does); until then every run re-runs.\n`,
491
- )
492
- }
493
- return first
494
- }
495
344
  const trigger = (label: string, abs: string): void => {
496
- if (!pendingPaths.has(abs)) pendingPaths.set(abs, label)
345
+ if (!changes.pending.has(abs)) changes.pending.set(abs, label)
497
346
  if (running) return
498
347
  if (debounceTimer) clearTimeout(debounceTimer)
499
348
  debounceTimer = setTimeout(() => {
500
349
  debounceTimer = null
501
350
  if (running) return
502
- const first = judge()
351
+ const first = changes.judge()
503
352
  if (first !== undefined) void cycle(first)
504
353
  }, DEBOUNCE_MS)
505
354
  }
@@ -520,7 +369,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
520
369
  held = undefined
521
370
  const start = Date.now()
522
371
  held = (await runOrchestrator(opts)).persistent
523
- lastCycle = { start, end: Date.now() }
372
+ changes.lastCycle = { start, end: Date.now() }
524
373
  if (reread && !stop.aborted) {
525
374
  reread = false
526
375
  await rearm()
@@ -538,15 +387,15 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
538
387
  }
539
388
  // What landed mid-run is judged on settled state, one window
540
389
  // after the run, under the label of what actually arrived.
541
- if (pendingPaths.size === 0 || stop.aborted) break
390
+ if (changes.pending.size === 0 || stop.aborted) break
542
391
  await Bun.sleep(DEBOUNCE_MS)
543
- label = judge()
392
+ label = changes.judge()
544
393
  }
545
394
  } finally {
546
395
  running = false
547
396
  // Anything that landed after the last judgement waits for a timer
548
397
  // like any other event.
549
- const next = pendingAfterCycle(pendingPaths, stop.aborted)
398
+ const next = pendingAfterCycle(changes.pending, stop.aborted)
550
399
  if (next !== undefined) trigger(next[1], next[0])
551
400
  }
552
401
  }
@@ -635,6 +484,13 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
635
484
 
636
485
  /** The instant the watchers go live, on the mtime clock (see `fsClockNow`): a path last modified before it is the initial run's, not an edit. */
637
486
  const armedAt = fsClockNow(cacheDir)
487
+ /** Which settled paths are changes (watch-judge.ts); `pending` holds what fired since. */
488
+ const changes = new ChangeJudge({
489
+ workspaceRoot,
490
+ armedAt,
491
+ held: () => held !== undefined,
492
+ uncached: () => uncached,
493
+ })
638
494
  /** Per-project arms by directory, so `rearm` can add and drop them. */
639
495
  const perProject = new Map<string, WatchHandle>()
640
496
  /** The root arm: recursive when workspace-wide, else the root's own files only. */
@@ -940,7 +796,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
940
796
  *
941
797
  * The inner loop re-runs while anything is pending, so this covers only the
942
798
  * narrow gap between its LAST judgement and the loop going idle: an event
943
- * landing there would otherwise sit in `pendingPaths` with no timer armed
799
+ * landing there would otherwise sit in `changes.pending` with no timer armed
944
800
  * and no cycle to notice it — watch quietly idle over an edit the user
945
801
  * made. Insertion order, because the map is "first label wins" and the
946
802
  * label is what the cycle announces.
@@ -10,10 +10,7 @@ export type DependencySpec =
10
10
  | { kind: 'wildcardDeps'; negated: boolean }
11
11
 
12
12
  export class DependencySpecError extends Error {
13
- constructor(
14
- public readonly raw: string,
15
- message: string,
16
- ) {
13
+ constructor(raw: string, message: string) {
17
14
  super(`Invalid dependency spec "${raw}": ${message}`)
18
15
  }
19
16
  }
@@ -67,20 +67,28 @@ export function delegatedScript(command: string): string | null {
67
67
  }
68
68
 
69
69
  /** What `<manager> <name>`, with no `run`, runs as the manager's own command. */
70
+ // Listing a name that runs the script only keeps the command verbatim, so
71
+ // each set leans wide; missing one of the manager's own made a group over
72
+ // a script the manager never runs (D-32: `pnpm docs` opened npm's docs page,
73
+ // `bun deploy` answered "reserved", `yarn check` verified dependencies).
70
74
  const OWN_COMMANDS: Readonly<Record<string, ReadonlySet<string>>> = {
71
- // `bun --help`, Bun 1.4.2, with the short aliases.
75
+ // `bun --help`, Bun 1.4.2, the short aliases, and the names it reserves
76
+ // or runs itself (probed: `bun lint` runs the script).
72
77
  bun: new Set(
73
- 'run test x repl exec install i add a remove rm update audit outdated link unlink publish patch pm info why build init create c upgrade feedback lint'.split(
78
+ 'run test x repl exec install i add a remove rm update audit dedupe prune outdated link unlink publish patch pm info why build init create c upgrade feedback list deploy config login logout whoami help'.split(
74
79
  ' ',
75
80
  ),
76
81
  ),
82
+ // pnpm 10.33.0's `commandNames`, and the npm commands it passes through
83
+ // to npm whatever the scripts say.
77
84
  pnpm: new Set(
78
- 'add install i update up remove rm link unlink import rebuild prune fetch patch audit list ls outdated why exec dlx create init publish pack store env setup config root bin licenses deploy doctor'.split(
85
+ 'add approve-builds audit bin c cache cat-file cat-index ci clean-install completion config create dedupe deploy dislink dlx doctor env exec fetch find-hash get i ic ignored-builds import init install install-clean install-test it la licenses link list ll ln ls m multi outdated pack patch patch-commit patch-remove prune publish rb rebuild recursive remove restart rm root run run-script self-update server set setup store un uni uninstall unlink up update upgrade why access adduser bugs deprecate dist-tag docs edit find home info issues login logout owner ping prefix profile pkg repo s se search set-script show star stars team token unpublish unstar v version view whoami xmas help'.split(
79
86
  ' ',
80
87
  ),
81
88
  ),
89
+ // `yarn help`, Yarn 1.22.22 and Yarn 4.5.0: one set for both.
82
90
  yarn: new Set(
83
- 'add install remove upgrade up info init link unlink pack publish why workspace workspaces config cache dlx exec bin node plugin set version constraints npm patch explain dedupe'.split(
91
+ 'access add audit autoclean bin cache check config constraints create dedupe dlx exec explain files generate-lock-entry global help import info init install licenses link list login logout node npm outdated owner pack patch patch-commit plugin policies publish rebuild remove run search set stage tag team unlink unplug up upgrade upgrade-interactive version versions why workspace workspaces'.split(
84
92
  ' ',
85
93
  ),
86
94
  ),