@vibgrate/cli 1.0.15 → 1.0.17

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-OPEOSIRY.js";
5
- import "./chunk-QZV77UWV.js";
4
+ } from "./chunk-XVGUFKAC.js";
5
+ import "./chunk-EVLKMTYE.js";
6
6
  export {
7
7
  baselineCommand,
8
8
  runBaseline
@@ -23,7 +23,7 @@ var Semaphore = class {
23
23
  this.available--;
24
24
  return Promise.resolve();
25
25
  }
26
- return new Promise((resolve6) => this.queue.push(resolve6));
26
+ return new Promise((resolve7) => this.queue.push(resolve7));
27
27
  }
28
28
  release() {
29
29
  const next = this.queue.shift();
@@ -50,6 +50,140 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
50
50
  "packages",
51
51
  "TestResults"
52
52
  ]);
53
+ var TEXT_CACHE_MAX_BYTES = 1048576;
54
+ var FileCache = class _FileCache {
55
+ /** Directory walk results keyed by rootDir */
56
+ walkCache = /* @__PURE__ */ new Map();
57
+ /** File content keyed by absolute path (only files ≤ TEXT_CACHE_MAX_BYTES) */
58
+ textCache = /* @__PURE__ */ new Map();
59
+ /** Parsed JSON keyed by absolute path */
60
+ jsonCache = /* @__PURE__ */ new Map();
61
+ /** pathExists keyed by absolute path */
62
+ existsCache = /* @__PURE__ */ new Map();
63
+ // ── Directory walking ──
64
+ /**
65
+ * Walk the directory tree from `rootDir` once, skipping SKIP_DIRS plus
66
+ * common framework output dirs (.nuxt, .output, .svelte-kit).
67
+ *
68
+ * The result is memoised so every scanner filters the same array.
69
+ * Consumers that need additional filtering (e.g. SOURCE_EXTENSIONS,
70
+ * SKIP_EXTENSIONS) do so on the returned entries — no separate walk.
71
+ */
72
+ walkDir(rootDir) {
73
+ const cached = this.walkCache.get(rootDir);
74
+ if (cached) return cached;
75
+ const promise = this._doWalk(rootDir);
76
+ this.walkCache.set(rootDir, promise);
77
+ return promise;
78
+ }
79
+ /** Additional dirs skipped only by the cached walk (framework outputs) */
80
+ static EXTRA_SKIP = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
81
+ async _doWalk(rootDir) {
82
+ const results = [];
83
+ const maxConcurrentReads = Math.max(8, Math.min(64, os.availableParallelism() * 4));
84
+ const sem = new Semaphore(maxConcurrentReads);
85
+ const extraSkip = _FileCache.EXTRA_SKIP;
86
+ async function walk(dir) {
87
+ let entries;
88
+ try {
89
+ entries = await fs.readdir(dir, { withFileTypes: true });
90
+ } catch {
91
+ return;
92
+ }
93
+ const subWalks = [];
94
+ for (const e of entries) {
95
+ const absPath = path.join(dir, e.name);
96
+ const relPath = path.relative(rootDir, absPath);
97
+ if (e.isDirectory()) {
98
+ if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
99
+ results.push({ absPath, relPath, name: e.name, isFile: false, isDirectory: true });
100
+ subWalks.push(sem.run(() => walk(absPath)));
101
+ } else if (e.isFile()) {
102
+ results.push({ absPath, relPath, name: e.name, isFile: true, isDirectory: false });
103
+ }
104
+ }
105
+ await Promise.all(subWalks);
106
+ }
107
+ await sem.run(() => walk(rootDir));
108
+ return results;
109
+ }
110
+ /**
111
+ * Find files matching a predicate from the cached walk.
112
+ * Returns absolute paths (same contract as the standalone `findFiles`).
113
+ */
114
+ async findFiles(rootDir, predicate) {
115
+ const entries = await this.walkDir(rootDir);
116
+ return entries.filter((e) => e.isFile && predicate(e.name)).map((e) => e.absPath);
117
+ }
118
+ async findPackageJsonFiles(rootDir) {
119
+ return this.findFiles(rootDir, (name) => name === "package.json");
120
+ }
121
+ async findCsprojFiles(rootDir) {
122
+ return this.findFiles(rootDir, (name) => name.endsWith(".csproj"));
123
+ }
124
+ async findSolutionFiles(rootDir) {
125
+ return this.findFiles(rootDir, (name) => name.endsWith(".sln"));
126
+ }
127
+ // ── File content reading ──
128
+ /**
129
+ * Read a text file. Files ≤ 1 MB are cached so subsequent calls from
130
+ * different scanners return the same string. Files > 1 MB (lockfiles,
131
+ * large generated files) are read directly and never retained.
132
+ */
133
+ readTextFile(filePath) {
134
+ const abs = path.resolve(filePath);
135
+ const cached = this.textCache.get(abs);
136
+ if (cached) return cached;
137
+ const promise = fs.readFile(abs, "utf8").then((content) => {
138
+ if (content.length > TEXT_CACHE_MAX_BYTES) {
139
+ this.textCache.delete(abs);
140
+ }
141
+ return content;
142
+ });
143
+ this.textCache.set(abs, promise);
144
+ return promise;
145
+ }
146
+ /**
147
+ * Read and parse a JSON file. The parsed object is cached; the raw
148
+ * text is evicted immediately so we never hold both representations.
149
+ */
150
+ readJsonFile(filePath) {
151
+ const abs = path.resolve(filePath);
152
+ const cached = this.jsonCache.get(abs);
153
+ if (cached) return cached;
154
+ const promise = this.readTextFile(abs).then((txt) => {
155
+ this.textCache.delete(abs);
156
+ return JSON.parse(txt);
157
+ });
158
+ this.jsonCache.set(abs, promise);
159
+ return promise;
160
+ }
161
+ // ── Existence checks ──
162
+ pathExists(p) {
163
+ const abs = path.resolve(p);
164
+ const cached = this.existsCache.get(abs);
165
+ if (cached) return cached;
166
+ const promise = fs.access(abs).then(() => true, () => false);
167
+ this.existsCache.set(abs, promise);
168
+ return promise;
169
+ }
170
+ // ── Lifecycle ──
171
+ /** Release all cached data. Call after the scan completes. */
172
+ clear() {
173
+ this.walkCache.clear();
174
+ this.textCache.clear();
175
+ this.jsonCache.clear();
176
+ this.existsCache.clear();
177
+ }
178
+ /** Number of file content entries currently held */
179
+ get textCacheSize() {
180
+ return this.textCache.size;
181
+ }
182
+ /** Number of parsed JSON entries currently held */
183
+ get jsonCacheSize() {
184
+ return this.jsonCache.size;
185
+ }
186
+ };
53
187
  async function findFiles(rootDir, predicate) {
54
188
  const results = [];
55
189
  const maxConcurrentReads = Math.max(8, Math.min(64, os.availableParallelism() * 4));
@@ -396,6 +530,9 @@ function formatText(artifact) {
396
530
  lines.push("");
397
531
  }
398
532
  }
533
+ if (artifact.extended?.architecture) {
534
+ lines.push(...formatArchitectureDiagram(artifact.extended.architecture));
535
+ }
399
536
  const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
400
537
  lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
401
538
  lines.push(chalk.bold.cyan("\u2551 Drift Score Summary \u2551"));
@@ -585,9 +722,6 @@ function formatExtended(ext) {
585
722
  lines.push("");
586
723
  }
587
724
  }
588
- if (ext.architecture) {
589
- lines.push(...formatArchitectureDiagram(ext.architecture));
590
- }
591
725
  return lines;
592
726
  }
593
727
  var LAYER_LABELS = {
@@ -1186,7 +1320,7 @@ function maxStable(versions) {
1186
1320
  return stable.sort(semver.rcompare)[0] ?? null;
1187
1321
  }
1188
1322
  async function npmViewJson(args, cwd) {
1189
- return new Promise((resolve6, reject) => {
1323
+ return new Promise((resolve7, reject) => {
1190
1324
  const child = spawn("npm", ["view", ...args, "--json"], {
1191
1325
  cwd,
1192
1326
  stdio: ["ignore", "pipe", "pipe"]
@@ -1203,13 +1337,13 @@ async function npmViewJson(args, cwd) {
1203
1337
  }
1204
1338
  const trimmed = out.trim();
1205
1339
  if (!trimmed) {
1206
- resolve6(null);
1340
+ resolve7(null);
1207
1341
  return;
1208
1342
  }
1209
1343
  try {
1210
- resolve6(JSON.parse(trimmed));
1344
+ resolve7(JSON.parse(trimmed));
1211
1345
  } catch {
1212
- resolve6(trimmed.replace(/^"|"$/g, ""));
1346
+ resolve7(trimmed.replace(/^"|"$/g, ""));
1213
1347
  }
1214
1348
  });
1215
1349
  });
@@ -1345,12 +1479,12 @@ var KNOWN_FRAMEWORKS = {
1345
1479
  "storybook": "Storybook",
1346
1480
  "@storybook/react": "Storybook"
1347
1481
  };
1348
- async function scanNodeProjects(rootDir, npmCache) {
1349
- const packageJsonFiles = await findPackageJsonFiles(rootDir);
1482
+ async function scanNodeProjects(rootDir, npmCache, cache) {
1483
+ const packageJsonFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
1350
1484
  const results = [];
1351
1485
  for (const pjPath of packageJsonFiles) {
1352
1486
  try {
1353
- const scan = await scanOnePackageJson(pjPath, rootDir, npmCache);
1487
+ const scan = await scanOnePackageJson(pjPath, rootDir, npmCache, cache);
1354
1488
  results.push(scan);
1355
1489
  } catch (e) {
1356
1490
  const msg = e instanceof Error ? e.message : String(e);
@@ -1359,8 +1493,8 @@ async function scanNodeProjects(rootDir, npmCache) {
1359
1493
  }
1360
1494
  return results;
1361
1495
  }
1362
- async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
1363
- const pj = await readJsonFile(packageJsonPath);
1496
+ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1497
+ const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
1364
1498
  const absProjectPath = path4.dirname(packageJsonPath);
1365
1499
  const projectPath = path4.relative(rootDir, absProjectPath) || ".";
1366
1500
  const nodeEngine = pj.engines?.node ?? void 0;
@@ -1687,13 +1821,13 @@ function parseCsproj(xml, filePath) {
1687
1821
  projectName: path5.basename(filePath, ".csproj")
1688
1822
  };
1689
1823
  }
1690
- async function scanDotnetProjects(rootDir) {
1691
- const csprojFiles = await findCsprojFiles(rootDir);
1692
- const slnFiles = await findSolutionFiles(rootDir);
1824
+ async function scanDotnetProjects(rootDir, cache) {
1825
+ const csprojFiles = cache ? await cache.findCsprojFiles(rootDir) : await findCsprojFiles(rootDir);
1826
+ const slnFiles = cache ? await cache.findSolutionFiles(rootDir) : await findSolutionFiles(rootDir);
1693
1827
  const slnCsprojPaths = /* @__PURE__ */ new Set();
1694
1828
  for (const slnPath of slnFiles) {
1695
1829
  try {
1696
- const slnContent = await readTextFile(slnPath);
1830
+ const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
1697
1831
  const slnDir = path5.dirname(slnPath);
1698
1832
  const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
1699
1833
  let match;
@@ -1710,7 +1844,7 @@ async function scanDotnetProjects(rootDir) {
1710
1844
  const results = [];
1711
1845
  for (const csprojPath of allCsprojFiles) {
1712
1846
  try {
1713
- const scan = await scanOneCsproj(csprojPath, rootDir);
1847
+ const scan = await scanOneCsproj(csprojPath, rootDir, cache);
1714
1848
  results.push(scan);
1715
1849
  } catch (e) {
1716
1850
  const msg = e instanceof Error ? e.message : String(e);
@@ -1719,8 +1853,8 @@ async function scanDotnetProjects(rootDir) {
1719
1853
  }
1720
1854
  return results;
1721
1855
  }
1722
- async function scanOneCsproj(csprojPath, rootDir) {
1723
- const xml = await readTextFile(csprojPath);
1856
+ async function scanOneCsproj(csprojPath, rootDir, cache) {
1857
+ const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
1724
1858
  const data = parseCsproj(xml, csprojPath);
1725
1859
  const primaryTfm = data.targetFrameworks[0];
1726
1860
  let runtimeMajorsBehind;
@@ -2328,7 +2462,7 @@ var OS_PATTERNS = [
2328
2462
  { pattern: /\bbash\b|#!\/bin\/bash/i, label: "bash-scripts" },
2329
2463
  { pattern: /\\\\/g, label: "backslash-paths" }
2330
2464
  ];
2331
- async function scanPlatformMatrix(rootDir) {
2465
+ async function scanPlatformMatrix(rootDir, cache) {
2332
2466
  const result = {
2333
2467
  dotnetTargetFrameworks: [],
2334
2468
  nativeModules: [],
@@ -2336,12 +2470,12 @@ async function scanPlatformMatrix(rootDir) {
2336
2470
  dockerBaseImages: [],
2337
2471
  nodeVersionFiles: []
2338
2472
  };
2339
- const pkgFiles = await findPackageJsonFiles(rootDir);
2473
+ const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
2340
2474
  const allDeps = /* @__PURE__ */ new Set();
2341
2475
  const osAssumptions = /* @__PURE__ */ new Set();
2342
2476
  for (const pjPath of pkgFiles) {
2343
2477
  try {
2344
- const pj = await readJsonFile(pjPath);
2478
+ const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
2345
2479
  if (pj.engines?.node && !result.nodeEngines) result.nodeEngines = pj.engines.node;
2346
2480
  if (pj.engines?.npm && !result.npmEngines) result.npmEngines = pj.engines.npm;
2347
2481
  if (pj.engines?.pnpm && !result.pnpmEngines) {
@@ -2377,11 +2511,11 @@ async function scanPlatformMatrix(rootDir) {
2377
2511
  }
2378
2512
  result.nativeModules.sort();
2379
2513
  result.osAssumptions = [...osAssumptions].sort();
2380
- const csprojFiles = await findFiles(rootDir, (name) => name.endsWith(".csproj"));
2514
+ const csprojFiles = cache ? await cache.findCsprojFiles(rootDir) : await findFiles(rootDir, (name) => name.endsWith(".csproj"));
2381
2515
  const tfms = /* @__PURE__ */ new Set();
2382
2516
  for (const csprojPath of csprojFiles) {
2383
2517
  try {
2384
- const xml = await readTextFile(csprojPath);
2518
+ const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
2385
2519
  const tfMatch = xml.match(/<TargetFramework>(.*?)<\/TargetFramework>/);
2386
2520
  if (tfMatch?.[1]) tfms.add(tfMatch[1]);
2387
2521
  const tfsMatch = xml.match(/<TargetFrameworks>(.*?)<\/TargetFrameworks>/);
@@ -2394,14 +2528,17 @@ async function scanPlatformMatrix(rootDir) {
2394
2528
  }
2395
2529
  }
2396
2530
  result.dotnetTargetFrameworks = [...tfms].sort();
2397
- const dockerfiles = await findFiles(
2531
+ const dockerfiles = cache ? await cache.findFiles(
2532
+ rootDir,
2533
+ (name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
2534
+ ) : await findFiles(
2398
2535
  rootDir,
2399
2536
  (name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
2400
2537
  );
2401
2538
  const baseImages = /* @__PURE__ */ new Set();
2402
2539
  for (const df of dockerfiles) {
2403
2540
  try {
2404
- const content = await readTextFile(df);
2541
+ const content = cache ? await cache.readTextFile(df) : await readTextFile(df);
2405
2542
  for (const line of content.split("\n")) {
2406
2543
  const trimmed = line.trim();
2407
2544
  if (/^FROM\s+/i.test(trimmed)) {
@@ -2419,7 +2556,8 @@ async function scanPlatformMatrix(rootDir) {
2419
2556
  }
2420
2557
  result.dockerBaseImages = [...baseImages].sort();
2421
2558
  for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
2422
- if (await pathExists(path8.join(rootDir, file))) {
2559
+ const exists = cache ? await cache.pathExists(path8.join(rootDir, file)) : await pathExists(path8.join(rootDir, file));
2560
+ if (exists) {
2423
2561
  result.nodeVersionFiles.push(file);
2424
2562
  }
2425
2563
  }
@@ -2545,7 +2683,7 @@ function parseYarnLock(content) {
2545
2683
  }
2546
2684
  return entries;
2547
2685
  }
2548
- async function scanDependencyGraph(rootDir) {
2686
+ async function scanDependencyGraph(rootDir, cache) {
2549
2687
  const result = {
2550
2688
  lockfileType: null,
2551
2689
  totalUnique: 0,
@@ -2557,17 +2695,19 @@ async function scanDependencyGraph(rootDir) {
2557
2695
  const pnpmLock = path9.join(rootDir, "pnpm-lock.yaml");
2558
2696
  const npmLock = path9.join(rootDir, "package-lock.json");
2559
2697
  const yarnLock = path9.join(rootDir, "yarn.lock");
2560
- if (await pathExists(pnpmLock)) {
2698
+ const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
2699
+ const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
2700
+ if (await _pathExists(pnpmLock)) {
2561
2701
  result.lockfileType = "pnpm";
2562
- const content = await readTextFile(pnpmLock);
2702
+ const content = await _readTextFile(pnpmLock);
2563
2703
  entries = parsePnpmLock(content);
2564
- } else if (await pathExists(npmLock)) {
2704
+ } else if (await _pathExists(npmLock)) {
2565
2705
  result.lockfileType = "npm";
2566
- const content = await readTextFile(npmLock);
2706
+ const content = await _readTextFile(npmLock);
2567
2707
  entries = parseNpmLock(content);
2568
- } else if (await pathExists(yarnLock)) {
2708
+ } else if (await _pathExists(yarnLock)) {
2569
2709
  result.lockfileType = "yarn";
2570
- const content = await readTextFile(yarnLock);
2710
+ const content = await _readTextFile(yarnLock);
2571
2711
  entries = parseYarnLock(content);
2572
2712
  }
2573
2713
  if (entries.length === 0) return result;
@@ -2595,12 +2735,12 @@ async function scanDependencyGraph(rootDir) {
2595
2735
  duplicated.sort((a, b) => b.versions.length - a.versions.length || a.name.localeCompare(b.name));
2596
2736
  result.duplicatedPackages = duplicated;
2597
2737
  const lockedNames = new Set(versionMap.keys());
2598
- const pkgFiles = await findPackageJsonFiles(rootDir);
2738
+ const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
2599
2739
  const phantoms = /* @__PURE__ */ new Set();
2600
2740
  const phantomDetails = [];
2601
2741
  for (const pjPath of pkgFiles) {
2602
2742
  try {
2603
- const pj = await readJsonFile(pjPath);
2743
+ const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
2604
2744
  const relPath = path9.relative(rootDir, pjPath);
2605
2745
  for (const section of ["dependencies", "devDependencies"]) {
2606
2746
  const deps = pj[section];
@@ -2985,7 +3125,7 @@ var IAC_EXTENSIONS = {
2985
3125
  ".tf": "terraform",
2986
3126
  ".bicep": "bicep"
2987
3127
  };
2988
- async function scanBuildDeploy(rootDir) {
3128
+ async function scanBuildDeploy(rootDir, cache) {
2989
3129
  const result = {
2990
3130
  ci: [],
2991
3131
  ciWorkflowCount: 0,
@@ -2995,26 +3135,37 @@ async function scanBuildDeploy(rootDir) {
2995
3135
  packageManagers: [],
2996
3136
  monorepoTools: []
2997
3137
  };
3138
+ const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
3139
+ const _findFiles = cache ? (dir, pred) => cache.findFiles(dir, pred) : findFiles;
3140
+ const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
2998
3141
  const ciSystems = /* @__PURE__ */ new Set();
2999
3142
  for (const [file, system] of Object.entries(CI_FILES)) {
3000
3143
  const fullPath = path10.join(rootDir, file);
3001
- if (await pathExists(fullPath)) {
3144
+ if (await _pathExists(fullPath)) {
3002
3145
  ciSystems.add(system);
3003
3146
  }
3004
3147
  }
3005
3148
  const ghWorkflowDir = path10.join(rootDir, ".github", "workflows");
3006
- if (await pathExists(ghWorkflowDir)) {
3149
+ if (await _pathExists(ghWorkflowDir)) {
3007
3150
  try {
3008
- const files = await findFiles(
3009
- ghWorkflowDir,
3010
- (name) => name.endsWith(".yml") || name.endsWith(".yaml")
3011
- );
3012
- result.ciWorkflowCount = files.length;
3151
+ if (cache) {
3152
+ const entries = await cache.walkDir(rootDir);
3153
+ const ghPrefix = path10.relative(rootDir, ghWorkflowDir) + path10.sep;
3154
+ result.ciWorkflowCount = entries.filter(
3155
+ (e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
3156
+ ).length;
3157
+ } else {
3158
+ const files = await _findFiles(
3159
+ ghWorkflowDir,
3160
+ (name) => name.endsWith(".yml") || name.endsWith(".yaml")
3161
+ );
3162
+ result.ciWorkflowCount = files.length;
3163
+ }
3013
3164
  } catch {
3014
3165
  }
3015
3166
  }
3016
3167
  result.ci = [...ciSystems].sort();
3017
- const dockerfiles = await findFiles(
3168
+ const dockerfiles = await _findFiles(
3018
3169
  rootDir,
3019
3170
  (name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
3020
3171
  );
@@ -3022,7 +3173,7 @@ async function scanBuildDeploy(rootDir) {
3022
3173
  const baseImages = /* @__PURE__ */ new Set();
3023
3174
  for (const df of dockerfiles) {
3024
3175
  try {
3025
- const content = await readTextFile(df);
3176
+ const content = await _readTextFile(df);
3026
3177
  for (const line of content.split("\n")) {
3027
3178
  const trimmed = line.trim();
3028
3179
  if (/^FROM\s+/i.test(trimmed)) {
@@ -3042,24 +3193,24 @@ async function scanBuildDeploy(rootDir) {
3042
3193
  result.docker.baseImages = [...baseImages].sort();
3043
3194
  const iacSystems = /* @__PURE__ */ new Set();
3044
3195
  for (const [ext, system] of Object.entries(IAC_EXTENSIONS)) {
3045
- const files = await findFiles(rootDir, (name) => name.endsWith(ext));
3196
+ const files = await _findFiles(rootDir, (name) => name.endsWith(ext));
3046
3197
  if (files.length > 0) iacSystems.add(system);
3047
3198
  }
3048
- const cfnFiles = await findFiles(
3199
+ const cfnFiles = await _findFiles(
3049
3200
  rootDir,
3050
3201
  (name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
3051
3202
  );
3052
3203
  if (cfnFiles.length > 0) iacSystems.add("cloudformation");
3053
- if (await pathExists(path10.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
3204
+ if (await _pathExists(path10.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
3054
3205
  result.iac = [...iacSystems].sort();
3055
3206
  const releaseTools = /* @__PURE__ */ new Set();
3056
3207
  for (const [file, tool] of Object.entries(RELEASE_FILES)) {
3057
- if (await pathExists(path10.join(rootDir, file))) releaseTools.add(tool);
3208
+ if (await _pathExists(path10.join(rootDir, file))) releaseTools.add(tool);
3058
3209
  }
3059
- const pkgFiles = await findPackageJsonFiles(rootDir);
3210
+ const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
3060
3211
  for (const pjPath of pkgFiles) {
3061
3212
  try {
3062
- const pj = await readJsonFile(pjPath);
3213
+ const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
3063
3214
  for (const section of ["dependencies", "devDependencies"]) {
3064
3215
  const deps = pj[section];
3065
3216
  if (!deps) continue;
@@ -3079,12 +3230,12 @@ async function scanBuildDeploy(rootDir) {
3079
3230
  };
3080
3231
  const managers = /* @__PURE__ */ new Set();
3081
3232
  for (const [file, manager] of Object.entries(lockfileMap)) {
3082
- if (await pathExists(path10.join(rootDir, file))) managers.add(manager);
3233
+ if (await _pathExists(path10.join(rootDir, file))) managers.add(manager);
3083
3234
  }
3084
3235
  result.packageManagers = [...managers].sort();
3085
3236
  const monoTools = /* @__PURE__ */ new Set();
3086
3237
  for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
3087
- if (await pathExists(path10.join(rootDir, file))) monoTools.add(tool);
3238
+ if (await _pathExists(path10.join(rootDir, file))) monoTools.add(tool);
3088
3239
  }
3089
3240
  result.monorepoTools = [...monoTools].sort();
3090
3241
  return result;
@@ -3092,7 +3243,7 @@ async function scanBuildDeploy(rootDir) {
3092
3243
 
3093
3244
  // src/scanners/ts-modernity.ts
3094
3245
  import * as path11 from "path";
3095
- async function scanTsModernity(rootDir) {
3246
+ async function scanTsModernity(rootDir, cache) {
3096
3247
  const result = {
3097
3248
  typescriptVersion: null,
3098
3249
  strict: null,
@@ -3104,12 +3255,12 @@ async function scanTsModernity(rootDir) {
3104
3255
  moduleType: null,
3105
3256
  exportsField: false
3106
3257
  };
3107
- const pkgFiles = await findPackageJsonFiles(rootDir);
3258
+ const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
3108
3259
  let hasEsm = false;
3109
3260
  let hasCjs = false;
3110
3261
  for (const pjPath of pkgFiles) {
3111
3262
  try {
3112
- const pj = await readJsonFile(pjPath);
3263
+ const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
3113
3264
  if (!result.typescriptVersion) {
3114
3265
  const tsVer = pj.devDependencies?.["typescript"] ?? pj.dependencies?.["typescript"];
3115
3266
  if (tsVer) {
@@ -3130,8 +3281,9 @@ async function scanTsModernity(rootDir) {
3130
3281
  else if (hasEsm) result.moduleType = "esm";
3131
3282
  else if (hasCjs) result.moduleType = "cjs";
3132
3283
  let tsConfigPath = path11.join(rootDir, "tsconfig.json");
3133
- if (!await pathExists(tsConfigPath)) {
3134
- const tsConfigs = await findFiles(rootDir, (name) => name === "tsconfig.json");
3284
+ const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
3285
+ if (!tsConfigExists) {
3286
+ const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
3135
3287
  if (tsConfigs.length > 0) {
3136
3288
  tsConfigPath = tsConfigs[0];
3137
3289
  } else {
@@ -3139,7 +3291,7 @@ async function scanTsModernity(rootDir) {
3139
3291
  }
3140
3292
  }
3141
3293
  try {
3142
- const raw = await readTextFile(tsConfigPath);
3294
+ const raw = cache ? await cache.readTextFile(tsConfigPath) : await readTextFile(tsConfigPath);
3143
3295
  const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
3144
3296
  const tsConfig = JSON.parse(stripped);
3145
3297
  const co = tsConfig?.compilerOptions;
@@ -3510,43 +3662,63 @@ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
3510
3662
  ".mp4",
3511
3663
  ".webm"
3512
3664
  ]);
3513
- async function scanFileHotspots(rootDir) {
3665
+ async function scanFileHotspots(rootDir, cache) {
3514
3666
  const extensionCounts = {};
3515
3667
  const allFiles = [];
3516
3668
  let maxDepth = 0;
3517
- async function walk(dir, depth) {
3518
- if (depth > maxDepth) maxDepth = depth;
3519
- let entries;
3520
- try {
3521
- const dirents = await fs4.readdir(dir, { withFileTypes: true });
3522
- entries = dirents.map((d) => ({
3523
- name: d.name,
3524
- isDirectory: d.isDirectory(),
3525
- isFile: d.isFile()
3526
- }));
3527
- } catch {
3528
- return;
3669
+ if (cache) {
3670
+ const entries = await cache.walkDir(rootDir);
3671
+ for (const entry of entries) {
3672
+ if (!entry.isFile) continue;
3673
+ const ext = path12.extname(entry.name).toLowerCase();
3674
+ if (SKIP_EXTENSIONS.has(ext)) continue;
3675
+ const depth = entry.relPath.split(path12.sep).length - 1;
3676
+ if (depth > maxDepth) maxDepth = depth;
3677
+ extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3678
+ try {
3679
+ const stat3 = await fs4.stat(entry.absPath);
3680
+ allFiles.push({
3681
+ path: entry.relPath,
3682
+ bytes: stat3.size
3683
+ });
3684
+ } catch {
3685
+ }
3529
3686
  }
3530
- for (const e of entries) {
3531
- if (e.isDirectory) {
3532
- if (SKIP_DIRS2.has(e.name)) continue;
3533
- await walk(path12.join(dir, e.name), depth + 1);
3534
- } else if (e.isFile) {
3535
- const ext = path12.extname(e.name).toLowerCase();
3536
- if (SKIP_EXTENSIONS.has(ext)) continue;
3537
- extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3538
- try {
3539
- const stat3 = await fs4.stat(path12.join(dir, e.name));
3540
- allFiles.push({
3541
- path: path12.relative(rootDir, path12.join(dir, e.name)),
3542
- bytes: stat3.size
3543
- });
3544
- } catch {
3687
+ } else {
3688
+ async function walk(dir, depth) {
3689
+ if (depth > maxDepth) maxDepth = depth;
3690
+ let entries;
3691
+ try {
3692
+ const dirents = await fs4.readdir(dir, { withFileTypes: true });
3693
+ entries = dirents.map((d) => ({
3694
+ name: d.name,
3695
+ isDirectory: d.isDirectory(),
3696
+ isFile: d.isFile()
3697
+ }));
3698
+ } catch {
3699
+ return;
3700
+ }
3701
+ for (const e of entries) {
3702
+ if (e.isDirectory) {
3703
+ if (SKIP_DIRS2.has(e.name)) continue;
3704
+ await walk(path12.join(dir, e.name), depth + 1);
3705
+ } else if (e.isFile) {
3706
+ const ext = path12.extname(e.name).toLowerCase();
3707
+ if (SKIP_EXTENSIONS.has(ext)) continue;
3708
+ extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3709
+ try {
3710
+ const stat3 = await fs4.stat(path12.join(dir, e.name));
3711
+ allFiles.push({
3712
+ path: path12.relative(rootDir, path12.join(dir, e.name)),
3713
+ bytes: stat3.size
3714
+ });
3715
+ } catch {
3716
+ }
3545
3717
  }
3546
3718
  }
3547
3719
  }
3720
+ await walk(rootDir, 0);
3548
3721
  }
3549
- await walk(rootDir, 0);
3550
3722
  allFiles.sort((a, b) => b.bytes - a.bytes);
3551
3723
  const largestFiles = allFiles.slice(0, 20);
3552
3724
  return {
@@ -3568,7 +3740,7 @@ var LOCKFILES = {
3568
3740
  "bun.lockb": "bun",
3569
3741
  "packages.lock.json": "nuget"
3570
3742
  };
3571
- async function scanSecurityPosture(rootDir) {
3743
+ async function scanSecurityPosture(rootDir, cache) {
3572
3744
  const result = {
3573
3745
  lockfilePresent: false,
3574
3746
  multipleLockfileTypes: false,
@@ -3577,9 +3749,11 @@ async function scanSecurityPosture(rootDir) {
3577
3749
  envFilesTracked: false,
3578
3750
  lockfileTypes: []
3579
3751
  };
3752
+ const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
3753
+ const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3580
3754
  const foundLockfiles = [];
3581
3755
  for (const [file, type] of Object.entries(LOCKFILES)) {
3582
- if (await pathExists(path13.join(rootDir, file))) {
3756
+ if (await _pathExists(path13.join(rootDir, file))) {
3583
3757
  foundLockfiles.push(type);
3584
3758
  }
3585
3759
  }
@@ -3587,9 +3761,9 @@ async function scanSecurityPosture(rootDir) {
3587
3761
  result.multipleLockfileTypes = foundLockfiles.length > 1;
3588
3762
  result.lockfileTypes = foundLockfiles.sort();
3589
3763
  const gitignorePath = path13.join(rootDir, ".gitignore");
3590
- if (await pathExists(gitignorePath)) {
3764
+ if (await _pathExists(gitignorePath)) {
3591
3765
  try {
3592
- const content = await readTextFile(gitignorePath);
3766
+ const content = await _readTextFile(gitignorePath);
3593
3767
  const lines = content.split("\n").map((l) => l.trim());
3594
3768
  result.gitignoreCoversEnv = lines.some(
3595
3769
  (line) => line === ".env" || line === ".env*" || line === ".env.*" || line === ".env.local" || line === "*.env"
@@ -3601,7 +3775,7 @@ async function scanSecurityPosture(rootDir) {
3601
3775
  }
3602
3776
  }
3603
3777
  for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
3604
- if (await pathExists(path13.join(rootDir, envFile))) {
3778
+ if (await _pathExists(path13.join(rootDir, envFile))) {
3605
3779
  if (!result.gitignoreCoversEnv) {
3606
3780
  result.envFilesTracked = true;
3607
3781
  break;
@@ -4320,7 +4494,17 @@ var PACKAGE_LAYER_MAP = {
4320
4494
  };
4321
4495
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cts", ".cjs", ".svelte", ".vue"]);
4322
4496
  var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", ".nuxt", ".output", ".svelte-kit", "coverage", ".vibgrate"]);
4323
- async function walkSourceFiles(rootDir) {
4497
+ async function walkSourceFiles(rootDir, cache) {
4498
+ if (cache) {
4499
+ const entries = await cache.walkDir(rootDir);
4500
+ return entries.filter((e) => {
4501
+ if (!e.isFile) return false;
4502
+ const name = path14.basename(e.absPath);
4503
+ if (name.startsWith(".") && name !== ".") return false;
4504
+ const ext = path14.extname(name);
4505
+ return SOURCE_EXTENSIONS.has(ext);
4506
+ }).map((e) => e.relPath);
4507
+ }
4324
4508
  const files = [];
4325
4509
  async function walk(dir) {
4326
4510
  let entries;
@@ -4446,9 +4630,9 @@ function mapToolingToLayers(tooling, services, depsByLayer) {
4446
4630
  }
4447
4631
  return { layerTooling, layerServices };
4448
4632
  }
4449
- async function scanArchitecture(rootDir, projects, tooling, services) {
4633
+ async function scanArchitecture(rootDir, projects, tooling, services, cache) {
4450
4634
  const { archetype, confidence: archetypeConfidence } = detectArchetype(projects);
4451
- const sourceFiles = await walkSourceFiles(rootDir);
4635
+ const sourceFiles = await walkSourceFiles(rootDir, cache);
4452
4636
  const classifications = [];
4453
4637
  let unclassified = 0;
4454
4638
  for (const file of sourceFiles) {
@@ -4553,6 +4737,7 @@ async function runScan(rootDir, opts) {
4553
4737
  const config = await loadConfig(rootDir);
4554
4738
  const sem = new Semaphore(opts.concurrency);
4555
4739
  const npmCache = new NpmCache(rootDir, sem);
4740
+ const fileCache = new FileCache();
4556
4741
  const scanners = config.scanners;
4557
4742
  let filesScanned = 0;
4558
4743
  const progress = new ScanProgress(rootDir);
@@ -4584,7 +4769,7 @@ async function runScan(rootDir, opts) {
4584
4769
  const vcsDetail = vcs.type !== "unknown" ? `${vcs.type}${vcs.branch ? ` ${vcs.branch}` : ""}${vcs.shortSha ? ` @ ${vcs.shortSha}` : ""}` : "none detected";
4585
4770
  progress.completeStep("vcs", vcsDetail);
4586
4771
  progress.startStep("node");
4587
- const nodeProjects = await scanNodeProjects(rootDir, npmCache);
4772
+ const nodeProjects = await scanNodeProjects(rootDir, npmCache, fileCache);
4588
4773
  for (const p of nodeProjects) {
4589
4774
  progress.addDependencies(p.dependencies.length);
4590
4775
  progress.addFrameworks(p.frameworks.length);
@@ -4593,7 +4778,7 @@ async function runScan(rootDir, opts) {
4593
4778
  progress.addProjects(nodeProjects.length);
4594
4779
  progress.completeStep("node", `${nodeProjects.length} project${nodeProjects.length !== 1 ? "s" : ""}`, nodeProjects.length);
4595
4780
  progress.startStep("dotnet");
4596
- const dotnetProjects = await scanDotnetProjects(rootDir);
4781
+ const dotnetProjects = await scanDotnetProjects(rootDir, fileCache);
4597
4782
  for (const p of dotnetProjects) {
4598
4783
  progress.addDependencies(p.dependencies.length);
4599
4784
  progress.addFrameworks(p.frameworks.length);
@@ -4615,7 +4800,7 @@ async function runScan(rootDir, opts) {
4615
4800
  if (scanners?.platformMatrix?.enabled !== false) {
4616
4801
  progress.startStep("platform");
4617
4802
  scannerTasks.push(
4618
- scanPlatformMatrix(rootDir).then((result) => {
4803
+ scanPlatformMatrix(rootDir, fileCache).then((result) => {
4619
4804
  extended.platformMatrix = result;
4620
4805
  const nativeCount = result.nativeModules.length;
4621
4806
  const dockerCount = result.dockerBaseImages.length;
@@ -4664,7 +4849,7 @@ async function runScan(rootDir, opts) {
4664
4849
  if (scanners?.securityPosture?.enabled !== false) {
4665
4850
  progress.startStep("security");
4666
4851
  scannerTasks.push(
4667
- scanSecurityPosture(rootDir).then((result) => {
4852
+ scanSecurityPosture(rootDir, fileCache).then((result) => {
4668
4853
  extended.securityPosture = result;
4669
4854
  const secDetail = result.lockfilePresent ? `lockfile \u2714${result.gitignoreCoversEnv ? " \xB7 .env \u2714" : " \xB7 .env \u2716"}` : "no lockfile";
4670
4855
  progress.completeStep("security", secDetail);
@@ -4674,7 +4859,7 @@ async function runScan(rootDir, opts) {
4674
4859
  if (scanners?.buildDeploy?.enabled !== false) {
4675
4860
  progress.startStep("build");
4676
4861
  scannerTasks.push(
4677
- scanBuildDeploy(rootDir).then((result) => {
4862
+ scanBuildDeploy(rootDir, fileCache).then((result) => {
4678
4863
  extended.buildDeploy = result;
4679
4864
  const bdParts = [];
4680
4865
  if (result.ci.length > 0) bdParts.push(result.ci.join(", "));
@@ -4686,7 +4871,7 @@ async function runScan(rootDir, opts) {
4686
4871
  if (scanners?.tsModernity?.enabled !== false) {
4687
4872
  progress.startStep("ts");
4688
4873
  scannerTasks.push(
4689
- scanTsModernity(rootDir).then((result) => {
4874
+ scanTsModernity(rootDir, fileCache).then((result) => {
4690
4875
  extended.tsModernity = result;
4691
4876
  const tsParts = [];
4692
4877
  if (result.typescriptVersion) tsParts.push(`v${result.typescriptVersion}`);
@@ -4699,7 +4884,7 @@ async function runScan(rootDir, opts) {
4699
4884
  if (scanners?.fileHotspots?.enabled !== false) {
4700
4885
  progress.startStep("hotspots");
4701
4886
  scannerTasks.push(
4702
- scanFileHotspots(rootDir).then((result) => {
4887
+ scanFileHotspots(rootDir, fileCache).then((result) => {
4703
4888
  extended.fileHotspots = result;
4704
4889
  progress.completeStep("hotspots", `${result.totalFiles} files`, result.totalFiles);
4705
4890
  })
@@ -4708,7 +4893,7 @@ async function runScan(rootDir, opts) {
4708
4893
  if (scanners?.dependencyGraph?.enabled !== false) {
4709
4894
  progress.startStep("depgraph");
4710
4895
  scannerTasks.push(
4711
- scanDependencyGraph(rootDir).then((result) => {
4896
+ scanDependencyGraph(rootDir, fileCache).then((result) => {
4712
4897
  extended.dependencyGraph = result;
4713
4898
  const dgDetail = result.lockfileType ? `${result.lockfileType} \xB7 ${result.totalUnique} unique` : "no lockfile";
4714
4899
  progress.completeStep("depgraph", dgDetail, result.totalUnique);
@@ -4735,7 +4920,8 @@ async function runScan(rootDir, opts) {
4735
4920
  rootDir,
4736
4921
  allProjects,
4737
4922
  extended.toolingInventory,
4738
- extended.serviceDependencies
4923
+ extended.serviceDependencies,
4924
+ fileCache
4739
4925
  );
4740
4926
  const arch = extended.architecture;
4741
4927
  const layerCount = arch.layers.filter((l) => l.fileCount > 0).length;
@@ -4761,6 +4947,7 @@ async function runScan(rootDir, opts) {
4761
4947
  if (noteCount > 0) findingParts.push(`${noteCount} note${noteCount !== 1 ? "s" : ""}`);
4762
4948
  progress.completeStep("findings", findingParts.join(", ") || "none");
4763
4949
  progress.finish();
4950
+ fileCache.clear();
4764
4951
  if (allProjects.length === 0) {
4765
4952
  console.log(chalk5.yellow("No projects found."));
4766
4953
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runScan,
3
3
  writeJsonFile
4
- } from "./chunk-QZV77UWV.js";
4
+ } from "./chunk-EVLKMTYE.js";
5
5
 
6
6
  // src/commands/baseline.ts
7
7
  import * as path from "path";
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VXZT34Y5.js";
5
5
  import {
6
6
  baselineCommand
7
- } from "./chunk-OPEOSIRY.js";
7
+ } from "./chunk-XVGUFKAC.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-QZV77UWV.js";
18
+ } from "./chunk-EVLKMTYE.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-M445KUZ4.js");
41
+ const { runBaseline } = await import("./baseline-TJLWFSMH.js");
42
42
  await runBaseline(rootDir);
43
43
  }
44
44
  console.log("");
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  formatText,
8
8
  generateFindings,
9
9
  runScan
10
- } from "./chunk-QZV77UWV.js";
10
+ } from "./chunk-EVLKMTYE.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.15",
3
+ "version": "1.0.17",
4
4
  "description": "CLI for measuring upgrade drift across Node & .NET projects",
5
5
  "type": "module",
6
6
  "bin": {