disk-space-saviour 0.15.2 → 0.17.0

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/src/env/local.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { checkSignal } from './scope.js';
2
+ import { retryProcessOwners, inspectionHint } from './inspection.js';
1
3
  /**
2
4
  * Base names of a process's executable and `argv[0]`, without duplicates
3
5
  * and without the ` (deleted)` suffix of replaced binaries.
@@ -58,9 +60,13 @@ import os from 'node:os';
58
60
  import path from 'node:path';
59
61
  import { URL, fileURLToPath } from 'node:url';
60
62
  import { procIdentity, procOpenPaths } from './proc.js';
63
+ import { CACHE_VARIABLES } from './cache-roots.js';
61
64
 
62
65
  import { hostExecutor } from '../exec.js';
63
66
  import { matchesGlob } from '../paths.js';
67
+ import { mapLimit } from './parallel.js';
68
+ import { directoryFingerprint, SIZE_MAX_AGE } from '../scan-state.js';
69
+ import { indexedDirectories } from './platform-index.js';
64
70
 
65
71
  // Directories read concurrently while measuring usage.
66
72
  const WALK_BATCH = 64;
@@ -189,11 +195,11 @@ export class LocalEnv {
189
195
  this.protectedPaths = [fileURLToPath(new URL('../../', import.meta.url))];
190
196
  this.currentHome = os.homedir();
191
197
  this.skipNames = new Set(options.skipNames ?? DEFAULT_SKIP_NAMES);
192
- this.vars = options.vars ?? {
193
- LOCALAPPDATA: process.env.LOCALAPPDATA,
194
- APPDATA: process.env.APPDATA,
195
- TMPDIR: process.env.TMPDIR,
196
- };
198
+ this.vars =
199
+ options.vars ??
200
+ Object.fromEntries(
201
+ CACHE_VARIABLES.map((name) => [name, process.env[name]])
202
+ );
197
203
  }
198
204
 
199
205
  recordAccessError(target, error) {
@@ -249,23 +255,29 @@ export class LocalEnv {
249
255
  size: stats.size,
250
256
  bytes: diskBytes(stats),
251
257
  mtimeMs: stats.mtimeMs,
258
+ ctimeMs: stats.ctimeMs,
252
259
  inode: `${stats.dev}:${stats.ino}`,
253
260
  uid: stats.uid,
261
+ gid: stats.gid,
262
+ mode: stats.mode,
254
263
  device: stats.dev,
255
264
  };
256
265
  }
257
266
 
258
267
  async readText(target, maxBytes = 1024 * 1024) {
268
+ checkSignal(this.signal);
259
269
  try {
260
270
  const handle = await fsp.open(target, 'r');
261
271
  try {
262
272
  const buffer = Buffer.alloc(maxBytes);
263
273
  const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
274
+ checkSignal(this.signal);
264
275
  return buffer.subarray(0, bytesRead).toString('utf8');
265
276
  } finally {
266
277
  await handle.close();
267
278
  }
268
279
  } catch {
280
+ checkSignal(this.signal);
269
281
  return null;
270
282
  }
271
283
  }
@@ -287,31 +299,36 @@ export class LocalEnv {
287
299
  return heads;
288
300
  }
289
301
 
290
- async list(dir) {
302
+ async list(dir, { discovery = false } = {}) {
291
303
  let names;
292
304
  try {
293
- names = await (this.options.fileFs ?? fsp).readdir(dir);
305
+ names = await (this.options.fileFs ?? fsp).readdir(dir, {
306
+ withFileTypes: true,
307
+ });
294
308
  } catch (error) {
295
309
  this.recordAccessError(dir, error);
296
310
  return [];
297
311
  }
298
- const entries = await Promise.all(
299
- names.map(async (name) => {
300
- const full = path.join(dir, name);
301
- const stats = await this.#lstat(full);
302
- if (!stats) {
303
- return null;
304
- }
305
- return {
306
- name,
307
- path: full,
308
- type: entryType(stats),
309
- size: stats.size,
310
- bytes: diskBytes(stats),
311
- mtimeMs: stats.mtimeMs,
312
- };
313
- })
314
- );
312
+ const entries = await mapLimit(names, 32, async (entry) => {
313
+ const name = typeof entry === 'string' ? entry : entry.name;
314
+ const full = path.join(dir, name);
315
+ if (discovery && typeof entry !== 'string' && !entry.isDirectory()) {
316
+ return { name, path: full, type: entryType(entry), mtimeMs: 0 };
317
+ }
318
+ const stats = await this.#lstat(full);
319
+ if (!stats) {
320
+ return null;
321
+ }
322
+ return {
323
+ name,
324
+ path: full,
325
+ type: entryType(stats),
326
+ size: stats.size,
327
+ bytes: diskBytes(stats),
328
+ mtimeMs: stats.mtimeMs,
329
+ device: stats.dev,
330
+ };
331
+ });
315
332
  return entries.filter(Boolean);
316
333
  }
317
334
 
@@ -320,14 +337,15 @@ export class LocalEnv {
320
337
  * counted a single time, symlinks not followed. Also reports the newest
321
338
  * file mtime.
322
339
  */
323
- async usage(target) {
324
- return (await this.usageMany([target])).get(target) ?? null;
340
+ async usage(target, options) {
341
+ return (await this.usageMany([target], options)).get(target) ?? null;
325
342
  }
326
343
 
327
344
  async statMany(targets) {
328
- const entries = await Promise.all(
329
- targets.map(async (target) => [target, await this.stat(target)])
330
- );
345
+ const entries = await mapLimit(targets, 32, async (target) => [
346
+ target,
347
+ await this.stat(target),
348
+ ]);
331
349
  return new Map(entries.filter(([, stat]) => stat));
332
350
  }
333
351
 
@@ -354,6 +372,9 @@ export class LocalEnv {
354
372
  apparentBytes += stats.size;
355
373
  if (!stats.isDirectory()) {
356
374
  files++;
375
+ if (this.scanMetrics) {
376
+ this.scanMetrics.filesSized++;
377
+ }
357
378
  newestMtimeMs = Math.max(newestMtimeMs, stats.mtimeMs);
358
379
  }
359
380
  };
@@ -367,12 +388,10 @@ export class LocalEnv {
367
388
  errors++;
368
389
  return [];
369
390
  }
370
- const children = await Promise.all(
371
- names.map(async (name) => {
372
- const full = path.join(dir, name);
373
- return [full, await this.#lstat(full)];
374
- })
375
- );
391
+ const children = await mapLimit(names, 32, async (name) => {
392
+ const full = path.join(dir, name);
393
+ return [full, await this.#lstat(full)];
394
+ });
376
395
  const subdirs = [];
377
396
  for (const [full, stats] of children) {
378
397
  if (!stats) {
@@ -394,6 +413,7 @@ export class LocalEnv {
394
413
  };
395
414
  let frontier = root.isDirectory() ? [target] : [];
396
415
  while (frontier.length > 0) {
416
+ checkSignal(this.signal);
397
417
  const next = [];
398
418
  for (let index = 0; index < frontier.length; index += WALK_BATCH) {
399
419
  const found = await Promise.all(
@@ -444,6 +464,10 @@ export class LocalEnv {
444
464
  return results;
445
465
  }
446
466
 
467
+ indexedDirs(roots, globs, maxDepth) {
468
+ return indexedDirectories(this, roots, globs, maxDepth);
469
+ }
470
+
447
471
  /**
448
472
  * Finds regular files whose basename matches one of `names`.
449
473
  * @returns {Promise<string[]>}
@@ -459,7 +483,14 @@ export class LocalEnv {
459
483
  results.push(entry.path);
460
484
  }
461
485
  }
462
- return null;
486
+ return new Set([
487
+ 'node_modules',
488
+ 'target',
489
+ 'vendor',
490
+ '.venv',
491
+ 'venv',
492
+ ...this.skipNames,
493
+ ]);
463
494
  });
464
495
  return results;
465
496
  }
@@ -471,54 +502,214 @@ export class LocalEnv {
471
502
  async #walk(roots, maxDepth, skipNames, visit) {
472
503
  const skip = skipNames ? new Set(skipNames) : this.skipNames;
473
504
  const visited = new Set();
474
- const queue = await Promise.all(
505
+ let queue = await Promise.all(
475
506
  roots.map(async (root) => [root, 0, await this.deviceId(root)])
476
507
  );
477
508
  while (queue.length > 0) {
478
- const [dir, depth, device] = queue.shift();
479
- if (visited.has(dir)) {
480
- continue;
481
- }
482
- visited.add(dir);
483
- const entries = [];
484
- for (const entry of await this.list(dir)) {
485
- if (
486
- entry.type !== 'dir' ||
487
- (await this.deviceId(entry.path)) === device
488
- ) {
489
- entries.push(entry);
509
+ checkSignal(this.signal);
510
+ const current = queue;
511
+ queue = [];
512
+ const children = await mapLimit(
513
+ current,
514
+ 8,
515
+ async ([dir, depth, device]) => {
516
+ if (visited.has(dir)) {
517
+ return [];
518
+ }
519
+ visited.add(dir);
520
+ const entries = [];
521
+ for (const entry of await this.discoveryList(dir)) {
522
+ if (
523
+ entry.type !== 'dir' ||
524
+ (entry.device ?? (await this.deviceId(entry.path))) === device
525
+ ) {
526
+ entries.push(entry);
527
+ }
528
+ }
529
+ const pruned = visit(dir, entries) ?? new Set();
530
+ if (depth + 1 >= maxDepth) {
531
+ return [];
532
+ }
533
+ const next = [];
534
+ for (const entry of entries) {
535
+ if (
536
+ entry.type === 'dir' &&
537
+ !pruned.has(entry.name) &&
538
+ !this.skipDiscoveryDirectory(dir, entry.name, skip) &&
539
+ (entry.device ?? (await this.deviceId(entry.path))) === device
540
+ ) {
541
+ next.push([entry.path, depth + 1, device]);
542
+ }
543
+ }
544
+ return next;
490
545
  }
546
+ );
547
+ queue = children.flat();
548
+ }
549
+ }
550
+
551
+ skipDiscoveryDirectory(dir, name, skip = this.skipNames) {
552
+ if (skip.has(name) || /\.photoslibrary$/i.test(name)) {
553
+ return true;
554
+ }
555
+ const homes = this.options.homes ?? [this.currentHome];
556
+ return (
557
+ (['Music', 'Pictures', 'Movies'].includes(name) && homes.includes(dir)) ||
558
+ (name === 'Volumes' && dir === this.path.parse(dir).root)
559
+ );
560
+ }
561
+
562
+ async discoveryStat(dir) {
563
+ let stats = await this.stat(dir);
564
+ // Explicit roots may be aliases (for example /tmp on macOS). Child links
565
+ // are never queued by the walker, so only an explicitly supplied root follows.
566
+ if (stats?.type === 'symlink') {
567
+ const resolved = await this.realPath(dir);
568
+ stats = resolved ? await this.stat(resolved) : null;
569
+ }
570
+ return stats;
571
+ }
572
+
573
+ async discoveryList(dir) {
574
+ checkSignal(this.signal);
575
+ const stats = await this.discoveryStat(dir);
576
+ if (!stats || stats.type !== 'dir') {
577
+ return [];
578
+ }
579
+ const key = `list:${dir}`;
580
+ const fingerprint = `${directoryFingerprint(stats)}:${JSON.stringify([this.scanPolicy?.exclude, this.scanPolicy?.pruned])}`;
581
+ let entries = this.scanIndex?.get(key, fingerprint);
582
+ if (entries) {
583
+ this.scanMetrics.indexHits++;
584
+ } else {
585
+ const errors = this.accessErrors?.size ?? 0;
586
+ entries = await this.list(dir, { discovery: true });
587
+ if ((this.accessErrors?.size ?? 0) === errors) {
588
+ this.scanIndex?.set(key, fingerprint, entries);
491
589
  }
492
- const pruned = visit(dir, entries) ?? new Set();
493
- if (depth + 1 >= maxDepth) {
494
- continue;
495
- }
496
- for (const entry of entries) {
497
- if (
498
- entry.type === 'dir' &&
499
- !pruned.has(entry.name) &&
500
- !skip.has(entry.name) &&
501
- (await this.deviceId(entry.path)) === device
502
- ) {
503
- queue.push([entry.path, depth + 1, device]);
590
+ }
591
+ // Stored metadata is a discovery hint; never accept external paths from it.
592
+ const listing = entries
593
+ .filter(
594
+ (entry) => entry.name && this.path.basename(entry.name) === entry.name
595
+ )
596
+ .map((entry) => ({ ...entry, path: this.path.join(dir, entry.name) }))
597
+ .filter(
598
+ (entry) => !this.scanPolicy || this.scanPolicy.allows(entry.path)
599
+ );
600
+ // Mounts and link replacements need not change the parent's mtime.
601
+ return (
602
+ await mapLimit(listing, 32, async (entry) => {
603
+ if (entry.type !== 'dir') {
604
+ return entry;
504
605
  }
505
- }
606
+ const fresh = await this.stat(entry.path);
607
+ return fresh ? { ...entry, ...fresh } : null;
608
+ })
609
+ ).filter(Boolean);
610
+ }
611
+
612
+ /** Source activity excludes generated trees, and stops as soon as recent work is seen. */
613
+ async projectActivity(project, globs, since) {
614
+ let newest = 0;
615
+ let frontier = [project];
616
+ const device = await this.deviceId(project);
617
+ while (frontier.length && newest < since) {
618
+ const children = await mapLimit(frontier, 8, async (dir) => {
619
+ checkSignal(this.signal);
620
+ const next = [];
621
+ for (const entry of await this.list(dir)) {
622
+ if (entry.type === 'dir') {
623
+ if (
624
+ !this.skipDiscoveryDirectory(dir, entry.name) &&
625
+ !globs.some((glob) => matchesGlob(entry.name, glob)) &&
626
+ entry.device === device
627
+ ) {
628
+ next.push(entry.path);
629
+ }
630
+ } else if (entry.type === 'file') {
631
+ newest = Math.max(newest, entry.mtimeMs);
632
+ }
633
+ }
634
+ return next;
635
+ });
636
+ frontier = children.flat();
506
637
  }
638
+ return newest;
507
639
  }
508
640
 
509
- async usageMany(targets) {
510
- const usages = await this.rawUsageMany(targets);
641
+ async usageMany(targets, options) {
642
+ const usages = await this.rawUsageMany(targets, options);
511
643
  return this.writableLayer
512
644
  ? this.writableLayer.measure(this, usages)
513
645
  : usages;
514
646
  }
515
647
 
516
- async rawUsageMany(targets) {
517
- const usages = new Map();
518
- for (const target of targets) {
519
- usages.set(target, await this.rawUsage(target));
648
+ async rawUsageMany(targets, { mtime = true, cache = false } = {}) {
649
+ const entries = await mapLimit([...new Set(targets)], 4, async (target) => [
650
+ target,
651
+ !this.writableLayer && cache
652
+ ? await this.cachedUsage(target, mtime)
653
+ : !mtime && !this.writableLayer
654
+ ? await this.quickUsage(target)
655
+ : await this.rawUsage(target),
656
+ ]);
657
+ return new Map(entries);
658
+ }
659
+
660
+ async cachedUsage(target, mtime) {
661
+ const stats = await this.stat(target);
662
+ if (!stats) {
663
+ return null;
664
+ }
665
+ const key = `usage:${mtime}:${target}`;
666
+ const fingerprint = `${directoryFingerprint(stats)}:${JSON.stringify([this.scanPolicy?.exclude, this.scanPolicy?.pruned])}`;
667
+ const cached = this.scanIndex?.get(key, fingerprint, SIZE_MAX_AGE);
668
+ if (cached) {
669
+ this.scanMetrics.sizeCacheHits++;
670
+ return { ...cached, estimated: true };
520
671
  }
521
- return usages;
672
+ const usage = mtime
673
+ ? await this.rawUsage(target)
674
+ : await this.quickUsage(target);
675
+ if (usage && !usage.errors) {
676
+ this.scanIndex?.set(key, fingerprint, usage);
677
+ }
678
+ return usage;
679
+ }
680
+
681
+ async quickUsage(target) {
682
+ const stats = await this.stat(target);
683
+ if (!stats) {
684
+ return null;
685
+ }
686
+ // du cannot enforce path-glob exclusions or detect a nested mount beneath a candidate.
687
+ if (
688
+ this.platform === 'win32' ||
689
+ this.scanPolicy?.exclude.length ||
690
+ this.scanPolicy?.mounts.some(
691
+ (mount) =>
692
+ mount.path !== '/' && mount.path.startsWith(`${target}${path.sep}`)
693
+ )
694
+ ) {
695
+ return this.rawUsage(target);
696
+ }
697
+ const result = await this.run(['du', '-skx', target]);
698
+ const match = /^\s*(\d+)\s/.exec(result.stdout);
699
+ if (result.code !== 0 || !match) {
700
+ return this.rawUsage(target);
701
+ }
702
+ const usage = {
703
+ bytes: Number(match[1]) * 1024,
704
+ apparentBytes: Number(match[1]) * 1024,
705
+ newestMtimeMs: 0,
706
+ files: 0,
707
+ errors: 0,
708
+ };
709
+ if (this.scanMetrics) {
710
+ this.scanMetrics.directoriesSized++;
711
+ }
712
+ return usage;
522
713
  }
523
714
 
524
715
  async readLink(target) {
@@ -613,11 +804,15 @@ export class LocalEnv {
613
804
 
614
805
  async #linuxProcesses() {
615
806
  const io = this.options.procFs ?? fsp;
807
+ this.mountNamespace = await io
808
+ .readlink('/proc/self/ns/mnt')
809
+ .catch(() => null);
616
810
  const pids = (await io.readdir('/proc')).filter((name) =>
617
811
  /^\d+$/.test(name)
618
812
  );
619
813
  const processes = [];
620
814
  for (const pid of pids) {
815
+ checkSignal(this.signal);
621
816
  const base = `/proc/${pid}`;
622
817
  const stat = parseProcStat(
623
818
  await io.readFile(`${base}/stat`, 'utf8').catch(() => '')
@@ -641,6 +836,7 @@ export class LocalEnv {
641
836
  ...(await procIdentity(base, io)),
642
837
  name,
643
838
  startTime: stat.startTime,
839
+ mountNamespace: await io.readlink(`${base}/ns/mnt`).catch(() => null),
644
840
  cwd,
645
841
  aliases: processAliases(exe, cmdline.split('\0')[0]),
646
842
  command: cmdline.replaceAll('\0', ' '),
@@ -658,29 +854,8 @@ export class LocalEnv {
658
854
  async openPaths({ strict = true } = {}) {
659
855
  try {
660
856
  let paths = await this.rawOpenPaths({ strict });
661
- if (strict && this.unreadableProcesses?.length && this.processInspector) {
662
- const privileged = await this.processInspector.openPaths();
663
- if (privileged) {
664
- this.unreadableProcesses =
665
- this.processInspector.unreadableProcesses ?? [];
666
- paths = privileged;
667
- }
668
- }
669
- if (strict && this.unreadableProcesses?.length) {
670
- const { OPEN_PATHS_SCRIPT } = await import('./shell.js');
671
- const elevated = await this.run([
672
- 'sudo',
673
- '-n',
674
- 'sh',
675
- '-c',
676
- OPEN_PATHS_SCRIPT,
677
- ]);
678
- if (elevated.code === 0) {
679
- paths = new Set(
680
- elevated.stdout.split('\n').filter((line) => line.startsWith('/'))
681
- );
682
- this.unreadableProcesses = [];
683
- }
857
+ if (strict) {
858
+ paths = await this.#completeInspection(paths);
684
859
  }
685
860
  this.#processHint();
686
861
  return paths;
@@ -688,6 +863,9 @@ export class LocalEnv {
688
863
  if (this.processInspector) {
689
864
  const paths = await this.processInspector.openPaths();
690
865
  this.probeDiagnostic = this.processInspector.probeDiagnostic;
866
+ this.unreadableProcesses =
867
+ this.processInspector.unreadableProcesses ?? [];
868
+ this.#processHint();
691
869
  return paths;
692
870
  }
693
871
  this.probeDiagnostic = error.message;
@@ -695,16 +873,56 @@ export class LocalEnv {
695
873
  }
696
874
  }
697
875
 
876
+ async #completeInspection(paths) {
877
+ paths = await this.#inspectOwners(paths);
878
+ if (this.unreadableProcesses?.length && this.processInspector) {
879
+ const privileged = await this.processInspector.openPaths();
880
+ if (privileged) {
881
+ this.unreadableProcesses =
882
+ this.processInspector.unreadableProcesses ?? [];
883
+ paths = new Set([...(paths ?? []), ...privileged]);
884
+ }
885
+ }
886
+ if (this.unreadableProcesses?.length) {
887
+ const { OPEN_PATHS_SCRIPT } = await import('./shell.js');
888
+ const elevated = await this.run([
889
+ 'sudo',
890
+ '-n',
891
+ 'sh',
892
+ '-c',
893
+ OPEN_PATHS_SCRIPT,
894
+ ]);
895
+ if (elevated.code === 0) {
896
+ paths = new Set(
897
+ elevated.stdout.split('\n').filter((line) => line.startsWith('/'))
898
+ );
899
+ this.unreadableProcesses = [];
900
+ }
901
+ }
902
+ return paths;
903
+ }
904
+
698
905
  #processHint() {
699
- this.probeHint = this.unreadableProcesses?.length
700
- ? `${this.unreadableProcesses.length} processes are unreadable; uncertainty is scoped to their paths and UID. Re-run as root for complete inspection (docker exec -u 0 -e HOME=<user-home> <container> dss scan).`
701
- : null;
906
+ this.probeHint = inspectionHint(this.unreadableProcesses ?? []);
907
+ }
908
+
909
+ async #inspectOwners(paths) {
910
+ if (!this.unreadableProcesses?.length || !(await this.isRoot())) {
911
+ return paths;
912
+ }
913
+ const evidence = await retryProcessOwners(this.executor, {
914
+ paths,
915
+ unreadable: this.unreadableProcesses,
916
+ });
917
+ this.unreadableProcesses = evidence.unreadable;
918
+ return evidence.paths;
702
919
  }
703
920
 
704
921
  async rawOpenPaths({ strict = true } = {}) {
705
922
  if (this.platform === 'linux') {
706
923
  const { paths, unreadable } = await procOpenPaths(
707
- this.options.procFs ?? fsp
924
+ this.options.procFs ?? fsp,
925
+ this.signal
708
926
  );
709
927
  this.unreadableProcesses = strict ? unreadable : [];
710
928
  return paths;
@@ -0,0 +1,23 @@
1
+ /** Finite worker pool; settle all in-flight work before propagating failure. */
2
+ export async function mapLimit(values, limit, operation) {
3
+ const results = new Array(values.length);
4
+ let cursor = 0;
5
+ let failure;
6
+ const worker = async () => {
7
+ while (!failure && cursor < values.length) {
8
+ const index = cursor++;
9
+ try {
10
+ results[index] = await operation(values[index], index);
11
+ } catch (error) {
12
+ failure ??= error;
13
+ }
14
+ }
15
+ };
16
+ await Promise.all(
17
+ Array.from({ length: Math.min(limit, values.length) }, worker)
18
+ );
19
+ if (failure) {
20
+ throw failure;
21
+ }
22
+ return results;
23
+ }
@@ -0,0 +1,87 @@
1
+ /** OS index hits are hints; stale/missing indexes always fall back to discovery. */
2
+ import { isWithin, matchesGlob } from '../paths.js';
3
+ import { mapLimit } from './parallel.js';
4
+
5
+ export async function indexedDirectories(env, roots, globs, maxDepth) {
6
+ if (!env.scanIndex?.directory || env.platformIndex === false) {
7
+ return [];
8
+ }
9
+ let hits = [];
10
+ try {
11
+ if (env.platform === 'darwin' && (await env.which('mdfind'))) {
12
+ const query = globs
13
+ .map((glob) => `kMDItemFSName == "${glob.replaceAll('"', '\\"')}"`)
14
+ .join(' || ');
15
+ for (const root of roots) {
16
+ const result = await env.run(['mdfind', '-0', '-onlyin', root, query]);
17
+ if (result.code === 0) {
18
+ hits.push(...result.stdout.split('\0').filter(Boolean));
19
+ }
20
+ }
21
+ } else if (env.platform === 'linux') {
22
+ const command = (await env.which('plocate'))
23
+ ? 'plocate'
24
+ : (await env.which('locate'))
25
+ ? 'locate'
26
+ : null;
27
+ if (command) {
28
+ const result = await env.run([command, '-0', '-e', '-b', ...globs]);
29
+ if (result.code === 0) {
30
+ hits = result.stdout.split('\0').filter(Boolean);
31
+ }
32
+ }
33
+ }
34
+ } catch {
35
+ // Budget/cancellation will be observed by the fallback walk too.
36
+ }
37
+ hits = [...new Set(hits)]
38
+ .filter((target) => {
39
+ const root = roots.find((candidate) =>
40
+ isWithin(target, candidate, env.path)
41
+ );
42
+ if (
43
+ !root ||
44
+ !env.path.isAbsolute(target) ||
45
+ !globs.some((glob) => matchesGlob(env.path.basename(target), glob))
46
+ ) {
47
+ return false;
48
+ }
49
+ const parts = env.path.relative(root, target).split(/[\\/]/);
50
+ return (
51
+ parts.length <= maxDepth &&
52
+ !parts
53
+ .slice(0, -1)
54
+ .some(
55
+ (name, index) =>
56
+ env.skipDiscoveryDirectory(
57
+ env.path.join(root, ...parts.slice(0, index)),
58
+ name
59
+ ) || globs.some((glob) => matchesGlob(name, glob))
60
+ )
61
+ );
62
+ })
63
+ .slice(0, 20000);
64
+ const candidates = await mapLimit(hits, 8, async (target) => {
65
+ const stat = await env.stat(target);
66
+ const root = roots.find((candidate) =>
67
+ isWithin(target, candidate, env.path)
68
+ );
69
+ if (stat?.type !== 'dir' || stat.device !== (await env.deviceId(root))) {
70
+ return null;
71
+ }
72
+ let parent = env.path.dirname(target);
73
+ while (parent !== root && isWithin(parent, root, env.path)) {
74
+ const ancestor = await env.stat(parent);
75
+ if (ancestor?.type !== 'dir' || ancestor.device !== stat.device) {
76
+ return null;
77
+ }
78
+ parent = env.path.dirname(parent);
79
+ }
80
+ return {
81
+ path: target,
82
+ name: env.path.basename(target),
83
+ parent: env.path.dirname(target),
84
+ };
85
+ });
86
+ return candidates.filter(Boolean);
87
+ }