@vzn/vx 0.0.450 → 0.0.452

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.450",
3
+ "version": "0.0.452",
4
4
  "description": "The fastest task runner and build cache for JavaScript monorepos.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,10 +21,10 @@
21
21
  "node": ">=18"
22
22
  },
23
23
  "optionalDependencies": {
24
- "@vzn/vx-linux-x64": "0.0.450",
25
- "@vzn/vx-linux-arm64": "0.0.450",
26
- "@vzn/vx-darwin-x64": "0.0.450",
27
- "@vzn/vx-darwin-arm64": "0.0.450"
24
+ "@vzn/vx-linux-x64": "0.0.452",
25
+ "@vzn/vx-linux-arm64": "0.0.452",
26
+ "@vzn/vx-darwin-x64": "0.0.452",
27
+ "@vzn/vx-darwin-arm64": "0.0.452"
28
28
  },
29
29
  "dependencies": {
30
30
  "@anthropic-ai/sandbox-runtime": "^0.0.76"
package/src/cli/watch.ts CHANGED
@@ -530,7 +530,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
530
530
  if (!packageDirs.has(dir)) want.add(dir)
531
531
  }
532
532
  for (const [dir, handle] of pending) {
533
- if (want.has(dir)) continue
533
+ if (want.has(dir) && !stale(dir)) continue
534
534
  handle.close()
535
535
  pending.delete(dir)
536
536
  }
@@ -548,6 +548,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
548
548
  if (filename === 'package.json') arrived()
549
549
  }),
550
550
  )
551
+ armedAs.set(dir, inodeOf(dir) ?? '')
551
552
  } catch (err) {
552
553
  sayCannot(`cannot watch ${dir}`, err)
553
554
  continue
@@ -578,12 +579,14 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
578
579
  }
579
580
  importNames = want
580
581
  for (const [dir, handle] of importArms) {
581
- if (want.has(dir)) continue
582
+ if (want.has(dir) && !stale(dir)) continue
582
583
  handle.close()
583
584
  importArms.delete(dir)
584
585
  }
585
586
  for (const dir of want.keys()) {
586
587
  if (importArms.has(dir)) continue
588
+ // Gone for now: its ancestor's arm (`armAncestors`) hears it return.
589
+ if (inodeOf(dir) === undefined) continue
587
590
  try {
588
591
  importArms.set(
589
592
  dir,
@@ -605,10 +608,12 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
605
608
  )
606
609
  }),
607
610
  )
611
+ armedAs.set(dir, inodeOf(dir) ?? '')
608
612
  } catch (err) {
609
613
  sayCannot(`cannot watch ${dir}`, err)
610
614
  }
611
615
  }
616
+ armAncestors()
612
617
  }
613
618
 
614
619
  const watchingLine = (count: number): string =>
@@ -676,34 +681,35 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
676
681
  // A base removed and made again is no event to its own watch (it holds
677
682
  // the deleted directory) nor to the root arm, which drops every name but
678
683
  // its own files: `watching 0 project(s)` and silence until a restart. So
679
- // each base is watched from its nearest directory that exists, for the
680
- // next name on the way down: its parent while it is there, an ancestor
681
- // when the parent went too (`apps/` removed under `apps/web/*`).
682
- const baseParents = new Map<string, WatchHandle>()
683
- let baseNames = new Map<string, Set<string>>()
684
- const armBaseParents = (): void => {
685
- baseNames = new Map()
686
- for (const base of memberBases) {
684
+ // each base, and each directory a config imports from, is watched from
685
+ // its nearest directory that exists, for the next name on the way down:
686
+ // its parent while it is there, an ancestor when the parent went too
687
+ // (`apps/` removed under `apps/web/*`).
688
+ const ancestorArms = new Map<string, WatchHandle>()
689
+ let ancestorNames = new Map<string, Set<string>>()
690
+ const armAncestors = (): void => {
691
+ ancestorNames = new Map()
692
+ for (const base of [...memberBases, ...importNames.keys()]) {
687
693
  let dir = path.dirname(base)
688
694
  let name = path.basename(base)
689
695
  while (inodeOf(dir) === undefined && dir !== workspaceRoot && dir !== path.dirname(dir)) {
690
696
  name = path.basename(dir)
691
697
  dir = path.dirname(dir)
692
698
  }
693
- baseNames.set(dir, (baseNames.get(dir) ?? new Set()).add(name))
699
+ ancestorNames.set(dir, (ancestorNames.get(dir) ?? new Set()).add(name))
694
700
  }
695
- for (const [dir, handle] of baseParents) {
696
- if (baseNames.has(dir) && !stale(dir)) continue
701
+ for (const [dir, handle] of ancestorArms) {
702
+ if (ancestorNames.has(dir) && !stale(dir)) continue
697
703
  handle.close()
698
- baseParents.delete(dir)
704
+ ancestorArms.delete(dir)
699
705
  }
700
- for (const dir of baseNames.keys()) {
701
- if (baseParents.has(dir)) continue
706
+ for (const dir of ancestorNames.keys()) {
707
+ if (ancestorArms.has(dir)) continue
702
708
  try {
703
- baseParents.set(
709
+ ancestorArms.set(
704
710
  dir,
705
711
  arm(dir, false, (filename) => {
706
- if (!baseNames.get(dir)?.has(filename)) return
712
+ if (!ancestorNames.get(dir)?.has(filename)) return
707
713
  reread = true
708
714
  const abs = path.join(dir, filename)
709
715
  trigger(path.relative(workspaceRoot, abs), abs)
@@ -716,7 +722,7 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
716
722
  }
717
723
  }
718
724
  const armBases = (): void => {
719
- armBaseParents()
725
+ armAncestors()
720
726
  const want = new Set(memberBases)
721
727
  for (const [base, handle] of baseArms) {
722
728
  if (want.has(base) && !stale(base)) continue
@@ -740,7 +746,12 @@ async function runWatchLoop(args: WatchLoopArgs): Promise<number> {
740
746
  // and cost an uncached task one execution per cycle (CI,
741
747
  // 2026-09-10).
742
748
  const now = memberEntries(base)
743
- if (sameMembers(members, now)) return
749
+ // The same names, but a directory still without a package made
750
+ // again (or replaced by a rename): its pending watch holds the
751
+ // deleted one, and the `package.json` that lands next is heard
752
+ // only by a new arm.
753
+ const entry = path.join(base, filename)
754
+ if (sameMembers(members, now) && !(pending.has(entry) && stale(entry))) return
744
755
  members = now
745
756
  reread = true
746
757
  trigger(`${path.relative(workspaceRoot, base)}/${filename}`, path.join(base, filename))
package/src/cli/why.ts CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  whyDidThisRerunQuery as whyDidThisRerun,
17
17
  resolveRunId,
18
18
  } from '../orchestrator/index.js'
19
- import { nearMatches, printable, UserError } from '../util/index.js'
19
+ import { MASKED, nearMatches, printable, UserError } from '../util/index.js'
20
20
  import { findWorkspaceRoot } from '../workspace/index.js'
21
21
  import { cliCacheDir, parseCacheDirFlag, warnToStderr } from './workspace-config.js'
22
22
 
@@ -245,14 +245,17 @@ export async function whyCmd(args: readonly string[]): Promise<number> {
245
245
  )
246
246
  const kindW = Math.max(...diff.entries.map((e) => e.kind.length), 4)
247
247
  for (const e of diff.entries) {
248
+ // A secret-named env input shows its name and its change, no hash.
248
249
  const beforeAfter =
249
- e.change === 'added'
250
- ? `+ ${e.after}`
251
- : e.change === 'removed'
252
- ? `- ${e.before}`
253
- : `${e.before} → ${e.after}`
250
+ e.before === MASKED || e.after === MASKED
251
+ ? ''
252
+ : e.change === 'added'
253
+ ? `+ ${e.after}`
254
+ : e.change === 'removed'
255
+ ? `- ${e.before}`
256
+ : `${e.before} → ${e.after}`
254
257
  lines.push(
255
- ` ${e.change.padEnd(7)} ${e.kind.padEnd(kindW)} ${printable(e.name)} ${beforeAfter}`,
258
+ ` ${e.change.padEnd(7)} ${e.kind.padEnd(kindW)} ${printable(e.name)} ${beforeAfter}`.trimEnd(),
256
259
  )
257
260
  }
258
261
  const kinds = [...new Set(diff.entries.map((e) => e.kind))].filter((k) => k in WHAT_TO_DO)
@@ -14,7 +14,7 @@ import type { Database } from 'bun:sqlite'
14
14
  // rule written once cannot drift.
15
15
  import { KEYED_RUNS_SQL } from '../cache/index.js'
16
16
  import { splitTaskId } from '../graph/index.js'
17
- import { clampInt } from '../util/index.js'
17
+ import { clampInt, MASKED, secretNamed } from '../util/index.js'
18
18
  import type { TaskInputComponent } from './task-hash.js'
19
19
  import { isPassStatus } from './telemetry.js'
20
20
 
@@ -557,6 +557,11 @@ export function diffKeyComponents(
557
557
  const old = new Map(before.map((c) => [id(c), c]))
558
558
  const entries: InputDiffEntry[] = []
559
559
  let unchangedCount = 0
560
+ // A secret-named env input's hash is its value, unsalted: a short one is
561
+ // recoverable from a log that printed `vx why` (M-62). The diff says it
562
+ // changed and never shows the hash.
563
+ const shown = (c: TaskInputComponent): string =>
564
+ c.kind === 'env' && secretNamed(c.name) ? MASKED : c.hash
560
565
  const keys = new Set<string>([...cur.keys(), ...old.keys()])
561
566
  for (const key of keys) {
562
567
  const a = cur.get(key)
@@ -568,13 +573,13 @@ export function diffKeyComponents(
568
573
  kind: a.kind,
569
574
  name: a.name,
570
575
  change: 'changed',
571
- before: b.hash,
572
- after: a.hash,
576
+ before: shown(b),
577
+ after: shown(a),
573
578
  })
574
579
  } else if (a) {
575
- entries.push({ kind: a.kind, name: a.name, change: 'added', before: null, after: a.hash })
580
+ entries.push({ kind: a.kind, name: a.name, change: 'added', before: null, after: shown(a) })
576
581
  } else if (b) {
577
- entries.push({ kind: b.kind, name: b.name, change: 'removed', before: b.hash, after: null })
582
+ entries.push({ kind: b.kind, name: b.name, change: 'removed', before: shown(b), after: null })
578
583
  }
579
584
  }
580
585
  // Code-unit order, not `localeCompare`: collation follows the machine's
package/src/util/index.ts CHANGED
@@ -61,5 +61,6 @@ export {
61
61
  maskedLine,
62
62
  MASKED,
63
63
  secretMask,
64
+ secretNamed,
64
65
  type SecretMask,
65
66
  } from './secret-mask.js'
@@ -23,7 +23,7 @@ const NOT_SECRET = /_(FILE|PATH|DIR)$|^GIT_CONFIG_KEY_\d+$/i
23
23
  */
24
24
  const secretNames = new Map<string, boolean>()
25
25
 
26
- function secretNamed(name: string): boolean {
26
+ export function secretNamed(name: string): boolean {
27
27
  let secret = secretNames.get(name)
28
28
  if (secret === undefined) {
29
29
  secret = SECRET_NAME.test(name) && !NOT_SECRET.test(name)