@vibgrate/cli 2026.6.5 → 2026.6.5154714

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.
@@ -0,0 +1,12 @@
1
+ import {
2
+ baselineCommand,
3
+ runBaseline
4
+ } from "./chunk-734OP6JR.js";
5
+ import "./chunk-K2D6JXLA.js";
6
+ import "./chunk-74ZJFYEM.js";
7
+ import "./chunk-HAT4W7NZ.js";
8
+ import "./chunk-JSBRDJBE.js";
9
+ export {
10
+ baselineCommand,
11
+ runBaseline
12
+ };
@@ -1,8 +1,11 @@
1
+ import {
2
+ runScan
3
+ } from "./chunk-K2D6JXLA.js";
4
+
1
5
  // src/commands/baseline.ts
2
6
  import * as path3 from "path";
3
7
  import { Command } from "commander";
4
8
  import chalk from "chalk";
5
- import { runScan } from "@vibgrate/core";
6
9
 
7
10
  // src/utils/fs.ts
8
11
  import { execFile } from "child_process";
@@ -0,0 +1,678 @@
1
+ // ../vibgrate-core/dist/chunk-JQHUH6A3.js
2
+ import { execFile } from "child_process";
3
+ import * as fs from "fs/promises";
4
+ import * as os from "os";
5
+ import * as path2 from "path";
6
+ import { promisify } from "util";
7
+ import * as path from "path";
8
+ var Semaphore = class {
9
+ available;
10
+ queue = [];
11
+ constructor(max) {
12
+ this.available = max;
13
+ }
14
+ async run(fn) {
15
+ await this.acquire();
16
+ try {
17
+ return await fn();
18
+ } finally {
19
+ this.release();
20
+ }
21
+ }
22
+ acquire() {
23
+ if (this.available > 0) {
24
+ this.available--;
25
+ return Promise.resolve();
26
+ }
27
+ return new Promise((resolve2) => this.queue.push(resolve2));
28
+ }
29
+ release() {
30
+ const next = this.queue.shift();
31
+ if (next) next();
32
+ else this.available++;
33
+ }
34
+ };
35
+ function compileGlobs(patterns) {
36
+ if (patterns.length === 0) return null;
37
+ const matchers = patterns.map((p) => compileOne(normalise(p)));
38
+ return (relPath) => {
39
+ const norm = normalise(relPath);
40
+ return matchers.some((m) => m(norm));
41
+ };
42
+ }
43
+ function normalise(p) {
44
+ return p.split(path.sep).join("/").replace(/\/+$/, "");
45
+ }
46
+ function compileOne(pattern) {
47
+ if (!pattern.includes("/") && !hasGlobChars(pattern)) {
48
+ const prefix = pattern + "/";
49
+ return (p) => p === pattern || p.startsWith(prefix);
50
+ }
51
+ const re = globToRegex(pattern);
52
+ return (p) => re.test(p);
53
+ }
54
+ function hasGlobChars(s) {
55
+ return /[*?[\]{}]/.test(s);
56
+ }
57
+ function globToRegex(pattern) {
58
+ let i = 0;
59
+ let re = "^";
60
+ const len = pattern.length;
61
+ while (i < len) {
62
+ const ch = pattern[i];
63
+ if (ch === "*") {
64
+ if (pattern[i + 1] === "*") {
65
+ i += 2;
66
+ if (pattern[i] === "/") {
67
+ i++;
68
+ re += "(?:.+/)?";
69
+ } else {
70
+ re += ".*";
71
+ }
72
+ } else {
73
+ i++;
74
+ re += "[^/]*";
75
+ }
76
+ } else if (ch === "?") {
77
+ i++;
78
+ re += "[^/]";
79
+ } else if (ch === "[") {
80
+ const start = i;
81
+ i++;
82
+ while (i < len && pattern[i] !== "]") i++;
83
+ i++;
84
+ re += pattern.slice(start, i);
85
+ } else if (ch === "{") {
86
+ i++;
87
+ const alternatives = [];
88
+ let current = "";
89
+ while (i < len && pattern[i] !== "}") {
90
+ if (pattern[i] === ",") {
91
+ alternatives.push(current);
92
+ current = "";
93
+ } else {
94
+ current += pattern[i];
95
+ }
96
+ i++;
97
+ }
98
+ alternatives.push(current);
99
+ i++;
100
+ re += "(?:" + alternatives.map(escapeRegex).join("|") + ")";
101
+ } else {
102
+ re += escapeRegex(ch);
103
+ i++;
104
+ }
105
+ }
106
+ re += "$";
107
+ return new RegExp(re);
108
+ }
109
+ function escapeRegex(s) {
110
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
111
+ }
112
+ var execFileAsync = promisify(execFile);
113
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
114
+ "node_modules",
115
+ ".git",
116
+ ".vibgrate",
117
+ ".wrangler",
118
+ ".next",
119
+ "dist",
120
+ "build",
121
+ "out",
122
+ ".turbo",
123
+ ".cache",
124
+ "coverage",
125
+ "bin",
126
+ "obj",
127
+ ".vs",
128
+ "TestResults"
129
+ ]);
130
+ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
131
+ // Fonts
132
+ ".woff",
133
+ ".woff2",
134
+ ".ttf",
135
+ ".otf",
136
+ ".eot",
137
+ // Images & vector
138
+ ".png",
139
+ ".jpg",
140
+ ".jpeg",
141
+ ".gif",
142
+ ".ico",
143
+ ".bmp",
144
+ ".tiff",
145
+ ".tif",
146
+ ".webp",
147
+ ".avif",
148
+ ".svg",
149
+ ".heic",
150
+ ".heif",
151
+ ".jfif",
152
+ ".psd",
153
+ ".ai",
154
+ ".eps",
155
+ ".raw",
156
+ ".cr2",
157
+ ".nef",
158
+ ".dng",
159
+ // Video
160
+ ".mp4",
161
+ ".webm",
162
+ ".avi",
163
+ ".mov",
164
+ ".mkv",
165
+ ".wmv",
166
+ ".flv",
167
+ ".m4v",
168
+ ".mpg",
169
+ ".mpeg",
170
+ ".3gp",
171
+ ".ogv",
172
+ // Audio
173
+ ".mp3",
174
+ ".wav",
175
+ ".ogg",
176
+ ".flac",
177
+ ".aac",
178
+ ".wma",
179
+ ".m4a",
180
+ ".opus",
181
+ ".aiff",
182
+ ".mid",
183
+ ".midi",
184
+ // Archives
185
+ ".zip",
186
+ ".tar",
187
+ ".gz",
188
+ ".bz2",
189
+ ".7z",
190
+ ".rar",
191
+ // Compiled / binary
192
+ ".exe",
193
+ ".dll",
194
+ ".so",
195
+ ".dylib",
196
+ ".o",
197
+ ".a",
198
+ ".class",
199
+ ".pyc",
200
+ ".pdb",
201
+ // Source maps & lockfiles (large, not useful for drift analysis)
202
+ ".map"
203
+ ]);
204
+ var EXTRA_SKIP_DIRS = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
205
+ var TEXT_CACHE_MAX_BYTES = 1048576;
206
+ var FileCache = class _FileCache {
207
+ /** Directory walk results keyed by rootDir */
208
+ walkCache = /* @__PURE__ */ new Map();
209
+ /** File content keyed by absolute path (only files ≤ TEXT_CACHE_MAX_BYTES) */
210
+ textCache = /* @__PURE__ */ new Map();
211
+ /** Parsed JSON keyed by absolute path */
212
+ jsonCache = /* @__PURE__ */ new Map();
213
+ /** pathExists keyed by absolute path */
214
+ existsCache = /* @__PURE__ */ new Map();
215
+ /** User-configured exclude predicate (compiled from glob patterns) */
216
+ excludePredicate = null;
217
+ /** Directories that were auto-skipped because they were stuck */
218
+ _stuckPaths = [];
219
+ /** Files skipped because they exceed maxFileSizeToScan */
220
+ _skippedLargeFiles = [];
221
+ /** Maximum file size (bytes) we will read. 0 = unlimited. */
222
+ _maxFileSize = 0;
223
+ /** Per-project / per-directory scan timeout in ms. */
224
+ _projectScanTimeout = 18e4;
225
+ /** Whether we have already shown the "increase projectScanTimeout" hint */
226
+ _timeoutHintShown = false;
227
+ /** Root dir for relative-path computation (set by the first walkDir call) */
228
+ _rootDir = null;
229
+ /** Cached tree summary captured during the shared walk */
230
+ walkSummary = /* @__PURE__ */ new Map();
231
+ /** Fast lookup for exact filename (e.g. package.json) */
232
+ fileNameIndex = /* @__PURE__ */ new Map();
233
+ /** Set exclude patterns from config (call once before the walk) */
234
+ setExcludePatterns(patterns) {
235
+ this.excludePredicate = compileGlobs(patterns);
236
+ }
237
+ /** Set the maximum file size in bytes that readTextFile / readJsonFile will process */
238
+ setMaxFileSize(bytes) {
239
+ this._maxFileSize = bytes;
240
+ }
241
+ /** Set the per-project scan timeout (milliseconds). Scanners use this
242
+ * instead of a hard-coded constant so the user can override it via config. */
243
+ setProjectScanTimeout(ms) {
244
+ this._projectScanTimeout = ms;
245
+ }
246
+ /** Current per-project scan timeout in milliseconds */
247
+ get projectScanTimeout() {
248
+ return this._projectScanTimeout;
249
+ }
250
+ /** Record a path that timed out or was stuck during scanning */
251
+ addStuckPath(relPath) {
252
+ this._stuckPaths.push(relPath);
253
+ }
254
+ /**
255
+ * Returns true the first time it is called, false thereafter.
256
+ * Used by scanners to print the "increase projectScanTimeout" hint
257
+ * only once per scan run.
258
+ */
259
+ shouldShowTimeoutHint() {
260
+ if (this._timeoutHintShown) return false;
261
+ this._timeoutHintShown = true;
262
+ return true;
263
+ }
264
+ /** Get all paths that were auto-skipped due to being stuck (dirs + scanner files) */
265
+ get stuckPaths() {
266
+ return this._stuckPaths;
267
+ }
268
+ /** @deprecated Use stuckPaths instead */
269
+ get stuckDirs() {
270
+ return this._stuckPaths;
271
+ }
272
+ /** Get files that were skipped because they exceeded maxFileSizeToScan */
273
+ get skippedLargeFiles() {
274
+ return this._skippedLargeFiles;
275
+ }
276
+ // ── Directory walking ──
277
+ /**
278
+ * Walk the directory tree from `rootDir` once, skipping SKIP_DIRS plus
279
+ * common framework output dirs (.nuxt, .output, .svelte-kit).
280
+ *
281
+ * The result is memoised so every scanner filters the same array.
282
+ * Consumers that need additional filtering (e.g. SOURCE_EXTENSIONS,
283
+ * SKIP_EXTENSIONS) do so on the returned entries — no separate walk.
284
+ */
285
+ walkDir(rootDir, onProgress) {
286
+ this._rootDir = rootDir;
287
+ const cached = this.walkCache.get(rootDir);
288
+ if (cached) return cached;
289
+ const promise = this._doWalk(rootDir, onProgress);
290
+ this.walkCache.set(rootDir, promise);
291
+ return promise;
292
+ }
293
+ /** Return tree summary from the cached walk, if available. */
294
+ getWalkSummary(rootDir) {
295
+ return this.walkSummary.get(rootDir);
296
+ }
297
+ /** Additional dirs skipped only by the cached walk (framework outputs) */
298
+ static EXTRA_SKIP = EXTRA_SKIP_DIRS;
299
+ async _doWalk(rootDir, onProgress) {
300
+ const results = [];
301
+ const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
302
+ const maxConcurrentReads = Math.max(8, Math.min(64, cores * 4));
303
+ let foundCount = 0;
304
+ let lastReported = 0;
305
+ const REPORT_INTERVAL = 50;
306
+ const sem = new Semaphore(maxConcurrentReads);
307
+ const STUCK_TIMEOUT_MS = this._projectScanTimeout;
308
+ const extraSkip = _FileCache.EXTRA_SKIP;
309
+ const isExcluded = this.excludePredicate;
310
+ const stuckDirs = this._stuckPaths;
311
+ async function walk(dir) {
312
+ const relDir = path2.relative(rootDir, dir);
313
+ if (onProgress) {
314
+ onProgress(foundCount, relDir || ".");
315
+ }
316
+ let entries;
317
+ try {
318
+ entries = await sem.run(async () => {
319
+ const readPromise = fs.readdir(dir, { withFileTypes: true });
320
+ let stuckTimer;
321
+ const result = await Promise.race([
322
+ readPromise.then((e) => ({ ok: true, entries: e })),
323
+ new Promise((resolve2) => {
324
+ stuckTimer = setTimeout(() => resolve2({ ok: false }), STUCK_TIMEOUT_MS);
325
+ stuckTimer.unref();
326
+ })
327
+ ]);
328
+ clearTimeout(stuckTimer);
329
+ if (!result.ok) {
330
+ stuckDirs.push(relDir || dir);
331
+ return null;
332
+ }
333
+ return result.entries;
334
+ });
335
+ } catch {
336
+ return;
337
+ }
338
+ if (!entries) return;
339
+ const subWalks = [];
340
+ for (const e of entries) {
341
+ const absPath = path2.join(dir, e.name);
342
+ const relPath = path2.relative(rootDir, absPath);
343
+ if (isExcluded && isExcluded(relPath)) continue;
344
+ if (e.isDirectory()) {
345
+ if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
346
+ results.push({ absPath, relPath, name: e.name, isFile: false, isDirectory: true });
347
+ subWalks.push(walk(absPath));
348
+ } else if (e.isFile()) {
349
+ const ext = path2.extname(e.name).toLowerCase();
350
+ if (SKIP_EXTENSIONS.has(ext)) continue;
351
+ results.push({ absPath, relPath, name: e.name, isFile: true, isDirectory: false });
352
+ foundCount++;
353
+ if (onProgress && foundCount - lastReported >= REPORT_INTERVAL) {
354
+ lastReported = foundCount;
355
+ onProgress(foundCount, relPath);
356
+ }
357
+ }
358
+ }
359
+ await Promise.all(subWalks);
360
+ }
361
+ await walk(rootDir);
362
+ let totalDirs = 0;
363
+ const rootNameIndex = /* @__PURE__ */ new Map();
364
+ for (const entry of results) {
365
+ if (entry.isDirectory) totalDirs++;
366
+ if (!entry.isFile) continue;
367
+ const bucket = rootNameIndex.get(entry.name);
368
+ if (bucket) {
369
+ bucket.push(entry.absPath);
370
+ } else {
371
+ rootNameIndex.set(entry.name, [entry.absPath]);
372
+ }
373
+ }
374
+ this.walkSummary.set(rootDir, { totalFiles: foundCount, totalDirs });
375
+ this.fileNameIndex.set(rootDir, rootNameIndex);
376
+ if (onProgress && foundCount !== lastReported) {
377
+ onProgress(foundCount, "");
378
+ }
379
+ return results;
380
+ }
381
+ /**
382
+ * Find files matching a predicate from the cached walk.
383
+ * Returns absolute paths (same contract as the standalone `findFiles`).
384
+ */
385
+ async findFiles(rootDir, predicate) {
386
+ const entries = await this.walkDir(rootDir);
387
+ return entries.filter((e) => e.isFile && predicate(e.name)).map((e) => e.absPath);
388
+ }
389
+ async findPackageJsonFiles(rootDir) {
390
+ await this.walkDir(rootDir);
391
+ return this.fileNameIndex.get(rootDir)?.get("package.json") ?? [];
392
+ }
393
+ async findCsprojFiles(rootDir) {
394
+ const entries = await this.walkDir(rootDir);
395
+ return entries.filter((e) => e.isFile && e.name.endsWith(".csproj")).map((e) => e.absPath);
396
+ }
397
+ async findSolutionFiles(rootDir) {
398
+ const entries = await this.walkDir(rootDir);
399
+ return entries.filter((e) => e.isFile && e.name.endsWith(".sln")).map((e) => e.absPath);
400
+ }
401
+ /**
402
+ * Count files under a given directory using the cached walk data.
403
+ * Avoids a redundant recursive readdir that can be slow on large
404
+ * project trees (the main cause of per-project timeout hits).
405
+ * Falls back to the standalone `countFilesInDir` if the walk hasn't
406
+ * been populated yet.
407
+ */
408
+ async countFilesUnder(rootDir, dir) {
409
+ const entries = this.walkCache.get(rootDir);
410
+ if (!entries) {
411
+ return countFilesInDir(dir);
412
+ }
413
+ const resolved = await entries;
414
+ const prefix = dir.endsWith(path2.sep) ? dir : dir + path2.sep;
415
+ let count = 0;
416
+ for (const e of resolved) {
417
+ if (!e.isFile) continue;
418
+ if (e.absPath === dir || e.absPath.startsWith(prefix)) {
419
+ count++;
420
+ }
421
+ }
422
+ return count;
423
+ }
424
+ // ── File content reading ──
425
+ /**
426
+ * Read a text file. Files ≤ 1 MB are cached so subsequent calls from
427
+ * different scanners return the same string. Files > 1 MB (lockfiles,
428
+ * large generated files) are read directly and never retained.
429
+ *
430
+ * If maxFileSizeToScan is set and the file exceeds it, the file is
431
+ * recorded as skipped and an empty string is returned.
432
+ */
433
+ readTextFile(filePath) {
434
+ const abs = path2.resolve(filePath);
435
+ const cached = this.textCache.get(abs);
436
+ if (cached) return cached;
437
+ const maxSize = this._maxFileSize;
438
+ const skippedLarge = this._skippedLargeFiles;
439
+ const rootDir = this._rootDir;
440
+ const promise = (async () => {
441
+ if (maxSize > 0) {
442
+ try {
443
+ const stat2 = await fs.stat(abs);
444
+ if (stat2.size > maxSize) {
445
+ const rel = rootDir ? path2.relative(rootDir, abs) : abs;
446
+ skippedLarge.push(rel);
447
+ this.textCache.delete(abs);
448
+ return "";
449
+ }
450
+ } catch {
451
+ }
452
+ }
453
+ const content = await fs.readFile(abs, "utf8");
454
+ if (content.length > TEXT_CACHE_MAX_BYTES) {
455
+ this.textCache.delete(abs);
456
+ }
457
+ return content;
458
+ })();
459
+ this.textCache.set(abs, promise);
460
+ return promise;
461
+ }
462
+ /**
463
+ * Read and parse a JSON file. The parsed object is cached; the raw
464
+ * text is evicted immediately so we never hold both representations.
465
+ */
466
+ readJsonFile(filePath) {
467
+ const abs = path2.resolve(filePath);
468
+ const cached = this.jsonCache.get(abs);
469
+ if (cached) return cached;
470
+ const promise = this.readTextFile(abs).then((txt) => {
471
+ this.textCache.delete(abs);
472
+ return JSON.parse(txt);
473
+ });
474
+ this.jsonCache.set(abs, promise);
475
+ return promise;
476
+ }
477
+ // ── Existence checks ──
478
+ pathExists(p) {
479
+ const abs = path2.resolve(p);
480
+ const cached = this.existsCache.get(abs);
481
+ if (cached) return cached;
482
+ const promise = fs.access(abs).then(() => true, () => false);
483
+ this.existsCache.set(abs, promise);
484
+ return promise;
485
+ }
486
+ // ── Lifecycle ──
487
+ /** Release all cached data. Call after the scan completes. */
488
+ clear() {
489
+ this.walkCache.clear();
490
+ this.walkSummary.clear();
491
+ this.fileNameIndex.clear();
492
+ this.textCache.clear();
493
+ this.jsonCache.clear();
494
+ this.existsCache.clear();
495
+ }
496
+ /** Number of file content entries currently held */
497
+ get textCacheSize() {
498
+ return this.textCache.size;
499
+ }
500
+ /** Number of parsed JSON entries currently held */
501
+ get jsonCacheSize() {
502
+ return this.jsonCache.size;
503
+ }
504
+ };
505
+ async function quickTreeCount(rootDir, excludePatterns) {
506
+ const native = await quickTreeCountWithRipgrep(rootDir, excludePatterns);
507
+ if (native) return native;
508
+ let totalFiles = 0;
509
+ let totalDirs = 0;
510
+ const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
511
+ const maxConcurrent = Math.max(8, Math.min(128, cores * 8));
512
+ const sem = new Semaphore(maxConcurrent);
513
+ const extraSkip = EXTRA_SKIP_DIRS;
514
+ const isExcluded = excludePatterns ? compileGlobs(excludePatterns) : null;
515
+ async function count(dir) {
516
+ let entries;
517
+ try {
518
+ entries = await sem.run(() => fs.readdir(dir, { withFileTypes: true }));
519
+ } catch {
520
+ return;
521
+ }
522
+ const subs = [];
523
+ for (const e of entries) {
524
+ const relPath = path2.relative(rootDir, path2.join(dir, e.name));
525
+ if (isExcluded && isExcluded(relPath)) continue;
526
+ if (e.isDirectory()) {
527
+ if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
528
+ totalDirs++;
529
+ subs.push(count(path2.join(dir, e.name)));
530
+ } else if (e.isFile()) {
531
+ const ext = path2.extname(e.name).toLowerCase();
532
+ if (!SKIP_EXTENSIONS.has(ext)) totalFiles++;
533
+ }
534
+ }
535
+ await Promise.all(subs);
536
+ }
537
+ await count(rootDir);
538
+ return { totalFiles, totalDirs };
539
+ }
540
+ function normalizeGlobForRipgrep(pattern) {
541
+ return pattern.replace(/\\/g, "/");
542
+ }
543
+ async function quickTreeCountWithRipgrep(rootDir, excludePatterns) {
544
+ const args = ["--files", "--hidden", "--no-ignore", "--null"];
545
+ for (const dir of SKIP_DIRS) {
546
+ args.push("-g", `!**/${dir}/**`);
547
+ }
548
+ for (const dir of EXTRA_SKIP_DIRS) {
549
+ args.push("-g", `!**/${dir}/**`);
550
+ }
551
+ for (const ext of SKIP_EXTENSIONS) {
552
+ args.push("-g", `!**/*${ext}`);
553
+ }
554
+ for (const pattern of excludePatterns ?? []) {
555
+ const trimmed = pattern.trim();
556
+ if (!trimmed) continue;
557
+ args.push("-g", `!${normalizeGlobForRipgrep(trimmed)}`);
558
+ }
559
+ try {
560
+ const { stdout } = await execFileAsync("rg", args, { cwd: rootDir, maxBuffer: 64 * 1024 * 1024, encoding: "utf8" });
561
+ if (!stdout) return { totalFiles: 0, totalDirs: 0 };
562
+ const files = stdout.split("\0").filter(Boolean);
563
+ const dirs = /* @__PURE__ */ new Set();
564
+ for (const file of files) {
565
+ const dir = path2.dirname(file);
566
+ if (dir && dir !== ".") dirs.add(dir);
567
+ }
568
+ return { totalFiles: files.length, totalDirs: dirs.size };
569
+ } catch {
570
+ return null;
571
+ }
572
+ }
573
+ async function countFilesInDir(dir, recursive = true) {
574
+ let count = 0;
575
+ const extraSkip = /* @__PURE__ */ new Set(["obj", "bin", "Debug", "Release", "TestResults"]);
576
+ async function walk(currentDir) {
577
+ let entries;
578
+ try {
579
+ entries = await fs.readdir(currentDir, { withFileTypes: true });
580
+ } catch {
581
+ return;
582
+ }
583
+ const subs = [];
584
+ for (const e of entries) {
585
+ if (e.isDirectory()) {
586
+ if (!recursive) continue;
587
+ if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
588
+ subs.push(walk(path2.join(currentDir, e.name)));
589
+ } else if (e.isFile()) {
590
+ const ext = path2.extname(e.name).toLowerCase();
591
+ if (!SKIP_EXTENSIONS.has(ext)) count++;
592
+ }
593
+ }
594
+ await Promise.all(subs);
595
+ }
596
+ await walk(dir);
597
+ return count;
598
+ }
599
+ async function findFiles(rootDir, predicate) {
600
+ const results = [];
601
+ const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
602
+ const maxConcurrentReads = Math.max(8, Math.min(64, cores * 4));
603
+ const readDirSemaphore = new Semaphore(maxConcurrentReads);
604
+ async function walk(dir) {
605
+ let entries;
606
+ try {
607
+ entries = await readDirSemaphore.run(() => fs.readdir(dir, { withFileTypes: true }));
608
+ } catch {
609
+ return;
610
+ }
611
+ const subDirectoryWalks = [];
612
+ for (const e of entries) {
613
+ if (e.isDirectory()) {
614
+ if (SKIP_DIRS.has(e.name)) continue;
615
+ subDirectoryWalks.push(walk(path2.join(dir, e.name)));
616
+ } else if (e.isFile() && predicate(e.name)) {
617
+ const ext = path2.extname(e.name).toLowerCase();
618
+ if (!SKIP_EXTENSIONS.has(ext)) results.push(path2.join(dir, e.name));
619
+ }
620
+ }
621
+ await Promise.all(subDirectoryWalks);
622
+ }
623
+ await walk(rootDir);
624
+ return results;
625
+ }
626
+ async function findPackageJsonFiles(rootDir) {
627
+ return findFiles(rootDir, (name) => name === "package.json");
628
+ }
629
+ async function findSolutionFiles(rootDir) {
630
+ return findFiles(rootDir, (name) => name.endsWith(".sln"));
631
+ }
632
+ async function findCsprojFiles(rootDir) {
633
+ return findFiles(rootDir, (name) => name.endsWith(".csproj"));
634
+ }
635
+ async function readJsonFile(filePath) {
636
+ const txt = await fs.readFile(filePath, "utf8");
637
+ return JSON.parse(txt);
638
+ }
639
+ async function readTextFile(filePath) {
640
+ return fs.readFile(filePath, "utf8");
641
+ }
642
+ async function pathExists(p) {
643
+ try {
644
+ await fs.access(p);
645
+ return true;
646
+ } catch {
647
+ return false;
648
+ }
649
+ }
650
+ async function ensureDir(dir) {
651
+ await fs.mkdir(dir, { recursive: true });
652
+ }
653
+ async function writeJsonFile(filePath, data) {
654
+ await ensureDir(path2.dirname(filePath));
655
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
656
+ }
657
+ async function writeTextFile(filePath, content) {
658
+ await ensureDir(path2.dirname(filePath));
659
+ await fs.writeFile(filePath, content, "utf8");
660
+ }
661
+
662
+ export {
663
+ Semaphore,
664
+ FileCache,
665
+ quickTreeCount,
666
+ normalizeGlobForRipgrep,
667
+ countFilesInDir,
668
+ findFiles,
669
+ findPackageJsonFiles,
670
+ findSolutionFiles,
671
+ findCsprojFiles,
672
+ readJsonFile,
673
+ readTextFile,
674
+ pathExists,
675
+ ensureDir,
676
+ writeJsonFile,
677
+ writeTextFile
678
+ };