@vibgrate/cli 1.0.19 → 1.0.21

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-XVGUFKAC.js";
5
- import "./chunk-EVLKMTYE.js";
4
+ } from "./chunk-74QSNBZA.js";
5
+ import "./chunk-L6R5WSCC.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-EVLKMTYE.js";
4
+ } from "./chunk-L6R5WSCC.js";
5
5
 
6
6
  // src/commands/baseline.ts
7
7
  import * as path from "path";
@@ -11,6 +11,7 @@ function formatMarkdown(artifact) {
11
11
  const scannedMeta = [artifact.timestamp];
12
12
  if (artifact.durationMs !== void 0) scannedMeta.push(`${(artifact.durationMs / 1e3).toFixed(1)}s`);
13
13
  if (artifact.filesScanned !== void 0) scannedMeta.push(`${artifact.filesScanned} files`);
14
+ if (artifact.treeSummary) scannedMeta.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files \xB7 ${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
14
15
  lines.push(`| **Scanned** | ${scannedMeta.join(" \xB7 ")} |`);
15
16
  if (artifact.vcs) {
16
17
  lines.push(`| **VCS** | ${artifact.vcs.type} |`);
@@ -69,18 +69,22 @@ var FileCache = class _FileCache {
69
69
  * Consumers that need additional filtering (e.g. SOURCE_EXTENSIONS,
70
70
  * SKIP_EXTENSIONS) do so on the returned entries — no separate walk.
71
71
  */
72
- walkDir(rootDir) {
72
+ walkDir(rootDir, onProgress) {
73
73
  const cached = this.walkCache.get(rootDir);
74
74
  if (cached) return cached;
75
- const promise = this._doWalk(rootDir);
75
+ const promise = this._doWalk(rootDir, onProgress);
76
76
  this.walkCache.set(rootDir, promise);
77
77
  return promise;
78
78
  }
79
79
  /** Additional dirs skipped only by the cached walk (framework outputs) */
80
80
  static EXTRA_SKIP = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
81
- async _doWalk(rootDir) {
81
+ async _doWalk(rootDir, onProgress) {
82
82
  const results = [];
83
- const maxConcurrentReads = Math.max(8, Math.min(64, os.availableParallelism() * 4));
83
+ const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
84
+ const maxConcurrentReads = Math.max(8, Math.min(64, cores * 4));
85
+ let foundCount = 0;
86
+ let lastReported = 0;
87
+ const REPORT_INTERVAL = 50;
84
88
  const sem = new Semaphore(maxConcurrentReads);
85
89
  const extraSkip = _FileCache.EXTRA_SKIP;
86
90
  async function walk(dir) {
@@ -100,11 +104,19 @@ var FileCache = class _FileCache {
100
104
  subWalks.push(sem.run(() => walk(absPath)));
101
105
  } else if (e.isFile()) {
102
106
  results.push({ absPath, relPath, name: e.name, isFile: true, isDirectory: false });
107
+ foundCount++;
108
+ if (onProgress && foundCount - lastReported >= REPORT_INTERVAL) {
109
+ lastReported = foundCount;
110
+ onProgress(foundCount);
111
+ }
103
112
  }
104
113
  }
105
114
  await Promise.all(subWalks);
106
115
  }
107
116
  await sem.run(() => walk(rootDir));
117
+ if (onProgress && foundCount !== lastReported) {
118
+ onProgress(foundCount);
119
+ }
108
120
  return results;
109
121
  }
110
122
  /**
@@ -184,9 +196,39 @@ var FileCache = class _FileCache {
184
196
  return this.jsonCache.size;
185
197
  }
186
198
  };
199
+ async function quickTreeCount(rootDir) {
200
+ let totalFiles = 0;
201
+ let totalDirs = 0;
202
+ const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
203
+ const maxConcurrent = Math.max(8, Math.min(128, cores * 8));
204
+ const sem = new Semaphore(maxConcurrent);
205
+ const extraSkip = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
206
+ async function count(dir) {
207
+ let entries;
208
+ try {
209
+ entries = await fs.readdir(dir, { withFileTypes: true });
210
+ } catch {
211
+ return;
212
+ }
213
+ const subs = [];
214
+ for (const e of entries) {
215
+ if (e.isDirectory()) {
216
+ if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
217
+ totalDirs++;
218
+ subs.push(sem.run(() => count(path.join(dir, e.name))));
219
+ } else if (e.isFile()) {
220
+ totalFiles++;
221
+ }
222
+ }
223
+ await Promise.all(subs);
224
+ }
225
+ await sem.run(() => count(rootDir));
226
+ return { totalFiles, totalDirs };
227
+ }
187
228
  async function findFiles(rootDir, predicate) {
188
229
  const results = [];
189
- const maxConcurrentReads = Math.max(8, Math.min(64, os.availableParallelism() * 4));
230
+ const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
231
+ const maxConcurrentReads = Math.max(8, Math.min(64, cores * 4));
190
232
  const readDirSemaphore = new Semaphore(maxConcurrentReads);
191
233
  async function walk(dir) {
192
234
  let entries;
@@ -321,12 +363,12 @@ function eolScore(projects) {
321
363
  }
322
364
  function computeDriftScore(projects) {
323
365
  const rs = runtimeScore(projects);
324
- const fs6 = frameworkScore(projects);
366
+ const fs7 = frameworkScore(projects);
325
367
  const ds = dependencyScore(projects);
326
368
  const es = eolScore(projects);
327
369
  const components = [
328
370
  { score: rs, weight: 0.25 },
329
- { score: fs6, weight: 0.25 },
371
+ { score: fs7, weight: 0.25 },
330
372
  { score: ds, weight: 0.3 },
331
373
  { score: es, weight: 0.2 }
332
374
  ];
@@ -337,7 +379,7 @@ function computeDriftScore(projects) {
337
379
  riskLevel: "low",
338
380
  components: {
339
381
  runtimeScore: Math.round(rs ?? 100),
340
- frameworkScore: Math.round(fs6 ?? 100),
382
+ frameworkScore: Math.round(fs7 ?? 100),
341
383
  dependencyScore: Math.round(ds ?? 100),
342
384
  eolScore: Math.round(es ?? 100)
343
385
  }
@@ -355,7 +397,7 @@ function computeDriftScore(projects) {
355
397
  else riskLevel = "high";
356
398
  const measured = [];
357
399
  if (rs !== null) measured.push("runtime");
358
- if (fs6 !== null) measured.push("framework");
400
+ if (fs7 !== null) measured.push("framework");
359
401
  if (ds !== null) measured.push("dependency");
360
402
  if (es !== null) measured.push("eol");
361
403
  return {
@@ -363,7 +405,7 @@ function computeDriftScore(projects) {
363
405
  riskLevel,
364
406
  components: {
365
407
  runtimeScore: Math.round(rs ?? 100),
366
- frameworkScore: Math.round(fs6 ?? 100),
408
+ frameworkScore: Math.round(fs7 ?? 100),
367
409
  dependencyScore: Math.round(ds ?? 100),
368
410
  eolScore: Math.round(es ?? 100)
369
411
  },
@@ -563,6 +605,10 @@ function formatText(artifact) {
563
605
  if (artifact.filesScanned !== void 0) {
564
606
  scannedParts.push(`${artifact.filesScanned} file${artifact.filesScanned !== 1 ? "s" : ""} scanned`);
565
607
  }
608
+ if (artifact.treeSummary) {
609
+ scannedParts.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files`);
610
+ scannedParts.push(`${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
611
+ }
566
612
  lines.push(chalk.dim(` ${scannedParts.join(" \xB7 ")}`));
567
613
  lines.push("");
568
614
  return lines.join("\n");
@@ -1300,7 +1346,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
1300
1346
  });
1301
1347
 
1302
1348
  // src/commands/scan.ts
1303
- import * as path15 from "path";
1349
+ import * as path16 from "path";
1304
1350
  import { Command as Command3 } from "commander";
1305
1351
  import chalk5 from "chalk";
1306
1352
 
@@ -2077,26 +2123,49 @@ var ScanProgress = class {
2077
2123
  startTime = Date.now();
2078
2124
  isTTY;
2079
2125
  rootDir = "";
2126
+ /** Estimated total scan duration in ms (from history or live calculation) */
2127
+ estimatedTotalMs = null;
2128
+ /** Per-step estimated durations from history */
2129
+ stepEstimates = /* @__PURE__ */ new Map();
2130
+ /** Per-step actual start times for timing */
2131
+ stepStartTimes = /* @__PURE__ */ new Map();
2132
+ /** Per-step recorded durations (completed steps) */
2133
+ stepTimings = [];
2080
2134
  constructor(rootDir) {
2081
2135
  this.isTTY = process.stderr.isTTY ?? false;
2082
2136
  this.rootDir = rootDir;
2083
2137
  }
2084
- /** Register all steps up front */
2138
+ /** Set the estimated total duration from scan history */
2139
+ setEstimatedTotal(estimatedMs) {
2140
+ this.estimatedTotalMs = estimatedMs;
2141
+ }
2142
+ /** Set per-step estimated durations from scan history */
2143
+ setStepEstimates(estimates) {
2144
+ this.stepEstimates = estimates;
2145
+ }
2146
+ /** Get completed step timings for persisting to history */
2147
+ getStepTimings() {
2148
+ return [...this.stepTimings];
2149
+ }
2150
+ /** Register all steps up front, optionally with weights */
2085
2151
  setSteps(steps) {
2086
- this.steps = steps.map((s) => ({ ...s, status: "pending" }));
2152
+ this.steps = steps.map((s) => ({ ...s, status: "pending", weight: s.weight ?? 1 }));
2087
2153
  if (this.isTTY) {
2088
2154
  this.startSpinner();
2089
2155
  }
2090
2156
  this.render();
2091
2157
  }
2092
- /** Mark a step as active (currently running) */
2093
- startStep(id) {
2158
+ /** Mark a step as active (currently running), optionally with expected total */
2159
+ startStep(id, subTotal) {
2094
2160
  const step = this.steps.find((s) => s.id === id);
2095
2161
  if (step) {
2096
2162
  step.status = "active";
2097
2163
  step.detail = void 0;
2098
2164
  step.count = void 0;
2165
+ step.subProgress = 0;
2166
+ step.subTotal = subTotal;
2099
2167
  }
2168
+ this.stepStartTimes.set(id, Date.now());
2100
2169
  this.render();
2101
2170
  }
2102
2171
  /** Mark a step as completed */
@@ -2107,6 +2176,10 @@ var ScanProgress = class {
2107
2176
  step.detail = detail;
2108
2177
  step.count = count;
2109
2178
  }
2179
+ const started = this.stepStartTimes.get(id);
2180
+ if (started) {
2181
+ this.stepTimings.push({ id, durationMs: Date.now() - started });
2182
+ }
2110
2183
  this.render();
2111
2184
  }
2112
2185
  /** Mark a step as skipped */
@@ -2118,6 +2191,15 @@ var ScanProgress = class {
2118
2191
  }
2119
2192
  this.render();
2120
2193
  }
2194
+ /** Update sub-step progress for the active step (files processed, etc.) */
2195
+ updateStepProgress(id, current, total) {
2196
+ const step = this.steps.find((s) => s.id === id);
2197
+ if (step) {
2198
+ step.subProgress = current;
2199
+ if (total !== void 0) step.subTotal = total;
2200
+ }
2201
+ this.render();
2202
+ }
2121
2203
  /** Update live stats */
2122
2204
  updateStats(partial) {
2123
2205
  Object.assign(this.stats, partial);
@@ -2151,10 +2233,10 @@ var ScanProgress = class {
2151
2233
  if (this.isTTY) {
2152
2234
  this.clearLines();
2153
2235
  }
2154
- const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
2236
+ const elapsed = this.formatElapsed(Date.now() - this.startTime);
2155
2237
  const doneCount = this.steps.filter((s) => s.status === "done").length;
2156
2238
  process.stderr.write(
2157
- chalk4.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}s
2239
+ chalk4.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}
2158
2240
 
2159
2241
  `)
2160
2242
  );
@@ -2188,14 +2270,32 @@ var ScanProgress = class {
2188
2270
  lines.push(` ${ROBOT[2]}`);
2189
2271
  lines.push(` ${ROBOT[3]} ${chalk4.dim(this.rootDir)}`);
2190
2272
  lines.push("");
2191
- const totalSteps = this.steps.length;
2192
- const doneSteps = this.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
2193
- const pct = totalSteps > 0 ? Math.round(doneSteps / totalSteps * 100) : 0;
2273
+ const totalWeight = this.steps.reduce((sum, s) => sum + (s.weight ?? 1), 0);
2274
+ let completedWeight = 0;
2275
+ for (const step of this.steps) {
2276
+ const w = step.weight ?? 1;
2277
+ if (step.status === "done" || step.status === "skipped") {
2278
+ completedWeight += w;
2279
+ } else if (step.status === "active" && step.subTotal && step.subTotal > 0 && step.subProgress !== void 0) {
2280
+ completedWeight += w * Math.min(step.subProgress / step.subTotal, 0.99);
2281
+ } else if (step.status === "active") {
2282
+ const stepStart = this.stepStartTimes.get(step.id);
2283
+ const estimate = this.stepEstimates.get(step.id);
2284
+ if (stepStart && estimate && estimate > 0) {
2285
+ const stepElapsed = Date.now() - stepStart;
2286
+ completedWeight += w * Math.min(stepElapsed / estimate, 0.95);
2287
+ }
2288
+ }
2289
+ }
2290
+ const pct = totalWeight > 0 ? Math.min(Math.round(completedWeight / totalWeight * 100), 99) : 0;
2194
2291
  const barWidth = 30;
2195
- const filled = Math.round(doneSteps / Math.max(totalSteps, 1) * barWidth);
2196
- const bar = chalk4.greenBright("\u2501".repeat(filled)) + chalk4.dim("\u254C".repeat(barWidth - filled));
2197
- const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
2198
- lines.push(` ${bar} ${chalk4.bold.white(`${pct}%`)} ${chalk4.dim(`${elapsed}s`)}`);
2292
+ const filled = Math.round(completedWeight / Math.max(totalWeight, 1) * barWidth);
2293
+ const bar = chalk4.greenBright("\u2501".repeat(Math.min(filled, barWidth))) + chalk4.dim("\u254C".repeat(Math.max(barWidth - filled, 0)));
2294
+ const elapsedMs = Date.now() - this.startTime;
2295
+ const elapsedStr = this.formatElapsed(elapsedMs);
2296
+ const etaStr = this.computeEtaString(elapsedMs, completedWeight, totalWeight);
2297
+ const treePart = this.stats.treeSummary ? chalk4.dim(` \xB7 ${this.stats.treeSummary.totalFiles.toLocaleString()} files \xB7 ${this.stats.treeSummary.totalDirs.toLocaleString()} dirs`) : "";
2298
+ lines.push(` ${bar} ${chalk4.bold.white(`${pct}%`)} ${chalk4.dim(elapsedStr)}${etaStr}${treePart}`);
2199
2299
  lines.push("");
2200
2300
  for (const step of this.steps) {
2201
2301
  lines.push(this.renderStep(step));
@@ -2220,6 +2320,9 @@ var ScanProgress = class {
2220
2320
  case "active":
2221
2321
  icon = chalk4.cyan(spinner);
2222
2322
  label = chalk4.bold.white(step.label);
2323
+ if (step.subTotal && step.subTotal > 0 && step.subProgress !== void 0 && step.subProgress > 0) {
2324
+ detail = chalk4.dim(` \xB7 ${step.subProgress.toLocaleString()} / ${step.subTotal.toLocaleString()}`);
2325
+ }
2223
2326
  break;
2224
2327
  case "skipped":
2225
2328
  icon = chalk4.dim("\u25CC");
@@ -2273,10 +2376,141 @@ var ScanProgress = class {
2273
2376
  }
2274
2377
  }
2275
2378
  }
2379
+ // ── Time formatting helpers ──
2380
+ /**
2381
+ * Format elapsed time:
2382
+ * - Under 90s → "12.3s"
2383
+ * - 90s and above → "1m 30s"
2384
+ */
2385
+ formatElapsed(ms) {
2386
+ const totalSecs = ms / 1e3;
2387
+ if (totalSecs < 90) {
2388
+ return `${totalSecs.toFixed(1)}s`;
2389
+ }
2390
+ const mins = Math.floor(totalSecs / 60);
2391
+ const secs = Math.floor(totalSecs % 60);
2392
+ return `${mins}m ${secs.toString().padStart(2, "0")}s`;
2393
+ }
2394
+ /**
2395
+ * Compute an ETA string for the progress bar.
2396
+ *
2397
+ * Uses two sources blended together:
2398
+ * 1. **Historical estimate** from `estimatedTotalMs` (if available)
2399
+ * 2. **Live rate** — extrapolated from `elapsedMs` and `completedWeight`
2400
+ *
2401
+ * Returns empty string if not enough data yet (< 3% progress or < 2s elapsed).
2402
+ */
2403
+ computeEtaString(elapsedMs, completedWeight, totalWeight) {
2404
+ if (totalWeight === 0 || elapsedMs < 2e3) return "";
2405
+ const fraction = completedWeight / totalWeight;
2406
+ if (fraction < 0.03) {
2407
+ if (this.estimatedTotalMs !== null && this.estimatedTotalMs > 0) {
2408
+ const remaining = Math.max(0, this.estimatedTotalMs - elapsedMs);
2409
+ if (remaining > 1e3) {
2410
+ return chalk4.dim(` \xB7 ~${this.formatElapsed(remaining)} left`);
2411
+ }
2412
+ }
2413
+ return "";
2414
+ }
2415
+ const liveRemaining = elapsedMs / fraction * (1 - fraction);
2416
+ let remainingMs;
2417
+ if (this.estimatedTotalMs !== null && this.estimatedTotalMs > 0) {
2418
+ const histRemaining = Math.max(0, this.estimatedTotalMs - elapsedMs);
2419
+ const histWeight = Math.max(0.1, 1 - fraction);
2420
+ remainingMs = histRemaining * histWeight + liveRemaining * (1 - histWeight);
2421
+ } else {
2422
+ remainingMs = liveRemaining;
2423
+ }
2424
+ if (remainingMs < 1500) return "";
2425
+ return chalk4.dim(` \xB7 ~${this.formatElapsed(remainingMs)} left`);
2426
+ }
2276
2427
  };
2277
2428
 
2278
- // src/scanners/platform-matrix.ts
2429
+ // src/ui/scan-history.ts
2430
+ import * as fs4 from "fs/promises";
2279
2431
  import * as path8 from "path";
2432
+ var HISTORY_FILENAME = "scan_history.json";
2433
+ var MAX_RECORDS = 10;
2434
+ async function loadScanHistory(rootDir) {
2435
+ const filePath = path8.join(rootDir, ".vibgrate", HISTORY_FILENAME);
2436
+ try {
2437
+ const txt = await fs4.readFile(filePath, "utf8");
2438
+ const data = JSON.parse(txt);
2439
+ if (data.version === 1 && Array.isArray(data.records)) {
2440
+ return data;
2441
+ }
2442
+ return null;
2443
+ } catch {
2444
+ return null;
2445
+ }
2446
+ }
2447
+ async function saveScanHistory(rootDir, record) {
2448
+ const dir = path8.join(rootDir, ".vibgrate");
2449
+ const filePath = path8.join(dir, HISTORY_FILENAME);
2450
+ let history;
2451
+ const existing = await loadScanHistory(rootDir);
2452
+ if (existing) {
2453
+ history = existing;
2454
+ history.records.push(record);
2455
+ if (history.records.length > MAX_RECORDS) {
2456
+ history.records = history.records.slice(-MAX_RECORDS);
2457
+ }
2458
+ } else {
2459
+ history = { version: 1, records: [record] };
2460
+ }
2461
+ try {
2462
+ await fs4.mkdir(dir, { recursive: true });
2463
+ await fs4.writeFile(filePath, JSON.stringify(history, null, 2) + "\n", "utf8");
2464
+ } catch {
2465
+ }
2466
+ }
2467
+ function estimateTotalDuration(history, currentFileCount) {
2468
+ if (!history || history.records.length === 0) return null;
2469
+ const similar = history.records.filter((r) => {
2470
+ if (r.totalFiles === 0 || currentFileCount === 0) return false;
2471
+ const ratio = currentFileCount / r.totalFiles;
2472
+ return ratio >= 0.33 && ratio <= 3;
2473
+ });
2474
+ if (similar.length > 0) {
2475
+ let weightedSum = 0;
2476
+ let weightTotal = 0;
2477
+ for (let i = 0; i < similar.length; i++) {
2478
+ const rec = similar[i];
2479
+ const weight = i + 1;
2480
+ const scale = currentFileCount / rec.totalFiles;
2481
+ weightedSum += rec.totalDurationMs * scale * weight;
2482
+ weightTotal += weight;
2483
+ }
2484
+ return Math.round(weightedSum / weightTotal);
2485
+ }
2486
+ const last = history.records[history.records.length - 1];
2487
+ if (last.totalFiles > 0 && currentFileCount > 0) {
2488
+ const scale = currentFileCount / last.totalFiles;
2489
+ return Math.round(last.totalDurationMs * scale);
2490
+ }
2491
+ return last.totalDurationMs;
2492
+ }
2493
+ function estimateStepDurations(history, currentFileCount) {
2494
+ const result = /* @__PURE__ */ new Map();
2495
+ if (!history || history.records.length === 0) return result;
2496
+ let best = null;
2497
+ for (let i = history.records.length - 1; i >= 0; i--) {
2498
+ const rec = history.records[i];
2499
+ if (rec.steps.length > 0) {
2500
+ best = rec;
2501
+ break;
2502
+ }
2503
+ }
2504
+ if (!best) return result;
2505
+ const scale = best.totalFiles > 0 && currentFileCount > 0 ? currentFileCount / best.totalFiles : 1;
2506
+ for (const step of best.steps) {
2507
+ result.set(step.id, Math.round(step.durationMs * scale));
2508
+ }
2509
+ return result;
2510
+ }
2511
+
2512
+ // src/scanners/platform-matrix.ts
2513
+ import * as path9 from "path";
2280
2514
  var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
2281
2515
  // Image / media processing
2282
2516
  "sharp",
@@ -2556,7 +2790,7 @@ async function scanPlatformMatrix(rootDir, cache) {
2556
2790
  }
2557
2791
  result.dockerBaseImages = [...baseImages].sort();
2558
2792
  for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
2559
- const exists = cache ? await cache.pathExists(path8.join(rootDir, file)) : await pathExists(path8.join(rootDir, file));
2793
+ const exists = cache ? await cache.pathExists(path9.join(rootDir, file)) : await pathExists(path9.join(rootDir, file));
2560
2794
  if (exists) {
2561
2795
  result.nodeVersionFiles.push(file);
2562
2796
  }
@@ -2633,7 +2867,7 @@ function scanDependencyRisk(projects) {
2633
2867
  }
2634
2868
 
2635
2869
  // src/scanners/dependency-graph.ts
2636
- import * as path9 from "path";
2870
+ import * as path10 from "path";
2637
2871
  function parsePnpmLock(content) {
2638
2872
  const entries = [];
2639
2873
  const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
@@ -2692,9 +2926,9 @@ async function scanDependencyGraph(rootDir, cache) {
2692
2926
  phantomDependencies: []
2693
2927
  };
2694
2928
  let entries = [];
2695
- const pnpmLock = path9.join(rootDir, "pnpm-lock.yaml");
2696
- const npmLock = path9.join(rootDir, "package-lock.json");
2697
- const yarnLock = path9.join(rootDir, "yarn.lock");
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");
2698
2932
  const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
2699
2933
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
2700
2934
  if (await _pathExists(pnpmLock)) {
@@ -2741,7 +2975,7 @@ async function scanDependencyGraph(rootDir, cache) {
2741
2975
  for (const pjPath of pkgFiles) {
2742
2976
  try {
2743
2977
  const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
2744
- const relPath = path9.relative(rootDir, pjPath);
2978
+ const relPath = path10.relative(rootDir, pjPath);
2745
2979
  for (const section of ["dependencies", "devDependencies"]) {
2746
2980
  const deps = pj[section];
2747
2981
  if (!deps) continue;
@@ -3087,7 +3321,7 @@ function scanToolingInventory(projects) {
3087
3321
  }
3088
3322
 
3089
3323
  // src/scanners/build-deploy.ts
3090
- import * as path10 from "path";
3324
+ import * as path11 from "path";
3091
3325
  var CI_FILES = {
3092
3326
  ".github/workflows": "github-actions",
3093
3327
  ".gitlab-ci.yml": "gitlab-ci",
@@ -3140,17 +3374,17 @@ async function scanBuildDeploy(rootDir, cache) {
3140
3374
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3141
3375
  const ciSystems = /* @__PURE__ */ new Set();
3142
3376
  for (const [file, system] of Object.entries(CI_FILES)) {
3143
- const fullPath = path10.join(rootDir, file);
3377
+ const fullPath = path11.join(rootDir, file);
3144
3378
  if (await _pathExists(fullPath)) {
3145
3379
  ciSystems.add(system);
3146
3380
  }
3147
3381
  }
3148
- const ghWorkflowDir = path10.join(rootDir, ".github", "workflows");
3382
+ const ghWorkflowDir = path11.join(rootDir, ".github", "workflows");
3149
3383
  if (await _pathExists(ghWorkflowDir)) {
3150
3384
  try {
3151
3385
  if (cache) {
3152
3386
  const entries = await cache.walkDir(rootDir);
3153
- const ghPrefix = path10.relative(rootDir, ghWorkflowDir) + path10.sep;
3387
+ const ghPrefix = path11.relative(rootDir, ghWorkflowDir) + path11.sep;
3154
3388
  result.ciWorkflowCount = entries.filter(
3155
3389
  (e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
3156
3390
  ).length;
@@ -3201,11 +3435,11 @@ async function scanBuildDeploy(rootDir, cache) {
3201
3435
  (name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
3202
3436
  );
3203
3437
  if (cfnFiles.length > 0) iacSystems.add("cloudformation");
3204
- if (await _pathExists(path10.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
3438
+ if (await _pathExists(path11.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
3205
3439
  result.iac = [...iacSystems].sort();
3206
3440
  const releaseTools = /* @__PURE__ */ new Set();
3207
3441
  for (const [file, tool] of Object.entries(RELEASE_FILES)) {
3208
- if (await _pathExists(path10.join(rootDir, file))) releaseTools.add(tool);
3442
+ if (await _pathExists(path11.join(rootDir, file))) releaseTools.add(tool);
3209
3443
  }
3210
3444
  const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
3211
3445
  for (const pjPath of pkgFiles) {
@@ -3230,19 +3464,19 @@ async function scanBuildDeploy(rootDir, cache) {
3230
3464
  };
3231
3465
  const managers = /* @__PURE__ */ new Set();
3232
3466
  for (const [file, manager] of Object.entries(lockfileMap)) {
3233
- if (await _pathExists(path10.join(rootDir, file))) managers.add(manager);
3467
+ if (await _pathExists(path11.join(rootDir, file))) managers.add(manager);
3234
3468
  }
3235
3469
  result.packageManagers = [...managers].sort();
3236
3470
  const monoTools = /* @__PURE__ */ new Set();
3237
3471
  for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
3238
- if (await _pathExists(path10.join(rootDir, file))) monoTools.add(tool);
3472
+ if (await _pathExists(path11.join(rootDir, file))) monoTools.add(tool);
3239
3473
  }
3240
3474
  result.monorepoTools = [...monoTools].sort();
3241
3475
  return result;
3242
3476
  }
3243
3477
 
3244
3478
  // src/scanners/ts-modernity.ts
3245
- import * as path11 from "path";
3479
+ import * as path12 from "path";
3246
3480
  async function scanTsModernity(rootDir, cache) {
3247
3481
  const result = {
3248
3482
  typescriptVersion: null,
@@ -3280,7 +3514,7 @@ async function scanTsModernity(rootDir, cache) {
3280
3514
  if (hasEsm && hasCjs) result.moduleType = "mixed";
3281
3515
  else if (hasEsm) result.moduleType = "esm";
3282
3516
  else if (hasCjs) result.moduleType = "cjs";
3283
- let tsConfigPath = path11.join(rootDir, "tsconfig.json");
3517
+ let tsConfigPath = path12.join(rootDir, "tsconfig.json");
3284
3518
  const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
3285
3519
  if (!tsConfigExists) {
3286
3520
  const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
@@ -3626,8 +3860,8 @@ function scanBreakingChangeExposure(projects) {
3626
3860
  }
3627
3861
 
3628
3862
  // src/scanners/file-hotspots.ts
3629
- import * as fs4 from "fs/promises";
3630
- import * as path12 from "path";
3863
+ import * as fs5 from "fs/promises";
3864
+ import * as path13 from "path";
3631
3865
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3632
3866
  "node_modules",
3633
3867
  ".git",
@@ -3670,13 +3904,13 @@ async function scanFileHotspots(rootDir, cache) {
3670
3904
  const entries = await cache.walkDir(rootDir);
3671
3905
  for (const entry of entries) {
3672
3906
  if (!entry.isFile) continue;
3673
- const ext = path12.extname(entry.name).toLowerCase();
3907
+ const ext = path13.extname(entry.name).toLowerCase();
3674
3908
  if (SKIP_EXTENSIONS.has(ext)) continue;
3675
- const depth = entry.relPath.split(path12.sep).length - 1;
3909
+ const depth = entry.relPath.split(path13.sep).length - 1;
3676
3910
  if (depth > maxDepth) maxDepth = depth;
3677
3911
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3678
3912
  try {
3679
- const stat3 = await fs4.stat(entry.absPath);
3913
+ const stat3 = await fs5.stat(entry.absPath);
3680
3914
  allFiles.push({
3681
3915
  path: entry.relPath,
3682
3916
  bytes: stat3.size
@@ -3689,7 +3923,7 @@ async function scanFileHotspots(rootDir, cache) {
3689
3923
  if (depth > maxDepth) maxDepth = depth;
3690
3924
  let entries;
3691
3925
  try {
3692
- const dirents = await fs4.readdir(dir, { withFileTypes: true });
3926
+ const dirents = await fs5.readdir(dir, { withFileTypes: true });
3693
3927
  entries = dirents.map((d) => ({
3694
3928
  name: d.name,
3695
3929
  isDirectory: d.isDirectory(),
@@ -3701,15 +3935,15 @@ async function scanFileHotspots(rootDir, cache) {
3701
3935
  for (const e of entries) {
3702
3936
  if (e.isDirectory) {
3703
3937
  if (SKIP_DIRS2.has(e.name)) continue;
3704
- await walk(path12.join(dir, e.name), depth + 1);
3938
+ await walk(path13.join(dir, e.name), depth + 1);
3705
3939
  } else if (e.isFile) {
3706
- const ext = path12.extname(e.name).toLowerCase();
3940
+ const ext = path13.extname(e.name).toLowerCase();
3707
3941
  if (SKIP_EXTENSIONS.has(ext)) continue;
3708
3942
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3709
3943
  try {
3710
- const stat3 = await fs4.stat(path12.join(dir, e.name));
3944
+ const stat3 = await fs5.stat(path13.join(dir, e.name));
3711
3945
  allFiles.push({
3712
- path: path12.relative(rootDir, path12.join(dir, e.name)),
3946
+ path: path13.relative(rootDir, path13.join(dir, e.name)),
3713
3947
  bytes: stat3.size
3714
3948
  });
3715
3949
  } catch {
@@ -3732,7 +3966,7 @@ async function scanFileHotspots(rootDir, cache) {
3732
3966
  }
3733
3967
 
3734
3968
  // src/scanners/security-posture.ts
3735
- import * as path13 from "path";
3969
+ import * as path14 from "path";
3736
3970
  var LOCKFILES = {
3737
3971
  "pnpm-lock.yaml": "pnpm",
3738
3972
  "package-lock.json": "npm",
@@ -3753,14 +3987,14 @@ async function scanSecurityPosture(rootDir, cache) {
3753
3987
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3754
3988
  const foundLockfiles = [];
3755
3989
  for (const [file, type] of Object.entries(LOCKFILES)) {
3756
- if (await _pathExists(path13.join(rootDir, file))) {
3990
+ if (await _pathExists(path14.join(rootDir, file))) {
3757
3991
  foundLockfiles.push(type);
3758
3992
  }
3759
3993
  }
3760
3994
  result.lockfilePresent = foundLockfiles.length > 0;
3761
3995
  result.multipleLockfileTypes = foundLockfiles.length > 1;
3762
3996
  result.lockfileTypes = foundLockfiles.sort();
3763
- const gitignorePath = path13.join(rootDir, ".gitignore");
3997
+ const gitignorePath = path14.join(rootDir, ".gitignore");
3764
3998
  if (await _pathExists(gitignorePath)) {
3765
3999
  try {
3766
4000
  const content = await _readTextFile(gitignorePath);
@@ -3775,7 +4009,7 @@ async function scanSecurityPosture(rootDir, cache) {
3775
4009
  }
3776
4010
  }
3777
4011
  for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
3778
- if (await _pathExists(path13.join(rootDir, envFile))) {
4012
+ if (await _pathExists(path14.join(rootDir, envFile))) {
3779
4013
  if (!result.gitignoreCoversEnv) {
3780
4014
  result.envFilesTracked = true;
3781
4015
  break;
@@ -4200,8 +4434,8 @@ function scanServiceDependencies(projects) {
4200
4434
  }
4201
4435
 
4202
4436
  // src/scanners/architecture.ts
4203
- import * as path14 from "path";
4204
- import * as fs5 from "fs/promises";
4437
+ import * as path15 from "path";
4438
+ import * as fs6 from "fs/promises";
4205
4439
  var ARCHETYPE_SIGNALS = [
4206
4440
  // Meta-frameworks (highest priority — they imply routing patterns)
4207
4441
  { packages: ["next", "@next/core"], archetype: "nextjs", weight: 10 },
@@ -4499,9 +4733,9 @@ async function walkSourceFiles(rootDir, cache) {
4499
4733
  const entries = await cache.walkDir(rootDir);
4500
4734
  return entries.filter((e) => {
4501
4735
  if (!e.isFile) return false;
4502
- const name = path14.basename(e.absPath);
4736
+ const name = path15.basename(e.absPath);
4503
4737
  if (name.startsWith(".") && name !== ".") return false;
4504
- const ext = path14.extname(name);
4738
+ const ext = path15.extname(name);
4505
4739
  return SOURCE_EXTENSIONS.has(ext);
4506
4740
  }).map((e) => e.relPath);
4507
4741
  }
@@ -4509,21 +4743,21 @@ async function walkSourceFiles(rootDir, cache) {
4509
4743
  async function walk(dir) {
4510
4744
  let entries;
4511
4745
  try {
4512
- entries = await fs5.readdir(dir, { withFileTypes: true });
4746
+ entries = await fs6.readdir(dir, { withFileTypes: true });
4513
4747
  } catch {
4514
4748
  return;
4515
4749
  }
4516
4750
  for (const entry of entries) {
4517
4751
  if (entry.name.startsWith(".") && entry.name !== ".") continue;
4518
- const fullPath = path14.join(dir, entry.name);
4752
+ const fullPath = path15.join(dir, entry.name);
4519
4753
  if (entry.isDirectory()) {
4520
4754
  if (!IGNORE_DIRS.has(entry.name)) {
4521
4755
  await walk(fullPath);
4522
4756
  }
4523
4757
  } else if (entry.isFile()) {
4524
- const ext = path14.extname(entry.name);
4758
+ const ext = path15.extname(entry.name);
4525
4759
  if (SOURCE_EXTENSIONS.has(ext)) {
4526
- files.push(path14.relative(rootDir, fullPath));
4760
+ files.push(path15.relative(rootDir, fullPath));
4527
4761
  }
4528
4762
  }
4529
4763
  }
@@ -4547,7 +4781,7 @@ function classifyFile(filePath, archetype) {
4547
4781
  }
4548
4782
  }
4549
4783
  if (!bestMatch || bestMatch.confidence < 0.7) {
4550
- const baseName = path14.basename(filePath, path14.extname(filePath));
4784
+ const baseName = path15.basename(filePath, path15.extname(filePath));
4551
4785
  const cleanBase = baseName.replace(/\.(test|spec)$/, "");
4552
4786
  for (const rule of SUFFIX_RULES) {
4553
4787
  if (cleanBase.endsWith(rule.suffix)) {
@@ -4743,9 +4977,11 @@ async function runScan(rootDir, opts) {
4743
4977
  const progress = new ScanProgress(rootDir);
4744
4978
  const steps = [
4745
4979
  { id: "config", label: "Loading configuration" },
4980
+ { id: "discovery", label: "Discovering workspace", weight: 3 },
4746
4981
  { id: "vcs", label: "Detecting version control" },
4747
- { id: "node", label: "Scanning Node projects" },
4748
- { id: "dotnet", label: "Scanning .NET projects" },
4982
+ { id: "walk", label: "Indexing files", weight: 8 },
4983
+ { id: "node", label: "Scanning Node projects", weight: 4 },
4984
+ { id: "dotnet", label: "Scanning .NET projects", weight: 2 },
4749
4985
  ...scanners !== false ? [
4750
4986
  ...scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
4751
4987
  ...scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
@@ -4764,10 +5000,26 @@ async function runScan(rootDir, opts) {
4764
5000
  ];
4765
5001
  progress.setSteps(steps);
4766
5002
  progress.completeStep("config", "loaded");
5003
+ progress.startStep("discovery");
5004
+ const treeCount = await quickTreeCount(rootDir);
5005
+ progress.updateStats({ treeSummary: treeCount });
5006
+ progress.completeStep(
5007
+ "discovery",
5008
+ `${treeCount.totalFiles.toLocaleString()} files \xB7 ${treeCount.totalDirs.toLocaleString()} dirs`
5009
+ );
5010
+ const scanHistory = await loadScanHistory(rootDir);
5011
+ const estimatedTotal = estimateTotalDuration(scanHistory, treeCount.totalFiles);
5012
+ progress.setEstimatedTotal(estimatedTotal);
5013
+ progress.setStepEstimates(estimateStepDurations(scanHistory, treeCount.totalFiles));
4767
5014
  progress.startStep("vcs");
4768
5015
  const vcs = await detectVcs(rootDir);
4769
5016
  const vcsDetail = vcs.type !== "unknown" ? `${vcs.type}${vcs.branch ? ` ${vcs.branch}` : ""}${vcs.shortSha ? ` @ ${vcs.shortSha}` : ""}` : "none detected";
4770
5017
  progress.completeStep("vcs", vcsDetail);
5018
+ progress.startStep("walk", treeCount.totalFiles);
5019
+ await fileCache.walkDir(rootDir, (found) => {
5020
+ progress.updateStepProgress("walk", found, treeCount.totalFiles);
5021
+ });
5022
+ progress.completeStep("walk", `${treeCount.totalFiles.toLocaleString()} files indexed`);
4771
5023
  progress.startStep("node");
4772
5024
  const nodeProjects = await scanNodeProjects(rootDir, npmCache, fileCache);
4773
5025
  for (const p of nodeProjects) {
@@ -4964,17 +5216,18 @@ async function runScan(rootDir, opts) {
4964
5216
  schemaVersion: "1.0",
4965
5217
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4966
5218
  vibgrateVersion: VERSION,
4967
- rootPath: path15.basename(rootDir),
5219
+ rootPath: path16.basename(rootDir),
4968
5220
  ...vcs.type !== "unknown" ? { vcs } : {},
4969
5221
  projects: allProjects,
4970
5222
  drift,
4971
5223
  findings,
4972
5224
  ...Object.keys(extended).length > 0 ? { extended } : {},
4973
5225
  durationMs,
4974
- filesScanned
5226
+ filesScanned,
5227
+ treeSummary: treeCount
4975
5228
  };
4976
5229
  if (opts.baseline) {
4977
- const baselinePath = path15.resolve(opts.baseline);
5230
+ const baselinePath = path16.resolve(opts.baseline);
4978
5231
  if (await pathExists(baselinePath)) {
4979
5232
  try {
4980
5233
  const baseline = await readJsonFile(baselinePath);
@@ -4985,15 +5238,22 @@ async function runScan(rootDir, opts) {
4985
5238
  }
4986
5239
  }
4987
5240
  }
4988
- const vibgrateDir = path15.join(rootDir, ".vibgrate");
5241
+ const vibgrateDir = path16.join(rootDir, ".vibgrate");
4989
5242
  await ensureDir(vibgrateDir);
4990
- await writeJsonFile(path15.join(vibgrateDir, "scan_result.json"), artifact);
5243
+ await writeJsonFile(path16.join(vibgrateDir, "scan_result.json"), artifact);
5244
+ await saveScanHistory(rootDir, {
5245
+ timestamp: artifact.timestamp,
5246
+ totalDurationMs: durationMs,
5247
+ totalFiles: treeCount.totalFiles,
5248
+ totalDirs: treeCount.totalDirs,
5249
+ steps: progress.getStepTimings()
5250
+ });
4991
5251
  for (const project of allProjects) {
4992
5252
  if (project.drift && project.path) {
4993
- const projectDir = path15.resolve(rootDir, project.path);
4994
- const projectVibgrateDir = path15.join(projectDir, ".vibgrate");
5253
+ const projectDir = path16.resolve(rootDir, project.path);
5254
+ const projectVibgrateDir = path16.join(projectDir, ".vibgrate");
4995
5255
  await ensureDir(projectVibgrateDir);
4996
- await writeJsonFile(path15.join(projectVibgrateDir, "project_score.json"), {
5256
+ await writeJsonFile(path16.join(projectVibgrateDir, "project_score.json"), {
4997
5257
  projectId: project.projectId,
4998
5258
  name: project.name,
4999
5259
  type: project.type,
@@ -5010,7 +5270,7 @@ async function runScan(rootDir, opts) {
5010
5270
  if (opts.format === "json") {
5011
5271
  const jsonStr = JSON.stringify(artifact, null, 2);
5012
5272
  if (opts.out) {
5013
- await writeTextFile(path15.resolve(opts.out), jsonStr);
5273
+ await writeTextFile(path16.resolve(opts.out), jsonStr);
5014
5274
  console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
5015
5275
  } else {
5016
5276
  console.log(jsonStr);
@@ -5019,7 +5279,7 @@ async function runScan(rootDir, opts) {
5019
5279
  const sarif = formatSarif(artifact);
5020
5280
  const sarifStr = JSON.stringify(sarif, null, 2);
5021
5281
  if (opts.out) {
5022
- await writeTextFile(path15.resolve(opts.out), sarifStr);
5282
+ await writeTextFile(path16.resolve(opts.out), sarifStr);
5023
5283
  console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
5024
5284
  } else {
5025
5285
  console.log(sarifStr);
@@ -5028,7 +5288,7 @@ async function runScan(rootDir, opts) {
5028
5288
  const text = formatText(artifact);
5029
5289
  console.log(text);
5030
5290
  if (opts.out) {
5031
- await writeTextFile(path15.resolve(opts.out), text);
5291
+ await writeTextFile(path16.resolve(opts.out), text);
5032
5292
  }
5033
5293
  }
5034
5294
  return artifact;
@@ -5087,7 +5347,7 @@ async function autoPush(artifact, rootDir, opts) {
5087
5347
  }
5088
5348
  }
5089
5349
  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) => {
5090
- const rootDir = path15.resolve(targetPath);
5350
+ const rootDir = path16.resolve(targetPath);
5091
5351
  if (!await pathExists(rootDir)) {
5092
5352
  console.error(chalk5.red(`Path does not exist: ${rootDir}`));
5093
5353
  process.exit(1);
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  formatMarkdown
4
- } from "./chunk-VXZT34Y5.js";
4
+ } from "./chunk-GN3IWKSY.js";
5
5
  import {
6
6
  baselineCommand
7
- } from "./chunk-XVGUFKAC.js";
7
+ } from "./chunk-74QSNBZA.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-EVLKMTYE.js";
18
+ } from "./chunk-L6R5WSCC.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-TJLWFSMH.js");
41
+ const { runBaseline } = await import("./baseline-7WI3SI6H.js");
42
42
  await runBaseline(rootDir);
43
43
  }
44
44
  console.log("");
package/dist/index.d.ts CHANGED
@@ -64,6 +64,12 @@ interface VcsInfo {
64
64
  shortSha?: string;
65
65
  branch?: string;
66
66
  }
67
+ interface TreeCount {
68
+ /** Total files discovered (excluding skipped dirs like node_modules, .git, dist) */
69
+ totalFiles: number;
70
+ /** Total subdirectories discovered (excluding skipped dirs) */
71
+ totalDirs: number;
72
+ }
67
73
  interface ScanArtifact {
68
74
  schemaVersion: '1.0';
69
75
  timestamp: string;
@@ -80,6 +86,8 @@ interface ScanArtifact {
80
86
  durationMs?: number;
81
87
  /** Number of manifest/config files scanned */
82
88
  filesScanned?: number;
89
+ /** Workspace tree summary (file & directory counts from discovery) */
90
+ treeSummary?: TreeCount;
83
91
  }
84
92
  interface ScanOptions {
85
93
  out?: string;
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  formatMarkdown
3
- } from "./chunk-VXZT34Y5.js";
3
+ } from "./chunk-GN3IWKSY.js";
4
4
  import {
5
5
  computeDriftScore,
6
6
  formatSarif,
7
7
  formatText,
8
8
  generateFindings,
9
9
  runScan
10
- } from "./chunk-EVLKMTYE.js";
10
+ } from "./chunk-L6R5WSCC.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.19",
3
+ "version": "1.0.21",
4
4
  "description": "CLI for measuring upgrade drift across Node & .NET projects",
5
5
  "type": "module",
6
6
  "bin": {