@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.
- package/dist/{baseline-7KBX56UU.js → baseline-7WI3SI6H.js} +2 -2
- package/dist/{chunk-YWBGG2KK.js → chunk-74QSNBZA.js} +1 -1
- package/dist/{chunk-VXZT34Y5.js → chunk-GN3IWKSY.js} +1 -0
- package/dist/{chunk-T6WMUKLV.js → chunk-L6R5WSCC.js} +334 -76
- 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,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
|
|
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:
|
|
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(
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
-
/**
|
|
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 = (
|
|
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}
|
|
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
|
|
2194
|
-
|
|
2195
|
-
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;
|
|
2196
2291
|
const barWidth = 30;
|
|
2197
|
-
const filled = Math.round(
|
|
2198
|
-
const bar = chalk4.greenBright("\u2501".repeat(filled)) + chalk4.dim("\u254C".repeat(barWidth - filled));
|
|
2199
|
-
const
|
|
2200
|
-
|
|
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/
|
|
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(
|
|
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
|
|
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 =
|
|
2698
|
-
const npmLock =
|
|
2699
|
-
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");
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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 =
|
|
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
|
|
3632
|
-
import * as
|
|
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 =
|
|
3907
|
+
const ext = path13.extname(entry.name).toLowerCase();
|
|
3676
3908
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
3677
|
-
const depth = entry.relPath.split(
|
|
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
|
|
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
|
|
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(
|
|
3938
|
+
await walk(path13.join(dir, e.name), depth + 1);
|
|
3707
3939
|
} else if (e.isFile) {
|
|
3708
|
-
const ext =
|
|
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
|
|
3944
|
+
const stat3 = await fs5.stat(path13.join(dir, e.name));
|
|
3713
3945
|
allFiles.push({
|
|
3714
|
-
path:
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
4206
|
-
import * as
|
|
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 =
|
|
4736
|
+
const name = path15.basename(e.absPath);
|
|
4505
4737
|
if (name.startsWith(".") && name !== ".") return false;
|
|
4506
|
-
const ext =
|
|
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
|
|
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 =
|
|
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 =
|
|
4758
|
+
const ext = path15.extname(entry.name);
|
|
4527
4759
|
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
4528
|
-
files.push(
|
|
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 =
|
|
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: "
|
|
4750
|
-
{ 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 },
|
|
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:
|
|
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 =
|
|
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 =
|
|
5241
|
+
const vibgrateDir = path16.join(rootDir, ".vibgrate");
|
|
4991
5242
|
await ensureDir(vibgrateDir);
|
|
4992
|
-
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
|
+
});
|
|
4993
5251
|
for (const project of allProjects) {
|
|
4994
5252
|
if (project.drift && project.path) {
|
|
4995
|
-
const projectDir =
|
|
4996
|
-
const projectVibgrateDir =
|
|
5253
|
+
const projectDir = path16.resolve(rootDir, project.path);
|
|
5254
|
+
const projectVibgrateDir = path16.join(projectDir, ".vibgrate");
|
|
4997
5255
|
await ensureDir(projectVibgrateDir);
|
|
4998
|
-
await writeJsonFile(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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-
|
|
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,
|