@vibgrate/cli 1.0.20 → 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-YWBGG2KK.js";
5
- import "./chunk-T6WMUKLV.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-T6WMUKLV.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,19 +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
83
  const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
84
84
  const maxConcurrentReads = Math.max(8, Math.min(64, cores * 4));
85
+ let foundCount = 0;
86
+ let lastReported = 0;
87
+ const REPORT_INTERVAL = 50;
85
88
  const sem = new Semaphore(maxConcurrentReads);
86
89
  const extraSkip = _FileCache.EXTRA_SKIP;
87
90
  async function walk(dir) {
@@ -101,11 +104,19 @@ var FileCache = class _FileCache {
101
104
  subWalks.push(sem.run(() => walk(absPath)));
102
105
  } else if (e.isFile()) {
103
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
+ }
104
112
  }
105
113
  }
106
114
  await Promise.all(subWalks);
107
115
  }
108
116
  await sem.run(() => walk(rootDir));
117
+ if (onProgress && foundCount !== lastReported) {
118
+ onProgress(foundCount);
119
+ }
109
120
  return results;
110
121
  }
111
122
  /**
@@ -185,6 +196,35 @@ var FileCache = class _FileCache {
185
196
  return this.jsonCache.size;
186
197
  }
187
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
+ }
188
228
  async function findFiles(rootDir, predicate) {
189
229
  const results = [];
190
230
  const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
@@ -323,12 +363,12 @@ function eolScore(projects) {
323
363
  }
324
364
  function computeDriftScore(projects) {
325
365
  const rs = runtimeScore(projects);
326
- const fs6 = frameworkScore(projects);
366
+ const fs7 = frameworkScore(projects);
327
367
  const ds = dependencyScore(projects);
328
368
  const es = eolScore(projects);
329
369
  const components = [
330
370
  { score: rs, weight: 0.25 },
331
- { score: fs6, weight: 0.25 },
371
+ { score: fs7, weight: 0.25 },
332
372
  { score: ds, weight: 0.3 },
333
373
  { score: es, weight: 0.2 }
334
374
  ];
@@ -339,7 +379,7 @@ function computeDriftScore(projects) {
339
379
  riskLevel: "low",
340
380
  components: {
341
381
  runtimeScore: Math.round(rs ?? 100),
342
- frameworkScore: Math.round(fs6 ?? 100),
382
+ frameworkScore: Math.round(fs7 ?? 100),
343
383
  dependencyScore: Math.round(ds ?? 100),
344
384
  eolScore: Math.round(es ?? 100)
345
385
  }
@@ -357,7 +397,7 @@ function computeDriftScore(projects) {
357
397
  else riskLevel = "high";
358
398
  const measured = [];
359
399
  if (rs !== null) measured.push("runtime");
360
- if (fs6 !== null) measured.push("framework");
400
+ if (fs7 !== null) measured.push("framework");
361
401
  if (ds !== null) measured.push("dependency");
362
402
  if (es !== null) measured.push("eol");
363
403
  return {
@@ -365,7 +405,7 @@ function computeDriftScore(projects) {
365
405
  riskLevel,
366
406
  components: {
367
407
  runtimeScore: Math.round(rs ?? 100),
368
- frameworkScore: Math.round(fs6 ?? 100),
408
+ frameworkScore: Math.round(fs7 ?? 100),
369
409
  dependencyScore: Math.round(ds ?? 100),
370
410
  eolScore: Math.round(es ?? 100)
371
411
  },
@@ -565,6 +605,10 @@ function formatText(artifact) {
565
605
  if (artifact.filesScanned !== void 0) {
566
606
  scannedParts.push(`${artifact.filesScanned} file${artifact.filesScanned !== 1 ? "s" : ""} scanned`);
567
607
  }
608
+ if (artifact.treeSummary) {
609
+ scannedParts.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files`);
610
+ scannedParts.push(`${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
611
+ }
568
612
  lines.push(chalk.dim(` ${scannedParts.join(" \xB7 ")}`));
569
613
  lines.push("");
570
614
  return lines.join("\n");
@@ -1302,7 +1346,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
1302
1346
  });
1303
1347
 
1304
1348
  // src/commands/scan.ts
1305
- import * as path15 from "path";
1349
+ import * as path16 from "path";
1306
1350
  import { Command as Command3 } from "commander";
1307
1351
  import chalk5 from "chalk";
1308
1352
 
@@ -2079,26 +2123,49 @@ var ScanProgress = class {
2079
2123
  startTime = Date.now();
2080
2124
  isTTY;
2081
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 = [];
2082
2134
  constructor(rootDir) {
2083
2135
  this.isTTY = process.stderr.isTTY ?? false;
2084
2136
  this.rootDir = rootDir;
2085
2137
  }
2086
- /** 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 */
2087
2151
  setSteps(steps) {
2088
- this.steps = steps.map((s) => ({ ...s, status: "pending" }));
2152
+ this.steps = steps.map((s) => ({ ...s, status: "pending", weight: s.weight ?? 1 }));
2089
2153
  if (this.isTTY) {
2090
2154
  this.startSpinner();
2091
2155
  }
2092
2156
  this.render();
2093
2157
  }
2094
- /** Mark a step as active (currently running) */
2095
- startStep(id) {
2158
+ /** Mark a step as active (currently running), optionally with expected total */
2159
+ startStep(id, subTotal) {
2096
2160
  const step = this.steps.find((s) => s.id === id);
2097
2161
  if (step) {
2098
2162
  step.status = "active";
2099
2163
  step.detail = void 0;
2100
2164
  step.count = void 0;
2165
+ step.subProgress = 0;
2166
+ step.subTotal = subTotal;
2101
2167
  }
2168
+ this.stepStartTimes.set(id, Date.now());
2102
2169
  this.render();
2103
2170
  }
2104
2171
  /** Mark a step as completed */
@@ -2109,6 +2176,10 @@ var ScanProgress = class {
2109
2176
  step.detail = detail;
2110
2177
  step.count = count;
2111
2178
  }
2179
+ const started = this.stepStartTimes.get(id);
2180
+ if (started) {
2181
+ this.stepTimings.push({ id, durationMs: Date.now() - started });
2182
+ }
2112
2183
  this.render();
2113
2184
  }
2114
2185
  /** Mark a step as skipped */
@@ -2120,6 +2191,15 @@ var ScanProgress = class {
2120
2191
  }
2121
2192
  this.render();
2122
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
+ }
2123
2203
  /** Update live stats */
2124
2204
  updateStats(partial) {
2125
2205
  Object.assign(this.stats, partial);
@@ -2153,10 +2233,10 @@ var ScanProgress = class {
2153
2233
  if (this.isTTY) {
2154
2234
  this.clearLines();
2155
2235
  }
2156
- const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
2236
+ const elapsed = this.formatElapsed(Date.now() - this.startTime);
2157
2237
  const doneCount = this.steps.filter((s) => s.status === "done").length;
2158
2238
  process.stderr.write(
2159
- chalk4.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}s
2239
+ chalk4.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}
2160
2240
 
2161
2241
  `)
2162
2242
  );
@@ -2190,14 +2270,32 @@ var ScanProgress = class {
2190
2270
  lines.push(` ${ROBOT[2]}`);
2191
2271
  lines.push(` ${ROBOT[3]} ${chalk4.dim(this.rootDir)}`);
2192
2272
  lines.push("");
2193
- const totalSteps = this.steps.length;
2194
- const doneSteps = this.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
2195
- 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;
2196
2291
  const barWidth = 30;
2197
- const filled = Math.round(doneSteps / Math.max(totalSteps, 1) * barWidth);
2198
- const bar = chalk4.greenBright("\u2501".repeat(filled)) + chalk4.dim("\u254C".repeat(barWidth - filled));
2199
- const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
2200
- 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}`);
2201
2299
  lines.push("");
2202
2300
  for (const step of this.steps) {
2203
2301
  lines.push(this.renderStep(step));
@@ -2222,6 +2320,9 @@ var ScanProgress = class {
2222
2320
  case "active":
2223
2321
  icon = chalk4.cyan(spinner);
2224
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
+ }
2225
2326
  break;
2226
2327
  case "skipped":
2227
2328
  icon = chalk4.dim("\u25CC");
@@ -2275,10 +2376,141 @@ var ScanProgress = class {
2275
2376
  }
2276
2377
  }
2277
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
+ }
2278
2427
  };
2279
2428
 
2280
- // src/scanners/platform-matrix.ts
2429
+ // src/ui/scan-history.ts
2430
+ import * as fs4 from "fs/promises";
2281
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";
2282
2514
  var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
2283
2515
  // Image / media processing
2284
2516
  "sharp",
@@ -2558,7 +2790,7 @@ async function scanPlatformMatrix(rootDir, cache) {
2558
2790
  }
2559
2791
  result.dockerBaseImages = [...baseImages].sort();
2560
2792
  for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
2561
- 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));
2562
2794
  if (exists) {
2563
2795
  result.nodeVersionFiles.push(file);
2564
2796
  }
@@ -2635,7 +2867,7 @@ function scanDependencyRisk(projects) {
2635
2867
  }
2636
2868
 
2637
2869
  // src/scanners/dependency-graph.ts
2638
- import * as path9 from "path";
2870
+ import * as path10 from "path";
2639
2871
  function parsePnpmLock(content) {
2640
2872
  const entries = [];
2641
2873
  const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
@@ -2694,9 +2926,9 @@ async function scanDependencyGraph(rootDir, cache) {
2694
2926
  phantomDependencies: []
2695
2927
  };
2696
2928
  let entries = [];
2697
- const pnpmLock = path9.join(rootDir, "pnpm-lock.yaml");
2698
- const npmLock = path9.join(rootDir, "package-lock.json");
2699
- 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");
2700
2932
  const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
2701
2933
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
2702
2934
  if (await _pathExists(pnpmLock)) {
@@ -2743,7 +2975,7 @@ async function scanDependencyGraph(rootDir, cache) {
2743
2975
  for (const pjPath of pkgFiles) {
2744
2976
  try {
2745
2977
  const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
2746
- const relPath = path9.relative(rootDir, pjPath);
2978
+ const relPath = path10.relative(rootDir, pjPath);
2747
2979
  for (const section of ["dependencies", "devDependencies"]) {
2748
2980
  const deps = pj[section];
2749
2981
  if (!deps) continue;
@@ -3089,7 +3321,7 @@ function scanToolingInventory(projects) {
3089
3321
  }
3090
3322
 
3091
3323
  // src/scanners/build-deploy.ts
3092
- import * as path10 from "path";
3324
+ import * as path11 from "path";
3093
3325
  var CI_FILES = {
3094
3326
  ".github/workflows": "github-actions",
3095
3327
  ".gitlab-ci.yml": "gitlab-ci",
@@ -3142,17 +3374,17 @@ async function scanBuildDeploy(rootDir, cache) {
3142
3374
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3143
3375
  const ciSystems = /* @__PURE__ */ new Set();
3144
3376
  for (const [file, system] of Object.entries(CI_FILES)) {
3145
- const fullPath = path10.join(rootDir, file);
3377
+ const fullPath = path11.join(rootDir, file);
3146
3378
  if (await _pathExists(fullPath)) {
3147
3379
  ciSystems.add(system);
3148
3380
  }
3149
3381
  }
3150
- const ghWorkflowDir = path10.join(rootDir, ".github", "workflows");
3382
+ const ghWorkflowDir = path11.join(rootDir, ".github", "workflows");
3151
3383
  if (await _pathExists(ghWorkflowDir)) {
3152
3384
  try {
3153
3385
  if (cache) {
3154
3386
  const entries = await cache.walkDir(rootDir);
3155
- const ghPrefix = path10.relative(rootDir, ghWorkflowDir) + path10.sep;
3387
+ const ghPrefix = path11.relative(rootDir, ghWorkflowDir) + path11.sep;
3156
3388
  result.ciWorkflowCount = entries.filter(
3157
3389
  (e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
3158
3390
  ).length;
@@ -3203,11 +3435,11 @@ async function scanBuildDeploy(rootDir, cache) {
3203
3435
  (name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
3204
3436
  );
3205
3437
  if (cfnFiles.length > 0) iacSystems.add("cloudformation");
3206
- if (await _pathExists(path10.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
3438
+ if (await _pathExists(path11.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
3207
3439
  result.iac = [...iacSystems].sort();
3208
3440
  const releaseTools = /* @__PURE__ */ new Set();
3209
3441
  for (const [file, tool] of Object.entries(RELEASE_FILES)) {
3210
- if (await _pathExists(path10.join(rootDir, file))) releaseTools.add(tool);
3442
+ if (await _pathExists(path11.join(rootDir, file))) releaseTools.add(tool);
3211
3443
  }
3212
3444
  const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
3213
3445
  for (const pjPath of pkgFiles) {
@@ -3232,19 +3464,19 @@ async function scanBuildDeploy(rootDir, cache) {
3232
3464
  };
3233
3465
  const managers = /* @__PURE__ */ new Set();
3234
3466
  for (const [file, manager] of Object.entries(lockfileMap)) {
3235
- if (await _pathExists(path10.join(rootDir, file))) managers.add(manager);
3467
+ if (await _pathExists(path11.join(rootDir, file))) managers.add(manager);
3236
3468
  }
3237
3469
  result.packageManagers = [...managers].sort();
3238
3470
  const monoTools = /* @__PURE__ */ new Set();
3239
3471
  for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
3240
- if (await _pathExists(path10.join(rootDir, file))) monoTools.add(tool);
3472
+ if (await _pathExists(path11.join(rootDir, file))) monoTools.add(tool);
3241
3473
  }
3242
3474
  result.monorepoTools = [...monoTools].sort();
3243
3475
  return result;
3244
3476
  }
3245
3477
 
3246
3478
  // src/scanners/ts-modernity.ts
3247
- import * as path11 from "path";
3479
+ import * as path12 from "path";
3248
3480
  async function scanTsModernity(rootDir, cache) {
3249
3481
  const result = {
3250
3482
  typescriptVersion: null,
@@ -3282,7 +3514,7 @@ async function scanTsModernity(rootDir, cache) {
3282
3514
  if (hasEsm && hasCjs) result.moduleType = "mixed";
3283
3515
  else if (hasEsm) result.moduleType = "esm";
3284
3516
  else if (hasCjs) result.moduleType = "cjs";
3285
- let tsConfigPath = path11.join(rootDir, "tsconfig.json");
3517
+ let tsConfigPath = path12.join(rootDir, "tsconfig.json");
3286
3518
  const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
3287
3519
  if (!tsConfigExists) {
3288
3520
  const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
@@ -3628,8 +3860,8 @@ function scanBreakingChangeExposure(projects) {
3628
3860
  }
3629
3861
 
3630
3862
  // src/scanners/file-hotspots.ts
3631
- import * as fs4 from "fs/promises";
3632
- import * as path12 from "path";
3863
+ import * as fs5 from "fs/promises";
3864
+ import * as path13 from "path";
3633
3865
  var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3634
3866
  "node_modules",
3635
3867
  ".git",
@@ -3672,13 +3904,13 @@ async function scanFileHotspots(rootDir, cache) {
3672
3904
  const entries = await cache.walkDir(rootDir);
3673
3905
  for (const entry of entries) {
3674
3906
  if (!entry.isFile) continue;
3675
- const ext = path12.extname(entry.name).toLowerCase();
3907
+ const ext = path13.extname(entry.name).toLowerCase();
3676
3908
  if (SKIP_EXTENSIONS.has(ext)) continue;
3677
- const depth = entry.relPath.split(path12.sep).length - 1;
3909
+ const depth = entry.relPath.split(path13.sep).length - 1;
3678
3910
  if (depth > maxDepth) maxDepth = depth;
3679
3911
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3680
3912
  try {
3681
- const stat3 = await fs4.stat(entry.absPath);
3913
+ const stat3 = await fs5.stat(entry.absPath);
3682
3914
  allFiles.push({
3683
3915
  path: entry.relPath,
3684
3916
  bytes: stat3.size
@@ -3691,7 +3923,7 @@ async function scanFileHotspots(rootDir, cache) {
3691
3923
  if (depth > maxDepth) maxDepth = depth;
3692
3924
  let entries;
3693
3925
  try {
3694
- const dirents = await fs4.readdir(dir, { withFileTypes: true });
3926
+ const dirents = await fs5.readdir(dir, { withFileTypes: true });
3695
3927
  entries = dirents.map((d) => ({
3696
3928
  name: d.name,
3697
3929
  isDirectory: d.isDirectory(),
@@ -3703,15 +3935,15 @@ async function scanFileHotspots(rootDir, cache) {
3703
3935
  for (const e of entries) {
3704
3936
  if (e.isDirectory) {
3705
3937
  if (SKIP_DIRS2.has(e.name)) continue;
3706
- await walk(path12.join(dir, e.name), depth + 1);
3938
+ await walk(path13.join(dir, e.name), depth + 1);
3707
3939
  } else if (e.isFile) {
3708
- const ext = path12.extname(e.name).toLowerCase();
3940
+ const ext = path13.extname(e.name).toLowerCase();
3709
3941
  if (SKIP_EXTENSIONS.has(ext)) continue;
3710
3942
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3711
3943
  try {
3712
- const stat3 = await fs4.stat(path12.join(dir, e.name));
3944
+ const stat3 = await fs5.stat(path13.join(dir, e.name));
3713
3945
  allFiles.push({
3714
- path: path12.relative(rootDir, path12.join(dir, e.name)),
3946
+ path: path13.relative(rootDir, path13.join(dir, e.name)),
3715
3947
  bytes: stat3.size
3716
3948
  });
3717
3949
  } catch {
@@ -3734,7 +3966,7 @@ async function scanFileHotspots(rootDir, cache) {
3734
3966
  }
3735
3967
 
3736
3968
  // src/scanners/security-posture.ts
3737
- import * as path13 from "path";
3969
+ import * as path14 from "path";
3738
3970
  var LOCKFILES = {
3739
3971
  "pnpm-lock.yaml": "pnpm",
3740
3972
  "package-lock.json": "npm",
@@ -3755,14 +3987,14 @@ async function scanSecurityPosture(rootDir, cache) {
3755
3987
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3756
3988
  const foundLockfiles = [];
3757
3989
  for (const [file, type] of Object.entries(LOCKFILES)) {
3758
- if (await _pathExists(path13.join(rootDir, file))) {
3990
+ if (await _pathExists(path14.join(rootDir, file))) {
3759
3991
  foundLockfiles.push(type);
3760
3992
  }
3761
3993
  }
3762
3994
  result.lockfilePresent = foundLockfiles.length > 0;
3763
3995
  result.multipleLockfileTypes = foundLockfiles.length > 1;
3764
3996
  result.lockfileTypes = foundLockfiles.sort();
3765
- const gitignorePath = path13.join(rootDir, ".gitignore");
3997
+ const gitignorePath = path14.join(rootDir, ".gitignore");
3766
3998
  if (await _pathExists(gitignorePath)) {
3767
3999
  try {
3768
4000
  const content = await _readTextFile(gitignorePath);
@@ -3777,7 +4009,7 @@ async function scanSecurityPosture(rootDir, cache) {
3777
4009
  }
3778
4010
  }
3779
4011
  for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
3780
- if (await _pathExists(path13.join(rootDir, envFile))) {
4012
+ if (await _pathExists(path14.join(rootDir, envFile))) {
3781
4013
  if (!result.gitignoreCoversEnv) {
3782
4014
  result.envFilesTracked = true;
3783
4015
  break;
@@ -4202,8 +4434,8 @@ function scanServiceDependencies(projects) {
4202
4434
  }
4203
4435
 
4204
4436
  // src/scanners/architecture.ts
4205
- import * as path14 from "path";
4206
- import * as fs5 from "fs/promises";
4437
+ import * as path15 from "path";
4438
+ import * as fs6 from "fs/promises";
4207
4439
  var ARCHETYPE_SIGNALS = [
4208
4440
  // Meta-frameworks (highest priority — they imply routing patterns)
4209
4441
  { packages: ["next", "@next/core"], archetype: "nextjs", weight: 10 },
@@ -4501,9 +4733,9 @@ async function walkSourceFiles(rootDir, cache) {
4501
4733
  const entries = await cache.walkDir(rootDir);
4502
4734
  return entries.filter((e) => {
4503
4735
  if (!e.isFile) return false;
4504
- const name = path14.basename(e.absPath);
4736
+ const name = path15.basename(e.absPath);
4505
4737
  if (name.startsWith(".") && name !== ".") return false;
4506
- const ext = path14.extname(name);
4738
+ const ext = path15.extname(name);
4507
4739
  return SOURCE_EXTENSIONS.has(ext);
4508
4740
  }).map((e) => e.relPath);
4509
4741
  }
@@ -4511,21 +4743,21 @@ async function walkSourceFiles(rootDir, cache) {
4511
4743
  async function walk(dir) {
4512
4744
  let entries;
4513
4745
  try {
4514
- entries = await fs5.readdir(dir, { withFileTypes: true });
4746
+ entries = await fs6.readdir(dir, { withFileTypes: true });
4515
4747
  } catch {
4516
4748
  return;
4517
4749
  }
4518
4750
  for (const entry of entries) {
4519
4751
  if (entry.name.startsWith(".") && entry.name !== ".") continue;
4520
- const fullPath = path14.join(dir, entry.name);
4752
+ const fullPath = path15.join(dir, entry.name);
4521
4753
  if (entry.isDirectory()) {
4522
4754
  if (!IGNORE_DIRS.has(entry.name)) {
4523
4755
  await walk(fullPath);
4524
4756
  }
4525
4757
  } else if (entry.isFile()) {
4526
- const ext = path14.extname(entry.name);
4758
+ const ext = path15.extname(entry.name);
4527
4759
  if (SOURCE_EXTENSIONS.has(ext)) {
4528
- files.push(path14.relative(rootDir, fullPath));
4760
+ files.push(path15.relative(rootDir, fullPath));
4529
4761
  }
4530
4762
  }
4531
4763
  }
@@ -4549,7 +4781,7 @@ function classifyFile(filePath, archetype) {
4549
4781
  }
4550
4782
  }
4551
4783
  if (!bestMatch || bestMatch.confidence < 0.7) {
4552
- const baseName = path14.basename(filePath, path14.extname(filePath));
4784
+ const baseName = path15.basename(filePath, path15.extname(filePath));
4553
4785
  const cleanBase = baseName.replace(/\.(test|spec)$/, "");
4554
4786
  for (const rule of SUFFIX_RULES) {
4555
4787
  if (cleanBase.endsWith(rule.suffix)) {
@@ -4745,9 +4977,11 @@ async function runScan(rootDir, opts) {
4745
4977
  const progress = new ScanProgress(rootDir);
4746
4978
  const steps = [
4747
4979
  { id: "config", label: "Loading configuration" },
4980
+ { id: "discovery", label: "Discovering workspace", weight: 3 },
4748
4981
  { id: "vcs", label: "Detecting version control" },
4749
- { id: "node", label: "Scanning Node projects" },
4750
- { 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 },
4751
4985
  ...scanners !== false ? [
4752
4986
  ...scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
4753
4987
  ...scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
@@ -4766,10 +5000,26 @@ async function runScan(rootDir, opts) {
4766
5000
  ];
4767
5001
  progress.setSteps(steps);
4768
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));
4769
5014
  progress.startStep("vcs");
4770
5015
  const vcs = await detectVcs(rootDir);
4771
5016
  const vcsDetail = vcs.type !== "unknown" ? `${vcs.type}${vcs.branch ? ` ${vcs.branch}` : ""}${vcs.shortSha ? ` @ ${vcs.shortSha}` : ""}` : "none detected";
4772
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`);
4773
5023
  progress.startStep("node");
4774
5024
  const nodeProjects = await scanNodeProjects(rootDir, npmCache, fileCache);
4775
5025
  for (const p of nodeProjects) {
@@ -4966,17 +5216,18 @@ async function runScan(rootDir, opts) {
4966
5216
  schemaVersion: "1.0",
4967
5217
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4968
5218
  vibgrateVersion: VERSION,
4969
- rootPath: path15.basename(rootDir),
5219
+ rootPath: path16.basename(rootDir),
4970
5220
  ...vcs.type !== "unknown" ? { vcs } : {},
4971
5221
  projects: allProjects,
4972
5222
  drift,
4973
5223
  findings,
4974
5224
  ...Object.keys(extended).length > 0 ? { extended } : {},
4975
5225
  durationMs,
4976
- filesScanned
5226
+ filesScanned,
5227
+ treeSummary: treeCount
4977
5228
  };
4978
5229
  if (opts.baseline) {
4979
- const baselinePath = path15.resolve(opts.baseline);
5230
+ const baselinePath = path16.resolve(opts.baseline);
4980
5231
  if (await pathExists(baselinePath)) {
4981
5232
  try {
4982
5233
  const baseline = await readJsonFile(baselinePath);
@@ -4987,15 +5238,22 @@ async function runScan(rootDir, opts) {
4987
5238
  }
4988
5239
  }
4989
5240
  }
4990
- const vibgrateDir = path15.join(rootDir, ".vibgrate");
5241
+ const vibgrateDir = path16.join(rootDir, ".vibgrate");
4991
5242
  await ensureDir(vibgrateDir);
4992
- 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
+ });
4993
5251
  for (const project of allProjects) {
4994
5252
  if (project.drift && project.path) {
4995
- const projectDir = path15.resolve(rootDir, project.path);
4996
- const projectVibgrateDir = path15.join(projectDir, ".vibgrate");
5253
+ const projectDir = path16.resolve(rootDir, project.path);
5254
+ const projectVibgrateDir = path16.join(projectDir, ".vibgrate");
4997
5255
  await ensureDir(projectVibgrateDir);
4998
- await writeJsonFile(path15.join(projectVibgrateDir, "project_score.json"), {
5256
+ await writeJsonFile(path16.join(projectVibgrateDir, "project_score.json"), {
4999
5257
  projectId: project.projectId,
5000
5258
  name: project.name,
5001
5259
  type: project.type,
@@ -5012,7 +5270,7 @@ async function runScan(rootDir, opts) {
5012
5270
  if (opts.format === "json") {
5013
5271
  const jsonStr = JSON.stringify(artifact, null, 2);
5014
5272
  if (opts.out) {
5015
- await writeTextFile(path15.resolve(opts.out), jsonStr);
5273
+ await writeTextFile(path16.resolve(opts.out), jsonStr);
5016
5274
  console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
5017
5275
  } else {
5018
5276
  console.log(jsonStr);
@@ -5021,7 +5279,7 @@ async function runScan(rootDir, opts) {
5021
5279
  const sarif = formatSarif(artifact);
5022
5280
  const sarifStr = JSON.stringify(sarif, null, 2);
5023
5281
  if (opts.out) {
5024
- await writeTextFile(path15.resolve(opts.out), sarifStr);
5282
+ await writeTextFile(path16.resolve(opts.out), sarifStr);
5025
5283
  console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
5026
5284
  } else {
5027
5285
  console.log(sarifStr);
@@ -5030,7 +5288,7 @@ async function runScan(rootDir, opts) {
5030
5288
  const text = formatText(artifact);
5031
5289
  console.log(text);
5032
5290
  if (opts.out) {
5033
- await writeTextFile(path15.resolve(opts.out), text);
5291
+ await writeTextFile(path16.resolve(opts.out), text);
5034
5292
  }
5035
5293
  }
5036
5294
  return artifact;
@@ -5089,7 +5347,7 @@ async function autoPush(artifact, rootDir, opts) {
5089
5347
  }
5090
5348
  }
5091
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) => {
5092
- const rootDir = path15.resolve(targetPath);
5350
+ const rootDir = path16.resolve(targetPath);
5093
5351
  if (!await pathExists(rootDir)) {
5094
5352
  console.error(chalk5.red(`Path does not exist: ${rootDir}`));
5095
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-YWBGG2KK.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-T6WMUKLV.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-7KBX56UU.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-T6WMUKLV.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.20",
3
+ "version": "1.0.21",
4
4
  "description": "CLI for measuring upgrade drift across Node & .NET projects",
5
5
  "type": "module",
6
6
  "bin": {