@vibgrate/cli 1.0.21 → 1.0.22

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,8 +1,8 @@
1
1
  import {
2
2
  baselineCommand,
3
3
  runBaseline
4
- } from "./chunk-74QSNBZA.js";
5
- import "./chunk-L6R5WSCC.js";
4
+ } from "./chunk-IMK7DUPY.js";
5
+ import "./chunk-JFMGFWKC.js";
6
6
  export {
7
7
  baselineCommand,
8
8
  runBaseline
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runScan,
3
3
  writeJsonFile
4
- } from "./chunk-L6R5WSCC.js";
4
+ } from "./chunk-JFMGFWKC.js";
5
5
 
6
6
  // src/commands/baseline.ts
7
7
  import * as path from "path";
@@ -1,7 +1,7 @@
1
1
  // src/utils/fs.ts
2
2
  import * as fs from "fs/promises";
3
3
  import * as os from "os";
4
- import * as path from "path";
4
+ import * as path2 from "path";
5
5
 
6
6
  // src/utils/semaphore.ts
7
7
  var Semaphore = class {
@@ -32,6 +32,86 @@ var Semaphore = class {
32
32
  }
33
33
  };
34
34
 
35
+ // src/utils/glob.ts
36
+ import * as path from "path";
37
+ function compileGlobs(patterns) {
38
+ if (patterns.length === 0) return null;
39
+ const matchers = patterns.map((p) => compileOne(normalise(p)));
40
+ return (relPath) => {
41
+ const norm = normalise(relPath);
42
+ return matchers.some((m) => m(norm));
43
+ };
44
+ }
45
+ function normalise(p) {
46
+ return p.split(path.sep).join("/").replace(/\/+$/, "");
47
+ }
48
+ function compileOne(pattern) {
49
+ if (!pattern.includes("/") && !hasGlobChars(pattern)) {
50
+ const prefix = pattern + "/";
51
+ return (p) => p === pattern || p.startsWith(prefix);
52
+ }
53
+ const re = globToRegex(pattern);
54
+ return (p) => re.test(p);
55
+ }
56
+ function hasGlobChars(s) {
57
+ return /[*?[\]{}]/.test(s);
58
+ }
59
+ function globToRegex(pattern) {
60
+ let i = 0;
61
+ let re = "^";
62
+ const len = pattern.length;
63
+ while (i < len) {
64
+ const ch = pattern[i];
65
+ if (ch === "*") {
66
+ if (pattern[i + 1] === "*") {
67
+ i += 2;
68
+ if (pattern[i] === "/") {
69
+ i++;
70
+ re += "(?:.+/)?";
71
+ } else {
72
+ re += ".*";
73
+ }
74
+ } else {
75
+ i++;
76
+ re += "[^/]*";
77
+ }
78
+ } else if (ch === "?") {
79
+ i++;
80
+ re += "[^/]";
81
+ } else if (ch === "[") {
82
+ const start = i;
83
+ i++;
84
+ while (i < len && pattern[i] !== "]") i++;
85
+ i++;
86
+ re += pattern.slice(start, i);
87
+ } else if (ch === "{") {
88
+ i++;
89
+ const alternatives = [];
90
+ let current = "";
91
+ while (i < len && pattern[i] !== "}") {
92
+ if (pattern[i] === ",") {
93
+ alternatives.push(current);
94
+ current = "";
95
+ } else {
96
+ current += pattern[i];
97
+ }
98
+ i++;
99
+ }
100
+ alternatives.push(current);
101
+ i++;
102
+ re += "(?:" + alternatives.map(escapeRegex).join("|") + ")";
103
+ } else {
104
+ re += escapeRegex(ch);
105
+ i++;
106
+ }
107
+ }
108
+ re += "$";
109
+ return new RegExp(re);
110
+ }
111
+ function escapeRegex(s) {
112
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
113
+ }
114
+
35
115
  // src/utils/fs.ts
36
116
  var SKIP_DIRS = /* @__PURE__ */ new Set([
37
117
  "node_modules",
@@ -60,6 +140,40 @@ var FileCache = class _FileCache {
60
140
  jsonCache = /* @__PURE__ */ new Map();
61
141
  /** pathExists keyed by absolute path */
62
142
  existsCache = /* @__PURE__ */ new Map();
143
+ /** User-configured exclude predicate (compiled from glob patterns) */
144
+ excludePredicate = null;
145
+ /** Directories that were auto-skipped because they were stuck (>60s) */
146
+ _stuckPaths = [];
147
+ /** Files skipped because they exceed maxFileSizeToScan */
148
+ _skippedLargeFiles = [];
149
+ /** Maximum file size (bytes) we will read. 0 = unlimited. */
150
+ _maxFileSize = 0;
151
+ /** Root dir for relative-path computation (set by the first walkDir call) */
152
+ _rootDir = null;
153
+ /** Set exclude patterns from config (call once before the walk) */
154
+ setExcludePatterns(patterns) {
155
+ this.excludePredicate = compileGlobs(patterns);
156
+ }
157
+ /** Set the maximum file size in bytes that readTextFile / readJsonFile will process */
158
+ setMaxFileSize(bytes) {
159
+ this._maxFileSize = bytes;
160
+ }
161
+ /** Record a path that timed out or was stuck during scanning */
162
+ addStuckPath(relPath) {
163
+ this._stuckPaths.push(relPath);
164
+ }
165
+ /** Get all paths that were auto-skipped due to being stuck (dirs + scanner files) */
166
+ get stuckPaths() {
167
+ return this._stuckPaths;
168
+ }
169
+ /** @deprecated Use stuckPaths instead */
170
+ get stuckDirs() {
171
+ return this._stuckPaths;
172
+ }
173
+ /** Get files that were skipped because they exceeded maxFileSizeToScan */
174
+ get skippedLargeFiles() {
175
+ return this._skippedLargeFiles;
176
+ }
63
177
  // ── Directory walking ──
64
178
  /**
65
179
  * Walk the directory tree from `rootDir` once, skipping SKIP_DIRS plus
@@ -70,6 +184,7 @@ var FileCache = class _FileCache {
70
184
  * SKIP_EXTENSIONS) do so on the returned entries — no separate walk.
71
185
  */
72
186
  walkDir(rootDir, onProgress) {
187
+ this._rootDir = rootDir;
73
188
  const cached = this.walkCache.get(rootDir);
74
189
  if (cached) return cached;
75
190
  const promise = this._doWalk(rootDir, onProgress);
@@ -86,18 +201,34 @@ var FileCache = class _FileCache {
86
201
  let lastReported = 0;
87
202
  const REPORT_INTERVAL = 50;
88
203
  const sem = new Semaphore(maxConcurrentReads);
204
+ const STUCK_TIMEOUT_MS = 6e4;
89
205
  const extraSkip = _FileCache.EXTRA_SKIP;
206
+ const isExcluded = this.excludePredicate;
207
+ const stuckDirs = this._stuckPaths;
90
208
  async function walk(dir) {
209
+ const relDir = path2.relative(rootDir, dir);
91
210
  let entries;
92
211
  try {
93
- entries = await fs.readdir(dir, { withFileTypes: true });
212
+ const readPromise = fs.readdir(dir, { withFileTypes: true });
213
+ const result = await Promise.race([
214
+ readPromise.then((e) => ({ ok: true, entries: e })),
215
+ new Promise(
216
+ (resolve7) => setTimeout(() => resolve7({ ok: false }), STUCK_TIMEOUT_MS)
217
+ )
218
+ ]);
219
+ if (!result.ok) {
220
+ stuckDirs.push(relDir || dir);
221
+ return;
222
+ }
223
+ entries = result.entries;
94
224
  } catch {
95
225
  return;
96
226
  }
97
227
  const subWalks = [];
98
228
  for (const e of entries) {
99
- const absPath = path.join(dir, e.name);
100
- const relPath = path.relative(rootDir, absPath);
229
+ const absPath = path2.join(dir, e.name);
230
+ const relPath = path2.relative(rootDir, absPath);
231
+ if (isExcluded && isExcluded(relPath)) continue;
101
232
  if (e.isDirectory()) {
102
233
  if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
103
234
  results.push({ absPath, relPath, name: e.name, isFile: false, isDirectory: true });
@@ -107,7 +238,7 @@ var FileCache = class _FileCache {
107
238
  foundCount++;
108
239
  if (onProgress && foundCount - lastReported >= REPORT_INTERVAL) {
109
240
  lastReported = foundCount;
110
- onProgress(foundCount);
241
+ onProgress(foundCount, relPath);
111
242
  }
112
243
  }
113
244
  }
@@ -115,7 +246,7 @@ var FileCache = class _FileCache {
115
246
  }
116
247
  await sem.run(() => walk(rootDir));
117
248
  if (onProgress && foundCount !== lastReported) {
118
- onProgress(foundCount);
249
+ onProgress(foundCount, "");
119
250
  }
120
251
  return results;
121
252
  }
@@ -141,17 +272,36 @@ var FileCache = class _FileCache {
141
272
  * Read a text file. Files ≤ 1 MB are cached so subsequent calls from
142
273
  * different scanners return the same string. Files > 1 MB (lockfiles,
143
274
  * large generated files) are read directly and never retained.
275
+ *
276
+ * If maxFileSizeToScan is set and the file exceeds it, the file is
277
+ * recorded as skipped and an empty string is returned.
144
278
  */
145
279
  readTextFile(filePath) {
146
- const abs = path.resolve(filePath);
280
+ const abs = path2.resolve(filePath);
147
281
  const cached = this.textCache.get(abs);
148
282
  if (cached) return cached;
149
- const promise = fs.readFile(abs, "utf8").then((content) => {
283
+ const maxSize = this._maxFileSize;
284
+ const skippedLarge = this._skippedLargeFiles;
285
+ const rootDir = this._rootDir;
286
+ const promise = (async () => {
287
+ if (maxSize > 0) {
288
+ try {
289
+ const stat4 = await fs.stat(abs);
290
+ if (stat4.size > maxSize) {
291
+ const rel = rootDir ? path2.relative(rootDir, abs) : abs;
292
+ skippedLarge.push(rel);
293
+ this.textCache.delete(abs);
294
+ return "";
295
+ }
296
+ } catch {
297
+ }
298
+ }
299
+ const content = await fs.readFile(abs, "utf8");
150
300
  if (content.length > TEXT_CACHE_MAX_BYTES) {
151
301
  this.textCache.delete(abs);
152
302
  }
153
303
  return content;
154
- });
304
+ })();
155
305
  this.textCache.set(abs, promise);
156
306
  return promise;
157
307
  }
@@ -160,7 +310,7 @@ var FileCache = class _FileCache {
160
310
  * text is evicted immediately so we never hold both representations.
161
311
  */
162
312
  readJsonFile(filePath) {
163
- const abs = path.resolve(filePath);
313
+ const abs = path2.resolve(filePath);
164
314
  const cached = this.jsonCache.get(abs);
165
315
  if (cached) return cached;
166
316
  const promise = this.readTextFile(abs).then((txt) => {
@@ -172,7 +322,7 @@ var FileCache = class _FileCache {
172
322
  }
173
323
  // ── Existence checks ──
174
324
  pathExists(p) {
175
- const abs = path.resolve(p);
325
+ const abs = path2.resolve(p);
176
326
  const cached = this.existsCache.get(abs);
177
327
  if (cached) return cached;
178
328
  const promise = fs.access(abs).then(() => true, () => false);
@@ -196,13 +346,14 @@ var FileCache = class _FileCache {
196
346
  return this.jsonCache.size;
197
347
  }
198
348
  };
199
- async function quickTreeCount(rootDir) {
349
+ async function quickTreeCount(rootDir, excludePatterns) {
200
350
  let totalFiles = 0;
201
351
  let totalDirs = 0;
202
352
  const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
203
353
  const maxConcurrent = Math.max(8, Math.min(128, cores * 8));
204
354
  const sem = new Semaphore(maxConcurrent);
205
355
  const extraSkip = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
356
+ const isExcluded = excludePatterns ? compileGlobs(excludePatterns) : null;
206
357
  async function count(dir) {
207
358
  let entries;
208
359
  try {
@@ -212,10 +363,12 @@ async function quickTreeCount(rootDir) {
212
363
  }
213
364
  const subs = [];
214
365
  for (const e of entries) {
366
+ const relPath = path2.relative(rootDir, path2.join(dir, e.name));
367
+ if (isExcluded && isExcluded(relPath)) continue;
215
368
  if (e.isDirectory()) {
216
369
  if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
217
370
  totalDirs++;
218
- subs.push(sem.run(() => count(path.join(dir, e.name))));
371
+ subs.push(sem.run(() => count(path2.join(dir, e.name))));
219
372
  } else if (e.isFile()) {
220
373
  totalFiles++;
221
374
  }
@@ -241,9 +394,9 @@ async function findFiles(rootDir, predicate) {
241
394
  for (const e of entries) {
242
395
  if (e.isDirectory()) {
243
396
  if (SKIP_DIRS.has(e.name)) continue;
244
- subDirectoryWalks.push(readDirSemaphore.run(() => walk(path.join(dir, e.name))));
397
+ subDirectoryWalks.push(readDirSemaphore.run(() => walk(path2.join(dir, e.name))));
245
398
  } else if (e.isFile() && predicate(e.name)) {
246
- results.push(path.join(dir, e.name));
399
+ results.push(path2.join(dir, e.name));
247
400
  }
248
401
  }
249
402
  await Promise.all(subDirectoryWalks);
@@ -279,11 +432,11 @@ async function ensureDir(dir) {
279
432
  await fs.mkdir(dir, { recursive: true });
280
433
  }
281
434
  async function writeJsonFile(filePath, data) {
282
- await ensureDir(path.dirname(filePath));
435
+ await ensureDir(path2.dirname(filePath));
283
436
  await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
284
437
  }
285
438
  async function writeTextFile(filePath, content) {
286
- await ensureDir(path.dirname(filePath));
439
+ await ensureDir(path2.dirname(filePath));
287
440
  await fs.writeFile(filePath, content, "utf8");
288
441
  }
289
442
 
@@ -1217,7 +1370,7 @@ function toSarifResult(finding) {
1217
1370
 
1218
1371
  // src/commands/dsn.ts
1219
1372
  import * as crypto2 from "crypto";
1220
- import * as path2 from "path";
1373
+ import * as path3 from "path";
1221
1374
  import { Command } from "commander";
1222
1375
  import chalk2 from "chalk";
1223
1376
  var REGION_HOSTS = {
@@ -1262,7 +1415,7 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
1262
1415
  console.log(chalk2.dim("Set this as VIBGRATE_DSN in your CI environment."));
1263
1416
  console.log(chalk2.dim("The secret must be registered on your Vibgrate ingest API."));
1264
1417
  if (opts.write) {
1265
- const writePath = path2.resolve(opts.write);
1418
+ const writePath = path3.resolve(opts.write);
1266
1419
  await writeTextFile(writePath, dsn + "\n");
1267
1420
  console.log("");
1268
1421
  console.log(chalk2.green("\u2714") + ` DSN written to ${opts.write}`);
@@ -1272,7 +1425,7 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
1272
1425
 
1273
1426
  // src/commands/push.ts
1274
1427
  import * as crypto3 from "crypto";
1275
- import * as path3 from "path";
1428
+ import * as path4 from "path";
1276
1429
  import { Command as Command2 } from "commander";
1277
1430
  import chalk3 from "chalk";
1278
1431
  function parseDsn(dsn) {
@@ -1301,7 +1454,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
1301
1454
  if (opts.strict) process.exit(1);
1302
1455
  return;
1303
1456
  }
1304
- const filePath = path3.resolve(opts.file);
1457
+ const filePath = path4.resolve(opts.file);
1305
1458
  if (!await pathExists(filePath)) {
1306
1459
  console.error(chalk3.red(`Scan artifact not found: ${filePath}`));
1307
1460
  console.error(chalk3.dim('Run "vibgrate scan" first.'));
@@ -1346,14 +1499,31 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
1346
1499
  });
1347
1500
 
1348
1501
  // src/commands/scan.ts
1349
- import * as path16 from "path";
1502
+ import * as path17 from "path";
1350
1503
  import { Command as Command3 } from "commander";
1351
1504
  import chalk5 from "chalk";
1352
1505
 
1353
1506
  // src/scanners/node-scanner.ts
1354
- import * as path4 from "path";
1507
+ import * as path5 from "path";
1355
1508
  import * as semver2 from "semver";
1356
1509
 
1510
+ // src/utils/timeout.ts
1511
+ async function withTimeout(promise, ms) {
1512
+ let timer;
1513
+ const timeout = new Promise((resolve7) => {
1514
+ timer = setTimeout(() => resolve7({ ok: false }), ms);
1515
+ });
1516
+ try {
1517
+ const result = await Promise.race([
1518
+ promise.then((value) => ({ ok: true, value })),
1519
+ timeout
1520
+ ]);
1521
+ return result;
1522
+ } finally {
1523
+ clearTimeout(timer);
1524
+ }
1525
+ }
1526
+
1357
1527
  // src/scanners/npm-cache.ts
1358
1528
  import { spawn } from "child_process";
1359
1529
  import * as semver from "semver";
@@ -1528,10 +1698,20 @@ var KNOWN_FRAMEWORKS = {
1528
1698
  async function scanNodeProjects(rootDir, npmCache, cache) {
1529
1699
  const packageJsonFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
1530
1700
  const results = [];
1701
+ const STUCK_TIMEOUT_MS = 6e4;
1531
1702
  for (const pjPath of packageJsonFiles) {
1532
1703
  try {
1533
- const scan = await scanOnePackageJson(pjPath, rootDir, npmCache, cache);
1534
- results.push(scan);
1704
+ const scanPromise = scanOnePackageJson(pjPath, rootDir, npmCache, cache);
1705
+ const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
1706
+ if (result.ok) {
1707
+ results.push(result.value);
1708
+ } else {
1709
+ const relPath = path5.relative(rootDir, path5.dirname(pjPath));
1710
+ if (cache) {
1711
+ cache.addStuckPath(relPath || ".");
1712
+ }
1713
+ console.error(`Timeout scanning ${pjPath} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
1714
+ }
1535
1715
  } catch (e) {
1536
1716
  const msg = e instanceof Error ? e.message : String(e);
1537
1717
  console.error(`Error scanning ${pjPath}: ${msg}`);
@@ -1541,8 +1721,8 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
1541
1721
  }
1542
1722
  async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1543
1723
  const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
1544
- const absProjectPath = path4.dirname(packageJsonPath);
1545
- const projectPath = path4.relative(rootDir, absProjectPath) || ".";
1724
+ const absProjectPath = path5.dirname(packageJsonPath);
1725
+ const projectPath = path5.relative(rootDir, absProjectPath) || ".";
1546
1726
  const nodeEngine = pj.engines?.node ?? void 0;
1547
1727
  let runtimeLatest;
1548
1728
  let runtimeMajorsBehind;
@@ -1624,7 +1804,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1624
1804
  return {
1625
1805
  type: "node",
1626
1806
  path: projectPath,
1627
- name: pj.name ?? path4.basename(absProjectPath),
1807
+ name: pj.name ?? path5.basename(absProjectPath),
1628
1808
  runtime: nodeEngine,
1629
1809
  runtimeLatest,
1630
1810
  runtimeMajorsBehind,
@@ -1635,7 +1815,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1635
1815
  }
1636
1816
 
1637
1817
  // src/scanners/dotnet-scanner.ts
1638
- import * as path5 from "path";
1818
+ import * as path6 from "path";
1639
1819
  import { XMLParser } from "fast-xml-parser";
1640
1820
  var parser = new XMLParser({
1641
1821
  ignoreAttributes: false,
@@ -1836,7 +2016,7 @@ function parseCsproj(xml, filePath) {
1836
2016
  const parsed = parser.parse(xml);
1837
2017
  const project = parsed?.Project;
1838
2018
  if (!project) {
1839
- return { targetFrameworks: [], packageReferences: [], projectName: path5.basename(filePath, ".csproj") };
2019
+ return { targetFrameworks: [], packageReferences: [], projectName: path6.basename(filePath, ".csproj") };
1840
2020
  }
1841
2021
  const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
1842
2022
  const targetFrameworks = [];
@@ -1864,7 +2044,7 @@ function parseCsproj(xml, filePath) {
1864
2044
  return {
1865
2045
  targetFrameworks: [...new Set(targetFrameworks)],
1866
2046
  packageReferences,
1867
- projectName: path5.basename(filePath, ".csproj")
2047
+ projectName: path6.basename(filePath, ".csproj")
1868
2048
  };
1869
2049
  }
1870
2050
  async function scanDotnetProjects(rootDir, cache) {
@@ -1874,12 +2054,12 @@ async function scanDotnetProjects(rootDir, cache) {
1874
2054
  for (const slnPath of slnFiles) {
1875
2055
  try {
1876
2056
  const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
1877
- const slnDir = path5.dirname(slnPath);
2057
+ const slnDir = path6.dirname(slnPath);
1878
2058
  const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
1879
2059
  let match;
1880
2060
  while ((match = projectRegex.exec(slnContent)) !== null) {
1881
2061
  if (match[1]) {
1882
- const csprojPath = path5.resolve(slnDir, match[1].replace(/\\/g, "/"));
2062
+ const csprojPath = path6.resolve(slnDir, match[1].replace(/\\/g, "/"));
1883
2063
  slnCsprojPaths.add(csprojPath);
1884
2064
  }
1885
2065
  }
@@ -1888,10 +2068,20 @@ async function scanDotnetProjects(rootDir, cache) {
1888
2068
  }
1889
2069
  const allCsprojFiles = /* @__PURE__ */ new Set([...csprojFiles, ...slnCsprojPaths]);
1890
2070
  const results = [];
2071
+ const STUCK_TIMEOUT_MS = 6e4;
1891
2072
  for (const csprojPath of allCsprojFiles) {
1892
2073
  try {
1893
- const scan = await scanOneCsproj(csprojPath, rootDir, cache);
1894
- results.push(scan);
2074
+ const scanPromise = scanOneCsproj(csprojPath, rootDir, cache);
2075
+ const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
2076
+ if (result.ok) {
2077
+ results.push(result.value);
2078
+ } else {
2079
+ const relPath = path6.relative(rootDir, path6.dirname(csprojPath));
2080
+ if (cache) {
2081
+ cache.addStuckPath(relPath || ".");
2082
+ }
2083
+ console.error(`Timeout scanning ${csprojPath} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
2084
+ }
1895
2085
  } catch (e) {
1896
2086
  const msg = e instanceof Error ? e.message : String(e);
1897
2087
  console.error(`Error scanning ${csprojPath}: ${msg}`);
@@ -1935,7 +2125,7 @@ async function scanOneCsproj(csprojPath, rootDir, cache) {
1935
2125
  const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: dependencies.length };
1936
2126
  return {
1937
2127
  type: "dotnet",
1938
- path: path5.relative(rootDir, path5.dirname(csprojPath)) || ".",
2128
+ path: path6.relative(rootDir, path6.dirname(csprojPath)) || ".",
1939
2129
  name: data.projectName,
1940
2130
  targetFramework,
1941
2131
  runtime: primaryTfm,
@@ -1948,15 +2138,17 @@ async function scanOneCsproj(csprojPath, rootDir, cache) {
1948
2138
  }
1949
2139
 
1950
2140
  // src/config.ts
1951
- import * as path6 from "path";
2141
+ import * as path7 from "path";
1952
2142
  import * as fs2 from "fs/promises";
1953
2143
  var CONFIG_FILES = [
1954
2144
  "vibgrate.config.ts",
1955
2145
  "vibgrate.config.js",
1956
2146
  "vibgrate.config.json"
1957
2147
  ];
2148
+ var DEFAULT_MAX_FILE_SIZE = 5242880;
1958
2149
  var DEFAULT_CONFIG = {
1959
2150
  exclude: [],
2151
+ maxFileSizeToScan: DEFAULT_MAX_FILE_SIZE,
1960
2152
  thresholds: {
1961
2153
  failOnError: {
1962
2154
  eolDays: 180,
@@ -1970,28 +2162,44 @@ var DEFAULT_CONFIG = {
1970
2162
  }
1971
2163
  };
1972
2164
  async function loadConfig(rootDir) {
2165
+ let config = DEFAULT_CONFIG;
1973
2166
  for (const file of CONFIG_FILES) {
1974
- const configPath = path6.join(rootDir, file);
2167
+ const configPath = path7.join(rootDir, file);
1975
2168
  if (await pathExists(configPath)) {
1976
2169
  if (file.endsWith(".json")) {
1977
2170
  const txt = await readTextFile(configPath);
1978
- return { ...DEFAULT_CONFIG, ...JSON.parse(txt) };
2171
+ config = { ...DEFAULT_CONFIG, ...JSON.parse(txt) };
2172
+ break;
1979
2173
  }
1980
2174
  try {
1981
2175
  const mod = await import(configPath);
1982
- return { ...DEFAULT_CONFIG, ...mod.default ?? mod };
2176
+ config = { ...DEFAULT_CONFIG, ...mod.default ?? mod };
2177
+ break;
1983
2178
  } catch {
1984
2179
  }
1985
2180
  }
1986
2181
  }
1987
- return DEFAULT_CONFIG;
2182
+ const sidecarPath = path7.join(rootDir, ".vibgrate", "auto-excludes.json");
2183
+ if (await pathExists(sidecarPath)) {
2184
+ try {
2185
+ const txt = await readTextFile(sidecarPath);
2186
+ const autoExcludes = JSON.parse(txt);
2187
+ if (Array.isArray(autoExcludes) && autoExcludes.length > 0) {
2188
+ const existing = config.exclude ?? [];
2189
+ config = { ...config, exclude: [.../* @__PURE__ */ new Set([...existing, ...autoExcludes])] };
2190
+ }
2191
+ } catch {
2192
+ }
2193
+ }
2194
+ return config;
1988
2195
  }
1989
2196
  async function writeDefaultConfig(rootDir) {
1990
- const configPath = path6.join(rootDir, "vibgrate.config.ts");
2197
+ const configPath = path7.join(rootDir, "vibgrate.config.ts");
1991
2198
  const content = `import type { VibgrateConfig } from '@vibgrate/cli';
1992
2199
 
1993
2200
  const config: VibgrateConfig = {
1994
2201
  // exclude: ['legacy/**'],
2202
+ // maxFileSizeToScan: 5_242_880, // 5 MB (default)
1995
2203
  thresholds: {
1996
2204
  failOnError: {
1997
2205
  eolDays: 180,
@@ -2010,9 +2218,44 @@ export default config;
2010
2218
  await fs2.writeFile(configPath, content, "utf8");
2011
2219
  return configPath;
2012
2220
  }
2221
+ async function appendExcludePatterns(rootDir, newPatterns) {
2222
+ if (newPatterns.length === 0) return false;
2223
+ const jsonPath = path7.join(rootDir, "vibgrate.config.json");
2224
+ if (await pathExists(jsonPath)) {
2225
+ try {
2226
+ const txt = await readTextFile(jsonPath);
2227
+ const cfg = JSON.parse(txt);
2228
+ const existing2 = Array.isArray(cfg.exclude) ? cfg.exclude : [];
2229
+ const merged2 = [.../* @__PURE__ */ new Set([...existing2, ...newPatterns])];
2230
+ cfg.exclude = merged2;
2231
+ await fs2.writeFile(jsonPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
2232
+ return true;
2233
+ } catch {
2234
+ }
2235
+ }
2236
+ const vibgrateDir = path7.join(rootDir, ".vibgrate");
2237
+ const sidecarPath = path7.join(vibgrateDir, "auto-excludes.json");
2238
+ let existing = [];
2239
+ if (await pathExists(sidecarPath)) {
2240
+ try {
2241
+ const txt = await readTextFile(sidecarPath);
2242
+ const parsed = JSON.parse(txt);
2243
+ if (Array.isArray(parsed)) existing = parsed;
2244
+ } catch {
2245
+ }
2246
+ }
2247
+ const merged = [.../* @__PURE__ */ new Set([...existing, ...newPatterns])];
2248
+ try {
2249
+ await fs2.mkdir(vibgrateDir, { recursive: true });
2250
+ await fs2.writeFile(sidecarPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
2251
+ return true;
2252
+ } catch {
2253
+ return false;
2254
+ }
2255
+ }
2013
2256
 
2014
2257
  // src/utils/vcs.ts
2015
- import * as path7 from "path";
2258
+ import * as path8 from "path";
2016
2259
  import * as fs3 from "fs/promises";
2017
2260
  async function detectVcs(rootDir) {
2018
2261
  try {
@@ -2026,7 +2269,7 @@ async function detectGit(rootDir) {
2026
2269
  if (!gitDir) {
2027
2270
  return { type: "unknown" };
2028
2271
  }
2029
- const headPath = path7.join(gitDir, "HEAD");
2272
+ const headPath = path8.join(gitDir, "HEAD");
2030
2273
  let headContent;
2031
2274
  try {
2032
2275
  headContent = (await fs3.readFile(headPath, "utf8")).trim();
@@ -2050,30 +2293,30 @@ async function detectGit(rootDir) {
2050
2293
  };
2051
2294
  }
2052
2295
  async function findGitDir(startDir) {
2053
- let dir = path7.resolve(startDir);
2054
- const root = path7.parse(dir).root;
2296
+ let dir = path8.resolve(startDir);
2297
+ const root = path8.parse(dir).root;
2055
2298
  while (dir !== root) {
2056
- const gitPath = path7.join(dir, ".git");
2299
+ const gitPath = path8.join(dir, ".git");
2057
2300
  try {
2058
- const stat3 = await fs3.stat(gitPath);
2059
- if (stat3.isDirectory()) {
2301
+ const stat4 = await fs3.stat(gitPath);
2302
+ if (stat4.isDirectory()) {
2060
2303
  return gitPath;
2061
2304
  }
2062
- if (stat3.isFile()) {
2305
+ if (stat4.isFile()) {
2063
2306
  const content = (await fs3.readFile(gitPath, "utf8")).trim();
2064
2307
  if (content.startsWith("gitdir: ")) {
2065
- const resolved = path7.resolve(dir, content.slice(8));
2308
+ const resolved = path8.resolve(dir, content.slice(8));
2066
2309
  return resolved;
2067
2310
  }
2068
2311
  }
2069
2312
  } catch {
2070
2313
  }
2071
- dir = path7.dirname(dir);
2314
+ dir = path8.dirname(dir);
2072
2315
  }
2073
2316
  return null;
2074
2317
  }
2075
2318
  async function resolveRef(gitDir, refPath) {
2076
- const loosePath = path7.join(gitDir, refPath);
2319
+ const loosePath = path8.join(gitDir, refPath);
2077
2320
  try {
2078
2321
  const sha = (await fs3.readFile(loosePath, "utf8")).trim();
2079
2322
  if (/^[0-9a-f]{40}$/i.test(sha)) {
@@ -2081,7 +2324,7 @@ async function resolveRef(gitDir, refPath) {
2081
2324
  }
2082
2325
  } catch {
2083
2326
  }
2084
- const packedPath = path7.join(gitDir, "packed-refs");
2327
+ const packedPath = path8.join(gitDir, "packed-refs");
2085
2328
  try {
2086
2329
  const packed = await fs3.readFile(packedPath, "utf8");
2087
2330
  for (const line of packed.split("\n")) {
@@ -2123,6 +2366,10 @@ var ScanProgress = class {
2123
2366
  startTime = Date.now();
2124
2367
  isTTY;
2125
2368
  rootDir = "";
2369
+ /** Last rendered frame content (strip to compare for dirty-checking) */
2370
+ lastFrame = "";
2371
+ /** Whether we've hidden the cursor */
2372
+ cursorHidden = false;
2126
2373
  /** Estimated total scan duration in ms (from history or live calculation) */
2127
2374
  estimatedTotalMs = null;
2128
2375
  /** Per-step estimated durations from history */
@@ -2134,6 +2381,23 @@ var ScanProgress = class {
2134
2381
  constructor(rootDir) {
2135
2382
  this.isTTY = process.stderr.isTTY ?? false;
2136
2383
  this.rootDir = rootDir;
2384
+ if (this.isTTY) {
2385
+ const restore = () => {
2386
+ if (this.cursorHidden) {
2387
+ process.stderr.write("\x1B[?25h");
2388
+ this.cursorHidden = false;
2389
+ }
2390
+ };
2391
+ process.on("exit", restore);
2392
+ process.on("SIGINT", () => {
2393
+ restore();
2394
+ process.exit(130);
2395
+ });
2396
+ process.on("SIGTERM", () => {
2397
+ restore();
2398
+ process.exit(143);
2399
+ });
2400
+ }
2137
2401
  }
2138
2402
  /** Set the estimated total duration from scan history */
2139
2403
  setEstimatedTotal(estimatedMs) {
@@ -2192,11 +2456,12 @@ var ScanProgress = class {
2192
2456
  this.render();
2193
2457
  }
2194
2458
  /** Update sub-step progress for the active step (files processed, etc.) */
2195
- updateStepProgress(id, current, total) {
2459
+ updateStepProgress(id, current, total, label) {
2196
2460
  const step = this.steps.find((s) => s.id === id);
2197
2461
  if (step) {
2198
2462
  step.subProgress = current;
2199
2463
  if (total !== void 0) step.subTotal = total;
2464
+ if (label !== void 0) step.subLabel = label;
2200
2465
  }
2201
2466
  this.render();
2202
2467
  }
@@ -2231,7 +2496,18 @@ var ScanProgress = class {
2231
2496
  this.timer = null;
2232
2497
  }
2233
2498
  if (this.isTTY) {
2234
- this.clearLines();
2499
+ let buf = "";
2500
+ if (this.lastLineCount > 0) {
2501
+ buf += `\x1B[${this.lastLineCount}A`;
2502
+ for (let i = 0; i < this.lastLineCount; i++) {
2503
+ buf += "\x1B[2K\n";
2504
+ }
2505
+ buf += `\x1B[${this.lastLineCount}A`;
2506
+ }
2507
+ buf += "\x1B[?25h";
2508
+ if (buf) process.stderr.write(buf);
2509
+ this.cursorHidden = false;
2510
+ this.lastLineCount = 0;
2235
2511
  }
2236
2512
  const elapsed = this.formatElapsed(Date.now() - this.startTime);
2237
2513
  const doneCount = this.steps.filter((s) => s.status === "done").length;
@@ -2243,26 +2519,22 @@ var ScanProgress = class {
2243
2519
  }
2244
2520
  // ── Internal rendering ──
2245
2521
  startSpinner() {
2522
+ if (!this.cursorHidden) {
2523
+ process.stderr.write("\x1B[?25l");
2524
+ this.cursorHidden = true;
2525
+ }
2246
2526
  this.timer = setInterval(() => {
2247
2527
  this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
2248
2528
  this.render();
2249
- }, 80);
2529
+ }, 120);
2250
2530
  }
2251
2531
  clearLines() {
2252
- if (this.lastLineCount > 0) {
2253
- process.stderr.write(`\x1B[${this.lastLineCount}A`);
2254
- for (let i = 0; i < this.lastLineCount; i++) {
2255
- process.stderr.write("\x1B[2K\n");
2256
- }
2257
- process.stderr.write(`\x1B[${this.lastLineCount}A`);
2258
- }
2259
2532
  }
2260
2533
  render() {
2261
2534
  if (!this.isTTY) {
2262
2535
  this.renderCI();
2263
2536
  return;
2264
2537
  }
2265
- this.clearLines();
2266
2538
  const lines = [];
2267
2539
  lines.push("");
2268
2540
  lines.push(` ${ROBOT[0]} ${BRAND[0]}`);
@@ -2303,8 +2575,21 @@ var ScanProgress = class {
2303
2575
  lines.push("");
2304
2576
  lines.push(this.renderStats());
2305
2577
  lines.push("");
2306
- const output = lines.join("\n") + "\n";
2307
- process.stderr.write(output);
2578
+ const content = lines.join("\n") + "\n";
2579
+ if (content === this.lastFrame && this.lastLineCount === lines.length) {
2580
+ return;
2581
+ }
2582
+ this.lastFrame = content;
2583
+ let buf = "";
2584
+ if (this.lastLineCount > 0) {
2585
+ buf += `\x1B[${this.lastLineCount}A`;
2586
+ for (let i = 0; i < this.lastLineCount; i++) {
2587
+ buf += "\x1B[2K\n";
2588
+ }
2589
+ buf += `\x1B[${this.lastLineCount}A`;
2590
+ }
2591
+ buf += content;
2592
+ process.stderr.write(buf);
2308
2593
  this.lastLineCount = lines.length;
2309
2594
  }
2310
2595
  renderStep(step) {
@@ -2323,6 +2608,11 @@ var ScanProgress = class {
2323
2608
  if (step.subTotal && step.subTotal > 0 && step.subProgress !== void 0 && step.subProgress > 0) {
2324
2609
  detail = chalk4.dim(` \xB7 ${step.subProgress.toLocaleString()} / ${step.subTotal.toLocaleString()}`);
2325
2610
  }
2611
+ if (step.subLabel) {
2612
+ const maxLen = 50;
2613
+ const displayPath = step.subLabel.length > maxLen ? "\u2026" + step.subLabel.slice(-maxLen + 1) : step.subLabel;
2614
+ detail += chalk4.dim(` ${displayPath}`);
2615
+ }
2326
2616
  break;
2327
2617
  case "skipped":
2328
2618
  icon = chalk4.dim("\u25CC");
@@ -2428,11 +2718,11 @@ var ScanProgress = class {
2428
2718
 
2429
2719
  // src/ui/scan-history.ts
2430
2720
  import * as fs4 from "fs/promises";
2431
- import * as path8 from "path";
2721
+ import * as path9 from "path";
2432
2722
  var HISTORY_FILENAME = "scan_history.json";
2433
2723
  var MAX_RECORDS = 10;
2434
2724
  async function loadScanHistory(rootDir) {
2435
- const filePath = path8.join(rootDir, ".vibgrate", HISTORY_FILENAME);
2725
+ const filePath = path9.join(rootDir, ".vibgrate", HISTORY_FILENAME);
2436
2726
  try {
2437
2727
  const txt = await fs4.readFile(filePath, "utf8");
2438
2728
  const data = JSON.parse(txt);
@@ -2445,8 +2735,8 @@ async function loadScanHistory(rootDir) {
2445
2735
  }
2446
2736
  }
2447
2737
  async function saveScanHistory(rootDir, record) {
2448
- const dir = path8.join(rootDir, ".vibgrate");
2449
- const filePath = path8.join(dir, HISTORY_FILENAME);
2738
+ const dir = path9.join(rootDir, ".vibgrate");
2739
+ const filePath = path9.join(dir, HISTORY_FILENAME);
2450
2740
  let history;
2451
2741
  const existing = await loadScanHistory(rootDir);
2452
2742
  if (existing) {
@@ -2510,7 +2800,7 @@ function estimateStepDurations(history, currentFileCount) {
2510
2800
  }
2511
2801
 
2512
2802
  // src/scanners/platform-matrix.ts
2513
- import * as path9 from "path";
2803
+ import * as path10 from "path";
2514
2804
  var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
2515
2805
  // Image / media processing
2516
2806
  "sharp",
@@ -2790,7 +3080,7 @@ async function scanPlatformMatrix(rootDir, cache) {
2790
3080
  }
2791
3081
  result.dockerBaseImages = [...baseImages].sort();
2792
3082
  for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
2793
- const exists = cache ? await cache.pathExists(path9.join(rootDir, file)) : await pathExists(path9.join(rootDir, file));
3083
+ const exists = cache ? await cache.pathExists(path10.join(rootDir, file)) : await pathExists(path10.join(rootDir, file));
2794
3084
  if (exists) {
2795
3085
  result.nodeVersionFiles.push(file);
2796
3086
  }
@@ -2867,7 +3157,7 @@ function scanDependencyRisk(projects) {
2867
3157
  }
2868
3158
 
2869
3159
  // src/scanners/dependency-graph.ts
2870
- import * as path10 from "path";
3160
+ import * as path11 from "path";
2871
3161
  function parsePnpmLock(content) {
2872
3162
  const entries = [];
2873
3163
  const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
@@ -2926,9 +3216,9 @@ async function scanDependencyGraph(rootDir, cache) {
2926
3216
  phantomDependencies: []
2927
3217
  };
2928
3218
  let entries = [];
2929
- const pnpmLock = path10.join(rootDir, "pnpm-lock.yaml");
2930
- const npmLock = path10.join(rootDir, "package-lock.json");
2931
- const yarnLock = path10.join(rootDir, "yarn.lock");
3219
+ const pnpmLock = path11.join(rootDir, "pnpm-lock.yaml");
3220
+ const npmLock = path11.join(rootDir, "package-lock.json");
3221
+ const yarnLock = path11.join(rootDir, "yarn.lock");
2932
3222
  const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
2933
3223
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
2934
3224
  if (await _pathExists(pnpmLock)) {
@@ -2975,7 +3265,7 @@ async function scanDependencyGraph(rootDir, cache) {
2975
3265
  for (const pjPath of pkgFiles) {
2976
3266
  try {
2977
3267
  const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
2978
- const relPath = path10.relative(rootDir, pjPath);
3268
+ const relPath = path11.relative(rootDir, pjPath);
2979
3269
  for (const section of ["dependencies", "devDependencies"]) {
2980
3270
  const deps = pj[section];
2981
3271
  if (!deps) continue;
@@ -3321,7 +3611,7 @@ function scanToolingInventory(projects) {
3321
3611
  }
3322
3612
 
3323
3613
  // src/scanners/build-deploy.ts
3324
- import * as path11 from "path";
3614
+ import * as path12 from "path";
3325
3615
  var CI_FILES = {
3326
3616
  ".github/workflows": "github-actions",
3327
3617
  ".gitlab-ci.yml": "gitlab-ci",
@@ -3374,17 +3664,17 @@ async function scanBuildDeploy(rootDir, cache) {
3374
3664
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3375
3665
  const ciSystems = /* @__PURE__ */ new Set();
3376
3666
  for (const [file, system] of Object.entries(CI_FILES)) {
3377
- const fullPath = path11.join(rootDir, file);
3667
+ const fullPath = path12.join(rootDir, file);
3378
3668
  if (await _pathExists(fullPath)) {
3379
3669
  ciSystems.add(system);
3380
3670
  }
3381
3671
  }
3382
- const ghWorkflowDir = path11.join(rootDir, ".github", "workflows");
3672
+ const ghWorkflowDir = path12.join(rootDir, ".github", "workflows");
3383
3673
  if (await _pathExists(ghWorkflowDir)) {
3384
3674
  try {
3385
3675
  if (cache) {
3386
3676
  const entries = await cache.walkDir(rootDir);
3387
- const ghPrefix = path11.relative(rootDir, ghWorkflowDir) + path11.sep;
3677
+ const ghPrefix = path12.relative(rootDir, ghWorkflowDir) + path12.sep;
3388
3678
  result.ciWorkflowCount = entries.filter(
3389
3679
  (e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
3390
3680
  ).length;
@@ -3435,11 +3725,11 @@ async function scanBuildDeploy(rootDir, cache) {
3435
3725
  (name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
3436
3726
  );
3437
3727
  if (cfnFiles.length > 0) iacSystems.add("cloudformation");
3438
- if (await _pathExists(path11.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
3728
+ if (await _pathExists(path12.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
3439
3729
  result.iac = [...iacSystems].sort();
3440
3730
  const releaseTools = /* @__PURE__ */ new Set();
3441
3731
  for (const [file, tool] of Object.entries(RELEASE_FILES)) {
3442
- if (await _pathExists(path11.join(rootDir, file))) releaseTools.add(tool);
3732
+ if (await _pathExists(path12.join(rootDir, file))) releaseTools.add(tool);
3443
3733
  }
3444
3734
  const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
3445
3735
  for (const pjPath of pkgFiles) {
@@ -3464,19 +3754,19 @@ async function scanBuildDeploy(rootDir, cache) {
3464
3754
  };
3465
3755
  const managers = /* @__PURE__ */ new Set();
3466
3756
  for (const [file, manager] of Object.entries(lockfileMap)) {
3467
- if (await _pathExists(path11.join(rootDir, file))) managers.add(manager);
3757
+ if (await _pathExists(path12.join(rootDir, file))) managers.add(manager);
3468
3758
  }
3469
3759
  result.packageManagers = [...managers].sort();
3470
3760
  const monoTools = /* @__PURE__ */ new Set();
3471
3761
  for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
3472
- if (await _pathExists(path11.join(rootDir, file))) monoTools.add(tool);
3762
+ if (await _pathExists(path12.join(rootDir, file))) monoTools.add(tool);
3473
3763
  }
3474
3764
  result.monorepoTools = [...monoTools].sort();
3475
3765
  return result;
3476
3766
  }
3477
3767
 
3478
3768
  // src/scanners/ts-modernity.ts
3479
- import * as path12 from "path";
3769
+ import * as path13 from "path";
3480
3770
  async function scanTsModernity(rootDir, cache) {
3481
3771
  const result = {
3482
3772
  typescriptVersion: null,
@@ -3514,7 +3804,7 @@ async function scanTsModernity(rootDir, cache) {
3514
3804
  if (hasEsm && hasCjs) result.moduleType = "mixed";
3515
3805
  else if (hasEsm) result.moduleType = "esm";
3516
3806
  else if (hasCjs) result.moduleType = "cjs";
3517
- let tsConfigPath = path12.join(rootDir, "tsconfig.json");
3807
+ let tsConfigPath = path13.join(rootDir, "tsconfig.json");
3518
3808
  const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
3519
3809
  if (!tsConfigExists) {
3520
3810
  const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
@@ -3861,7 +4151,7 @@ function scanBreakingChangeExposure(projects) {
3861
4151
 
3862
4152
  // src/scanners/file-hotspots.ts
3863
4153
  import * as fs5 from "fs/promises";
3864
- import * as path13 from "path";
4154
+ import * as path14 from "path";
3865
4155
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3866
4156
  "node_modules",
3867
4157
  ".git",
@@ -3904,16 +4194,16 @@ async function scanFileHotspots(rootDir, cache) {
3904
4194
  const entries = await cache.walkDir(rootDir);
3905
4195
  for (const entry of entries) {
3906
4196
  if (!entry.isFile) continue;
3907
- const ext = path13.extname(entry.name).toLowerCase();
4197
+ const ext = path14.extname(entry.name).toLowerCase();
3908
4198
  if (SKIP_EXTENSIONS.has(ext)) continue;
3909
- const depth = entry.relPath.split(path13.sep).length - 1;
4199
+ const depth = entry.relPath.split(path14.sep).length - 1;
3910
4200
  if (depth > maxDepth) maxDepth = depth;
3911
4201
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3912
4202
  try {
3913
- const stat3 = await fs5.stat(entry.absPath);
4203
+ const stat4 = await fs5.stat(entry.absPath);
3914
4204
  allFiles.push({
3915
4205
  path: entry.relPath,
3916
- bytes: stat3.size
4206
+ bytes: stat4.size
3917
4207
  });
3918
4208
  } catch {
3919
4209
  }
@@ -3935,16 +4225,16 @@ async function scanFileHotspots(rootDir, cache) {
3935
4225
  for (const e of entries) {
3936
4226
  if (e.isDirectory) {
3937
4227
  if (SKIP_DIRS2.has(e.name)) continue;
3938
- await walk(path13.join(dir, e.name), depth + 1);
4228
+ await walk(path14.join(dir, e.name), depth + 1);
3939
4229
  } else if (e.isFile) {
3940
- const ext = path13.extname(e.name).toLowerCase();
4230
+ const ext = path14.extname(e.name).toLowerCase();
3941
4231
  if (SKIP_EXTENSIONS.has(ext)) continue;
3942
4232
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3943
4233
  try {
3944
- const stat3 = await fs5.stat(path13.join(dir, e.name));
4234
+ const stat4 = await fs5.stat(path14.join(dir, e.name));
3945
4235
  allFiles.push({
3946
- path: path13.relative(rootDir, path13.join(dir, e.name)),
3947
- bytes: stat3.size
4236
+ path: path14.relative(rootDir, path14.join(dir, e.name)),
4237
+ bytes: stat4.size
3948
4238
  });
3949
4239
  } catch {
3950
4240
  }
@@ -3966,7 +4256,7 @@ async function scanFileHotspots(rootDir, cache) {
3966
4256
  }
3967
4257
 
3968
4258
  // src/scanners/security-posture.ts
3969
- import * as path14 from "path";
4259
+ import * as path15 from "path";
3970
4260
  var LOCKFILES = {
3971
4261
  "pnpm-lock.yaml": "pnpm",
3972
4262
  "package-lock.json": "npm",
@@ -3987,14 +4277,14 @@ async function scanSecurityPosture(rootDir, cache) {
3987
4277
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3988
4278
  const foundLockfiles = [];
3989
4279
  for (const [file, type] of Object.entries(LOCKFILES)) {
3990
- if (await _pathExists(path14.join(rootDir, file))) {
4280
+ if (await _pathExists(path15.join(rootDir, file))) {
3991
4281
  foundLockfiles.push(type);
3992
4282
  }
3993
4283
  }
3994
4284
  result.lockfilePresent = foundLockfiles.length > 0;
3995
4285
  result.multipleLockfileTypes = foundLockfiles.length > 1;
3996
4286
  result.lockfileTypes = foundLockfiles.sort();
3997
- const gitignorePath = path14.join(rootDir, ".gitignore");
4287
+ const gitignorePath = path15.join(rootDir, ".gitignore");
3998
4288
  if (await _pathExists(gitignorePath)) {
3999
4289
  try {
4000
4290
  const content = await _readTextFile(gitignorePath);
@@ -4009,7 +4299,7 @@ async function scanSecurityPosture(rootDir, cache) {
4009
4299
  }
4010
4300
  }
4011
4301
  for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
4012
- if (await _pathExists(path14.join(rootDir, envFile))) {
4302
+ if (await _pathExists(path15.join(rootDir, envFile))) {
4013
4303
  if (!result.gitignoreCoversEnv) {
4014
4304
  result.envFilesTracked = true;
4015
4305
  break;
@@ -4434,7 +4724,7 @@ function scanServiceDependencies(projects) {
4434
4724
  }
4435
4725
 
4436
4726
  // src/scanners/architecture.ts
4437
- import * as path15 from "path";
4727
+ import * as path16 from "path";
4438
4728
  import * as fs6 from "fs/promises";
4439
4729
  var ARCHETYPE_SIGNALS = [
4440
4730
  // Meta-frameworks (highest priority — they imply routing patterns)
@@ -4733,9 +5023,9 @@ async function walkSourceFiles(rootDir, cache) {
4733
5023
  const entries = await cache.walkDir(rootDir);
4734
5024
  return entries.filter((e) => {
4735
5025
  if (!e.isFile) return false;
4736
- const name = path15.basename(e.absPath);
5026
+ const name = path16.basename(e.absPath);
4737
5027
  if (name.startsWith(".") && name !== ".") return false;
4738
- const ext = path15.extname(name);
5028
+ const ext = path16.extname(name);
4739
5029
  return SOURCE_EXTENSIONS.has(ext);
4740
5030
  }).map((e) => e.relPath);
4741
5031
  }
@@ -4749,15 +5039,15 @@ async function walkSourceFiles(rootDir, cache) {
4749
5039
  }
4750
5040
  for (const entry of entries) {
4751
5041
  if (entry.name.startsWith(".") && entry.name !== ".") continue;
4752
- const fullPath = path15.join(dir, entry.name);
5042
+ const fullPath = path16.join(dir, entry.name);
4753
5043
  if (entry.isDirectory()) {
4754
5044
  if (!IGNORE_DIRS.has(entry.name)) {
4755
5045
  await walk(fullPath);
4756
5046
  }
4757
5047
  } else if (entry.isFile()) {
4758
- const ext = path15.extname(entry.name);
5048
+ const ext = path16.extname(entry.name);
4759
5049
  if (SOURCE_EXTENSIONS.has(ext)) {
4760
- files.push(path15.relative(rootDir, fullPath));
5050
+ files.push(path16.relative(rootDir, fullPath));
4761
5051
  }
4762
5052
  }
4763
5053
  }
@@ -4781,7 +5071,7 @@ function classifyFile(filePath, archetype) {
4781
5071
  }
4782
5072
  }
4783
5073
  if (!bestMatch || bestMatch.confidence < 0.7) {
4784
- const baseName = path15.basename(filePath, path15.extname(filePath));
5074
+ const baseName = path16.basename(filePath, path16.extname(filePath));
4785
5075
  const cleanBase = baseName.replace(/\.(test|spec)$/, "");
4786
5076
  for (const rule of SUFFIX_RULES) {
4787
5077
  if (cleanBase.endsWith(rule.suffix)) {
@@ -4972,6 +5262,9 @@ async function runScan(rootDir, opts) {
4972
5262
  const sem = new Semaphore(opts.concurrency);
4973
5263
  const npmCache = new NpmCache(rootDir, sem);
4974
5264
  const fileCache = new FileCache();
5265
+ const excludePatterns = config.exclude ?? [];
5266
+ fileCache.setExcludePatterns(excludePatterns);
5267
+ fileCache.setMaxFileSize(config.maxFileSizeToScan ?? 5242880);
4975
5268
  const scanners = config.scanners;
4976
5269
  let filesScanned = 0;
4977
5270
  const progress = new ScanProgress(rootDir);
@@ -5001,7 +5294,7 @@ async function runScan(rootDir, opts) {
5001
5294
  progress.setSteps(steps);
5002
5295
  progress.completeStep("config", "loaded");
5003
5296
  progress.startStep("discovery");
5004
- const treeCount = await quickTreeCount(rootDir);
5297
+ const treeCount = await quickTreeCount(rootDir, excludePatterns);
5005
5298
  progress.updateStats({ treeSummary: treeCount });
5006
5299
  progress.completeStep(
5007
5300
  "discovery",
@@ -5016,8 +5309,8 @@ async function runScan(rootDir, opts) {
5016
5309
  const vcsDetail = vcs.type !== "unknown" ? `${vcs.type}${vcs.branch ? ` ${vcs.branch}` : ""}${vcs.shortSha ? ` @ ${vcs.shortSha}` : ""}` : "none detected";
5017
5310
  progress.completeStep("vcs", vcsDetail);
5018
5311
  progress.startStep("walk", treeCount.totalFiles);
5019
- await fileCache.walkDir(rootDir, (found) => {
5020
- progress.updateStepProgress("walk", found, treeCount.totalFiles);
5312
+ await fileCache.walkDir(rootDir, (found, currentPath) => {
5313
+ progress.updateStepProgress("walk", found, treeCount.totalFiles, currentPath);
5021
5314
  });
5022
5315
  progress.completeStep("walk", `${treeCount.totalFiles.toLocaleString()} files indexed`);
5023
5316
  progress.startStep("node");
@@ -5199,6 +5492,36 @@ async function runScan(rootDir, opts) {
5199
5492
  if (noteCount > 0) findingParts.push(`${noteCount} note${noteCount !== 1 ? "s" : ""}`);
5200
5493
  progress.completeStep("findings", findingParts.join(", ") || "none");
5201
5494
  progress.finish();
5495
+ const stuckPaths = fileCache.stuckPaths;
5496
+ const skippedLarge = fileCache.skippedLargeFiles;
5497
+ if (stuckPaths.length > 0) {
5498
+ console.log(
5499
+ chalk5.yellow(`
5500
+ \u26A0 ${stuckPaths.length} path${stuckPaths.length === 1 ? "" : "s"} timed out (>60s) and ${stuckPaths.length === 1 ? "was" : "were"} skipped:`)
5501
+ );
5502
+ for (const d of stuckPaths) {
5503
+ console.log(chalk5.dim(` \u2192 ${d}`));
5504
+ }
5505
+ const newExcludes = stuckPaths.map((d) => `${d}/**`);
5506
+ const updated = await appendExcludePatterns(rootDir, newExcludes);
5507
+ if (updated) {
5508
+ console.log(chalk5.green("\u2714") + ` Added ${newExcludes.length} pattern${newExcludes.length !== 1 ? "s" : ""} to exclude list in config`);
5509
+ }
5510
+ }
5511
+ if (skippedLarge.length > 0) {
5512
+ const sizeLimit = config.maxFileSizeToScan ?? 5242880;
5513
+ const sizeMB = (sizeLimit / 1048576).toFixed(0);
5514
+ console.log(
5515
+ chalk5.yellow(`
5516
+ \u26A0 ${skippedLarge.length} file${skippedLarge.length === 1 ? "" : "s"} skipped (>${sizeMB} MB):`)
5517
+ );
5518
+ for (const f of skippedLarge.slice(0, 10)) {
5519
+ console.log(chalk5.dim(` \u2192 ${f}`));
5520
+ }
5521
+ if (skippedLarge.length > 10) {
5522
+ console.log(chalk5.dim(` \u2026 and ${skippedLarge.length - 10} more`));
5523
+ }
5524
+ }
5202
5525
  fileCache.clear();
5203
5526
  if (allProjects.length === 0) {
5204
5527
  console.log(chalk5.yellow("No projects found."));
@@ -5216,7 +5539,7 @@ async function runScan(rootDir, opts) {
5216
5539
  schemaVersion: "1.0",
5217
5540
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5218
5541
  vibgrateVersion: VERSION,
5219
- rootPath: path16.basename(rootDir),
5542
+ rootPath: path17.basename(rootDir),
5220
5543
  ...vcs.type !== "unknown" ? { vcs } : {},
5221
5544
  projects: allProjects,
5222
5545
  drift,
@@ -5227,7 +5550,7 @@ async function runScan(rootDir, opts) {
5227
5550
  treeSummary: treeCount
5228
5551
  };
5229
5552
  if (opts.baseline) {
5230
- const baselinePath = path16.resolve(opts.baseline);
5553
+ const baselinePath = path17.resolve(opts.baseline);
5231
5554
  if (await pathExists(baselinePath)) {
5232
5555
  try {
5233
5556
  const baseline = await readJsonFile(baselinePath);
@@ -5238,9 +5561,9 @@ async function runScan(rootDir, opts) {
5238
5561
  }
5239
5562
  }
5240
5563
  }
5241
- const vibgrateDir = path16.join(rootDir, ".vibgrate");
5564
+ const vibgrateDir = path17.join(rootDir, ".vibgrate");
5242
5565
  await ensureDir(vibgrateDir);
5243
- await writeJsonFile(path16.join(vibgrateDir, "scan_result.json"), artifact);
5566
+ await writeJsonFile(path17.join(vibgrateDir, "scan_result.json"), artifact);
5244
5567
  await saveScanHistory(rootDir, {
5245
5568
  timestamp: artifact.timestamp,
5246
5569
  totalDurationMs: durationMs,
@@ -5250,10 +5573,10 @@ async function runScan(rootDir, opts) {
5250
5573
  });
5251
5574
  for (const project of allProjects) {
5252
5575
  if (project.drift && project.path) {
5253
- const projectDir = path16.resolve(rootDir, project.path);
5254
- const projectVibgrateDir = path16.join(projectDir, ".vibgrate");
5576
+ const projectDir = path17.resolve(rootDir, project.path);
5577
+ const projectVibgrateDir = path17.join(projectDir, ".vibgrate");
5255
5578
  await ensureDir(projectVibgrateDir);
5256
- await writeJsonFile(path16.join(projectVibgrateDir, "project_score.json"), {
5579
+ await writeJsonFile(path17.join(projectVibgrateDir, "project_score.json"), {
5257
5580
  projectId: project.projectId,
5258
5581
  name: project.name,
5259
5582
  type: project.type,
@@ -5270,7 +5593,7 @@ async function runScan(rootDir, opts) {
5270
5593
  if (opts.format === "json") {
5271
5594
  const jsonStr = JSON.stringify(artifact, null, 2);
5272
5595
  if (opts.out) {
5273
- await writeTextFile(path16.resolve(opts.out), jsonStr);
5596
+ await writeTextFile(path17.resolve(opts.out), jsonStr);
5274
5597
  console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
5275
5598
  } else {
5276
5599
  console.log(jsonStr);
@@ -5279,7 +5602,7 @@ async function runScan(rootDir, opts) {
5279
5602
  const sarif = formatSarif(artifact);
5280
5603
  const sarifStr = JSON.stringify(sarif, null, 2);
5281
5604
  if (opts.out) {
5282
- await writeTextFile(path16.resolve(opts.out), sarifStr);
5605
+ await writeTextFile(path17.resolve(opts.out), sarifStr);
5283
5606
  console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
5284
5607
  } else {
5285
5608
  console.log(sarifStr);
@@ -5288,7 +5611,7 @@ async function runScan(rootDir, opts) {
5288
5611
  const text = formatText(artifact);
5289
5612
  console.log(text);
5290
5613
  if (opts.out) {
5291
- await writeTextFile(path16.resolve(opts.out), text);
5614
+ await writeTextFile(path17.resolve(opts.out), text);
5292
5615
  }
5293
5616
  }
5294
5617
  return artifact;
@@ -5347,7 +5670,7 @@ async function autoPush(artifact, rootDir, opts) {
5347
5670
  }
5348
5671
  }
5349
5672
  var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").action(async (targetPath, opts) => {
5350
- const rootDir = path16.resolve(targetPath);
5673
+ const rootDir = path17.resolve(targetPath);
5351
5674
  if (!await pathExists(rootDir)) {
5352
5675
  console.error(chalk5.red(`Path does not exist: ${rootDir}`));
5353
5676
  process.exit(1);
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-GN3IWKSY.js";
5
5
  import {
6
6
  baselineCommand
7
- } from "./chunk-74QSNBZA.js";
7
+ } from "./chunk-IMK7DUPY.js";
8
8
  import {
9
9
  VERSION,
10
10
  dsnCommand,
@@ -15,7 +15,7 @@ import {
15
15
  readJsonFile,
16
16
  scanCommand,
17
17
  writeDefaultConfig
18
- } from "./chunk-L6R5WSCC.js";
18
+ } from "./chunk-JFMGFWKC.js";
19
19
 
20
20
  // src/cli.ts
21
21
  import { Command as Command4 } from "commander";
@@ -38,7 +38,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
38
38
  console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
39
39
  }
40
40
  if (opts.baseline) {
41
- const { runBaseline } = await import("./baseline-7WI3SI6H.js");
41
+ const { runBaseline } = await import("./baseline-IOXJPCX7.js");
42
42
  await runBaseline(rootDir);
43
43
  }
44
44
  console.log("");
package/dist/index.d.ts CHANGED
@@ -123,6 +123,9 @@ interface ScannersConfig {
123
123
  interface VibgrateConfig {
124
124
  include?: string[];
125
125
  exclude?: string[];
126
+ /** Maximum file size (bytes) the CLI will read during a scan. Files larger
127
+ * than this are silently skipped. Default: 5 242 880 (5 MB). */
128
+ maxFileSizeToScan?: number;
126
129
  scanners?: ScannersConfig | false;
127
130
  thresholds?: {
128
131
  failOnError?: {
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  formatText,
8
8
  generateFindings,
9
9
  runScan
10
- } from "./chunk-L6R5WSCC.js";
10
+ } from "./chunk-JFMGFWKC.js";
11
11
  export {
12
12
  computeDriftScore,
13
13
  formatMarkdown,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibgrate/cli",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "CLI for measuring upgrade drift across Node & .NET projects",
5
5
  "type": "module",
6
6
  "bin": {