@vibgrate/cli 1.0.21 → 1.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,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",
|
|
@@ -50,6 +130,80 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
50
130
|
"packages",
|
|
51
131
|
"TestResults"
|
|
52
132
|
]);
|
|
133
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
134
|
+
// Fonts
|
|
135
|
+
".woff",
|
|
136
|
+
".woff2",
|
|
137
|
+
".ttf",
|
|
138
|
+
".otf",
|
|
139
|
+
".eot",
|
|
140
|
+
// Images & vector
|
|
141
|
+
".png",
|
|
142
|
+
".jpg",
|
|
143
|
+
".jpeg",
|
|
144
|
+
".gif",
|
|
145
|
+
".ico",
|
|
146
|
+
".bmp",
|
|
147
|
+
".tiff",
|
|
148
|
+
".tif",
|
|
149
|
+
".webp",
|
|
150
|
+
".avif",
|
|
151
|
+
".svg",
|
|
152
|
+
".heic",
|
|
153
|
+
".heif",
|
|
154
|
+
".jfif",
|
|
155
|
+
".psd",
|
|
156
|
+
".ai",
|
|
157
|
+
".eps",
|
|
158
|
+
".raw",
|
|
159
|
+
".cr2",
|
|
160
|
+
".nef",
|
|
161
|
+
".dng",
|
|
162
|
+
// Video
|
|
163
|
+
".mp4",
|
|
164
|
+
".webm",
|
|
165
|
+
".avi",
|
|
166
|
+
".mov",
|
|
167
|
+
".mkv",
|
|
168
|
+
".wmv",
|
|
169
|
+
".flv",
|
|
170
|
+
".m4v",
|
|
171
|
+
".mpg",
|
|
172
|
+
".mpeg",
|
|
173
|
+
".3gp",
|
|
174
|
+
".ogv",
|
|
175
|
+
// Audio
|
|
176
|
+
".mp3",
|
|
177
|
+
".wav",
|
|
178
|
+
".ogg",
|
|
179
|
+
".flac",
|
|
180
|
+
".aac",
|
|
181
|
+
".wma",
|
|
182
|
+
".m4a",
|
|
183
|
+
".opus",
|
|
184
|
+
".aiff",
|
|
185
|
+
".mid",
|
|
186
|
+
".midi",
|
|
187
|
+
// Archives
|
|
188
|
+
".zip",
|
|
189
|
+
".tar",
|
|
190
|
+
".gz",
|
|
191
|
+
".bz2",
|
|
192
|
+
".7z",
|
|
193
|
+
".rar",
|
|
194
|
+
// Compiled / binary
|
|
195
|
+
".exe",
|
|
196
|
+
".dll",
|
|
197
|
+
".so",
|
|
198
|
+
".dylib",
|
|
199
|
+
".o",
|
|
200
|
+
".a",
|
|
201
|
+
".class",
|
|
202
|
+
".pyc",
|
|
203
|
+
".pdb",
|
|
204
|
+
// Source maps & lockfiles (large, not useful for drift analysis)
|
|
205
|
+
".map"
|
|
206
|
+
]);
|
|
53
207
|
var TEXT_CACHE_MAX_BYTES = 1048576;
|
|
54
208
|
var FileCache = class _FileCache {
|
|
55
209
|
/** Directory walk results keyed by rootDir */
|
|
@@ -60,6 +214,40 @@ var FileCache = class _FileCache {
|
|
|
60
214
|
jsonCache = /* @__PURE__ */ new Map();
|
|
61
215
|
/** pathExists keyed by absolute path */
|
|
62
216
|
existsCache = /* @__PURE__ */ new Map();
|
|
217
|
+
/** User-configured exclude predicate (compiled from glob patterns) */
|
|
218
|
+
excludePredicate = null;
|
|
219
|
+
/** Directories that were auto-skipped because they were stuck (>60s) */
|
|
220
|
+
_stuckPaths = [];
|
|
221
|
+
/** Files skipped because they exceed maxFileSizeToScan */
|
|
222
|
+
_skippedLargeFiles = [];
|
|
223
|
+
/** Maximum file size (bytes) we will read. 0 = unlimited. */
|
|
224
|
+
_maxFileSize = 0;
|
|
225
|
+
/** Root dir for relative-path computation (set by the first walkDir call) */
|
|
226
|
+
_rootDir = null;
|
|
227
|
+
/** Set exclude patterns from config (call once before the walk) */
|
|
228
|
+
setExcludePatterns(patterns) {
|
|
229
|
+
this.excludePredicate = compileGlobs(patterns);
|
|
230
|
+
}
|
|
231
|
+
/** Set the maximum file size in bytes that readTextFile / readJsonFile will process */
|
|
232
|
+
setMaxFileSize(bytes) {
|
|
233
|
+
this._maxFileSize = bytes;
|
|
234
|
+
}
|
|
235
|
+
/** Record a path that timed out or was stuck during scanning */
|
|
236
|
+
addStuckPath(relPath) {
|
|
237
|
+
this._stuckPaths.push(relPath);
|
|
238
|
+
}
|
|
239
|
+
/** Get all paths that were auto-skipped due to being stuck (dirs + scanner files) */
|
|
240
|
+
get stuckPaths() {
|
|
241
|
+
return this._stuckPaths;
|
|
242
|
+
}
|
|
243
|
+
/** @deprecated Use stuckPaths instead */
|
|
244
|
+
get stuckDirs() {
|
|
245
|
+
return this._stuckPaths;
|
|
246
|
+
}
|
|
247
|
+
/** Get files that were skipped because they exceeded maxFileSizeToScan */
|
|
248
|
+
get skippedLargeFiles() {
|
|
249
|
+
return this._skippedLargeFiles;
|
|
250
|
+
}
|
|
63
251
|
// ── Directory walking ──
|
|
64
252
|
/**
|
|
65
253
|
* Walk the directory tree from `rootDir` once, skipping SKIP_DIRS plus
|
|
@@ -70,6 +258,7 @@ var FileCache = class _FileCache {
|
|
|
70
258
|
* SKIP_EXTENSIONS) do so on the returned entries — no separate walk.
|
|
71
259
|
*/
|
|
72
260
|
walkDir(rootDir, onProgress) {
|
|
261
|
+
this._rootDir = rootDir;
|
|
73
262
|
const cached = this.walkCache.get(rootDir);
|
|
74
263
|
if (cached) return cached;
|
|
75
264
|
const promise = this._doWalk(rootDir, onProgress);
|
|
@@ -86,28 +275,49 @@ var FileCache = class _FileCache {
|
|
|
86
275
|
let lastReported = 0;
|
|
87
276
|
const REPORT_INTERVAL = 50;
|
|
88
277
|
const sem = new Semaphore(maxConcurrentReads);
|
|
278
|
+
const STUCK_TIMEOUT_MS = 6e4;
|
|
89
279
|
const extraSkip = _FileCache.EXTRA_SKIP;
|
|
280
|
+
const isExcluded = this.excludePredicate;
|
|
281
|
+
const stuckDirs = this._stuckPaths;
|
|
90
282
|
async function walk(dir) {
|
|
283
|
+
const relDir = path2.relative(rootDir, dir);
|
|
284
|
+
if (onProgress) {
|
|
285
|
+
onProgress(foundCount, relDir || ".");
|
|
286
|
+
}
|
|
91
287
|
let entries;
|
|
92
288
|
try {
|
|
93
|
-
|
|
289
|
+
const readPromise = fs.readdir(dir, { withFileTypes: true });
|
|
290
|
+
const result = await Promise.race([
|
|
291
|
+
readPromise.then((e) => ({ ok: true, entries: e })),
|
|
292
|
+
new Promise(
|
|
293
|
+
(resolve7) => setTimeout(() => resolve7({ ok: false }), STUCK_TIMEOUT_MS)
|
|
294
|
+
)
|
|
295
|
+
]);
|
|
296
|
+
if (!result.ok) {
|
|
297
|
+
stuckDirs.push(relDir || dir);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
entries = result.entries;
|
|
94
301
|
} catch {
|
|
95
302
|
return;
|
|
96
303
|
}
|
|
97
304
|
const subWalks = [];
|
|
98
305
|
for (const e of entries) {
|
|
99
|
-
const absPath =
|
|
100
|
-
const relPath =
|
|
306
|
+
const absPath = path2.join(dir, e.name);
|
|
307
|
+
const relPath = path2.relative(rootDir, absPath);
|
|
308
|
+
if (isExcluded && isExcluded(relPath)) continue;
|
|
101
309
|
if (e.isDirectory()) {
|
|
102
310
|
if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
|
|
103
311
|
results.push({ absPath, relPath, name: e.name, isFile: false, isDirectory: true });
|
|
104
312
|
subWalks.push(sem.run(() => walk(absPath)));
|
|
105
313
|
} else if (e.isFile()) {
|
|
314
|
+
const ext = path2.extname(e.name).toLowerCase();
|
|
315
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
106
316
|
results.push({ absPath, relPath, name: e.name, isFile: true, isDirectory: false });
|
|
107
317
|
foundCount++;
|
|
108
318
|
if (onProgress && foundCount - lastReported >= REPORT_INTERVAL) {
|
|
109
319
|
lastReported = foundCount;
|
|
110
|
-
onProgress(foundCount);
|
|
320
|
+
onProgress(foundCount, relPath);
|
|
111
321
|
}
|
|
112
322
|
}
|
|
113
323
|
}
|
|
@@ -115,7 +325,7 @@ var FileCache = class _FileCache {
|
|
|
115
325
|
}
|
|
116
326
|
await sem.run(() => walk(rootDir));
|
|
117
327
|
if (onProgress && foundCount !== lastReported) {
|
|
118
|
-
onProgress(foundCount);
|
|
328
|
+
onProgress(foundCount, "");
|
|
119
329
|
}
|
|
120
330
|
return results;
|
|
121
331
|
}
|
|
@@ -141,17 +351,36 @@ var FileCache = class _FileCache {
|
|
|
141
351
|
* Read a text file. Files ≤ 1 MB are cached so subsequent calls from
|
|
142
352
|
* different scanners return the same string. Files > 1 MB (lockfiles,
|
|
143
353
|
* large generated files) are read directly and never retained.
|
|
354
|
+
*
|
|
355
|
+
* If maxFileSizeToScan is set and the file exceeds it, the file is
|
|
356
|
+
* recorded as skipped and an empty string is returned.
|
|
144
357
|
*/
|
|
145
358
|
readTextFile(filePath) {
|
|
146
|
-
const abs =
|
|
359
|
+
const abs = path2.resolve(filePath);
|
|
147
360
|
const cached = this.textCache.get(abs);
|
|
148
361
|
if (cached) return cached;
|
|
149
|
-
const
|
|
362
|
+
const maxSize = this._maxFileSize;
|
|
363
|
+
const skippedLarge = this._skippedLargeFiles;
|
|
364
|
+
const rootDir = this._rootDir;
|
|
365
|
+
const promise = (async () => {
|
|
366
|
+
if (maxSize > 0) {
|
|
367
|
+
try {
|
|
368
|
+
const stat4 = await fs.stat(abs);
|
|
369
|
+
if (stat4.size > maxSize) {
|
|
370
|
+
const rel = rootDir ? path2.relative(rootDir, abs) : abs;
|
|
371
|
+
skippedLarge.push(rel);
|
|
372
|
+
this.textCache.delete(abs);
|
|
373
|
+
return "";
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const content = await fs.readFile(abs, "utf8");
|
|
150
379
|
if (content.length > TEXT_CACHE_MAX_BYTES) {
|
|
151
380
|
this.textCache.delete(abs);
|
|
152
381
|
}
|
|
153
382
|
return content;
|
|
154
|
-
});
|
|
383
|
+
})();
|
|
155
384
|
this.textCache.set(abs, promise);
|
|
156
385
|
return promise;
|
|
157
386
|
}
|
|
@@ -160,7 +389,7 @@ var FileCache = class _FileCache {
|
|
|
160
389
|
* text is evicted immediately so we never hold both representations.
|
|
161
390
|
*/
|
|
162
391
|
readJsonFile(filePath) {
|
|
163
|
-
const abs =
|
|
392
|
+
const abs = path2.resolve(filePath);
|
|
164
393
|
const cached = this.jsonCache.get(abs);
|
|
165
394
|
if (cached) return cached;
|
|
166
395
|
const promise = this.readTextFile(abs).then((txt) => {
|
|
@@ -172,7 +401,7 @@ var FileCache = class _FileCache {
|
|
|
172
401
|
}
|
|
173
402
|
// ── Existence checks ──
|
|
174
403
|
pathExists(p) {
|
|
175
|
-
const abs =
|
|
404
|
+
const abs = path2.resolve(p);
|
|
176
405
|
const cached = this.existsCache.get(abs);
|
|
177
406
|
if (cached) return cached;
|
|
178
407
|
const promise = fs.access(abs).then(() => true, () => false);
|
|
@@ -196,13 +425,14 @@ var FileCache = class _FileCache {
|
|
|
196
425
|
return this.jsonCache.size;
|
|
197
426
|
}
|
|
198
427
|
};
|
|
199
|
-
async function quickTreeCount(rootDir) {
|
|
428
|
+
async function quickTreeCount(rootDir, excludePatterns) {
|
|
200
429
|
let totalFiles = 0;
|
|
201
430
|
let totalDirs = 0;
|
|
202
431
|
const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
|
|
203
432
|
const maxConcurrent = Math.max(8, Math.min(128, cores * 8));
|
|
204
433
|
const sem = new Semaphore(maxConcurrent);
|
|
205
434
|
const extraSkip = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
|
|
435
|
+
const isExcluded = excludePatterns ? compileGlobs(excludePatterns) : null;
|
|
206
436
|
async function count(dir) {
|
|
207
437
|
let entries;
|
|
208
438
|
try {
|
|
@@ -212,12 +442,15 @@ async function quickTreeCount(rootDir) {
|
|
|
212
442
|
}
|
|
213
443
|
const subs = [];
|
|
214
444
|
for (const e of entries) {
|
|
445
|
+
const relPath = path2.relative(rootDir, path2.join(dir, e.name));
|
|
446
|
+
if (isExcluded && isExcluded(relPath)) continue;
|
|
215
447
|
if (e.isDirectory()) {
|
|
216
448
|
if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
|
|
217
449
|
totalDirs++;
|
|
218
|
-
subs.push(sem.run(() => count(
|
|
450
|
+
subs.push(sem.run(() => count(path2.join(dir, e.name))));
|
|
219
451
|
} else if (e.isFile()) {
|
|
220
|
-
|
|
452
|
+
const ext = path2.extname(e.name).toLowerCase();
|
|
453
|
+
if (!SKIP_EXTENSIONS.has(ext)) totalFiles++;
|
|
221
454
|
}
|
|
222
455
|
}
|
|
223
456
|
await Promise.all(subs);
|
|
@@ -241,9 +474,10 @@ async function findFiles(rootDir, predicate) {
|
|
|
241
474
|
for (const e of entries) {
|
|
242
475
|
if (e.isDirectory()) {
|
|
243
476
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
244
|
-
subDirectoryWalks.push(readDirSemaphore.run(() => walk(
|
|
477
|
+
subDirectoryWalks.push(readDirSemaphore.run(() => walk(path2.join(dir, e.name))));
|
|
245
478
|
} else if (e.isFile() && predicate(e.name)) {
|
|
246
|
-
|
|
479
|
+
const ext = path2.extname(e.name).toLowerCase();
|
|
480
|
+
if (!SKIP_EXTENSIONS.has(ext)) results.push(path2.join(dir, e.name));
|
|
247
481
|
}
|
|
248
482
|
}
|
|
249
483
|
await Promise.all(subDirectoryWalks);
|
|
@@ -279,11 +513,11 @@ async function ensureDir(dir) {
|
|
|
279
513
|
await fs.mkdir(dir, { recursive: true });
|
|
280
514
|
}
|
|
281
515
|
async function writeJsonFile(filePath, data) {
|
|
282
|
-
await ensureDir(
|
|
516
|
+
await ensureDir(path2.dirname(filePath));
|
|
283
517
|
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
284
518
|
}
|
|
285
519
|
async function writeTextFile(filePath, content) {
|
|
286
|
-
await ensureDir(
|
|
520
|
+
await ensureDir(path2.dirname(filePath));
|
|
287
521
|
await fs.writeFile(filePath, content, "utf8");
|
|
288
522
|
}
|
|
289
523
|
|
|
@@ -1217,7 +1451,7 @@ function toSarifResult(finding) {
|
|
|
1217
1451
|
|
|
1218
1452
|
// src/commands/dsn.ts
|
|
1219
1453
|
import * as crypto2 from "crypto";
|
|
1220
|
-
import * as
|
|
1454
|
+
import * as path3 from "path";
|
|
1221
1455
|
import { Command } from "commander";
|
|
1222
1456
|
import chalk2 from "chalk";
|
|
1223
1457
|
var REGION_HOSTS = {
|
|
@@ -1262,7 +1496,7 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
|
|
|
1262
1496
|
console.log(chalk2.dim("Set this as VIBGRATE_DSN in your CI environment."));
|
|
1263
1497
|
console.log(chalk2.dim("The secret must be registered on your Vibgrate ingest API."));
|
|
1264
1498
|
if (opts.write) {
|
|
1265
|
-
const writePath =
|
|
1499
|
+
const writePath = path3.resolve(opts.write);
|
|
1266
1500
|
await writeTextFile(writePath, dsn + "\n");
|
|
1267
1501
|
console.log("");
|
|
1268
1502
|
console.log(chalk2.green("\u2714") + ` DSN written to ${opts.write}`);
|
|
@@ -1272,7 +1506,7 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
|
|
|
1272
1506
|
|
|
1273
1507
|
// src/commands/push.ts
|
|
1274
1508
|
import * as crypto3 from "crypto";
|
|
1275
|
-
import * as
|
|
1509
|
+
import * as path4 from "path";
|
|
1276
1510
|
import { Command as Command2 } from "commander";
|
|
1277
1511
|
import chalk3 from "chalk";
|
|
1278
1512
|
function parseDsn(dsn) {
|
|
@@ -1301,7 +1535,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1301
1535
|
if (opts.strict) process.exit(1);
|
|
1302
1536
|
return;
|
|
1303
1537
|
}
|
|
1304
|
-
const filePath =
|
|
1538
|
+
const filePath = path4.resolve(opts.file);
|
|
1305
1539
|
if (!await pathExists(filePath)) {
|
|
1306
1540
|
console.error(chalk3.red(`Scan artifact not found: ${filePath}`));
|
|
1307
1541
|
console.error(chalk3.dim('Run "vibgrate scan" first.'));
|
|
@@ -1346,14 +1580,31 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1346
1580
|
});
|
|
1347
1581
|
|
|
1348
1582
|
// src/commands/scan.ts
|
|
1349
|
-
import * as
|
|
1583
|
+
import * as path17 from "path";
|
|
1350
1584
|
import { Command as Command3 } from "commander";
|
|
1351
1585
|
import chalk5 from "chalk";
|
|
1352
1586
|
|
|
1353
1587
|
// src/scanners/node-scanner.ts
|
|
1354
|
-
import * as
|
|
1588
|
+
import * as path5 from "path";
|
|
1355
1589
|
import * as semver2 from "semver";
|
|
1356
1590
|
|
|
1591
|
+
// src/utils/timeout.ts
|
|
1592
|
+
async function withTimeout(promise, ms) {
|
|
1593
|
+
let timer;
|
|
1594
|
+
const timeout = new Promise((resolve7) => {
|
|
1595
|
+
timer = setTimeout(() => resolve7({ ok: false }), ms);
|
|
1596
|
+
});
|
|
1597
|
+
try {
|
|
1598
|
+
const result = await Promise.race([
|
|
1599
|
+
promise.then((value) => ({ ok: true, value })),
|
|
1600
|
+
timeout
|
|
1601
|
+
]);
|
|
1602
|
+
return result;
|
|
1603
|
+
} finally {
|
|
1604
|
+
clearTimeout(timer);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1357
1608
|
// src/scanners/npm-cache.ts
|
|
1358
1609
|
import { spawn } from "child_process";
|
|
1359
1610
|
import * as semver from "semver";
|
|
@@ -1528,10 +1779,20 @@ var KNOWN_FRAMEWORKS = {
|
|
|
1528
1779
|
async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
1529
1780
|
const packageJsonFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
1530
1781
|
const results = [];
|
|
1782
|
+
const STUCK_TIMEOUT_MS = 6e4;
|
|
1531
1783
|
for (const pjPath of packageJsonFiles) {
|
|
1532
1784
|
try {
|
|
1533
|
-
const
|
|
1534
|
-
|
|
1785
|
+
const scanPromise = scanOnePackageJson(pjPath, rootDir, npmCache, cache);
|
|
1786
|
+
const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
|
|
1787
|
+
if (result.ok) {
|
|
1788
|
+
results.push(result.value);
|
|
1789
|
+
} else {
|
|
1790
|
+
const relPath = path5.relative(rootDir, path5.dirname(pjPath));
|
|
1791
|
+
if (cache) {
|
|
1792
|
+
cache.addStuckPath(relPath || ".");
|
|
1793
|
+
}
|
|
1794
|
+
console.error(`Timeout scanning ${pjPath} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
1795
|
+
}
|
|
1535
1796
|
} catch (e) {
|
|
1536
1797
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1537
1798
|
console.error(`Error scanning ${pjPath}: ${msg}`);
|
|
@@ -1541,8 +1802,8 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
|
1541
1802
|
}
|
|
1542
1803
|
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
1543
1804
|
const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
|
|
1544
|
-
const absProjectPath =
|
|
1545
|
-
const projectPath =
|
|
1805
|
+
const absProjectPath = path5.dirname(packageJsonPath);
|
|
1806
|
+
const projectPath = path5.relative(rootDir, absProjectPath) || ".";
|
|
1546
1807
|
const nodeEngine = pj.engines?.node ?? void 0;
|
|
1547
1808
|
let runtimeLatest;
|
|
1548
1809
|
let runtimeMajorsBehind;
|
|
@@ -1624,7 +1885,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
1624
1885
|
return {
|
|
1625
1886
|
type: "node",
|
|
1626
1887
|
path: projectPath,
|
|
1627
|
-
name: pj.name ??
|
|
1888
|
+
name: pj.name ?? path5.basename(absProjectPath),
|
|
1628
1889
|
runtime: nodeEngine,
|
|
1629
1890
|
runtimeLatest,
|
|
1630
1891
|
runtimeMajorsBehind,
|
|
@@ -1635,7 +1896,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
1635
1896
|
}
|
|
1636
1897
|
|
|
1637
1898
|
// src/scanners/dotnet-scanner.ts
|
|
1638
|
-
import * as
|
|
1899
|
+
import * as path6 from "path";
|
|
1639
1900
|
import { XMLParser } from "fast-xml-parser";
|
|
1640
1901
|
var parser = new XMLParser({
|
|
1641
1902
|
ignoreAttributes: false,
|
|
@@ -1836,7 +2097,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1836
2097
|
const parsed = parser.parse(xml);
|
|
1837
2098
|
const project = parsed?.Project;
|
|
1838
2099
|
if (!project) {
|
|
1839
|
-
return { targetFrameworks: [], packageReferences: [], projectName:
|
|
2100
|
+
return { targetFrameworks: [], packageReferences: [], projectName: path6.basename(filePath, ".csproj") };
|
|
1840
2101
|
}
|
|
1841
2102
|
const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
|
|
1842
2103
|
const targetFrameworks = [];
|
|
@@ -1864,7 +2125,7 @@ function parseCsproj(xml, filePath) {
|
|
|
1864
2125
|
return {
|
|
1865
2126
|
targetFrameworks: [...new Set(targetFrameworks)],
|
|
1866
2127
|
packageReferences,
|
|
1867
|
-
projectName:
|
|
2128
|
+
projectName: path6.basename(filePath, ".csproj")
|
|
1868
2129
|
};
|
|
1869
2130
|
}
|
|
1870
2131
|
async function scanDotnetProjects(rootDir, cache) {
|
|
@@ -1874,12 +2135,12 @@ async function scanDotnetProjects(rootDir, cache) {
|
|
|
1874
2135
|
for (const slnPath of slnFiles) {
|
|
1875
2136
|
try {
|
|
1876
2137
|
const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
|
|
1877
|
-
const slnDir =
|
|
2138
|
+
const slnDir = path6.dirname(slnPath);
|
|
1878
2139
|
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
1879
2140
|
let match;
|
|
1880
2141
|
while ((match = projectRegex.exec(slnContent)) !== null) {
|
|
1881
2142
|
if (match[1]) {
|
|
1882
|
-
const csprojPath =
|
|
2143
|
+
const csprojPath = path6.resolve(slnDir, match[1].replace(/\\/g, "/"));
|
|
1883
2144
|
slnCsprojPaths.add(csprojPath);
|
|
1884
2145
|
}
|
|
1885
2146
|
}
|
|
@@ -1888,10 +2149,20 @@ async function scanDotnetProjects(rootDir, cache) {
|
|
|
1888
2149
|
}
|
|
1889
2150
|
const allCsprojFiles = /* @__PURE__ */ new Set([...csprojFiles, ...slnCsprojPaths]);
|
|
1890
2151
|
const results = [];
|
|
2152
|
+
const STUCK_TIMEOUT_MS = 6e4;
|
|
1891
2153
|
for (const csprojPath of allCsprojFiles) {
|
|
1892
2154
|
try {
|
|
1893
|
-
const
|
|
1894
|
-
|
|
2155
|
+
const scanPromise = scanOneCsproj(csprojPath, rootDir, cache);
|
|
2156
|
+
const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
|
|
2157
|
+
if (result.ok) {
|
|
2158
|
+
results.push(result.value);
|
|
2159
|
+
} else {
|
|
2160
|
+
const relPath = path6.relative(rootDir, path6.dirname(csprojPath));
|
|
2161
|
+
if (cache) {
|
|
2162
|
+
cache.addStuckPath(relPath || ".");
|
|
2163
|
+
}
|
|
2164
|
+
console.error(`Timeout scanning ${csprojPath} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
2165
|
+
}
|
|
1895
2166
|
} catch (e) {
|
|
1896
2167
|
const msg = e instanceof Error ? e.message : String(e);
|
|
1897
2168
|
console.error(`Error scanning ${csprojPath}: ${msg}`);
|
|
@@ -1935,7 +2206,7 @@ async function scanOneCsproj(csprojPath, rootDir, cache) {
|
|
|
1935
2206
|
const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: dependencies.length };
|
|
1936
2207
|
return {
|
|
1937
2208
|
type: "dotnet",
|
|
1938
|
-
path:
|
|
2209
|
+
path: path6.relative(rootDir, path6.dirname(csprojPath)) || ".",
|
|
1939
2210
|
name: data.projectName,
|
|
1940
2211
|
targetFramework,
|
|
1941
2212
|
runtime: primaryTfm,
|
|
@@ -1948,15 +2219,17 @@ async function scanOneCsproj(csprojPath, rootDir, cache) {
|
|
|
1948
2219
|
}
|
|
1949
2220
|
|
|
1950
2221
|
// src/config.ts
|
|
1951
|
-
import * as
|
|
2222
|
+
import * as path7 from "path";
|
|
1952
2223
|
import * as fs2 from "fs/promises";
|
|
1953
2224
|
var CONFIG_FILES = [
|
|
1954
2225
|
"vibgrate.config.ts",
|
|
1955
2226
|
"vibgrate.config.js",
|
|
1956
2227
|
"vibgrate.config.json"
|
|
1957
2228
|
];
|
|
2229
|
+
var DEFAULT_MAX_FILE_SIZE = 5242880;
|
|
1958
2230
|
var DEFAULT_CONFIG = {
|
|
1959
2231
|
exclude: [],
|
|
2232
|
+
maxFileSizeToScan: DEFAULT_MAX_FILE_SIZE,
|
|
1960
2233
|
thresholds: {
|
|
1961
2234
|
failOnError: {
|
|
1962
2235
|
eolDays: 180,
|
|
@@ -1970,28 +2243,44 @@ var DEFAULT_CONFIG = {
|
|
|
1970
2243
|
}
|
|
1971
2244
|
};
|
|
1972
2245
|
async function loadConfig(rootDir) {
|
|
2246
|
+
let config = DEFAULT_CONFIG;
|
|
1973
2247
|
for (const file of CONFIG_FILES) {
|
|
1974
|
-
const configPath =
|
|
2248
|
+
const configPath = path7.join(rootDir, file);
|
|
1975
2249
|
if (await pathExists(configPath)) {
|
|
1976
2250
|
if (file.endsWith(".json")) {
|
|
1977
2251
|
const txt = await readTextFile(configPath);
|
|
1978
|
-
|
|
2252
|
+
config = { ...DEFAULT_CONFIG, ...JSON.parse(txt) };
|
|
2253
|
+
break;
|
|
1979
2254
|
}
|
|
1980
2255
|
try {
|
|
1981
2256
|
const mod = await import(configPath);
|
|
1982
|
-
|
|
2257
|
+
config = { ...DEFAULT_CONFIG, ...mod.default ?? mod };
|
|
2258
|
+
break;
|
|
1983
2259
|
} catch {
|
|
1984
2260
|
}
|
|
1985
2261
|
}
|
|
1986
2262
|
}
|
|
1987
|
-
|
|
2263
|
+
const sidecarPath = path7.join(rootDir, ".vibgrate", "auto-excludes.json");
|
|
2264
|
+
if (await pathExists(sidecarPath)) {
|
|
2265
|
+
try {
|
|
2266
|
+
const txt = await readTextFile(sidecarPath);
|
|
2267
|
+
const autoExcludes = JSON.parse(txt);
|
|
2268
|
+
if (Array.isArray(autoExcludes) && autoExcludes.length > 0) {
|
|
2269
|
+
const existing = config.exclude ?? [];
|
|
2270
|
+
config = { ...config, exclude: [.../* @__PURE__ */ new Set([...existing, ...autoExcludes])] };
|
|
2271
|
+
}
|
|
2272
|
+
} catch {
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return config;
|
|
1988
2276
|
}
|
|
1989
2277
|
async function writeDefaultConfig(rootDir) {
|
|
1990
|
-
const configPath =
|
|
2278
|
+
const configPath = path7.join(rootDir, "vibgrate.config.ts");
|
|
1991
2279
|
const content = `import type { VibgrateConfig } from '@vibgrate/cli';
|
|
1992
2280
|
|
|
1993
2281
|
const config: VibgrateConfig = {
|
|
1994
2282
|
// exclude: ['legacy/**'],
|
|
2283
|
+
// maxFileSizeToScan: 5_242_880, // 5 MB (default)
|
|
1995
2284
|
thresholds: {
|
|
1996
2285
|
failOnError: {
|
|
1997
2286
|
eolDays: 180,
|
|
@@ -2010,9 +2299,44 @@ export default config;
|
|
|
2010
2299
|
await fs2.writeFile(configPath, content, "utf8");
|
|
2011
2300
|
return configPath;
|
|
2012
2301
|
}
|
|
2302
|
+
async function appendExcludePatterns(rootDir, newPatterns) {
|
|
2303
|
+
if (newPatterns.length === 0) return false;
|
|
2304
|
+
const jsonPath = path7.join(rootDir, "vibgrate.config.json");
|
|
2305
|
+
if (await pathExists(jsonPath)) {
|
|
2306
|
+
try {
|
|
2307
|
+
const txt = await readTextFile(jsonPath);
|
|
2308
|
+
const cfg = JSON.parse(txt);
|
|
2309
|
+
const existing2 = Array.isArray(cfg.exclude) ? cfg.exclude : [];
|
|
2310
|
+
const merged2 = [.../* @__PURE__ */ new Set([...existing2, ...newPatterns])];
|
|
2311
|
+
cfg.exclude = merged2;
|
|
2312
|
+
await fs2.writeFile(jsonPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
2313
|
+
return true;
|
|
2314
|
+
} catch {
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
const vibgrateDir = path7.join(rootDir, ".vibgrate");
|
|
2318
|
+
const sidecarPath = path7.join(vibgrateDir, "auto-excludes.json");
|
|
2319
|
+
let existing = [];
|
|
2320
|
+
if (await pathExists(sidecarPath)) {
|
|
2321
|
+
try {
|
|
2322
|
+
const txt = await readTextFile(sidecarPath);
|
|
2323
|
+
const parsed = JSON.parse(txt);
|
|
2324
|
+
if (Array.isArray(parsed)) existing = parsed;
|
|
2325
|
+
} catch {
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...newPatterns])];
|
|
2329
|
+
try {
|
|
2330
|
+
await fs2.mkdir(vibgrateDir, { recursive: true });
|
|
2331
|
+
await fs2.writeFile(sidecarPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2332
|
+
return true;
|
|
2333
|
+
} catch {
|
|
2334
|
+
return false;
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2013
2337
|
|
|
2014
2338
|
// src/utils/vcs.ts
|
|
2015
|
-
import * as
|
|
2339
|
+
import * as path8 from "path";
|
|
2016
2340
|
import * as fs3 from "fs/promises";
|
|
2017
2341
|
async function detectVcs(rootDir) {
|
|
2018
2342
|
try {
|
|
@@ -2026,7 +2350,7 @@ async function detectGit(rootDir) {
|
|
|
2026
2350
|
if (!gitDir) {
|
|
2027
2351
|
return { type: "unknown" };
|
|
2028
2352
|
}
|
|
2029
|
-
const headPath =
|
|
2353
|
+
const headPath = path8.join(gitDir, "HEAD");
|
|
2030
2354
|
let headContent;
|
|
2031
2355
|
try {
|
|
2032
2356
|
headContent = (await fs3.readFile(headPath, "utf8")).trim();
|
|
@@ -2050,30 +2374,30 @@ async function detectGit(rootDir) {
|
|
|
2050
2374
|
};
|
|
2051
2375
|
}
|
|
2052
2376
|
async function findGitDir(startDir) {
|
|
2053
|
-
let dir =
|
|
2054
|
-
const root =
|
|
2377
|
+
let dir = path8.resolve(startDir);
|
|
2378
|
+
const root = path8.parse(dir).root;
|
|
2055
2379
|
while (dir !== root) {
|
|
2056
|
-
const gitPath =
|
|
2380
|
+
const gitPath = path8.join(dir, ".git");
|
|
2057
2381
|
try {
|
|
2058
|
-
const
|
|
2059
|
-
if (
|
|
2382
|
+
const stat4 = await fs3.stat(gitPath);
|
|
2383
|
+
if (stat4.isDirectory()) {
|
|
2060
2384
|
return gitPath;
|
|
2061
2385
|
}
|
|
2062
|
-
if (
|
|
2386
|
+
if (stat4.isFile()) {
|
|
2063
2387
|
const content = (await fs3.readFile(gitPath, "utf8")).trim();
|
|
2064
2388
|
if (content.startsWith("gitdir: ")) {
|
|
2065
|
-
const resolved =
|
|
2389
|
+
const resolved = path8.resolve(dir, content.slice(8));
|
|
2066
2390
|
return resolved;
|
|
2067
2391
|
}
|
|
2068
2392
|
}
|
|
2069
2393
|
} catch {
|
|
2070
2394
|
}
|
|
2071
|
-
dir =
|
|
2395
|
+
dir = path8.dirname(dir);
|
|
2072
2396
|
}
|
|
2073
2397
|
return null;
|
|
2074
2398
|
}
|
|
2075
2399
|
async function resolveRef(gitDir, refPath) {
|
|
2076
|
-
const loosePath =
|
|
2400
|
+
const loosePath = path8.join(gitDir, refPath);
|
|
2077
2401
|
try {
|
|
2078
2402
|
const sha = (await fs3.readFile(loosePath, "utf8")).trim();
|
|
2079
2403
|
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
@@ -2081,7 +2405,7 @@ async function resolveRef(gitDir, refPath) {
|
|
|
2081
2405
|
}
|
|
2082
2406
|
} catch {
|
|
2083
2407
|
}
|
|
2084
|
-
const packedPath =
|
|
2408
|
+
const packedPath = path8.join(gitDir, "packed-refs");
|
|
2085
2409
|
try {
|
|
2086
2410
|
const packed = await fs3.readFile(packedPath, "utf8");
|
|
2087
2411
|
for (const line of packed.split("\n")) {
|
|
@@ -2123,6 +2447,10 @@ var ScanProgress = class {
|
|
|
2123
2447
|
startTime = Date.now();
|
|
2124
2448
|
isTTY;
|
|
2125
2449
|
rootDir = "";
|
|
2450
|
+
/** Last rendered frame content (strip to compare for dirty-checking) */
|
|
2451
|
+
lastFrame = "";
|
|
2452
|
+
/** Whether we've hidden the cursor */
|
|
2453
|
+
cursorHidden = false;
|
|
2126
2454
|
/** Estimated total scan duration in ms (from history or live calculation) */
|
|
2127
2455
|
estimatedTotalMs = null;
|
|
2128
2456
|
/** Per-step estimated durations from history */
|
|
@@ -2134,6 +2462,23 @@ var ScanProgress = class {
|
|
|
2134
2462
|
constructor(rootDir) {
|
|
2135
2463
|
this.isTTY = process.stderr.isTTY ?? false;
|
|
2136
2464
|
this.rootDir = rootDir;
|
|
2465
|
+
if (this.isTTY) {
|
|
2466
|
+
const restore = () => {
|
|
2467
|
+
if (this.cursorHidden) {
|
|
2468
|
+
process.stderr.write("\x1B[?25h");
|
|
2469
|
+
this.cursorHidden = false;
|
|
2470
|
+
}
|
|
2471
|
+
};
|
|
2472
|
+
process.on("exit", restore);
|
|
2473
|
+
process.on("SIGINT", () => {
|
|
2474
|
+
restore();
|
|
2475
|
+
process.exit(130);
|
|
2476
|
+
});
|
|
2477
|
+
process.on("SIGTERM", () => {
|
|
2478
|
+
restore();
|
|
2479
|
+
process.exit(143);
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2137
2482
|
}
|
|
2138
2483
|
/** Set the estimated total duration from scan history */
|
|
2139
2484
|
setEstimatedTotal(estimatedMs) {
|
|
@@ -2192,11 +2537,12 @@ var ScanProgress = class {
|
|
|
2192
2537
|
this.render();
|
|
2193
2538
|
}
|
|
2194
2539
|
/** Update sub-step progress for the active step (files processed, etc.) */
|
|
2195
|
-
updateStepProgress(id, current, total) {
|
|
2540
|
+
updateStepProgress(id, current, total, label) {
|
|
2196
2541
|
const step = this.steps.find((s) => s.id === id);
|
|
2197
2542
|
if (step) {
|
|
2198
2543
|
step.subProgress = current;
|
|
2199
2544
|
if (total !== void 0) step.subTotal = total;
|
|
2545
|
+
if (label !== void 0) step.subLabel = label;
|
|
2200
2546
|
}
|
|
2201
2547
|
this.render();
|
|
2202
2548
|
}
|
|
@@ -2231,7 +2577,18 @@ var ScanProgress = class {
|
|
|
2231
2577
|
this.timer = null;
|
|
2232
2578
|
}
|
|
2233
2579
|
if (this.isTTY) {
|
|
2234
|
-
|
|
2580
|
+
let buf = "";
|
|
2581
|
+
if (this.lastLineCount > 0) {
|
|
2582
|
+
buf += `\x1B[${this.lastLineCount}A`;
|
|
2583
|
+
for (let i = 0; i < this.lastLineCount; i++) {
|
|
2584
|
+
buf += "\x1B[2K\n";
|
|
2585
|
+
}
|
|
2586
|
+
buf += `\x1B[${this.lastLineCount}A`;
|
|
2587
|
+
}
|
|
2588
|
+
buf += "\x1B[?25h";
|
|
2589
|
+
if (buf) process.stderr.write(buf);
|
|
2590
|
+
this.cursorHidden = false;
|
|
2591
|
+
this.lastLineCount = 0;
|
|
2235
2592
|
}
|
|
2236
2593
|
const elapsed = this.formatElapsed(Date.now() - this.startTime);
|
|
2237
2594
|
const doneCount = this.steps.filter((s) => s.status === "done").length;
|
|
@@ -2243,26 +2600,22 @@ var ScanProgress = class {
|
|
|
2243
2600
|
}
|
|
2244
2601
|
// ── Internal rendering ──
|
|
2245
2602
|
startSpinner() {
|
|
2603
|
+
if (!this.cursorHidden) {
|
|
2604
|
+
process.stderr.write("\x1B[?25l");
|
|
2605
|
+
this.cursorHidden = true;
|
|
2606
|
+
}
|
|
2246
2607
|
this.timer = setInterval(() => {
|
|
2247
2608
|
this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
2248
2609
|
this.render();
|
|
2249
|
-
},
|
|
2610
|
+
}, 120);
|
|
2250
2611
|
}
|
|
2251
2612
|
clearLines() {
|
|
2252
|
-
if (this.lastLineCount > 0) {
|
|
2253
|
-
process.stderr.write(`\x1B[${this.lastLineCount}A`);
|
|
2254
|
-
for (let i = 0; i < this.lastLineCount; i++) {
|
|
2255
|
-
process.stderr.write("\x1B[2K\n");
|
|
2256
|
-
}
|
|
2257
|
-
process.stderr.write(`\x1B[${this.lastLineCount}A`);
|
|
2258
|
-
}
|
|
2259
2613
|
}
|
|
2260
2614
|
render() {
|
|
2261
2615
|
if (!this.isTTY) {
|
|
2262
2616
|
this.renderCI();
|
|
2263
2617
|
return;
|
|
2264
2618
|
}
|
|
2265
|
-
this.clearLines();
|
|
2266
2619
|
const lines = [];
|
|
2267
2620
|
lines.push("");
|
|
2268
2621
|
lines.push(` ${ROBOT[0]} ${BRAND[0]}`);
|
|
@@ -2303,8 +2656,21 @@ var ScanProgress = class {
|
|
|
2303
2656
|
lines.push("");
|
|
2304
2657
|
lines.push(this.renderStats());
|
|
2305
2658
|
lines.push("");
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2659
|
+
const content = lines.join("\n") + "\n";
|
|
2660
|
+
if (content === this.lastFrame && this.lastLineCount === lines.length) {
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
this.lastFrame = content;
|
|
2664
|
+
let buf = "";
|
|
2665
|
+
if (this.lastLineCount > 0) {
|
|
2666
|
+
buf += `\x1B[${this.lastLineCount}A`;
|
|
2667
|
+
for (let i = 0; i < this.lastLineCount; i++) {
|
|
2668
|
+
buf += "\x1B[2K\n";
|
|
2669
|
+
}
|
|
2670
|
+
buf += `\x1B[${this.lastLineCount}A`;
|
|
2671
|
+
}
|
|
2672
|
+
buf += content;
|
|
2673
|
+
process.stderr.write(buf);
|
|
2308
2674
|
this.lastLineCount = lines.length;
|
|
2309
2675
|
}
|
|
2310
2676
|
renderStep(step) {
|
|
@@ -2323,6 +2689,11 @@ var ScanProgress = class {
|
|
|
2323
2689
|
if (step.subTotal && step.subTotal > 0 && step.subProgress !== void 0 && step.subProgress > 0) {
|
|
2324
2690
|
detail = chalk4.dim(` \xB7 ${step.subProgress.toLocaleString()} / ${step.subTotal.toLocaleString()}`);
|
|
2325
2691
|
}
|
|
2692
|
+
if (step.subLabel) {
|
|
2693
|
+
const maxLen = 50;
|
|
2694
|
+
const displayPath = step.subLabel.length > maxLen ? "\u2026" + step.subLabel.slice(-maxLen + 1) : step.subLabel;
|
|
2695
|
+
detail += chalk4.dim(` ${displayPath}`);
|
|
2696
|
+
}
|
|
2326
2697
|
break;
|
|
2327
2698
|
case "skipped":
|
|
2328
2699
|
icon = chalk4.dim("\u25CC");
|
|
@@ -2428,11 +2799,11 @@ var ScanProgress = class {
|
|
|
2428
2799
|
|
|
2429
2800
|
// src/ui/scan-history.ts
|
|
2430
2801
|
import * as fs4 from "fs/promises";
|
|
2431
|
-
import * as
|
|
2802
|
+
import * as path9 from "path";
|
|
2432
2803
|
var HISTORY_FILENAME = "scan_history.json";
|
|
2433
2804
|
var MAX_RECORDS = 10;
|
|
2434
2805
|
async function loadScanHistory(rootDir) {
|
|
2435
|
-
const filePath =
|
|
2806
|
+
const filePath = path9.join(rootDir, ".vibgrate", HISTORY_FILENAME);
|
|
2436
2807
|
try {
|
|
2437
2808
|
const txt = await fs4.readFile(filePath, "utf8");
|
|
2438
2809
|
const data = JSON.parse(txt);
|
|
@@ -2445,8 +2816,8 @@ async function loadScanHistory(rootDir) {
|
|
|
2445
2816
|
}
|
|
2446
2817
|
}
|
|
2447
2818
|
async function saveScanHistory(rootDir, record) {
|
|
2448
|
-
const dir =
|
|
2449
|
-
const filePath =
|
|
2819
|
+
const dir = path9.join(rootDir, ".vibgrate");
|
|
2820
|
+
const filePath = path9.join(dir, HISTORY_FILENAME);
|
|
2450
2821
|
let history;
|
|
2451
2822
|
const existing = await loadScanHistory(rootDir);
|
|
2452
2823
|
if (existing) {
|
|
@@ -2510,7 +2881,7 @@ function estimateStepDurations(history, currentFileCount) {
|
|
|
2510
2881
|
}
|
|
2511
2882
|
|
|
2512
2883
|
// src/scanners/platform-matrix.ts
|
|
2513
|
-
import * as
|
|
2884
|
+
import * as path10 from "path";
|
|
2514
2885
|
var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
|
|
2515
2886
|
// Image / media processing
|
|
2516
2887
|
"sharp",
|
|
@@ -2790,7 +3161,7 @@ async function scanPlatformMatrix(rootDir, cache) {
|
|
|
2790
3161
|
}
|
|
2791
3162
|
result.dockerBaseImages = [...baseImages].sort();
|
|
2792
3163
|
for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
|
|
2793
|
-
const exists = cache ? await cache.pathExists(
|
|
3164
|
+
const exists = cache ? await cache.pathExists(path10.join(rootDir, file)) : await pathExists(path10.join(rootDir, file));
|
|
2794
3165
|
if (exists) {
|
|
2795
3166
|
result.nodeVersionFiles.push(file);
|
|
2796
3167
|
}
|
|
@@ -2867,7 +3238,7 @@ function scanDependencyRisk(projects) {
|
|
|
2867
3238
|
}
|
|
2868
3239
|
|
|
2869
3240
|
// src/scanners/dependency-graph.ts
|
|
2870
|
-
import * as
|
|
3241
|
+
import * as path11 from "path";
|
|
2871
3242
|
function parsePnpmLock(content) {
|
|
2872
3243
|
const entries = [];
|
|
2873
3244
|
const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
|
|
@@ -2926,9 +3297,9 @@ async function scanDependencyGraph(rootDir, cache) {
|
|
|
2926
3297
|
phantomDependencies: []
|
|
2927
3298
|
};
|
|
2928
3299
|
let entries = [];
|
|
2929
|
-
const pnpmLock =
|
|
2930
|
-
const npmLock =
|
|
2931
|
-
const yarnLock =
|
|
3300
|
+
const pnpmLock = path11.join(rootDir, "pnpm-lock.yaml");
|
|
3301
|
+
const npmLock = path11.join(rootDir, "package-lock.json");
|
|
3302
|
+
const yarnLock = path11.join(rootDir, "yarn.lock");
|
|
2932
3303
|
const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
|
|
2933
3304
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
2934
3305
|
if (await _pathExists(pnpmLock)) {
|
|
@@ -2975,7 +3346,7 @@ async function scanDependencyGraph(rootDir, cache) {
|
|
|
2975
3346
|
for (const pjPath of pkgFiles) {
|
|
2976
3347
|
try {
|
|
2977
3348
|
const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
|
|
2978
|
-
const relPath =
|
|
3349
|
+
const relPath = path11.relative(rootDir, pjPath);
|
|
2979
3350
|
for (const section of ["dependencies", "devDependencies"]) {
|
|
2980
3351
|
const deps = pj[section];
|
|
2981
3352
|
if (!deps) continue;
|
|
@@ -3321,7 +3692,7 @@ function scanToolingInventory(projects) {
|
|
|
3321
3692
|
}
|
|
3322
3693
|
|
|
3323
3694
|
// src/scanners/build-deploy.ts
|
|
3324
|
-
import * as
|
|
3695
|
+
import * as path12 from "path";
|
|
3325
3696
|
var CI_FILES = {
|
|
3326
3697
|
".github/workflows": "github-actions",
|
|
3327
3698
|
".gitlab-ci.yml": "gitlab-ci",
|
|
@@ -3374,17 +3745,17 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
3374
3745
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
3375
3746
|
const ciSystems = /* @__PURE__ */ new Set();
|
|
3376
3747
|
for (const [file, system] of Object.entries(CI_FILES)) {
|
|
3377
|
-
const fullPath =
|
|
3748
|
+
const fullPath = path12.join(rootDir, file);
|
|
3378
3749
|
if (await _pathExists(fullPath)) {
|
|
3379
3750
|
ciSystems.add(system);
|
|
3380
3751
|
}
|
|
3381
3752
|
}
|
|
3382
|
-
const ghWorkflowDir =
|
|
3753
|
+
const ghWorkflowDir = path12.join(rootDir, ".github", "workflows");
|
|
3383
3754
|
if (await _pathExists(ghWorkflowDir)) {
|
|
3384
3755
|
try {
|
|
3385
3756
|
if (cache) {
|
|
3386
3757
|
const entries = await cache.walkDir(rootDir);
|
|
3387
|
-
const ghPrefix =
|
|
3758
|
+
const ghPrefix = path12.relative(rootDir, ghWorkflowDir) + path12.sep;
|
|
3388
3759
|
result.ciWorkflowCount = entries.filter(
|
|
3389
3760
|
(e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
|
|
3390
3761
|
).length;
|
|
@@ -3435,11 +3806,11 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
3435
3806
|
(name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
|
|
3436
3807
|
);
|
|
3437
3808
|
if (cfnFiles.length > 0) iacSystems.add("cloudformation");
|
|
3438
|
-
if (await _pathExists(
|
|
3809
|
+
if (await _pathExists(path12.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
|
|
3439
3810
|
result.iac = [...iacSystems].sort();
|
|
3440
3811
|
const releaseTools = /* @__PURE__ */ new Set();
|
|
3441
3812
|
for (const [file, tool] of Object.entries(RELEASE_FILES)) {
|
|
3442
|
-
if (await _pathExists(
|
|
3813
|
+
if (await _pathExists(path12.join(rootDir, file))) releaseTools.add(tool);
|
|
3443
3814
|
}
|
|
3444
3815
|
const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
|
|
3445
3816
|
for (const pjPath of pkgFiles) {
|
|
@@ -3464,19 +3835,19 @@ async function scanBuildDeploy(rootDir, cache) {
|
|
|
3464
3835
|
};
|
|
3465
3836
|
const managers = /* @__PURE__ */ new Set();
|
|
3466
3837
|
for (const [file, manager] of Object.entries(lockfileMap)) {
|
|
3467
|
-
if (await _pathExists(
|
|
3838
|
+
if (await _pathExists(path12.join(rootDir, file))) managers.add(manager);
|
|
3468
3839
|
}
|
|
3469
3840
|
result.packageManagers = [...managers].sort();
|
|
3470
3841
|
const monoTools = /* @__PURE__ */ new Set();
|
|
3471
3842
|
for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
|
|
3472
|
-
if (await _pathExists(
|
|
3843
|
+
if (await _pathExists(path12.join(rootDir, file))) monoTools.add(tool);
|
|
3473
3844
|
}
|
|
3474
3845
|
result.monorepoTools = [...monoTools].sort();
|
|
3475
3846
|
return result;
|
|
3476
3847
|
}
|
|
3477
3848
|
|
|
3478
3849
|
// src/scanners/ts-modernity.ts
|
|
3479
|
-
import * as
|
|
3850
|
+
import * as path13 from "path";
|
|
3480
3851
|
async function scanTsModernity(rootDir, cache) {
|
|
3481
3852
|
const result = {
|
|
3482
3853
|
typescriptVersion: null,
|
|
@@ -3514,7 +3885,7 @@ async function scanTsModernity(rootDir, cache) {
|
|
|
3514
3885
|
if (hasEsm && hasCjs) result.moduleType = "mixed";
|
|
3515
3886
|
else if (hasEsm) result.moduleType = "esm";
|
|
3516
3887
|
else if (hasCjs) result.moduleType = "cjs";
|
|
3517
|
-
let tsConfigPath =
|
|
3888
|
+
let tsConfigPath = path13.join(rootDir, "tsconfig.json");
|
|
3518
3889
|
const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
|
|
3519
3890
|
if (!tsConfigExists) {
|
|
3520
3891
|
const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
|
|
@@ -3861,7 +4232,7 @@ function scanBreakingChangeExposure(projects) {
|
|
|
3861
4232
|
|
|
3862
4233
|
// src/scanners/file-hotspots.ts
|
|
3863
4234
|
import * as fs5 from "fs/promises";
|
|
3864
|
-
import * as
|
|
4235
|
+
import * as path14 from "path";
|
|
3865
4236
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
3866
4237
|
"node_modules",
|
|
3867
4238
|
".git",
|
|
@@ -3880,7 +4251,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
|
3880
4251
|
".output",
|
|
3881
4252
|
".svelte-kit"
|
|
3882
4253
|
]);
|
|
3883
|
-
var
|
|
4254
|
+
var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
3884
4255
|
".map",
|
|
3885
4256
|
".lock",
|
|
3886
4257
|
".png",
|
|
@@ -3892,6 +4263,7 @@ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
3892
4263
|
".woff",
|
|
3893
4264
|
".woff2",
|
|
3894
4265
|
".ttf",
|
|
4266
|
+
".otf",
|
|
3895
4267
|
".eot",
|
|
3896
4268
|
".mp4",
|
|
3897
4269
|
".webm"
|
|
@@ -3904,16 +4276,16 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
3904
4276
|
const entries = await cache.walkDir(rootDir);
|
|
3905
4277
|
for (const entry of entries) {
|
|
3906
4278
|
if (!entry.isFile) continue;
|
|
3907
|
-
const ext =
|
|
3908
|
-
if (
|
|
3909
|
-
const depth = entry.relPath.split(
|
|
4279
|
+
const ext = path14.extname(entry.name).toLowerCase();
|
|
4280
|
+
if (SKIP_EXTENSIONS2.has(ext)) continue;
|
|
4281
|
+
const depth = entry.relPath.split(path14.sep).length - 1;
|
|
3910
4282
|
if (depth > maxDepth) maxDepth = depth;
|
|
3911
4283
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
3912
4284
|
try {
|
|
3913
|
-
const
|
|
4285
|
+
const stat4 = await fs5.stat(entry.absPath);
|
|
3914
4286
|
allFiles.push({
|
|
3915
4287
|
path: entry.relPath,
|
|
3916
|
-
bytes:
|
|
4288
|
+
bytes: stat4.size
|
|
3917
4289
|
});
|
|
3918
4290
|
} catch {
|
|
3919
4291
|
}
|
|
@@ -3935,16 +4307,16 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
3935
4307
|
for (const e of entries) {
|
|
3936
4308
|
if (e.isDirectory) {
|
|
3937
4309
|
if (SKIP_DIRS2.has(e.name)) continue;
|
|
3938
|
-
await walk(
|
|
4310
|
+
await walk(path14.join(dir, e.name), depth + 1);
|
|
3939
4311
|
} else if (e.isFile) {
|
|
3940
|
-
const ext =
|
|
3941
|
-
if (
|
|
4312
|
+
const ext = path14.extname(e.name).toLowerCase();
|
|
4313
|
+
if (SKIP_EXTENSIONS2.has(ext)) continue;
|
|
3942
4314
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
3943
4315
|
try {
|
|
3944
|
-
const
|
|
4316
|
+
const stat4 = await fs5.stat(path14.join(dir, e.name));
|
|
3945
4317
|
allFiles.push({
|
|
3946
|
-
path:
|
|
3947
|
-
bytes:
|
|
4318
|
+
path: path14.relative(rootDir, path14.join(dir, e.name)),
|
|
4319
|
+
bytes: stat4.size
|
|
3948
4320
|
});
|
|
3949
4321
|
} catch {
|
|
3950
4322
|
}
|
|
@@ -3966,7 +4338,7 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
3966
4338
|
}
|
|
3967
4339
|
|
|
3968
4340
|
// src/scanners/security-posture.ts
|
|
3969
|
-
import * as
|
|
4341
|
+
import * as path15 from "path";
|
|
3970
4342
|
var LOCKFILES = {
|
|
3971
4343
|
"pnpm-lock.yaml": "pnpm",
|
|
3972
4344
|
"package-lock.json": "npm",
|
|
@@ -3987,14 +4359,14 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
3987
4359
|
const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
|
|
3988
4360
|
const foundLockfiles = [];
|
|
3989
4361
|
for (const [file, type] of Object.entries(LOCKFILES)) {
|
|
3990
|
-
if (await _pathExists(
|
|
4362
|
+
if (await _pathExists(path15.join(rootDir, file))) {
|
|
3991
4363
|
foundLockfiles.push(type);
|
|
3992
4364
|
}
|
|
3993
4365
|
}
|
|
3994
4366
|
result.lockfilePresent = foundLockfiles.length > 0;
|
|
3995
4367
|
result.multipleLockfileTypes = foundLockfiles.length > 1;
|
|
3996
4368
|
result.lockfileTypes = foundLockfiles.sort();
|
|
3997
|
-
const gitignorePath =
|
|
4369
|
+
const gitignorePath = path15.join(rootDir, ".gitignore");
|
|
3998
4370
|
if (await _pathExists(gitignorePath)) {
|
|
3999
4371
|
try {
|
|
4000
4372
|
const content = await _readTextFile(gitignorePath);
|
|
@@ -4009,7 +4381,7 @@ async function scanSecurityPosture(rootDir, cache) {
|
|
|
4009
4381
|
}
|
|
4010
4382
|
}
|
|
4011
4383
|
for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
|
|
4012
|
-
if (await _pathExists(
|
|
4384
|
+
if (await _pathExists(path15.join(rootDir, envFile))) {
|
|
4013
4385
|
if (!result.gitignoreCoversEnv) {
|
|
4014
4386
|
result.envFilesTracked = true;
|
|
4015
4387
|
break;
|
|
@@ -4434,7 +4806,7 @@ function scanServiceDependencies(projects) {
|
|
|
4434
4806
|
}
|
|
4435
4807
|
|
|
4436
4808
|
// src/scanners/architecture.ts
|
|
4437
|
-
import * as
|
|
4809
|
+
import * as path16 from "path";
|
|
4438
4810
|
import * as fs6 from "fs/promises";
|
|
4439
4811
|
var ARCHETYPE_SIGNALS = [
|
|
4440
4812
|
// Meta-frameworks (highest priority — they imply routing patterns)
|
|
@@ -4733,9 +5105,9 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
4733
5105
|
const entries = await cache.walkDir(rootDir);
|
|
4734
5106
|
return entries.filter((e) => {
|
|
4735
5107
|
if (!e.isFile) return false;
|
|
4736
|
-
const name =
|
|
5108
|
+
const name = path16.basename(e.absPath);
|
|
4737
5109
|
if (name.startsWith(".") && name !== ".") return false;
|
|
4738
|
-
const ext =
|
|
5110
|
+
const ext = path16.extname(name);
|
|
4739
5111
|
return SOURCE_EXTENSIONS.has(ext);
|
|
4740
5112
|
}).map((e) => e.relPath);
|
|
4741
5113
|
}
|
|
@@ -4749,15 +5121,15 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
4749
5121
|
}
|
|
4750
5122
|
for (const entry of entries) {
|
|
4751
5123
|
if (entry.name.startsWith(".") && entry.name !== ".") continue;
|
|
4752
|
-
const fullPath =
|
|
5124
|
+
const fullPath = path16.join(dir, entry.name);
|
|
4753
5125
|
if (entry.isDirectory()) {
|
|
4754
5126
|
if (!IGNORE_DIRS.has(entry.name)) {
|
|
4755
5127
|
await walk(fullPath);
|
|
4756
5128
|
}
|
|
4757
5129
|
} else if (entry.isFile()) {
|
|
4758
|
-
const ext =
|
|
5130
|
+
const ext = path16.extname(entry.name);
|
|
4759
5131
|
if (SOURCE_EXTENSIONS.has(ext)) {
|
|
4760
|
-
files.push(
|
|
5132
|
+
files.push(path16.relative(rootDir, fullPath));
|
|
4761
5133
|
}
|
|
4762
5134
|
}
|
|
4763
5135
|
}
|
|
@@ -4781,7 +5153,7 @@ function classifyFile(filePath, archetype) {
|
|
|
4781
5153
|
}
|
|
4782
5154
|
}
|
|
4783
5155
|
if (!bestMatch || bestMatch.confidence < 0.7) {
|
|
4784
|
-
const baseName =
|
|
5156
|
+
const baseName = path16.basename(filePath, path16.extname(filePath));
|
|
4785
5157
|
const cleanBase = baseName.replace(/\.(test|spec)$/, "");
|
|
4786
5158
|
for (const rule of SUFFIX_RULES) {
|
|
4787
5159
|
if (cleanBase.endsWith(rule.suffix)) {
|
|
@@ -4972,6 +5344,9 @@ async function runScan(rootDir, opts) {
|
|
|
4972
5344
|
const sem = new Semaphore(opts.concurrency);
|
|
4973
5345
|
const npmCache = new NpmCache(rootDir, sem);
|
|
4974
5346
|
const fileCache = new FileCache();
|
|
5347
|
+
const excludePatterns = config.exclude ?? [];
|
|
5348
|
+
fileCache.setExcludePatterns(excludePatterns);
|
|
5349
|
+
fileCache.setMaxFileSize(config.maxFileSizeToScan ?? 5242880);
|
|
4975
5350
|
const scanners = config.scanners;
|
|
4976
5351
|
let filesScanned = 0;
|
|
4977
5352
|
const progress = new ScanProgress(rootDir);
|
|
@@ -5001,7 +5376,7 @@ async function runScan(rootDir, opts) {
|
|
|
5001
5376
|
progress.setSteps(steps);
|
|
5002
5377
|
progress.completeStep("config", "loaded");
|
|
5003
5378
|
progress.startStep("discovery");
|
|
5004
|
-
const treeCount = await quickTreeCount(rootDir);
|
|
5379
|
+
const treeCount = await quickTreeCount(rootDir, excludePatterns);
|
|
5005
5380
|
progress.updateStats({ treeSummary: treeCount });
|
|
5006
5381
|
progress.completeStep(
|
|
5007
5382
|
"discovery",
|
|
@@ -5016,8 +5391,8 @@ async function runScan(rootDir, opts) {
|
|
|
5016
5391
|
const vcsDetail = vcs.type !== "unknown" ? `${vcs.type}${vcs.branch ? ` ${vcs.branch}` : ""}${vcs.shortSha ? ` @ ${vcs.shortSha}` : ""}` : "none detected";
|
|
5017
5392
|
progress.completeStep("vcs", vcsDetail);
|
|
5018
5393
|
progress.startStep("walk", treeCount.totalFiles);
|
|
5019
|
-
await fileCache.walkDir(rootDir, (found) => {
|
|
5020
|
-
progress.updateStepProgress("walk", found, treeCount.totalFiles);
|
|
5394
|
+
await fileCache.walkDir(rootDir, (found, currentPath) => {
|
|
5395
|
+
progress.updateStepProgress("walk", found, treeCount.totalFiles, currentPath);
|
|
5021
5396
|
});
|
|
5022
5397
|
progress.completeStep("walk", `${treeCount.totalFiles.toLocaleString()} files indexed`);
|
|
5023
5398
|
progress.startStep("node");
|
|
@@ -5199,6 +5574,36 @@ async function runScan(rootDir, opts) {
|
|
|
5199
5574
|
if (noteCount > 0) findingParts.push(`${noteCount} note${noteCount !== 1 ? "s" : ""}`);
|
|
5200
5575
|
progress.completeStep("findings", findingParts.join(", ") || "none");
|
|
5201
5576
|
progress.finish();
|
|
5577
|
+
const stuckPaths = fileCache.stuckPaths;
|
|
5578
|
+
const skippedLarge = fileCache.skippedLargeFiles;
|
|
5579
|
+
if (stuckPaths.length > 0) {
|
|
5580
|
+
console.log(
|
|
5581
|
+
chalk5.yellow(`
|
|
5582
|
+
\u26A0 ${stuckPaths.length} path${stuckPaths.length === 1 ? "" : "s"} timed out (>60s) and ${stuckPaths.length === 1 ? "was" : "were"} skipped:`)
|
|
5583
|
+
);
|
|
5584
|
+
for (const d of stuckPaths) {
|
|
5585
|
+
console.log(chalk5.dim(` \u2192 ${d}`));
|
|
5586
|
+
}
|
|
5587
|
+
const newExcludes = stuckPaths.map((d) => `${d}/**`);
|
|
5588
|
+
const updated = await appendExcludePatterns(rootDir, newExcludes);
|
|
5589
|
+
if (updated) {
|
|
5590
|
+
console.log(chalk5.green("\u2714") + ` Added ${newExcludes.length} pattern${newExcludes.length !== 1 ? "s" : ""} to exclude list in config`);
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
if (skippedLarge.length > 0) {
|
|
5594
|
+
const sizeLimit = config.maxFileSizeToScan ?? 5242880;
|
|
5595
|
+
const sizeMB = (sizeLimit / 1048576).toFixed(0);
|
|
5596
|
+
console.log(
|
|
5597
|
+
chalk5.yellow(`
|
|
5598
|
+
\u26A0 ${skippedLarge.length} file${skippedLarge.length === 1 ? "" : "s"} skipped (>${sizeMB} MB):`)
|
|
5599
|
+
);
|
|
5600
|
+
for (const f of skippedLarge.slice(0, 10)) {
|
|
5601
|
+
console.log(chalk5.dim(` \u2192 ${f}`));
|
|
5602
|
+
}
|
|
5603
|
+
if (skippedLarge.length > 10) {
|
|
5604
|
+
console.log(chalk5.dim(` \u2026 and ${skippedLarge.length - 10} more`));
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5202
5607
|
fileCache.clear();
|
|
5203
5608
|
if (allProjects.length === 0) {
|
|
5204
5609
|
console.log(chalk5.yellow("No projects found."));
|
|
@@ -5216,7 +5621,7 @@ async function runScan(rootDir, opts) {
|
|
|
5216
5621
|
schemaVersion: "1.0",
|
|
5217
5622
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5218
5623
|
vibgrateVersion: VERSION,
|
|
5219
|
-
rootPath:
|
|
5624
|
+
rootPath: path17.basename(rootDir),
|
|
5220
5625
|
...vcs.type !== "unknown" ? { vcs } : {},
|
|
5221
5626
|
projects: allProjects,
|
|
5222
5627
|
drift,
|
|
@@ -5227,7 +5632,7 @@ async function runScan(rootDir, opts) {
|
|
|
5227
5632
|
treeSummary: treeCount
|
|
5228
5633
|
};
|
|
5229
5634
|
if (opts.baseline) {
|
|
5230
|
-
const baselinePath =
|
|
5635
|
+
const baselinePath = path17.resolve(opts.baseline);
|
|
5231
5636
|
if (await pathExists(baselinePath)) {
|
|
5232
5637
|
try {
|
|
5233
5638
|
const baseline = await readJsonFile(baselinePath);
|
|
@@ -5238,9 +5643,9 @@ async function runScan(rootDir, opts) {
|
|
|
5238
5643
|
}
|
|
5239
5644
|
}
|
|
5240
5645
|
}
|
|
5241
|
-
const vibgrateDir =
|
|
5646
|
+
const vibgrateDir = path17.join(rootDir, ".vibgrate");
|
|
5242
5647
|
await ensureDir(vibgrateDir);
|
|
5243
|
-
await writeJsonFile(
|
|
5648
|
+
await writeJsonFile(path17.join(vibgrateDir, "scan_result.json"), artifact);
|
|
5244
5649
|
await saveScanHistory(rootDir, {
|
|
5245
5650
|
timestamp: artifact.timestamp,
|
|
5246
5651
|
totalDurationMs: durationMs,
|
|
@@ -5250,10 +5655,10 @@ async function runScan(rootDir, opts) {
|
|
|
5250
5655
|
});
|
|
5251
5656
|
for (const project of allProjects) {
|
|
5252
5657
|
if (project.drift && project.path) {
|
|
5253
|
-
const projectDir =
|
|
5254
|
-
const projectVibgrateDir =
|
|
5658
|
+
const projectDir = path17.resolve(rootDir, project.path);
|
|
5659
|
+
const projectVibgrateDir = path17.join(projectDir, ".vibgrate");
|
|
5255
5660
|
await ensureDir(projectVibgrateDir);
|
|
5256
|
-
await writeJsonFile(
|
|
5661
|
+
await writeJsonFile(path17.join(projectVibgrateDir, "project_score.json"), {
|
|
5257
5662
|
projectId: project.projectId,
|
|
5258
5663
|
name: project.name,
|
|
5259
5664
|
type: project.type,
|
|
@@ -5270,7 +5675,7 @@ async function runScan(rootDir, opts) {
|
|
|
5270
5675
|
if (opts.format === "json") {
|
|
5271
5676
|
const jsonStr = JSON.stringify(artifact, null, 2);
|
|
5272
5677
|
if (opts.out) {
|
|
5273
|
-
await writeTextFile(
|
|
5678
|
+
await writeTextFile(path17.resolve(opts.out), jsonStr);
|
|
5274
5679
|
console.log(chalk5.green("\u2714") + ` JSON written to ${opts.out}`);
|
|
5275
5680
|
} else {
|
|
5276
5681
|
console.log(jsonStr);
|
|
@@ -5279,7 +5684,7 @@ async function runScan(rootDir, opts) {
|
|
|
5279
5684
|
const sarif = formatSarif(artifact);
|
|
5280
5685
|
const sarifStr = JSON.stringify(sarif, null, 2);
|
|
5281
5686
|
if (opts.out) {
|
|
5282
|
-
await writeTextFile(
|
|
5687
|
+
await writeTextFile(path17.resolve(opts.out), sarifStr);
|
|
5283
5688
|
console.log(chalk5.green("\u2714") + ` SARIF written to ${opts.out}`);
|
|
5284
5689
|
} else {
|
|
5285
5690
|
console.log(sarifStr);
|
|
@@ -5288,7 +5693,7 @@ async function runScan(rootDir, opts) {
|
|
|
5288
5693
|
const text = formatText(artifact);
|
|
5289
5694
|
console.log(text);
|
|
5290
5695
|
if (opts.out) {
|
|
5291
|
-
await writeTextFile(
|
|
5696
|
+
await writeTextFile(path17.resolve(opts.out), text);
|
|
5292
5697
|
}
|
|
5293
5698
|
}
|
|
5294
5699
|
return artifact;
|
|
@@ -5347,7 +5752,7 @@ async function autoPush(artifact, rootDir, opts) {
|
|
|
5347
5752
|
}
|
|
5348
5753
|
}
|
|
5349
5754
|
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) => {
|
|
5350
|
-
const rootDir =
|
|
5755
|
+
const rootDir = path17.resolve(targetPath);
|
|
5351
5756
|
if (!await pathExists(rootDir)) {
|
|
5352
5757
|
console.error(chalk5.red(`Path does not exist: ${rootDir}`));
|
|
5353
5758
|
process.exit(1);
|