@vibgrate/cli 1.0.20 → 1.0.22
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-IOXJPCX7.js} +2 -2
- package/dist/{chunk-VXZT34Y5.js → chunk-GN3IWKSY.js} +1 -0
- package/dist/{chunk-YWBGG2KK.js → chunk-IMK7DUPY.js} +1 -1
- package/dist/{chunk-T6WMUKLV.js → chunk-JFMGFWKC.js} +720 -139
- package/dist/cli.js +4 -4
- package/dist/index.d.ts +11 -0
- package/dist/index.js +2 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/utils/fs.ts
|
|
2
2
|
import * as fs from "fs/promises";
|
|
3
3
|
import * as os from "os";
|
|
4
|
-
import * as
|
|
4
|
+
import * as path2 from "path";
|
|
5
5
|
|
|
6
6
|
// src/utils/semaphore.ts
|
|
7
7
|
var Semaphore = class {
|
|
@@ -32,6 +32,86 @@ var Semaphore = class {
|
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// src/utils/glob.ts
|
|
36
|
+
import * as path from "path";
|
|
37
|
+
function compileGlobs(patterns) {
|
|
38
|
+
if (patterns.length === 0) return null;
|
|
39
|
+
const matchers = patterns.map((p) => compileOne(normalise(p)));
|
|
40
|
+
return (relPath) => {
|
|
41
|
+
const norm = normalise(relPath);
|
|
42
|
+
return matchers.some((m) => m(norm));
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function normalise(p) {
|
|
46
|
+
return p.split(path.sep).join("/").replace(/\/+$/, "");
|
|
47
|
+
}
|
|
48
|
+
function compileOne(pattern) {
|
|
49
|
+
if (!pattern.includes("/") && !hasGlobChars(pattern)) {
|
|
50
|
+
const prefix = pattern + "/";
|
|
51
|
+
return (p) => p === pattern || p.startsWith(prefix);
|
|
52
|
+
}
|
|
53
|
+
const re = globToRegex(pattern);
|
|
54
|
+
return (p) => re.test(p);
|
|
55
|
+
}
|
|
56
|
+
function hasGlobChars(s) {
|
|
57
|
+
return /[*?[\]{}]/.test(s);
|
|
58
|
+
}
|
|
59
|
+
function globToRegex(pattern) {
|
|
60
|
+
let i = 0;
|
|
61
|
+
let re = "^";
|
|
62
|
+
const len = pattern.length;
|
|
63
|
+
while (i < len) {
|
|
64
|
+
const ch = pattern[i];
|
|
65
|
+
if (ch === "*") {
|
|
66
|
+
if (pattern[i + 1] === "*") {
|
|
67
|
+
i += 2;
|
|
68
|
+
if (pattern[i] === "/") {
|
|
69
|
+
i++;
|
|
70
|
+
re += "(?:.+/)?";
|
|
71
|
+
} else {
|
|
72
|
+
re += ".*";
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
i++;
|
|
76
|
+
re += "[^/]*";
|
|
77
|
+
}
|
|
78
|
+
} else if (ch === "?") {
|
|
79
|
+
i++;
|
|
80
|
+
re += "[^/]";
|
|
81
|
+
} else if (ch === "[") {
|
|
82
|
+
const start = i;
|
|
83
|
+
i++;
|
|
84
|
+
while (i < len && pattern[i] !== "]") i++;
|
|
85
|
+
i++;
|
|
86
|
+
re += pattern.slice(start, i);
|
|
87
|
+
} else if (ch === "{") {
|
|
88
|
+
i++;
|
|
89
|
+
const alternatives = [];
|
|
90
|
+
let current = "";
|
|
91
|
+
while (i < len && pattern[i] !== "}") {
|
|
92
|
+
if (pattern[i] === ",") {
|
|
93
|
+
alternatives.push(current);
|
|
94
|
+
current = "";
|
|
95
|
+
} else {
|
|
96
|
+
current += pattern[i];
|
|
97
|
+
}
|
|
98
|
+
i++;
|
|
99
|
+
}
|
|
100
|
+
alternatives.push(current);
|
|
101
|
+
i++;
|
|
102
|
+
re += "(?:" + alternatives.map(escapeRegex).join("|") + ")";
|
|
103
|
+
} else {
|
|
104
|
+
re += escapeRegex(ch);
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
re += "$";
|
|
109
|
+
return new RegExp(re);
|
|
110
|
+
}
|
|
111
|
+
function escapeRegex(s) {
|
|
112
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
113
|
+
}
|
|
114
|
+
|
|
35
115
|
// src/utils/fs.ts
|
|
36
116
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
37
117
|
"node_modules",
|
|
@@ -60,6 +140,40 @@ var FileCache = class _FileCache {
|
|
|
60
140
|
jsonCache = /* @__PURE__ */ new Map();
|
|
61
141
|
/** pathExists keyed by absolute path */
|
|
62
142
|
existsCache = /* @__PURE__ */ new Map();
|
|
143
|
+
/** User-configured exclude predicate (compiled from glob patterns) */
|
|
144
|
+
excludePredicate = null;
|
|
145
|
+
/** Directories that were auto-skipped because they were stuck (>60s) */
|
|
146
|
+
_stuckPaths = [];
|
|
147
|
+
/** Files skipped because they exceed maxFileSizeToScan */
|
|
148
|
+
_skippedLargeFiles = [];
|
|
149
|
+
/** Maximum file size (bytes) we will read. 0 = unlimited. */
|
|
150
|
+
_maxFileSize = 0;
|
|
151
|
+
/** Root dir for relative-path computation (set by the first walkDir call) */
|
|
152
|
+
_rootDir = null;
|
|
153
|
+
/** Set exclude patterns from config (call once before the walk) */
|
|
154
|
+
setExcludePatterns(patterns) {
|
|
155
|
+
this.excludePredicate = compileGlobs(patterns);
|
|
156
|
+
}
|
|
157
|
+
/** Set the maximum file size in bytes that readTextFile / readJsonFile will process */
|
|
158
|
+
setMaxFileSize(bytes) {
|
|
159
|
+
this._maxFileSize = bytes;
|
|
160
|
+
}
|
|
161
|
+
/** Record a path that timed out or was stuck during scanning */
|
|
162
|
+
addStuckPath(relPath) {
|
|
163
|
+
this._stuckPaths.push(relPath);
|
|
164
|
+
}
|
|
165
|
+
/** Get all paths that were auto-skipped due to being stuck (dirs + scanner files) */
|
|
166
|
+
get stuckPaths() {
|
|
167
|
+
return this._stuckPaths;
|
|
168
|
+
}
|
|
169
|
+
/** @deprecated Use stuckPaths instead */
|
|
170
|
+
get stuckDirs() {
|
|
171
|
+
return this._stuckPaths;
|
|
172
|
+
}
|
|
173
|
+
/** Get files that were skipped because they exceeded maxFileSizeToScan */
|
|
174
|
+
get skippedLargeFiles() {
|
|
175
|
+
return this._skippedLargeFiles;
|
|
176
|
+
}
|
|
63
177
|
// ── Directory walking ──
|
|
64
178
|
/**
|
|
65
179
|
* Walk the directory tree from `rootDir` once, skipping SKIP_DIRS plus
|
|
@@ -69,43 +183,71 @@ var FileCache = class _FileCache {
|
|
|
69
183
|
* Consumers that need additional filtering (e.g. SOURCE_EXTENSIONS,
|
|
70
184
|
* SKIP_EXTENSIONS) do so on the returned entries — no separate walk.
|
|
71
185
|
*/
|
|
72
|
-
walkDir(rootDir) {
|
|
186
|
+
walkDir(rootDir, onProgress) {
|
|
187
|
+
this._rootDir = rootDir;
|
|
73
188
|
const cached = this.walkCache.get(rootDir);
|
|
74
189
|
if (cached) return cached;
|
|
75
|
-
const promise = this._doWalk(rootDir);
|
|
190
|
+
const promise = this._doWalk(rootDir, onProgress);
|
|
76
191
|
this.walkCache.set(rootDir, promise);
|
|
77
192
|
return promise;
|
|
78
193
|
}
|
|
79
194
|
/** Additional dirs skipped only by the cached walk (framework outputs) */
|
|
80
195
|
static EXTRA_SKIP = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
|
|
81
|
-
async _doWalk(rootDir) {
|
|
196
|
+
async _doWalk(rootDir, onProgress) {
|
|
82
197
|
const results = [];
|
|
83
198
|
const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
|
|
84
199
|
const maxConcurrentReads = Math.max(8, Math.min(64, cores * 4));
|
|
200
|
+
let foundCount = 0;
|
|
201
|
+
let lastReported = 0;
|
|
202
|
+
const REPORT_INTERVAL = 50;
|
|
85
203
|
const sem = new Semaphore(maxConcurrentReads);
|
|
204
|
+
const STUCK_TIMEOUT_MS = 6e4;
|
|
86
205
|
const extraSkip = _FileCache.EXTRA_SKIP;
|
|
206
|
+
const isExcluded = this.excludePredicate;
|
|
207
|
+
const stuckDirs = this._stuckPaths;
|
|
87
208
|
async function walk(dir) {
|
|
209
|
+
const relDir = path2.relative(rootDir, dir);
|
|
88
210
|
let entries;
|
|
89
211
|
try {
|
|
90
|
-
|
|
212
|
+
const readPromise = fs.readdir(dir, { withFileTypes: true });
|
|
213
|
+
const result = await Promise.race([
|
|
214
|
+
readPromise.then((e) => ({ ok: true, entries: e })),
|
|
215
|
+
new Promise(
|
|
216
|
+
(resolve7) => setTimeout(() => resolve7({ ok: false }), STUCK_TIMEOUT_MS)
|
|
217
|
+
)
|
|
218
|
+
]);
|
|
219
|
+
if (!result.ok) {
|
|
220
|
+
stuckDirs.push(relDir || dir);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
entries = result.entries;
|
|
91
224
|
} catch {
|
|
92
225
|
return;
|
|
93
226
|
}
|
|
94
227
|
const subWalks = [];
|
|
95
228
|
for (const e of entries) {
|
|
96
|
-
const absPath =
|
|
97
|
-
const relPath =
|
|
229
|
+
const absPath = path2.join(dir, e.name);
|
|
230
|
+
const relPath = path2.relative(rootDir, absPath);
|
|
231
|
+
if (isExcluded && isExcluded(relPath)) continue;
|
|
98
232
|
if (e.isDirectory()) {
|
|
99
233
|
if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
|
|
100
234
|
results.push({ absPath, relPath, name: e.name, isFile: false, isDirectory: true });
|
|
101
235
|
subWalks.push(sem.run(() => walk(absPath)));
|
|
102
236
|
} else if (e.isFile()) {
|
|
103
237
|
results.push({ absPath, relPath, name: e.name, isFile: true, isDirectory: false });
|
|
238
|
+
foundCount++;
|
|
239
|
+
if (onProgress && foundCount - lastReported >= REPORT_INTERVAL) {
|
|
240
|
+
lastReported = foundCount;
|
|
241
|
+
onProgress(foundCount, relPath);
|
|
242
|
+
}
|
|
104
243
|
}
|
|
105
244
|
}
|
|
106
245
|
await Promise.all(subWalks);
|
|
107
246
|
}
|
|
108
247
|
await sem.run(() => walk(rootDir));
|
|
248
|
+
if (onProgress && foundCount !== lastReported) {
|
|
249
|
+
onProgress(foundCount, "");
|
|
250
|
+
}
|
|
109
251
|
return results;
|
|
110
252
|
}
|
|
111
253
|
/**
|
|
@@ -130,17 +272,36 @@ var FileCache = class _FileCache {
|
|
|
130
272
|
* Read a text file. Files ≤ 1 MB are cached so subsequent calls from
|
|
131
273
|
* different scanners return the same string. Files > 1 MB (lockfiles,
|
|
132
274
|
* large generated files) are read directly and never retained.
|
|
275
|
+
*
|
|
276
|
+
* If maxFileSizeToScan is set and the file exceeds it, the file is
|
|
277
|
+
* recorded as skipped and an empty string is returned.
|
|
133
278
|
*/
|
|
134
279
|
readTextFile(filePath) {
|
|
135
|
-
const abs =
|
|
280
|
+
const abs = path2.resolve(filePath);
|
|
136
281
|
const cached = this.textCache.get(abs);
|
|
137
282
|
if (cached) return cached;
|
|
138
|
-
const
|
|
283
|
+
const maxSize = this._maxFileSize;
|
|
284
|
+
const skippedLarge = this._skippedLargeFiles;
|
|
285
|
+
const rootDir = this._rootDir;
|
|
286
|
+
const promise = (async () => {
|
|
287
|
+
if (maxSize > 0) {
|
|
288
|
+
try {
|
|
289
|
+
const stat4 = await fs.stat(abs);
|
|
290
|
+
if (stat4.size > maxSize) {
|
|
291
|
+
const rel = rootDir ? path2.relative(rootDir, abs) : abs;
|
|
292
|
+
skippedLarge.push(rel);
|
|
293
|
+
this.textCache.delete(abs);
|
|
294
|
+
return "";
|
|
295
|
+
}
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const content = await fs.readFile(abs, "utf8");
|
|
139
300
|
if (content.length > TEXT_CACHE_MAX_BYTES) {
|
|
140
301
|
this.textCache.delete(abs);
|
|
141
302
|
}
|
|
142
303
|
return content;
|
|
143
|
-
});
|
|
304
|
+
})();
|
|
144
305
|
this.textCache.set(abs, promise);
|
|
145
306
|
return promise;
|
|
146
307
|
}
|
|
@@ -149,7 +310,7 @@ var FileCache = class _FileCache {
|
|
|
149
310
|
* text is evicted immediately so we never hold both representations.
|
|
150
311
|
*/
|
|
151
312
|
readJsonFile(filePath) {
|
|
152
|
-
const abs =
|
|
313
|
+
const abs = path2.resolve(filePath);
|
|
153
314
|
const cached = this.jsonCache.get(abs);
|
|
154
315
|
if (cached) return cached;
|
|
155
316
|
const promise = this.readTextFile(abs).then((txt) => {
|
|
@@ -161,7 +322,7 @@ var FileCache = class _FileCache {
|
|
|
161
322
|
}
|
|
162
323
|
// ── Existence checks ──
|
|
163
324
|
pathExists(p) {
|
|
164
|
-
const abs =
|
|
325
|
+
const abs = path2.resolve(p);
|
|
165
326
|
const cached = this.existsCache.get(abs);
|
|
166
327
|
if (cached) return cached;
|
|
167
328
|
const promise = fs.access(abs).then(() => true, () => false);
|
|
@@ -185,6 +346,38 @@ var FileCache = class _FileCache {
|
|
|
185
346
|
return this.jsonCache.size;
|
|
186
347
|
}
|
|
187
348
|
};
|
|
349
|
+
async function quickTreeCount(rootDir, excludePatterns) {
|
|
350
|
+
let totalFiles = 0;
|
|
351
|
+
let totalDirs = 0;
|
|
352
|
+
const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
|
|
353
|
+
const maxConcurrent = Math.max(8, Math.min(128, cores * 8));
|
|
354
|
+
const sem = new Semaphore(maxConcurrent);
|
|
355
|
+
const extraSkip = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
|
|
356
|
+
const isExcluded = excludePatterns ? compileGlobs(excludePatterns) : null;
|
|
357
|
+
async function count(dir) {
|
|
358
|
+
let entries;
|
|
359
|
+
try {
|
|
360
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
361
|
+
} catch {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const subs = [];
|
|
365
|
+
for (const e of entries) {
|
|
366
|
+
const relPath = path2.relative(rootDir, path2.join(dir, e.name));
|
|
367
|
+
if (isExcluded && isExcluded(relPath)) continue;
|
|
368
|
+
if (e.isDirectory()) {
|
|
369
|
+
if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
|
|
370
|
+
totalDirs++;
|
|
371
|
+
subs.push(sem.run(() => count(path2.join(dir, e.name))));
|
|
372
|
+
} else if (e.isFile()) {
|
|
373
|
+
totalFiles++;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
await Promise.all(subs);
|
|
377
|
+
}
|
|
378
|
+
await sem.run(() => count(rootDir));
|
|
379
|
+
return { totalFiles, totalDirs };
|
|
380
|
+
}
|
|
188
381
|
async function findFiles(rootDir, predicate) {
|
|
189
382
|
const results = [];
|
|
190
383
|
const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
|
|
@@ -201,9 +394,9 @@ async function findFiles(rootDir, predicate) {
|
|
|
201
394
|
for (const e of entries) {
|
|
202
395
|
if (e.isDirectory()) {
|
|
203
396
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
204
|
-
subDirectoryWalks.push(readDirSemaphore.run(() => walk(
|
|
397
|
+
subDirectoryWalks.push(readDirSemaphore.run(() => walk(path2.join(dir, e.name))));
|
|
205
398
|
} else if (e.isFile() && predicate(e.name)) {
|
|
206
|
-
results.push(
|
|
399
|
+
results.push(path2.join(dir, e.name));
|
|
207
400
|
}
|
|
208
401
|
}
|
|
209
402
|
await Promise.all(subDirectoryWalks);
|
|
@@ -239,11 +432,11 @@ async function ensureDir(dir) {
|
|
|
239
432
|
await fs.mkdir(dir, { recursive: true });
|
|
240
433
|
}
|
|
241
434
|
async function writeJsonFile(filePath, data) {
|
|
242
|
-
await ensureDir(
|
|
435
|
+
await ensureDir(path2.dirname(filePath));
|
|
243
436
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
244
437
|
}
|
|
245
438
|
async function writeTextFile(filePath, content) {
|
|
246
|
-
await ensureDir(
|
|
439
|
+
await ensureDir(path2.dirname(filePath));
|
|
247
440
|
await fs.writeFile(filePath, content, "utf8");
|
|
248
441
|
}
|
|
249
442
|
|
|
@@ -323,12 +516,12 @@ function eolScore(projects) {
|
|
|
323
516
|
}
|
|
324
517
|
function computeDriftScore(projects) {
|
|
325
518
|
const rs = runtimeScore(projects);
|
|
326
|
-
const
|
|
519
|
+
const fs7 = frameworkScore(projects);
|
|
327
520
|
const ds = dependencyScore(projects);
|
|
328
521
|
const es = eolScore(projects);
|
|
329
522
|
const components = [
|
|
330
523
|
{ score: rs, weight: 0.25 },
|
|
331
|
-
{ score:
|
|
524
|
+
{ score: fs7, weight: 0.25 },
|
|
332
525
|
{ score: ds, weight: 0.3 },
|
|
333
526
|
{ score: es, weight: 0.2 }
|
|
334
527
|
];
|
|
@@ -339,7 +532,7 @@ function computeDriftScore(projects) {
|
|
|
339
532
|
riskLevel: "low",
|
|
340
533
|
components: {
|
|
341
534
|
runtimeScore: Math.round(rs ?? 100),
|
|
342
|
-
frameworkScore: Math.round(
|
|
535
|
+
frameworkScore: Math.round(fs7 ?? 100),
|
|
343
536
|
dependencyScore: Math.round(ds ?? 100),
|
|
344
537
|
eolScore: Math.round(es ?? 100)
|
|
345
538
|
}
|
|
@@ -357,7 +550,7 @@ function computeDriftScore(projects) {
|
|
|
357
550
|
else riskLevel = "high";
|
|
358
551
|
const measured = [];
|
|
359
552
|
if (rs !== null) measured.push("runtime");
|
|
360
|
-
if (
|
|
553
|
+
if (fs7 !== null) measured.push("framework");
|
|
361
554
|
if (ds !== null) measured.push("dependency");
|
|
362
555
|
if (es !== null) measured.push("eol");
|
|
363
556
|
return {
|
|
@@ -365,7 +558,7 @@ function computeDriftScore(projects) {
|
|
|
365
558
|
riskLevel,
|
|
366
559
|
components: {
|
|
367
560
|
runtimeScore: Math.round(rs ?? 100),
|
|
368
|
-
frameworkScore: Math.round(
|
|
561
|
+
frameworkScore: Math.round(fs7 ?? 100),
|
|
369
562
|
dependencyScore: Math.round(ds ?? 100),
|
|
370
563
|
eolScore: Math.round(es ?? 100)
|
|
371
564
|
},
|
|
@@ -565,6 +758,10 @@ function formatText(artifact) {
|
|
|
565
758
|
if (artifact.filesScanned !== void 0) {
|
|
566
759
|
scannedParts.push(`${artifact.filesScanned} file${artifact.filesScanned !== 1 ? "s" : ""} scanned`);
|
|
567
760
|
}
|
|
761
|
+
if (artifact.treeSummary) {
|
|
762
|
+
scannedParts.push(`${artifact.treeSummary.totalFiles.toLocaleString()} workspace files`);
|
|
763
|
+
scannedParts.push(`${artifact.treeSummary.totalDirs.toLocaleString()} dirs`);
|
|
764
|
+
}
|
|
568
765
|
lines.push(chalk.dim(` ${scannedParts.join(" \xB7 ")}`));
|
|
569
766
|
lines.push("");
|
|
570
767
|
return lines.join("\n");
|
|
@@ -1173,7 +1370,7 @@ function toSarifResult(finding) {
|
|
|
1173
1370
|
|
|
1174
1371
|
// src/commands/dsn.ts
|
|
1175
1372
|
import * as crypto2 from "crypto";
|
|
1176
|
-
import * as
|
|
1373
|
+
import * as path3 from "path";
|
|
1177
1374
|
import { Command } from "commander";
|
|
1178
1375
|
import chalk2 from "chalk";
|
|
1179
1376
|
var REGION_HOSTS = {
|
|
@@ -1218,7 +1415,7 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
|
|
|
1218
1415
|
console.log(chalk2.dim("Set this as VIBGRATE_DSN in your CI environment."));
|
|
1219
1416
|
console.log(chalk2.dim("The secret must be registered on your Vibgrate ingest API."));
|
|
1220
1417
|
if (opts.write) {
|
|
1221
|
-
const writePath =
|
|
1418
|
+
const writePath = path3.resolve(opts.write);
|
|
1222
1419
|
await writeTextFile(writePath, dsn + "\n");
|
|
1223
1420
|
console.log("");
|
|
1224
1421
|
console.log(chalk2.green("\u2714") + ` DSN written to ${opts.write}`);
|
|
@@ -1228,7 +1425,7 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
|
|
|
1228
1425
|
|
|
1229
1426
|
// src/commands/push.ts
|
|
1230
1427
|
import * as crypto3 from "crypto";
|
|
1231
|
-
import * as
|
|
1428
|
+
import * as path4 from "path";
|
|
1232
1429
|
import { Command as Command2 } from "commander";
|
|
1233
1430
|
import chalk3 from "chalk";
|
|
1234
1431
|
function parseDsn(dsn) {
|
|
@@ -1257,7 +1454,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1257
1454
|
if (opts.strict) process.exit(1);
|
|
1258
1455
|
return;
|
|
1259
1456
|
}
|
|
1260
|
-
const filePath =
|
|
1457
|
+
const filePath = path4.resolve(opts.file);
|
|
1261
1458
|
if (!await pathExists(filePath)) {
|
|
1262
1459
|
console.error(chalk3.red(`Scan artifact not found: ${filePath}`));
|
|
1263
1460
|
console.error(chalk3.dim('Run "vibgrate scan" first.'));
|
|
@@ -1302,14 +1499,31 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1302
1499
|
});
|
|
1303
1500
|
|
|
1304
1501
|
// src/commands/scan.ts
|
|
1305
|
-
import * as
|
|
1502
|
+
import * as path17 from "path";
|
|
1306
1503
|
import { Command as Command3 } from "commander";
|
|
1307
1504
|
import chalk5 from "chalk";
|
|
1308
1505
|
|
|
1309
1506
|
// src/scanners/node-scanner.ts
|
|
1310
|
-
import * as
|
|
1507
|
+
import * as path5 from "path";
|
|
1311
1508
|
import * as semver2 from "semver";
|
|
1312
1509
|
|
|
1510
|
+
// src/utils/timeout.ts
|
|
1511
|
+
async function withTimeout(promise, ms) {
|
|
1512
|
+
let timer;
|
|
1513
|
+
const timeout = new Promise((resolve7) => {
|
|
1514
|
+
timer = setTimeout(() => resolve7({ ok: false }), ms);
|
|
1515
|
+
});
|
|
1516
|
+
try {
|
|
1517
|
+
const result = await Promise.race([
|
|
1518
|
+
promise.then((value) => ({ ok: true, value })),
|
|
1519
|
+
timeout
|
|
1520
|
+
]);
|
|
1521
|
+
return result;
|
|
1522
|
+
} finally {
|
|
1523
|
+
clearTimeout(timer);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1313
1527
|
// src/scanners/npm-cache.ts
|
|
1314
1528
|
import { spawn } from "child_process";
|
|
1315
1529
|
import * as semver from "semver";
|
|
@@ -1484,10 +1698,20 @@ var KNOWN_FRAMEWORKS = {
|
|
|
1484
1698
|
async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
1485
1699
|
const packageJsonFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
1486
1700
|
const results = [];
|
|
1701
|
+
const STUCK_TIMEOUT_MS = 6e4;
|
|
1487
1702
|
for (const pjPath of packageJsonFiles) {
|
|
1488
1703
|
try {
|
|
1489
|
-
const
|
|
1490
|
-
|
|
1704
|
+
const scanPromise = scanOnePackageJson(pjPath, rootDir, npmCache, cache);
|
|
1705
|
+
const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
|
|
1706
|
+
if (result.ok) {
|
|
1707
|
+
results.push(result.value);
|
|
1708
|
+
} else {
|
|
1709
|
+
const relPath = path5.relative(rootDir, path5.dirname(pjPath));
|
|
1710
|
+
if (cache) {
|
|
1711
|
+
cache.addStuckPath(relPath || ".");
|
|
1712
|
+
}
|
|
1713
|
+
console.error(`Timeout scanning ${pjPath} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
1714
|
+
}
|
|
1491
1715
|
} catch (e) {
|
|
1492
1716
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1493
1717
|
console.error(`Error scanning ${pjPath}: ${msg}`);
|
|
@@ -1497,8 +1721,8 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
|
1497
1721
|
}
|
|
1498
1722
|
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
1499
1723
|
const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
|
|
1500
|
-
const absProjectPath =
|
|
1501
|
-
const projectPath =
|
|
1724
|
+
const absProjectPath = path5.dirname(packageJsonPath);
|
|
1725
|
+
const projectPath = path5.relative(rootDir, absProjectPath) || ".";
|
|
1502
1726
|
const nodeEngine = pj.engines?.node ?? void 0;
|
|
1503
1727
|
let runtimeLatest;
|
|
1504
1728
|
let runtimeMajorsBehind;
|
|
@@ -1580,7 +1804,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
1580
1804
|
return {
|
|
1581
1805
|
type: "node",
|
|
1582
1806
|
path: projectPath,
|
|
1583
|
-
name: pj.name ??
|
|
1807
|
+
name: pj.name ?? path5.basename(absProjectPath),
|
|
1584
1808
|
runtime: nodeEngine,
|
|
1585
1809
|
runtimeLatest,
|
|
1586
1810
|
runtimeMajorsBehind,
|
|
@@ -1591,7 +1815,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
1591
1815
|
}
|
|
1592
1816
|
|
|
1593
1817
|
// src/scanners/dotnet-scanner.ts
|
|
1594
|
-
import * as
|
|
1818
|
+
import * as path6 from "path";
|
|
1595
1819
|
import { XMLParser } from "fast-xml-parser";
|
|
1596
1820
|
var parser = new XMLParser({
|
|
1597
1821
|
ignoreAttributes: false,
|
|
@@ -1792,7 +2016,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1792
2016
|
const parsed = parser.parse(xml);
|
|
1793
2017
|
const project = parsed?.Project;
|
|
1794
2018
|
if (!project) {
|
|
1795
|
-
return { targetFrameworks: [], packageReferences: [], projectName:
|
|
2019
|
+
return { targetFrameworks: [], packageReferences: [], projectName: path6.basename(filePath, ".csproj") };
|
|
1796
2020
|
}
|
|
1797
2021
|
const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
|
|
1798
2022
|
const targetFrameworks = [];
|
|
@@ -1820,7 +2044,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1820
2044
|
return {
|
|
1821
2045
|
targetFrameworks: [...new Set(targetFrameworks)],
|
|
1822
2046
|
packageReferences,
|
|
1823
|
-
projectName:
|
|
2047
|
+
projectName: path6.basename(filePath, ".csproj")
|
|
1824
2048
|
};
|
|
1825
2049
|
}
|
|
1826
2050
|
async function scanDotnetProjects(rootDir, cache) {
|
|
@@ -1830,12 +2054,12 @@ async function scanDotnetProjects(rootDir, cache) {
|
|
|
1830
2054
|
for (const slnPath of slnFiles) {
|
|
1831
2055
|
try {
|
|
1832
2056
|
const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
|
|
1833
|
-
const slnDir =
|
|
2057
|
+
const slnDir = path6.dirname(slnPath);
|
|
1834
2058
|
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
1835
2059
|
let match;
|
|
1836
2060
|
while ((match = projectRegex.exec(slnContent)) !== null) {
|
|
1837
2061
|
if (match[1]) {
|
|
1838
|
-
const csprojPath =
|
|
2062
|
+
const csprojPath = path6.resolve(slnDir, match[1].replace(/\\/g, "/"));
|
|
1839
2063
|
slnCsprojPaths.add(csprojPath);
|
|
1840
2064
|
}
|
|
1841
2065
|
}
|
|
@@ -1844,10 +2068,20 @@ async function scanDotnetProjects(rootDir, cache) {
|
|
|
1844
2068
|
}
|
|
1845
2069
|
const allCsprojFiles = /* @__PURE__ */ new Set([...csprojFiles, ...slnCsprojPaths]);
|
|
1846
2070
|
const results = [];
|
|
2071
|
+
const STUCK_TIMEOUT_MS = 6e4;
|
|
1847
2072
|
for (const csprojPath of allCsprojFiles) {
|
|
1848
2073
|
try {
|
|
1849
|
-
const
|
|
1850
|
-
|
|
2074
|
+
const scanPromise = scanOneCsproj(csprojPath, rootDir, cache);
|
|
2075
|
+
const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
|
|
2076
|
+
if (result.ok) {
|
|
2077
|
+
results.push(result.value);
|
|
2078
|
+
} else {
|
|
2079
|
+
const relPath = path6.relative(rootDir, path6.dirname(csprojPath));
|
|
2080
|
+
if (cache) {
|
|
2081
|
+
cache.addStuckPath(relPath || ".");
|
|
2082
|
+
}
|
|
2083
|
+
console.error(`Timeout scanning ${csprojPath} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
2084
|
+
}
|
|
1851
2085
|
} catch (e) {
|
|
1852
2086
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1853
2087
|
console.error(`Error scanning ${csprojPath}: ${msg}`);
|
|
@@ -1891,7 +2125,7 @@ async function scanOneCsproj(csprojPath, rootDir, cache) {
|
|
|
1891
2125
|
const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: dependencies.length };
|
|
1892
2126
|
return {
|
|
1893
2127
|
type: "dotnet",
|
|
1894
|
-
path:
|
|
2128
|
+
path: path6.relative(rootDir, path6.dirname(csprojPath)) || ".",
|
|
1895
2129
|
name: data.projectName,
|
|
1896
2130
|
targetFramework,
|
|
1897
2131
|
runtime: primaryTfm,
|
|
@@ -1904,15 +2138,17 @@ async function scanOneCsproj(csprojPath, rootDir, cache) {
|
|
|
1904
2138
|
}
|
|
1905
2139
|
|
|
1906
2140
|
// src/config.ts
|
|
1907
|
-
import * as
|
|
2141
|
+
import * as path7 from "path";
|
|
1908
2142
|
import * as fs2 from "fs/promises";
|
|
1909
2143
|
var CONFIG_FILES = [
|
|
1910
2144
|
"vibgrate.config.ts",
|
|
1911
2145
|
"vibgrate.config.js",
|
|
1912
2146
|
"vibgrate.config.json"
|
|
1913
2147
|
];
|
|
2148
|
+
var DEFAULT_MAX_FILE_SIZE = 5242880;
|
|
1914
2149
|
var DEFAULT_CONFIG = {
|
|
1915
2150
|
exclude: [],
|
|
2151
|
+
maxFileSizeToScan: DEFAULT_MAX_FILE_SIZE,
|
|
1916
2152
|
thresholds: {
|
|
1917
2153
|
failOnError: {
|
|
1918
2154
|
eolDays: 180,
|
|
@@ -1926,28 +2162,44 @@ var DEFAULT_CONFIG = {
|
|
|
1926
2162
|
}
|
|
1927
2163
|
};
|
|
1928
2164
|
async function loadConfig(rootDir) {
|
|
2165
|
+
let config = DEFAULT_CONFIG;
|
|
1929
2166
|
for (const file of CONFIG_FILES) {
|
|
1930
|
-
const configPath =
|
|
2167
|
+
const configPath = path7.join(rootDir, file);
|
|
1931
2168
|
if (await pathExists(configPath)) {
|
|
1932
2169
|
if (file.endsWith(".json")) {
|
|
1933
2170
|
const txt = await readTextFile(configPath);
|
|
1934
|
-
|
|
2171
|
+
config = { ...DEFAULT_CONFIG, ...JSON.parse(txt) };
|
|
2172
|
+
break;
|
|
1935
2173
|
}
|
|
1936
2174
|
try {
|
|
1937
2175
|
const mod = await import(configPath);
|
|
1938
|
-
|
|
2176
|
+
config = { ...DEFAULT_CONFIG, ...mod.default ?? mod };
|
|
2177
|
+
break;
|
|
1939
2178
|
} catch {
|
|
1940
2179
|
}
|
|
1941
2180
|
}
|
|
1942
2181
|
}
|
|
1943
|
-
|
|
2182
|
+
const sidecarPath = path7.join(rootDir, ".vibgrate", "auto-excludes.json");
|
|
2183
|
+
if (await pathExists(sidecarPath)) {
|
|
2184
|
+
try {
|
|
2185
|
+
const txt = await readTextFile(sidecarPath);
|
|
2186
|
+
const autoExcludes = JSON.parse(txt);
|
|
2187
|
+
if (Array.isArray(autoExcludes) && autoExcludes.length > 0) {
|
|
2188
|
+
const existing = config.exclude ?? [];
|
|
2189
|
+
config = { ...config, exclude: [.../* @__PURE__ */ new Set([...existing, ...autoExcludes])] };
|
|
2190
|
+
}
|
|
2191
|
+
} catch {
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
return config;
|
|
1944
2195
|
}
|
|
1945
2196
|
async function writeDefaultConfig(rootDir) {
|
|
1946
|
-
const configPath =
|
|
2197
|
+
const configPath = path7.join(rootDir, "vibgrate.config.ts");
|
|
1947
2198
|
const content = `import type { VibgrateConfig } from '@vibgrate/cli';
|
|
1948
2199
|
|
|
1949
2200
|
const config: VibgrateConfig = {
|
|
1950
2201
|
// exclude: ['legacy/**'],
|
|
2202
|
+
// maxFileSizeToScan: 5_242_880, // 5 MB (default)
|
|
1951
2203
|
thresholds: {
|
|
1952
2204
|
failOnError: {
|
|
1953
2205
|
eolDays: 180,
|
|
@@ -1966,9 +2218,44 @@ export default config;
|
|
|
1966
2218
|
await fs2.writeFile(configPath, content, "utf8");
|
|
1967
2219
|
return configPath;
|
|
1968
2220
|
}
|
|
2221
|
+
async function appendExcludePatterns(rootDir, newPatterns) {
|
|
2222
|
+
if (newPatterns.length === 0) return false;
|
|
2223
|
+
const jsonPath = path7.join(rootDir, "vibgrate.config.json");
|
|
2224
|
+
if (await pathExists(jsonPath)) {
|
|
2225
|
+
try {
|
|
2226
|
+
const txt = await readTextFile(jsonPath);
|
|
2227
|
+
const cfg = JSON.parse(txt);
|
|
2228
|
+
const existing2 = Array.isArray(cfg.exclude) ? cfg.exclude : [];
|
|
2229
|
+
const merged2 = [.../* @__PURE__ */ new Set([...existing2, ...newPatterns])];
|
|
2230
|
+
cfg.exclude = merged2;
|
|
2231
|
+
await fs2.writeFile(jsonPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
2232
|
+
return true;
|
|
2233
|
+
} catch {
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
const vibgrateDir = path7.join(rootDir, ".vibgrate");
|
|
2237
|
+
const sidecarPath = path7.join(vibgrateDir, "auto-excludes.json");
|
|
2238
|
+
let existing = [];
|
|
2239
|
+
if (await pathExists(sidecarPath)) {
|
|
2240
|
+
try {
|
|
2241
|
+
const txt = await readTextFile(sidecarPath);
|
|
2242
|
+
const parsed = JSON.parse(txt);
|
|
2243
|
+
if (Array.isArray(parsed)) existing = parsed;
|
|
2244
|
+
} catch {
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...newPatterns])];
|
|
2248
|
+
try {
|
|
2249
|
+
await fs2.mkdir(vibgrateDir, { recursive: true });
|
|
2250
|
+
await fs2.writeFile(sidecarPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2251
|
+
return true;
|
|
2252
|
+
} catch {
|
|
2253
|
+
return false;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
1969
2256
|
|
|
1970
2257
|
// src/utils/vcs.ts
|
|
1971
|
-
import * as
|
|
2258
|
+
import * as path8 from "path";
|
|
1972
2259
|
import * as fs3 from "fs/promises";
|
|
1973
2260
|
async function detectVcs(rootDir) {
|
|
1974
2261
|
try {
|
|
@@ -1982,7 +2269,7 @@ async function detectGit(rootDir) {
|
|
|
1982
2269
|
if (!gitDir) {
|
|
1983
2270
|
return { type: "unknown" };
|
|
1984
2271
|
}
|
|
1985
|
-
const headPath =
|
|
2272
|
+
const headPath = path8.join(gitDir, "HEAD");
|
|
1986
2273
|
let headContent;
|
|
1987
2274
|
try {
|
|
1988
2275
|
headContent = (await fs3.readFile(headPath, "utf8")).trim();
|
|
@@ -2006,30 +2293,30 @@ async function detectGit(rootDir) {
|
|
|
2006
2293
|
};
|
|
2007
2294
|
}
|
|
2008
2295
|
async function findGitDir(startDir) {
|
|
2009
|
-
let dir =
|
|
2010
|
-
const root =
|
|
2296
|
+
let dir = path8.resolve(startDir);
|
|
2297
|
+
const root = path8.parse(dir).root;
|
|
2011
2298
|
while (dir !== root) {
|
|
2012
|
-
const gitPath =
|
|
2299
|
+
const gitPath = path8.join(dir, ".git");
|
|
2013
2300
|
try {
|
|
2014
|
-
const
|
|
2015
|
-
if (
|
|
2301
|
+
const stat4 = await fs3.stat(gitPath);
|
|
2302
|
+
if (stat4.isDirectory()) {
|
|
2016
2303
|
return gitPath;
|
|
2017
2304
|
}
|
|
2018
|
-
if (
|
|
2305
|
+
if (stat4.isFile()) {
|
|
2019
2306
|
const content = (await fs3.readFile(gitPath, "utf8")).trim();
|
|
2020
2307
|
if (content.startsWith("gitdir: ")) {
|
|
2021
|
-
const resolved =
|
|
2308
|
+
const resolved = path8.resolve(dir, content.slice(8));
|
|
2022
2309
|
return resolved;
|
|
2023
2310
|
}
|
|
2024
2311
|
}
|
|
2025
2312
|
} catch {
|
|
2026
2313
|
}
|
|
2027
|
-
dir =
|
|
2314
|
+
dir = path8.dirname(dir);
|
|
2028
2315
|
}
|
|
2029
2316
|
return null;
|
|
2030
2317
|
}
|
|
2031
2318
|
async function resolveRef(gitDir, refPath) {
|
|
2032
|
-
const loosePath =
|
|
2319
|
+
const loosePath = path8.join(gitDir, refPath);
|
|
2033
2320
|
try {
|
|
2034
2321
|
const sha = (await fs3.readFile(loosePath, "utf8")).trim();
|
|
2035
2322
|
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
@@ -2037,7 +2324,7 @@ async function resolveRef(gitDir, refPath) {
|
|
|
2037
2324
|
}
|
|
2038
2325
|
} catch {
|
|
2039
2326
|
}
|
|
2040
|
-
const packedPath =
|
|
2327
|
+
const packedPath = path8.join(gitDir, "packed-refs");
|
|
2041
2328
|
try {
|
|
2042
2329
|
const packed = await fs3.readFile(packedPath, "utf8");
|
|
2043
2330
|
for (const line of packed.split("\n")) {
|
|
@@ -2079,26 +2366,70 @@ var ScanProgress = class {
|
|
|
2079
2366
|
startTime = Date.now();
|
|
2080
2367
|
isTTY;
|
|
2081
2368
|
rootDir = "";
|
|
2369
|
+
/** Last rendered frame content (strip to compare for dirty-checking) */
|
|
2370
|
+
lastFrame = "";
|
|
2371
|
+
/** Whether we've hidden the cursor */
|
|
2372
|
+
cursorHidden = false;
|
|
2373
|
+
/** Estimated total scan duration in ms (from history or live calculation) */
|
|
2374
|
+
estimatedTotalMs = null;
|
|
2375
|
+
/** Per-step estimated durations from history */
|
|
2376
|
+
stepEstimates = /* @__PURE__ */ new Map();
|
|
2377
|
+
/** Per-step actual start times for timing */
|
|
2378
|
+
stepStartTimes = /* @__PURE__ */ new Map();
|
|
2379
|
+
/** Per-step recorded durations (completed steps) */
|
|
2380
|
+
stepTimings = [];
|
|
2082
2381
|
constructor(rootDir) {
|
|
2083
2382
|
this.isTTY = process.stderr.isTTY ?? false;
|
|
2084
2383
|
this.rootDir = rootDir;
|
|
2384
|
+
if (this.isTTY) {
|
|
2385
|
+
const restore = () => {
|
|
2386
|
+
if (this.cursorHidden) {
|
|
2387
|
+
process.stderr.write("\x1B[?25h");
|
|
2388
|
+
this.cursorHidden = false;
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
process.on("exit", restore);
|
|
2392
|
+
process.on("SIGINT", () => {
|
|
2393
|
+
restore();
|
|
2394
|
+
process.exit(130);
|
|
2395
|
+
});
|
|
2396
|
+
process.on("SIGTERM", () => {
|
|
2397
|
+
restore();
|
|
2398
|
+
process.exit(143);
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/** Set the estimated total duration from scan history */
|
|
2403
|
+
setEstimatedTotal(estimatedMs) {
|
|
2404
|
+
this.estimatedTotalMs = estimatedMs;
|
|
2405
|
+
}
|
|
2406
|
+
/** Set per-step estimated durations from scan history */
|
|
2407
|
+
setStepEstimates(estimates) {
|
|
2408
|
+
this.stepEstimates = estimates;
|
|
2409
|
+
}
|
|
2410
|
+
/** Get completed step timings for persisting to history */
|
|
2411
|
+
getStepTimings() {
|
|
2412
|
+
return [...this.stepTimings];
|
|
2085
2413
|
}
|
|
2086
|
-
/** Register all steps up front */
|
|
2414
|
+
/** Register all steps up front, optionally with weights */
|
|
2087
2415
|
setSteps(steps) {
|
|
2088
|
-
this.steps = steps.map((s) => ({ ...s, status: "pending" }));
|
|
2416
|
+
this.steps = steps.map((s) => ({ ...s, status: "pending", weight: s.weight ?? 1 }));
|
|
2089
2417
|
if (this.isTTY) {
|
|
2090
2418
|
this.startSpinner();
|
|
2091
2419
|
}
|
|
2092
2420
|
this.render();
|
|
2093
2421
|
}
|
|
2094
|
-
/** Mark a step as active (currently running) */
|
|
2095
|
-
startStep(id) {
|
|
2422
|
+
/** Mark a step as active (currently running), optionally with expected total */
|
|
2423
|
+
startStep(id, subTotal) {
|
|
2096
2424
|
const step = this.steps.find((s) => s.id === id);
|
|
2097
2425
|
if (step) {
|
|
2098
2426
|
step.status = "active";
|
|
2099
2427
|
step.detail = void 0;
|
|
2100
2428
|
step.count = void 0;
|
|
2429
|
+
step.subProgress = 0;
|
|
2430
|
+
step.subTotal = subTotal;
|
|
2101
2431
|
}
|
|
2432
|
+
this.stepStartTimes.set(id, Date.now());
|
|
2102
2433
|
this.render();
|
|
2103
2434
|
}
|
|
2104
2435
|
/** Mark a step as completed */
|
|
@@ -2109,6 +2440,10 @@ var ScanProgress = class {
|
|
|
2109
2440
|
step.detail = detail;
|
|
2110
2441
|
step.count = count;
|
|
2111
2442
|
}
|
|
2443
|
+
const started = this.stepStartTimes.get(id);
|
|
2444
|
+
if (started) {
|
|
2445
|
+
this.stepTimings.push({ id, durationMs: Date.now() - started });
|
|
2446
|
+
}
|
|
2112
2447
|
this.render();
|
|
2113
2448
|
}
|
|
2114
2449
|
/** Mark a step as skipped */
|
|
@@ -2120,6 +2455,16 @@ var ScanProgress = class {
|
|
|
2120
2455
|
}
|
|
2121
2456
|
this.render();
|
|
2122
2457
|
}
|
|
2458
|
+
/** Update sub-step progress for the active step (files processed, etc.) */
|
|
2459
|
+
updateStepProgress(id, current, total, label) {
|
|
2460
|
+
const step = this.steps.find((s) => s.id === id);
|
|
2461
|
+
if (step) {
|
|
2462
|
+
step.subProgress = current;
|
|
2463
|
+
if (total !== void 0) step.subTotal = total;
|
|
2464
|
+
if (label !== void 0) step.subLabel = label;
|
|
2465
|
+
}
|
|
2466
|
+
this.render();
|
|
2467
|
+
}
|
|
2123
2468
|
/** Update live stats */
|
|
2124
2469
|
updateStats(partial) {
|
|
2125
2470
|
Object.assign(this.stats, partial);
|
|
@@ -2151,38 +2496,45 @@ var ScanProgress = class {
|
|
|
2151
2496
|
this.timer = null;
|
|
2152
2497
|
}
|
|
2153
2498
|
if (this.isTTY) {
|
|
2154
|
-
|
|
2499
|
+
let buf = "";
|
|
2500
|
+
if (this.lastLineCount > 0) {
|
|
2501
|
+
buf += `\x1B[${this.lastLineCount}A`;
|
|
2502
|
+
for (let i = 0; i < this.lastLineCount; i++) {
|
|
2503
|
+
buf += "\x1B[2K\n";
|
|
2504
|
+
}
|
|
2505
|
+
buf += `\x1B[${this.lastLineCount}A`;
|
|
2506
|
+
}
|
|
2507
|
+
buf += "\x1B[?25h";
|
|
2508
|
+
if (buf) process.stderr.write(buf);
|
|
2509
|
+
this.cursorHidden = false;
|
|
2510
|
+
this.lastLineCount = 0;
|
|
2155
2511
|
}
|
|
2156
|
-
const elapsed = (
|
|
2512
|
+
const elapsed = this.formatElapsed(Date.now() - this.startTime);
|
|
2157
2513
|
const doneCount = this.steps.filter((s) => s.status === "done").length;
|
|
2158
2514
|
process.stderr.write(
|
|
2159
|
-
chalk4.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}
|
|
2515
|
+
chalk4.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}
|
|
2160
2516
|
|
|
2161
2517
|
`)
|
|
2162
2518
|
);
|
|
2163
2519
|
}
|
|
2164
2520
|
// ── Internal rendering ──
|
|
2165
2521
|
startSpinner() {
|
|
2522
|
+
if (!this.cursorHidden) {
|
|
2523
|
+
process.stderr.write("\x1B[?25l");
|
|
2524
|
+
this.cursorHidden = true;
|
|
2525
|
+
}
|
|
2166
2526
|
this.timer = setInterval(() => {
|
|
2167
2527
|
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
2168
2528
|
this.render();
|
|
2169
|
-
},
|
|
2529
|
+
}, 120);
|
|
2170
2530
|
}
|
|
2171
2531
|
clearLines() {
|
|
2172
|
-
if (this.lastLineCount > 0) {
|
|
2173
|
-
process.stderr.write(`\x1B[${this.lastLineCount}A`);
|
|
2174
|
-
for (let i = 0; i < this.lastLineCount; i++) {
|
|
2175
|
-
process.stderr.write("\x1B[2K\n");
|
|
2176
|
-
}
|
|
2177
|
-
process.stderr.write(`\x1B[${this.lastLineCount}A`);
|
|
2178
|
-
}
|
|
2179
2532
|
}
|
|
2180
2533
|
render() {
|
|
2181
2534
|
if (!this.isTTY) {
|
|
2182
2535
|
this.renderCI();
|
|
2183
2536
|
return;
|
|
2184
2537
|
}
|
|
2185
|
-
this.clearLines();
|
|
2186
2538
|
const lines = [];
|
|
2187
2539
|
lines.push("");
|
|
2188
2540
|
lines.push(` ${ROBOT[0]} ${BRAND[0]}`);
|
|
@@ -2190,14 +2542,32 @@ var ScanProgress = class {
|
|
|
2190
2542
|
lines.push(` ${ROBOT[2]}`);
|
|
2191
2543
|
lines.push(` ${ROBOT[3]} ${chalk4.dim(this.rootDir)}`);
|
|
2192
2544
|
lines.push("");
|
|
2193
|
-
const
|
|
2194
|
-
|
|
2195
|
-
const
|
|
2545
|
+
const totalWeight = this.steps.reduce((sum, s) => sum + (s.weight ?? 1), 0);
|
|
2546
|
+
let completedWeight = 0;
|
|
2547
|
+
for (const step of this.steps) {
|
|
2548
|
+
const w = step.weight ?? 1;
|
|
2549
|
+
if (step.status === "done" || step.status === "skipped") {
|
|
2550
|
+
completedWeight += w;
|
|
2551
|
+
} else if (step.status === "active" && step.subTotal && step.subTotal > 0 && step.subProgress !== void 0) {
|
|
2552
|
+
completedWeight += w * Math.min(step.subProgress / step.subTotal, 0.99);
|
|
2553
|
+
} else if (step.status === "active") {
|
|
2554
|
+
const stepStart = this.stepStartTimes.get(step.id);
|
|
2555
|
+
const estimate = this.stepEstimates.get(step.id);
|
|
2556
|
+
if (stepStart && estimate && estimate > 0) {
|
|
2557
|
+
const stepElapsed = Date.now() - stepStart;
|
|
2558
|
+
completedWeight += w * Math.min(stepElapsed / estimate, 0.95);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
const pct = totalWeight > 0 ? Math.min(Math.round(completedWeight / totalWeight * 100), 99) : 0;
|
|
2196
2563
|
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
|
-
|
|
2564
|
+
const filled = Math.round(completedWeight / Math.max(totalWeight, 1) * barWidth);
|
|
2565
|
+
const bar = chalk4.greenBright("\u2501".repeat(Math.min(filled, barWidth))) + chalk4.dim("\u254C".repeat(Math.max(barWidth - filled, 0)));
|
|
2566
|
+
const elapsedMs = Date.now() - this.startTime;
|
|
2567
|
+
const elapsedStr = this.formatElapsed(elapsedMs);
|
|
2568
|
+
const etaStr = this.computeEtaString(elapsedMs, completedWeight, totalWeight);
|
|
2569
|
+
const treePart = this.stats.treeSummary ? chalk4.dim(` \xB7 ${this.stats.treeSummary.totalFiles.toLocaleString()} files \xB7 ${this.stats.treeSummary.totalDirs.toLocaleString()} dirs`) : "";
|
|
2570
|
+
lines.push(` ${bar} ${chalk4.bold.white(`${pct}%`)} ${chalk4.dim(elapsedStr)}${etaStr}${treePart}`);
|
|
2201
2571
|
lines.push("");
|
|
2202
2572
|
for (const step of this.steps) {
|
|
2203
2573
|
lines.push(this.renderStep(step));
|
|
@@ -2205,8 +2575,21 @@ var ScanProgress = class {
|
|
|
2205
2575
|
lines.push("");
|
|
2206
2576
|
lines.push(this.renderStats());
|
|
2207
2577
|
lines.push("");
|
|
2208
|
-
const
|
|
2209
|
-
|
|
2578
|
+
const content = lines.join("\n") + "\n";
|
|
2579
|
+
if (content === this.lastFrame && this.lastLineCount === lines.length) {
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
this.lastFrame = content;
|
|
2583
|
+
let buf = "";
|
|
2584
|
+
if (this.lastLineCount > 0) {
|
|
2585
|
+
buf += `\x1B[${this.lastLineCount}A`;
|
|
2586
|
+
for (let i = 0; i < this.lastLineCount; i++) {
|
|
2587
|
+
buf += "\x1B[2K\n";
|
|
2588
|
+
}
|
|
2589
|
+
buf += `\x1B[${this.lastLineCount}A`;
|
|
2590
|
+
}
|
|
2591
|
+
buf += content;
|
|
2592
|
+
process.stderr.write(buf);
|
|
2210
2593
|
this.lastLineCount = lines.length;
|
|
2211
2594
|
}
|
|
2212
2595
|
renderStep(step) {
|
|
@@ -2222,6 +2605,14 @@ var ScanProgress = class {
|
|
|
2222
2605
|
case "active":
|
|
2223
2606
|
icon = chalk4.cyan(spinner);
|
|
2224
2607
|
label = chalk4.bold.white(step.label);
|
|
2608
|
+
if (step.subTotal && step.subTotal > 0 && step.subProgress !== void 0 && step.subProgress > 0) {
|
|
2609
|
+
detail = chalk4.dim(` \xB7 ${step.subProgress.toLocaleString()} / ${step.subTotal.toLocaleString()}`);
|
|
2610
|
+
}
|
|
2611
|
+
if (step.subLabel) {
|
|
2612
|
+
const maxLen = 50;
|
|
2613
|
+
const displayPath = step.subLabel.length > maxLen ? "\u2026" + step.subLabel.slice(-maxLen + 1) : step.subLabel;
|
|
2614
|
+
detail += chalk4.dim(` ${displayPath}`);
|
|
2615
|
+
}
|
|
2225
2616
|
break;
|
|
2226
2617
|
case "skipped":
|
|
2227
2618
|
icon = chalk4.dim("\u25CC");
|
|
@@ -2275,10 +2666,141 @@ var ScanProgress = class {
|
|
|
2275
2666
|
}
|
|
2276
2667
|
}
|
|
2277
2668
|
}
|
|
2669
|
+
// ── Time formatting helpers ──
|
|
2670
|
+
/**
|
|
2671
|
+
* Format elapsed time:
|
|
2672
|
+
* - Under 90s → "12.3s"
|
|
2673
|
+
* - 90s and above → "1m 30s"
|
|
2674
|
+
*/
|
|
2675
|
+
formatElapsed(ms) {
|
|
2676
|
+
const totalSecs = ms / 1e3;
|
|
2677
|
+
if (totalSecs < 90) {
|
|
2678
|
+
return `${totalSecs.toFixed(1)}s`;
|
|
2679
|
+
}
|
|
2680
|
+
const mins = Math.floor(totalSecs / 60);
|
|
2681
|
+
const secs = Math.floor(totalSecs % 60);
|
|
2682
|
+
return `${mins}m ${secs.toString().padStart(2, "0")}s`;
|
|
2683
|
+
}
|
|
2684
|
+
/**
|
|
2685
|
+
* Compute an ETA string for the progress bar.
|
|
2686
|
+
*
|
|
2687
|
+
* Uses two sources blended together:
|
|
2688
|
+
* 1. **Historical estimate** from `estimatedTotalMs` (if available)
|
|
2689
|
+
* 2. **Live rate** — extrapolated from `elapsedMs` and `completedWeight`
|
|
2690
|
+
*
|
|
2691
|
+
* Returns empty string if not enough data yet (< 3% progress or < 2s elapsed).
|
|
2692
|
+
*/
|
|
2693
|
+
computeEtaString(elapsedMs, completedWeight, totalWeight) {
|
|
2694
|
+
if (totalWeight === 0 || elapsedMs < 2e3) return "";
|
|
2695
|
+
const fraction = completedWeight / totalWeight;
|
|
2696
|
+
if (fraction < 0.03) {
|
|
2697
|
+
if (this.estimatedTotalMs !== null && this.estimatedTotalMs > 0) {
|
|
2698
|
+
const remaining = Math.max(0, this.estimatedTotalMs - elapsedMs);
|
|
2699
|
+
if (remaining > 1e3) {
|
|
2700
|
+
return chalk4.dim(` \xB7 ~${this.formatElapsed(remaining)} left`);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
return "";
|
|
2704
|
+
}
|
|
2705
|
+
const liveRemaining = elapsedMs / fraction * (1 - fraction);
|
|
2706
|
+
let remainingMs;
|
|
2707
|
+
if (this.estimatedTotalMs !== null && this.estimatedTotalMs > 0) {
|
|
2708
|
+
const histRemaining = Math.max(0, this.estimatedTotalMs - elapsedMs);
|
|
2709
|
+
const histWeight = Math.max(0.1, 1 - fraction);
|
|
2710
|
+
remainingMs = histRemaining * histWeight + liveRemaining * (1 - histWeight);
|
|
2711
|
+
} else {
|
|
2712
|
+
remainingMs = liveRemaining;
|
|
2713
|
+
}
|
|
2714
|
+
if (remainingMs < 1500) return "";
|
|
2715
|
+
return chalk4.dim(` \xB7 ~${this.formatElapsed(remainingMs)} left`);
|
|
2716
|
+
}
|
|
2278
2717
|
};
|
|
2279
2718
|
|
|
2719
|
+
// src/ui/scan-history.ts
|
|
2720
|
+
import * as fs4 from "fs/promises";
|
|
2721
|
+
import * as path9 from "path";
|
|
2722
|
+
var HISTORY_FILENAME = "scan_history.json";
|
|
2723
|
+
var MAX_RECORDS = 10;
|
|
2724
|
+
async function loadScanHistory(rootDir) {
|
|
2725
|
+
const filePath = path9.join(rootDir, ".vibgrate", HISTORY_FILENAME);
|
|
2726
|
+
try {
|
|
2727
|
+
const txt = await fs4.readFile(filePath, "utf8");
|
|
2728
|
+
const data = JSON.parse(txt);
|
|
2729
|
+
if (data.version === 1 && Array.isArray(data.records)) {
|
|
2730
|
+
return data;
|
|
2731
|
+
}
|
|
2732
|
+
return null;
|
|
2733
|
+
} catch {
|
|
2734
|
+
return null;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
async function saveScanHistory(rootDir, record) {
|
|
2738
|
+
const dir = path9.join(rootDir, ".vibgrate");
|
|
2739
|
+
const filePath = path9.join(dir, HISTORY_FILENAME);
|
|
2740
|
+
let history;
|
|
2741
|
+
const existing = await loadScanHistory(rootDir);
|
|
2742
|
+
if (existing) {
|
|
2743
|
+
history = existing;
|
|
2744
|
+
history.records.push(record);
|
|
2745
|
+
if (history.records.length > MAX_RECORDS) {
|
|
2746
|
+
history.records = history.records.slice(-MAX_RECORDS);
|
|
2747
|
+
}
|
|
2748
|
+
} else {
|
|
2749
|
+
history = { version: 1, records: [record] };
|
|
2750
|
+
}
|
|
2751
|
+
try {
|
|
2752
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
2753
|
+
await fs4.writeFile(filePath, JSON.stringify(history, null, 2) + "\n", "utf8");
|
|
2754
|
+
} catch {
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
function estimateTotalDuration(history, currentFileCount) {
|
|
2758
|
+
if (!history || history.records.length === 0) return null;
|
|
2759
|
+
const similar = history.records.filter((r) => {
|
|
2760
|
+
if (r.totalFiles === 0 || currentFileCount === 0) return false;
|
|
2761
|
+
const ratio = currentFileCount / r.totalFiles;
|
|
2762
|
+
return ratio >= 0.33 && ratio <= 3;
|
|
2763
|
+
});
|
|
2764
|
+
if (similar.length > 0) {
|
|
2765
|
+
let weightedSum = 0;
|
|
2766
|
+
let weightTotal = 0;
|
|
2767
|
+
for (let i = 0; i < similar.length; i++) {
|
|
2768
|
+
const rec = similar[i];
|
|
2769
|
+
const weight = i + 1;
|
|
2770
|
+
const scale = currentFileCount / rec.totalFiles;
|
|
2771
|
+
weightedSum += rec.totalDurationMs * scale * weight;
|
|
2772
|
+
weightTotal += weight;
|
|
2773
|
+
}
|
|
2774
|
+
return Math.round(weightedSum / weightTotal);
|
|
2775
|
+
}
|
|
2776
|
+
const last = history.records[history.records.length - 1];
|
|
2777
|
+
if (last.totalFiles > 0 && currentFileCount > 0) {
|
|
2778
|
+
const scale = currentFileCount / last.totalFiles;
|
|
2779
|
+
return Math.round(last.totalDurationMs * scale);
|
|
2780
|
+
}
|
|
2781
|
+
return last.totalDurationMs;
|
|
2782
|
+
}
|
|
2783
|
+
function estimateStepDurations(history, currentFileCount) {
|
|
2784
|
+
const result = /* @__PURE__ */ new Map();
|
|
2785
|
+
if (!history || history.records.length === 0) return result;
|
|
2786
|
+
let best = null;
|
|
2787
|
+
for (let i = history.records.length - 1; i >= 0; i--) {
|
|
2788
|
+
const rec = history.records[i];
|
|
2789
|
+
if (rec.steps.length > 0) {
|
|
2790
|
+
best = rec;
|
|
2791
|
+
break;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
if (!best) return result;
|
|
2795
|
+
const scale = best.totalFiles > 0 && currentFileCount > 0 ? currentFileCount / best.totalFiles : 1;
|
|
2796
|
+
for (const step of best.steps) {
|
|
2797
|
+
result.set(step.id, Math.round(step.durationMs * scale));
|
|
2798
|
+
}
|
|
2799
|
+
return result;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2280
2802
|
// src/scanners/platform-matrix.ts
|
|
2281
|
-
import * as
|
|
2803
|
+
import * as path10 from "path";
|
|
2282
2804
|
var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
|
|
2283
2805
|
// Image / media processing
|
|
2284
2806
|
"sharp",
|
|
@@ -2558,7 +3080,7 @@ async function scanPlatformMatrix(rootDir, cache) {
|
|
|
2558
3080
|
}
|
|
2559
3081
|
result.dockerBaseImages = [...baseImages].sort();
|
|
2560
3082
|
for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
|
|
2561
|
-
const exists = cache ? await cache.pathExists(
|
|
3083
|
+
const exists = cache ? await cache.pathExists(path10.join(rootDir, file)) : await pathExists(path10.join(rootDir, file));
|
|
2562
3084
|
if (exists) {
|
|
2563
3085
|
result.nodeVersionFiles.push(file);
|
|
2564
3086
|
}
|
|
@@ -2635,7 +3157,7 @@ function scanDependencyRisk(projects) {
|
|
|
2635
3157
|
}
|
|
2636
3158
|
|
|
2637
3159
|
// src/scanners/dependency-graph.ts
|
|
2638
|
-
import * as
|
|
3160
|
+
import * as path11 from "path";
|
|
2639
3161
|
function parsePnpmLock(content) {
|
|
2640
3162
|
const entries = [];
|
|
2641
3163
|
const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
|
|
@@ -2694,9 +3216,9 @@ async function scanDependencyGraph(rootDir, cache) {
|
|
|
2694
3216
|
phantomDependencies: []
|
|
2695
3217
|
};
|
|
2696
3218
|
let entries = [];
|
|
2697
|
-
const pnpmLock =
|
|
2698
|
-
const npmLock =
|
|
2699
|
-
const yarnLock =
|
|
3219
|
+
const pnpmLock = path11.join(rootDir, "pnpm-lock.yaml");
|
|
3220
|
+
const npmLock = path11.join(rootDir, "package-lock.json");
|
|
3221
|
+
const yarnLock = path11.join(rootDir, "yarn.lock");
|
|
2700
3222
|
const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
|
|
2701
3223
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
2702
3224
|
if (await _pathExists(pnpmLock)) {
|
|
@@ -2743,7 +3265,7 @@ async function scanDependencyGraph(rootDir, cache) {
|
|
|
2743
3265
|
for (const pjPath of pkgFiles) {
|
|
2744
3266
|
try {
|
|
2745
3267
|
const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
|
|
2746
|
-
const relPath =
|
|
3268
|
+
const relPath = path11.relative(rootDir, pjPath);
|
|
2747
3269
|
for (const section of ["dependencies", "devDependencies"]) {
|
|
2748
3270
|
const deps = pj[section];
|
|
2749
3271
|
if (!deps) continue;
|
|
@@ -3089,7 +3611,7 @@ function scanToolingInventory(projects) {
|
|
|
3089
3611
|
}
|
|
3090
3612
|
|
|
3091
3613
|
// src/scanners/build-deploy.ts
|
|
3092
|
-
import * as
|
|
3614
|
+
import * as path12 from "path";
|
|
3093
3615
|
var CI_FILES = {
|
|
3094
3616
|
".github/workflows": "github-actions",
|
|
3095
3617
|
".gitlab-ci.yml": "gitlab-ci",
|
|
@@ -3142,17 +3664,17 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
3142
3664
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
3143
3665
|
const ciSystems = /* @__PURE__ */ new Set();
|
|
3144
3666
|
for (const [file, system] of Object.entries(CI_FILES)) {
|
|
3145
|
-
const fullPath =
|
|
3667
|
+
const fullPath = path12.join(rootDir, file);
|
|
3146
3668
|
if (await _pathExists(fullPath)) {
|
|
3147
3669
|
ciSystems.add(system);
|
|
3148
3670
|
}
|
|
3149
3671
|
}
|
|
3150
|
-
const ghWorkflowDir =
|
|
3672
|
+
const ghWorkflowDir = path12.join(rootDir, ".github", "workflows");
|
|
3151
3673
|
if (await _pathExists(ghWorkflowDir)) {
|
|
3152
3674
|
try {
|
|
3153
3675
|
if (cache) {
|
|
3154
3676
|
const entries = await cache.walkDir(rootDir);
|
|
3155
|
-
const ghPrefix =
|
|
3677
|
+
const ghPrefix = path12.relative(rootDir, ghWorkflowDir) + path12.sep;
|
|
3156
3678
|
result.ciWorkflowCount = entries.filter(
|
|
3157
3679
|
(e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
|
|
3158
3680
|
).length;
|
|
@@ -3203,11 +3725,11 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
3203
3725
|
(name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
|
|
3204
3726
|
);
|
|
3205
3727
|
if (cfnFiles.length > 0) iacSystems.add("cloudformation");
|
|
3206
|
-
if (await _pathExists(
|
|
3728
|
+
if (await _pathExists(path12.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
|
|
3207
3729
|
result.iac = [...iacSystems].sort();
|
|
3208
3730
|
const releaseTools = /* @__PURE__ */ new Set();
|
|
3209
3731
|
for (const [file, tool] of Object.entries(RELEASE_FILES)) {
|
|
3210
|
-
if (await _pathExists(
|
|
3732
|
+
if (await _pathExists(path12.join(rootDir, file))) releaseTools.add(tool);
|
|
3211
3733
|
}
|
|
3212
3734
|
const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
3213
3735
|
for (const pjPath of pkgFiles) {
|
|
@@ -3232,19 +3754,19 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
3232
3754
|
};
|
|
3233
3755
|
const managers = /* @__PURE__ */ new Set();
|
|
3234
3756
|
for (const [file, manager] of Object.entries(lockfileMap)) {
|
|
3235
|
-
if (await _pathExists(
|
|
3757
|
+
if (await _pathExists(path12.join(rootDir, file))) managers.add(manager);
|
|
3236
3758
|
}
|
|
3237
3759
|
result.packageManagers = [...managers].sort();
|
|
3238
3760
|
const monoTools = /* @__PURE__ */ new Set();
|
|
3239
3761
|
for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
|
|
3240
|
-
if (await _pathExists(
|
|
3762
|
+
if (await _pathExists(path12.join(rootDir, file))) monoTools.add(tool);
|
|
3241
3763
|
}
|
|
3242
3764
|
result.monorepoTools = [...monoTools].sort();
|
|
3243
3765
|
return result;
|
|
3244
3766
|
}
|
|
3245
3767
|
|
|
3246
3768
|
// src/scanners/ts-modernity.ts
|
|
3247
|
-
import * as
|
|
3769
|
+
import * as path13 from "path";
|
|
3248
3770
|
async function scanTsModernity(rootDir, cache) {
|
|
3249
3771
|
const result = {
|
|
3250
3772
|
typescriptVersion: null,
|
|
@@ -3282,7 +3804,7 @@ async function scanTsModernity(rootDir, cache) {
|
|
|
3282
3804
|
if (hasEsm && hasCjs) result.moduleType = "mixed";
|
|
3283
3805
|
else if (hasEsm) result.moduleType = "esm";
|
|
3284
3806
|
else if (hasCjs) result.moduleType = "cjs";
|
|
3285
|
-
let tsConfigPath =
|
|
3807
|
+
let tsConfigPath = path13.join(rootDir, "tsconfig.json");
|
|
3286
3808
|
const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
|
|
3287
3809
|
if (!tsConfigExists) {
|
|
3288
3810
|
const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
|
|
@@ -3628,8 +4150,8 @@ function scanBreakingChangeExposure(projects) {
|
|
|
3628
4150
|
}
|
|
3629
4151
|
|
|
3630
4152
|
// src/scanners/file-hotspots.ts
|
|
3631
|
-
import * as
|
|
3632
|
-
import * as
|
|
4153
|
+
import * as fs5 from "fs/promises";
|
|
4154
|
+
import * as path14 from "path";
|
|
3633
4155
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3634
4156
|
"node_modules",
|
|
3635
4157
|
".git",
|
|
@@ -3672,16 +4194,16 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
3672
4194
|
const entries = await cache.walkDir(rootDir);
|
|
3673
4195
|
for (const entry of entries) {
|
|
3674
4196
|
if (!entry.isFile) continue;
|
|
3675
|
-
const ext =
|
|
4197
|
+
const ext = path14.extname(entry.name).toLowerCase();
|
|
3676
4198
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
3677
|
-
const depth = entry.relPath.split(
|
|
4199
|
+
const depth = entry.relPath.split(path14.sep).length - 1;
|
|
3678
4200
|
if (depth > maxDepth) maxDepth = depth;
|
|
3679
4201
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
3680
4202
|
try {
|
|
3681
|
-
const
|
|
4203
|
+
const stat4 = await fs5.stat(entry.absPath);
|
|
3682
4204
|
allFiles.push({
|
|
3683
4205
|
path: entry.relPath,
|
|
3684
|
-
bytes:
|
|
4206
|
+
bytes: stat4.size
|
|
3685
4207
|
});
|
|
3686
4208
|
} catch {
|
|
3687
4209
|
}
|
|
@@ -3691,7 +4213,7 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
3691
4213
|
if (depth > maxDepth) maxDepth = depth;
|
|
3692
4214
|
let entries;
|
|
3693
4215
|
try {
|
|
3694
|
-
const dirents = await
|
|
4216
|
+
const dirents = await fs5.readdir(dir, { withFileTypes: true });
|
|
3695
4217
|
entries = dirents.map((d) => ({
|
|
3696
4218
|
name: d.name,
|
|
3697
4219
|
isDirectory: d.isDirectory(),
|
|
@@ -3703,16 +4225,16 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
3703
4225
|
for (const e of entries) {
|
|
3704
4226
|
if (e.isDirectory) {
|
|
3705
4227
|
if (SKIP_DIRS2.has(e.name)) continue;
|
|
3706
|
-
await walk(
|
|
4228
|
+
await walk(path14.join(dir, e.name), depth + 1);
|
|
3707
4229
|
} else if (e.isFile) {
|
|
3708
|
-
const ext =
|
|
4230
|
+
const ext = path14.extname(e.name).toLowerCase();
|
|
3709
4231
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
3710
4232
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
3711
4233
|
try {
|
|
3712
|
-
const
|
|
4234
|
+
const stat4 = await fs5.stat(path14.join(dir, e.name));
|
|
3713
4235
|
allFiles.push({
|
|
3714
|
-
path:
|
|
3715
|
-
bytes:
|
|
4236
|
+
path: path14.relative(rootDir, path14.join(dir, e.name)),
|
|
4237
|
+
bytes: stat4.size
|
|
3716
4238
|
});
|
|
3717
4239
|
} catch {
|
|
3718
4240
|
}
|
|
@@ -3734,7 +4256,7 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
3734
4256
|
}
|
|
3735
4257
|
|
|
3736
4258
|
// src/scanners/security-posture.ts
|
|
3737
|
-
import * as
|
|
4259
|
+
import * as path15 from "path";
|
|
3738
4260
|
var LOCKFILES = {
|
|
3739
4261
|
"pnpm-lock.yaml": "pnpm",
|
|
3740
4262
|
"package-lock.json": "npm",
|
|
@@ -3755,14 +4277,14 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
3755
4277
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
3756
4278
|
const foundLockfiles = [];
|
|
3757
4279
|
for (const [file, type] of Object.entries(LOCKFILES)) {
|
|
3758
|
-
if (await _pathExists(
|
|
4280
|
+
if (await _pathExists(path15.join(rootDir, file))) {
|
|
3759
4281
|
foundLockfiles.push(type);
|
|
3760
4282
|
}
|
|
3761
4283
|
}
|
|
3762
4284
|
result.lockfilePresent = foundLockfiles.length > 0;
|
|
3763
4285
|
result.multipleLockfileTypes = foundLockfiles.length > 1;
|
|
3764
4286
|
result.lockfileTypes = foundLockfiles.sort();
|
|
3765
|
-
const gitignorePath =
|
|
4287
|
+
const gitignorePath = path15.join(rootDir, ".gitignore");
|
|
3766
4288
|
if (await _pathExists(gitignorePath)) {
|
|
3767
4289
|
try {
|
|
3768
4290
|
const content = await _readTextFile(gitignorePath);
|
|
@@ -3777,7 +4299,7 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
3777
4299
|
}
|
|
3778
4300
|
}
|
|
3779
4301
|
for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
|
|
3780
|
-
if (await _pathExists(
|
|
4302
|
+
if (await _pathExists(path15.join(rootDir, envFile))) {
|
|
3781
4303
|
if (!result.gitignoreCoversEnv) {
|
|
3782
4304
|
result.envFilesTracked = true;
|
|
3783
4305
|
break;
|
|
@@ -4202,8 +4724,8 @@ function scanServiceDependencies(projects) {
|
|
|
4202
4724
|
}
|
|
4203
4725
|
|
|
4204
4726
|
// src/scanners/architecture.ts
|
|
4205
|
-
import * as
|
|
4206
|
-
import * as
|
|
4727
|
+
import * as path16 from "path";
|
|
4728
|
+
import * as fs6 from "fs/promises";
|
|
4207
4729
|
var ARCHETYPE_SIGNALS = [
|
|
4208
4730
|
// Meta-frameworks (highest priority — they imply routing patterns)
|
|
4209
4731
|
{ packages: ["next", "@next/core"], archetype: "nextjs", weight: 10 },
|
|
@@ -4501,9 +5023,9 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
4501
5023
|
const entries = await cache.walkDir(rootDir);
|
|
4502
5024
|
return entries.filter((e) => {
|
|
4503
5025
|
if (!e.isFile) return false;
|
|
4504
|
-
const name =
|
|
5026
|
+
const name = path16.basename(e.absPath);
|
|
4505
5027
|
if (name.startsWith(".") && name !== ".") return false;
|
|
4506
|
-
const ext =
|
|
5028
|
+
const ext = path16.extname(name);
|
|
4507
5029
|
return SOURCE_EXTENSIONS.has(ext);
|
|
4508
5030
|
}).map((e) => e.relPath);
|
|
4509
5031
|
}
|
|
@@ -4511,21 +5033,21 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
4511
5033
|
async function walk(dir) {
|
|
4512
5034
|
let entries;
|
|
4513
5035
|
try {
|
|
4514
|
-
entries = await
|
|
5036
|
+
entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
4515
5037
|
} catch {
|
|
4516
5038
|
return;
|
|
4517
5039
|
}
|
|
4518
5040
|
for (const entry of entries) {
|
|
4519
5041
|
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
4520
|
-
const fullPath =
|
|
5042
|
+
const fullPath = path16.join(dir, entry.name);
|
|
4521
5043
|
if (entry.isDirectory()) {
|
|
4522
5044
|
if (!IGNORE_DIRS.has(entry.name)) {
|
|
4523
5045
|
await walk(fullPath);
|
|
4524
5046
|
}
|
|
4525
5047
|
} else if (entry.isFile()) {
|
|
4526
|
-
const ext =
|
|
5048
|
+
const ext = path16.extname(entry.name);
|
|
4527
5049
|
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
4528
|
-
files.push(
|
|
5050
|
+
files.push(path16.relative(rootDir, fullPath));
|
|
4529
5051
|
}
|
|
4530
5052
|
}
|
|
4531
5053
|
}
|
|
@@ -4549,7 +5071,7 @@ function classifyFile(filePath, archetype) {
|
|
|
4549
5071
|
}
|
|
4550
5072
|
}
|
|
4551
5073
|
if (!bestMatch || bestMatch.confidence < 0.7) {
|
|
4552
|
-
const baseName =
|
|
5074
|
+
const baseName = path16.basename(filePath, path16.extname(filePath));
|
|
4553
5075
|
const cleanBase = baseName.replace(/\.(test|spec)$/, "");
|
|
4554
5076
|
for (const rule of SUFFIX_RULES) {
|
|
4555
5077
|
if (cleanBase.endsWith(rule.suffix)) {
|
|
@@ -4740,14 +5262,19 @@ async function runScan(rootDir, opts) {
|
|
|
4740
5262
|
const sem = new Semaphore(opts.concurrency);
|
|
4741
5263
|
const npmCache = new NpmCache(rootDir, sem);
|
|
4742
5264
|
const fileCache = new FileCache();
|
|
5265
|
+
const excludePatterns = config.exclude ?? [];
|
|
5266
|
+
fileCache.setExcludePatterns(excludePatterns);
|
|
5267
|
+
fileCache.setMaxFileSize(config.maxFileSizeToScan ?? 5242880);
|
|
4743
5268
|
const scanners = config.scanners;
|
|
4744
5269
|
let filesScanned = 0;
|
|
4745
5270
|
const progress = new ScanProgress(rootDir);
|
|
4746
5271
|
const steps = [
|
|
4747
5272
|
{ id: "config", label: "Loading configuration" },
|
|
5273
|
+
{ id: "discovery", label: "Discovering workspace", weight: 3 },
|
|
4748
5274
|
{ id: "vcs", label: "Detecting version control" },
|
|
4749
|
-
{ id: "
|
|
4750
|
-
{ id: "
|
|
5275
|
+
{ id: "walk", label: "Indexing files", weight: 8 },
|
|
5276
|
+
{ id: "node", label: "Scanning Node projects", weight: 4 },
|
|
5277
|
+
{ id: "dotnet", label: "Scanning .NET projects", weight: 2 },
|
|
4751
5278
|
...scanners !== false ? [
|
|
4752
5279
|
...scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
|
|
4753
5280
|
...scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
|
|
@@ -4766,10 +5293,26 @@ async function runScan(rootDir, opts) {
|
|
|
4766
5293
|
];
|
|
4767
5294
|
progress.setSteps(steps);
|
|
4768
5295
|
progress.completeStep("config", "loaded");
|
|
5296
|
+
progress.startStep("discovery");
|
|
5297
|
+
const treeCount = await quickTreeCount(rootDir, excludePatterns);
|
|
5298
|
+
progress.updateStats({ treeSummary: treeCount });
|
|
5299
|
+
progress.completeStep(
|
|
5300
|
+
"discovery",
|
|
5301
|
+
`${treeCount.totalFiles.toLocaleString()} files \xB7 ${treeCount.totalDirs.toLocaleString()} dirs`
|
|
5302
|
+
);
|
|
5303
|
+
const scanHistory = await loadScanHistory(rootDir);
|
|
5304
|
+
const estimatedTotal = estimateTotalDuration(scanHistory, treeCount.totalFiles);
|
|
5305
|
+
progress.setEstimatedTotal(estimatedTotal);
|
|
5306
|
+
progress.setStepEstimates(estimateStepDurations(scanHistory, treeCount.totalFiles));
|
|
4769
5307
|
progress.startStep("vcs");
|
|
4770
5308
|
const vcs = await detectVcs(rootDir);
|
|
4771
5309
|
const vcsDetail = vcs.type !== "unknown" ? `${vcs.type}${vcs.branch ? ` ${vcs.branch}` : ""}${vcs.shortSha ? ` @ ${vcs.shortSha}` : ""}` : "none detected";
|
|
4772
5310
|
progress.completeStep("vcs", vcsDetail);
|
|
5311
|
+
progress.startStep("walk", treeCount.totalFiles);
|
|
5312
|
+
await fileCache.walkDir(rootDir, (found, currentPath) => {
|
|
5313
|
+
progress.updateStepProgress("walk", found, treeCount.totalFiles, currentPath);
|
|
5314
|
+
});
|
|
5315
|
+
progress.completeStep("walk", `${treeCount.totalFiles.toLocaleString()} files indexed`);
|
|
4773
5316
|
progress.startStep("node");
|
|
4774
5317
|
const nodeProjects = await scanNodeProjects(rootDir, npmCache, fileCache);
|
|
4775
5318
|
for (const p of nodeProjects) {
|
|
@@ -4949,6 +5492,36 @@ async function runScan(rootDir, opts) {
|
|
|
4949
5492
|
if (noteCount > 0) findingParts.push(`${noteCount} note${noteCount !== 1 ? "s" : ""}`);
|
|
4950
5493
|
progress.completeStep("findings", findingParts.join(", ") || "none");
|
|
4951
5494
|
progress.finish();
|
|
5495
|
+
const stuckPaths = fileCache.stuckPaths;
|
|
5496
|
+
const skippedLarge = fileCache.skippedLargeFiles;
|
|
5497
|
+
if (stuckPaths.length > 0) {
|
|
5498
|
+
console.log(
|
|
5499
|
+
chalk5.yellow(`
|
|
5500
|
+
\u26A0 ${stuckPaths.length} path${stuckPaths.length === 1 ? "" : "s"} timed out (>60s) and ${stuckPaths.length === 1 ? "was" : "were"} skipped:`)
|
|
5501
|
+
);
|
|
5502
|
+
for (const d of stuckPaths) {
|
|
5503
|
+
console.log(chalk5.dim(` \u2192 ${d}`));
|
|
5504
|
+
}
|
|
5505
|
+
const newExcludes = stuckPaths.map((d) => `${d}/**`);
|
|
5506
|
+
const updated = await appendExcludePatterns(rootDir, newExcludes);
|
|
5507
|
+
if (updated) {
|
|
5508
|
+
console.log(chalk5.green("\u2714") + ` Added ${newExcludes.length} pattern${newExcludes.length !== 1 ? "s" : ""} to exclude list in config`);
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
if (skippedLarge.length > 0) {
|
|
5512
|
+
const sizeLimit = config.maxFileSizeToScan ?? 5242880;
|
|
5513
|
+
const sizeMB = (sizeLimit / 1048576).toFixed(0);
|
|
5514
|
+
console.log(
|
|
5515
|
+
chalk5.yellow(`
|
|
5516
|
+
\u26A0 ${skippedLarge.length} file${skippedLarge.length === 1 ? "" : "s"} skipped (>${sizeMB} MB):`)
|
|
5517
|
+
);
|
|
5518
|
+
for (const f of skippedLarge.slice(0, 10)) {
|
|
5519
|
+
console.log(chalk5.dim(` \u2192 ${f}`));
|
|
5520
|
+
}
|
|
5521
|
+
if (skippedLarge.length > 10) {
|
|
5522
|
+
console.log(chalk5.dim(` \u2026 and ${skippedLarge.length - 10} more`));
|
|
5523
|
+
}
|
|
5524
|
+
}
|
|
4952
5525
|
fileCache.clear();
|
|
4953
5526
|
if (allProjects.length === 0) {
|
|
4954
5527
|
console.log(chalk5.yellow("No projects found."));
|
|
@@ -4966,17 +5539,18 @@ async function runScan(rootDir, opts) {
|
|
|
4966
5539
|
schemaVersion: "1.0",
|
|
4967
5540
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4968
5541
|
vibgrateVersion: VERSION,
|
|
4969
|
-
rootPath:
|
|
5542
|
+
rootPath: path17.basename(rootDir),
|
|
4970
5543
|
...vcs.type !== "unknown" ? { vcs } : {},
|
|
4971
5544
|
projects: allProjects,
|
|
4972
5545
|
drift,
|
|
4973
5546
|
findings,
|
|
4974
5547
|
...Object.keys(extended).length > 0 ? { extended } : {},
|
|
4975
5548
|
durationMs,
|
|
4976
|
-
filesScanned
|
|
5549
|
+
filesScanned,
|
|
5550
|
+
treeSummary: treeCount
|
|
4977
5551
|
};
|
|
4978
5552
|
if (opts.baseline) {
|
|
4979
|
-
const baselinePath =
|
|
5553
|
+
const baselinePath = path17.resolve(opts.baseline);
|
|
4980
5554
|
if (await pathExists(baselinePath)) {
|
|
4981
5555
|
try {
|
|
4982
5556
|
const baseline = await readJsonFile(baselinePath);
|
|
@@ -4987,15 +5561,22 @@ async function runScan(rootDir, opts) {
|
|
|
4987
5561
|
}
|
|
4988
5562
|
}
|
|
4989
5563
|
}
|
|
4990
|
-
const vibgrateDir =
|
|
5564
|
+
const vibgrateDir = path17.join(rootDir, ".vibgrate");
|
|
4991
5565
|
await ensureDir(vibgrateDir);
|
|
4992
|
-
await writeJsonFile(
|
|
5566
|
+
await writeJsonFile(path17.join(vibgrateDir, "scan_result.json"), artifact);
|
|
5567
|
+
await saveScanHistory(rootDir, {
|
|
5568
|
+
timestamp: artifact.timestamp,
|
|
5569
|
+
totalDurationMs: durationMs,
|
|
5570
|
+
totalFiles: treeCount.totalFiles,
|
|
5571
|
+
totalDirs: treeCount.totalDirs,
|
|
5572
|
+
steps: progress.getStepTimings()
|
|
5573
|
+
});
|
|
4993
5574
|
for (const project of allProjects) {
|
|
4994
5575
|
if (project.drift && project.path) {
|
|
4995
|
-
const projectDir =
|
|
4996
|
-
const projectVibgrateDir =
|
|
5576
|
+
const projectDir = path17.resolve(rootDir, project.path);
|
|
5577
|
+
const projectVibgrateDir = path17.join(projectDir, ".vibgrate");
|
|
4997
5578
|
await ensureDir(projectVibgrateDir);
|
|
4998
|
-
await writeJsonFile(
|
|
5579
|
+
await writeJsonFile(path17.join(projectVibgrateDir, "project_score.json"), {
|
|
4999
5580
|
projectId: project.projectId,
|
|
5000
5581
|
name: project.name,
|
|
5001
5582
|
type: project.type,
|
|
@@ -5012,7 +5593,7 @@ async function runScan(rootDir, opts) {
|
|
|
5012
5593
|
if (opts.format === "json") {
|
|
5013
5594
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
5014
5595
|
if (opts.out) {
|
|
5015
|
-
await writeTextFile(
|
|
5596
|
+
await writeTextFile(path17.resolve(opts.out), jsonStr);
|
|
5016
5597
|
console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
5017
5598
|
} else {
|
|
5018
5599
|
console.log(jsonStr);
|
|
@@ -5021,7 +5602,7 @@ async function runScan(rootDir, opts) {
|
|
|
5021
5602
|
const sarif = formatSarif(artifact);
|
|
5022
5603
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
5023
5604
|
if (opts.out) {
|
|
5024
|
-
await writeTextFile(
|
|
5605
|
+
await writeTextFile(path17.resolve(opts.out), sarifStr);
|
|
5025
5606
|
console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
5026
5607
|
} else {
|
|
5027
5608
|
console.log(sarifStr);
|
|
@@ -5030,7 +5611,7 @@ async function runScan(rootDir, opts) {
|
|
|
5030
5611
|
const text = formatText(artifact);
|
|
5031
5612
|
console.log(text);
|
|
5032
5613
|
if (opts.out) {
|
|
5033
|
-
await writeTextFile(
|
|
5614
|
+
await writeTextFile(path17.resolve(opts.out), text);
|
|
5034
5615
|
}
|
|
5035
5616
|
}
|
|
5036
5617
|
return artifact;
|
|
@@ -5089,7 +5670,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
5089
5670
|
}
|
|
5090
5671
|
}
|
|
5091
5672
|
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 =
|
|
5673
|
+
const rootDir = path17.resolve(targetPath);
|
|
5093
5674
|
if (!await pathExists(rootDir)) {
|
|
5094
5675
|
console.error(chalk5.red(`Path does not exist: ${rootDir}`));
|
|
5095
5676
|
process.exit(1);
|