@vibgrate/cli 2026.618.2 → 2026.623.2

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