@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.
- package/dist/{baseline-TJLWFSMH.js → baseline-7WI3SI6H.js} +2 -2
- package/dist/{chunk-XVGUFKAC.js → chunk-74QSNBZA.js} +1 -1
- package/dist/{chunk-VXZT34Y5.js → chunk-GN3IWKSY.js} +1 -0
- package/dist/{chunk-EVLKMTYE.js → chunk-L6R5WSCC.js} +338 -78
- package/dist/cli.js +4 -4
- package/dist/index.d.ts +8 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
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:
|
|
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(
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
-
/**
|
|
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 = (
|
|
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}
|
|
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
|
|
2192
|
-
|
|
2193
|
-
const
|
|
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(
|
|
2196
|
-
const bar = chalk4.greenBright("\u2501".repeat(filled)) + chalk4.dim("\u254C".repeat(barWidth - filled));
|
|
2197
|
-
const
|
|
2198
|
-
|
|
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/
|
|
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(
|
|
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
|
|
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 =
|
|
2696
|
-
const npmLock =
|
|
2697
|
-
const yarnLock =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
3630
|
-
import * as
|
|
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 =
|
|
3907
|
+
const ext = path13.extname(entry.name).toLowerCase();
|
|
3674
3908
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
3675
|
-
const depth = entry.relPath.split(
|
|
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
|
|
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
|
|
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(
|
|
3938
|
+
await walk(path13.join(dir, e.name), depth + 1);
|
|
3705
3939
|
} else if (e.isFile) {
|
|
3706
|
-
const ext =
|
|
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
|
|
3944
|
+
const stat3 = await fs5.stat(path13.join(dir, e.name));
|
|
3711
3945
|
allFiles.push({
|
|
3712
|
-
path:
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
4204
|
-
import * as
|
|
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 =
|
|
4736
|
+
const name = path15.basename(e.absPath);
|
|
4503
4737
|
if (name.startsWith(".") && name !== ".") return false;
|
|
4504
|
-
const ext =
|
|
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
|
|
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 =
|
|
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 =
|
|
4758
|
+
const ext = path15.extname(entry.name);
|
|
4525
4759
|
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
4526
|
-
files.push(
|
|
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 =
|
|
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: "
|
|
4748
|
-
{ id: "
|
|
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:
|
|
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 =
|
|
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 =
|
|
5241
|
+
const vibgrateDir = path16.join(rootDir, ".vibgrate");
|
|
4989
5242
|
await ensureDir(vibgrateDir);
|
|
4990
|
-
await writeJsonFile(
|
|
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 =
|
|
4994
|
-
const projectVibgrateDir =
|
|
5253
|
+
const projectDir = path16.resolve(rootDir, project.path);
|
|
5254
|
+
const projectVibgrateDir = path16.join(projectDir, ".vibgrate");
|
|
4995
5255
|
await ensureDir(projectVibgrateDir);
|
|
4996
|
-
await writeJsonFile(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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-
|
|
4
|
+
} from "./chunk-GN3IWKSY.js";
|
|
5
5
|
import {
|
|
6
6
|
baselineCommand
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
10
|
+
} from "./chunk-L6R5WSCC.js";
|
|
11
11
|
export {
|
|
12
12
|
computeDriftScore,
|
|
13
13
|
formatMarkdown,
|