@vibgrate/cli 1.0.41 → 1.0.42
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-FDWMBM2O.js +10 -0
- package/dist/{chunk-LBZR6G43.js → chunk-LO66M6OC.js} +4 -2
- package/dist/chunk-RNVZIZNL.js +569 -0
- package/dist/{chunk-NU7IIHIG.js → chunk-YFJC5JSQ.js} +1146 -650
- package/dist/cli.js +8 -6
- package/dist/fs-Q63DRR7L.js +30 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/package.json +2 -2
- package/dist/baseline-BTB34JBE.js +0 -9
|
@@ -1,555 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} finally {
|
|
18
|
-
this.release();
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
acquire() {
|
|
22
|
-
if (this.available > 0) {
|
|
23
|
-
this.available--;
|
|
24
|
-
return Promise.resolve();
|
|
25
|
-
}
|
|
26
|
-
return new Promise((resolve9) => this.queue.push(resolve9));
|
|
27
|
-
}
|
|
28
|
-
release() {
|
|
29
|
-
const next = this.queue.shift();
|
|
30
|
-
if (next) next();
|
|
31
|
-
else this.available++;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
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
|
-
|
|
115
|
-
// src/utils/fs.ts
|
|
116
|
-
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
117
|
-
"node_modules",
|
|
118
|
-
".git",
|
|
119
|
-
".vibgrate",
|
|
120
|
-
".wrangler",
|
|
121
|
-
".next",
|
|
122
|
-
"dist",
|
|
123
|
-
"build",
|
|
124
|
-
"out",
|
|
125
|
-
".turbo",
|
|
126
|
-
".cache",
|
|
127
|
-
"coverage",
|
|
128
|
-
"bin",
|
|
129
|
-
"obj",
|
|
130
|
-
".vs",
|
|
131
|
-
"packages",
|
|
132
|
-
"TestResults"
|
|
133
|
-
]);
|
|
134
|
-
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
135
|
-
// Fonts
|
|
136
|
-
".woff",
|
|
137
|
-
".woff2",
|
|
138
|
-
".ttf",
|
|
139
|
-
".otf",
|
|
140
|
-
".eot",
|
|
141
|
-
// Images & vector
|
|
142
|
-
".png",
|
|
143
|
-
".jpg",
|
|
144
|
-
".jpeg",
|
|
145
|
-
".gif",
|
|
146
|
-
".ico",
|
|
147
|
-
".bmp",
|
|
148
|
-
".tiff",
|
|
149
|
-
".tif",
|
|
150
|
-
".webp",
|
|
151
|
-
".avif",
|
|
152
|
-
".svg",
|
|
153
|
-
".heic",
|
|
154
|
-
".heif",
|
|
155
|
-
".jfif",
|
|
156
|
-
".psd",
|
|
157
|
-
".ai",
|
|
158
|
-
".eps",
|
|
159
|
-
".raw",
|
|
160
|
-
".cr2",
|
|
161
|
-
".nef",
|
|
162
|
-
".dng",
|
|
163
|
-
// Video
|
|
164
|
-
".mp4",
|
|
165
|
-
".webm",
|
|
166
|
-
".avi",
|
|
167
|
-
".mov",
|
|
168
|
-
".mkv",
|
|
169
|
-
".wmv",
|
|
170
|
-
".flv",
|
|
171
|
-
".m4v",
|
|
172
|
-
".mpg",
|
|
173
|
-
".mpeg",
|
|
174
|
-
".3gp",
|
|
175
|
-
".ogv",
|
|
176
|
-
// Audio
|
|
177
|
-
".mp3",
|
|
178
|
-
".wav",
|
|
179
|
-
".ogg",
|
|
180
|
-
".flac",
|
|
181
|
-
".aac",
|
|
182
|
-
".wma",
|
|
183
|
-
".m4a",
|
|
184
|
-
".opus",
|
|
185
|
-
".aiff",
|
|
186
|
-
".mid",
|
|
187
|
-
".midi",
|
|
188
|
-
// Archives
|
|
189
|
-
".zip",
|
|
190
|
-
".tar",
|
|
191
|
-
".gz",
|
|
192
|
-
".bz2",
|
|
193
|
-
".7z",
|
|
194
|
-
".rar",
|
|
195
|
-
// Compiled / binary
|
|
196
|
-
".exe",
|
|
197
|
-
".dll",
|
|
198
|
-
".so",
|
|
199
|
-
".dylib",
|
|
200
|
-
".o",
|
|
201
|
-
".a",
|
|
202
|
-
".class",
|
|
203
|
-
".pyc",
|
|
204
|
-
".pdb",
|
|
205
|
-
// Source maps & lockfiles (large, not useful for drift analysis)
|
|
206
|
-
".map"
|
|
207
|
-
]);
|
|
208
|
-
var TEXT_CACHE_MAX_BYTES = 1048576;
|
|
209
|
-
var FileCache = class _FileCache {
|
|
210
|
-
/** Directory walk results keyed by rootDir */
|
|
211
|
-
walkCache = /* @__PURE__ */ new Map();
|
|
212
|
-
/** File content keyed by absolute path (only files ≤ TEXT_CACHE_MAX_BYTES) */
|
|
213
|
-
textCache = /* @__PURE__ */ new Map();
|
|
214
|
-
/** Parsed JSON keyed by absolute path */
|
|
215
|
-
jsonCache = /* @__PURE__ */ new Map();
|
|
216
|
-
/** pathExists keyed by absolute path */
|
|
217
|
-
existsCache = /* @__PURE__ */ new Map();
|
|
218
|
-
/** User-configured exclude predicate (compiled from glob patterns) */
|
|
219
|
-
excludePredicate = null;
|
|
220
|
-
/** Directories that were auto-skipped because they were stuck (>60s) */
|
|
221
|
-
_stuckPaths = [];
|
|
222
|
-
/** Files skipped because they exceed maxFileSizeToScan */
|
|
223
|
-
_skippedLargeFiles = [];
|
|
224
|
-
/** Maximum file size (bytes) we will read. 0 = unlimited. */
|
|
225
|
-
_maxFileSize = 0;
|
|
226
|
-
/** Root dir for relative-path computation (set by the first walkDir call) */
|
|
227
|
-
_rootDir = null;
|
|
228
|
-
/** Set exclude patterns from config (call once before the walk) */
|
|
229
|
-
setExcludePatterns(patterns) {
|
|
230
|
-
this.excludePredicate = compileGlobs(patterns);
|
|
231
|
-
}
|
|
232
|
-
/** Set the maximum file size in bytes that readTextFile / readJsonFile will process */
|
|
233
|
-
setMaxFileSize(bytes) {
|
|
234
|
-
this._maxFileSize = bytes;
|
|
235
|
-
}
|
|
236
|
-
/** Record a path that timed out or was stuck during scanning */
|
|
237
|
-
addStuckPath(relPath) {
|
|
238
|
-
this._stuckPaths.push(relPath);
|
|
239
|
-
}
|
|
240
|
-
/** Get all paths that were auto-skipped due to being stuck (dirs + scanner files) */
|
|
241
|
-
get stuckPaths() {
|
|
242
|
-
return this._stuckPaths;
|
|
243
|
-
}
|
|
244
|
-
/** @deprecated Use stuckPaths instead */
|
|
245
|
-
get stuckDirs() {
|
|
246
|
-
return this._stuckPaths;
|
|
247
|
-
}
|
|
248
|
-
/** Get files that were skipped because they exceeded maxFileSizeToScan */
|
|
249
|
-
get skippedLargeFiles() {
|
|
250
|
-
return this._skippedLargeFiles;
|
|
251
|
-
}
|
|
252
|
-
// ── Directory walking ──
|
|
253
|
-
/**
|
|
254
|
-
* Walk the directory tree from `rootDir` once, skipping SKIP_DIRS plus
|
|
255
|
-
* common framework output dirs (.nuxt, .output, .svelte-kit).
|
|
256
|
-
*
|
|
257
|
-
* The result is memoised so every scanner filters the same array.
|
|
258
|
-
* Consumers that need additional filtering (e.g. SOURCE_EXTENSIONS,
|
|
259
|
-
* SKIP_EXTENSIONS) do so on the returned entries — no separate walk.
|
|
260
|
-
*/
|
|
261
|
-
walkDir(rootDir, onProgress) {
|
|
262
|
-
this._rootDir = rootDir;
|
|
263
|
-
const cached = this.walkCache.get(rootDir);
|
|
264
|
-
if (cached) return cached;
|
|
265
|
-
const promise = this._doWalk(rootDir, onProgress);
|
|
266
|
-
this.walkCache.set(rootDir, promise);
|
|
267
|
-
return promise;
|
|
268
|
-
}
|
|
269
|
-
/** Additional dirs skipped only by the cached walk (framework outputs) */
|
|
270
|
-
static EXTRA_SKIP = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
|
|
271
|
-
async _doWalk(rootDir, onProgress) {
|
|
272
|
-
const results = [];
|
|
273
|
-
const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
|
|
274
|
-
const maxConcurrentReads = Math.max(8, Math.min(64, cores * 4));
|
|
275
|
-
let foundCount = 0;
|
|
276
|
-
let lastReported = 0;
|
|
277
|
-
const REPORT_INTERVAL = 50;
|
|
278
|
-
const sem = new Semaphore(maxConcurrentReads);
|
|
279
|
-
const STUCK_TIMEOUT_MS = 6e4;
|
|
280
|
-
const extraSkip = _FileCache.EXTRA_SKIP;
|
|
281
|
-
const isExcluded = this.excludePredicate;
|
|
282
|
-
const stuckDirs = this._stuckPaths;
|
|
283
|
-
async function walk(dir) {
|
|
284
|
-
const relDir = path2.relative(rootDir, dir);
|
|
285
|
-
if (onProgress) {
|
|
286
|
-
onProgress(foundCount, relDir || ".");
|
|
287
|
-
}
|
|
288
|
-
let entries;
|
|
289
|
-
try {
|
|
290
|
-
entries = await sem.run(async () => {
|
|
291
|
-
const readPromise = fs.readdir(dir, { withFileTypes: true });
|
|
292
|
-
const result = await Promise.race([
|
|
293
|
-
readPromise.then((e) => ({ ok: true, entries: e })),
|
|
294
|
-
new Promise(
|
|
295
|
-
(resolve9) => setTimeout(() => resolve9({ ok: false }), STUCK_TIMEOUT_MS)
|
|
296
|
-
)
|
|
297
|
-
]);
|
|
298
|
-
if (!result.ok) {
|
|
299
|
-
stuckDirs.push(relDir || dir);
|
|
300
|
-
return null;
|
|
301
|
-
}
|
|
302
|
-
return result.entries;
|
|
303
|
-
});
|
|
304
|
-
} catch {
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (!entries) return;
|
|
308
|
-
const subWalks = [];
|
|
309
|
-
for (const e of entries) {
|
|
310
|
-
const absPath = path2.join(dir, e.name);
|
|
311
|
-
const relPath = path2.relative(rootDir, absPath);
|
|
312
|
-
if (isExcluded && isExcluded(relPath)) continue;
|
|
313
|
-
if (e.isDirectory()) {
|
|
314
|
-
if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
|
|
315
|
-
results.push({ absPath, relPath, name: e.name, isFile: false, isDirectory: true });
|
|
316
|
-
subWalks.push(walk(absPath));
|
|
317
|
-
} else if (e.isFile()) {
|
|
318
|
-
const ext = path2.extname(e.name).toLowerCase();
|
|
319
|
-
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
320
|
-
results.push({ absPath, relPath, name: e.name, isFile: true, isDirectory: false });
|
|
321
|
-
foundCount++;
|
|
322
|
-
if (onProgress && foundCount - lastReported >= REPORT_INTERVAL) {
|
|
323
|
-
lastReported = foundCount;
|
|
324
|
-
onProgress(foundCount, relPath);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
await Promise.all(subWalks);
|
|
329
|
-
}
|
|
330
|
-
await walk(rootDir);
|
|
331
|
-
if (onProgress && foundCount !== lastReported) {
|
|
332
|
-
onProgress(foundCount, "");
|
|
333
|
-
}
|
|
334
|
-
return results;
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Find files matching a predicate from the cached walk.
|
|
338
|
-
* Returns absolute paths (same contract as the standalone `findFiles`).
|
|
339
|
-
*/
|
|
340
|
-
async findFiles(rootDir, predicate) {
|
|
341
|
-
const entries = await this.walkDir(rootDir);
|
|
342
|
-
return entries.filter((e) => e.isFile && predicate(e.name)).map((e) => e.absPath);
|
|
343
|
-
}
|
|
344
|
-
async findPackageJsonFiles(rootDir) {
|
|
345
|
-
return this.findFiles(rootDir, (name) => name === "package.json");
|
|
346
|
-
}
|
|
347
|
-
async findCsprojFiles(rootDir) {
|
|
348
|
-
return this.findFiles(rootDir, (name) => name.endsWith(".csproj"));
|
|
349
|
-
}
|
|
350
|
-
async findSolutionFiles(rootDir) {
|
|
351
|
-
return this.findFiles(rootDir, (name) => name.endsWith(".sln"));
|
|
352
|
-
}
|
|
353
|
-
// ── File content reading ──
|
|
354
|
-
/**
|
|
355
|
-
* Read a text file. Files ≤ 1 MB are cached so subsequent calls from
|
|
356
|
-
* different scanners return the same string. Files > 1 MB (lockfiles,
|
|
357
|
-
* large generated files) are read directly and never retained.
|
|
358
|
-
*
|
|
359
|
-
* If maxFileSizeToScan is set and the file exceeds it, the file is
|
|
360
|
-
* recorded as skipped and an empty string is returned.
|
|
361
|
-
*/
|
|
362
|
-
readTextFile(filePath) {
|
|
363
|
-
const abs = path2.resolve(filePath);
|
|
364
|
-
const cached = this.textCache.get(abs);
|
|
365
|
-
if (cached) return cached;
|
|
366
|
-
const maxSize = this._maxFileSize;
|
|
367
|
-
const skippedLarge = this._skippedLargeFiles;
|
|
368
|
-
const rootDir = this._rootDir;
|
|
369
|
-
const promise = (async () => {
|
|
370
|
-
if (maxSize > 0) {
|
|
371
|
-
try {
|
|
372
|
-
const stat4 = await fs.stat(abs);
|
|
373
|
-
if (stat4.size > maxSize) {
|
|
374
|
-
const rel = rootDir ? path2.relative(rootDir, abs) : abs;
|
|
375
|
-
skippedLarge.push(rel);
|
|
376
|
-
this.textCache.delete(abs);
|
|
377
|
-
return "";
|
|
378
|
-
}
|
|
379
|
-
} catch {
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
const content = await fs.readFile(abs, "utf8");
|
|
383
|
-
if (content.length > TEXT_CACHE_MAX_BYTES) {
|
|
384
|
-
this.textCache.delete(abs);
|
|
385
|
-
}
|
|
386
|
-
return content;
|
|
387
|
-
})();
|
|
388
|
-
this.textCache.set(abs, promise);
|
|
389
|
-
return promise;
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Read and parse a JSON file. The parsed object is cached; the raw
|
|
393
|
-
* text is evicted immediately so we never hold both representations.
|
|
394
|
-
*/
|
|
395
|
-
readJsonFile(filePath) {
|
|
396
|
-
const abs = path2.resolve(filePath);
|
|
397
|
-
const cached = this.jsonCache.get(abs);
|
|
398
|
-
if (cached) return cached;
|
|
399
|
-
const promise = this.readTextFile(abs).then((txt) => {
|
|
400
|
-
this.textCache.delete(abs);
|
|
401
|
-
return JSON.parse(txt);
|
|
402
|
-
});
|
|
403
|
-
this.jsonCache.set(abs, promise);
|
|
404
|
-
return promise;
|
|
405
|
-
}
|
|
406
|
-
// ── Existence checks ──
|
|
407
|
-
pathExists(p) {
|
|
408
|
-
const abs = path2.resolve(p);
|
|
409
|
-
const cached = this.existsCache.get(abs);
|
|
410
|
-
if (cached) return cached;
|
|
411
|
-
const promise = fs.access(abs).then(() => true, () => false);
|
|
412
|
-
this.existsCache.set(abs, promise);
|
|
413
|
-
return promise;
|
|
414
|
-
}
|
|
415
|
-
// ── Lifecycle ──
|
|
416
|
-
/** Release all cached data. Call after the scan completes. */
|
|
417
|
-
clear() {
|
|
418
|
-
this.walkCache.clear();
|
|
419
|
-
this.textCache.clear();
|
|
420
|
-
this.jsonCache.clear();
|
|
421
|
-
this.existsCache.clear();
|
|
422
|
-
}
|
|
423
|
-
/** Number of file content entries currently held */
|
|
424
|
-
get textCacheSize() {
|
|
425
|
-
return this.textCache.size;
|
|
426
|
-
}
|
|
427
|
-
/** Number of parsed JSON entries currently held */
|
|
428
|
-
get jsonCacheSize() {
|
|
429
|
-
return this.jsonCache.size;
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
async function quickTreeCount(rootDir, excludePatterns) {
|
|
433
|
-
let totalFiles = 0;
|
|
434
|
-
let totalDirs = 0;
|
|
435
|
-
const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
|
|
436
|
-
const maxConcurrent = Math.max(8, Math.min(128, cores * 8));
|
|
437
|
-
const sem = new Semaphore(maxConcurrent);
|
|
438
|
-
const extraSkip = /* @__PURE__ */ new Set([".nuxt", ".output", ".svelte-kit"]);
|
|
439
|
-
const isExcluded = excludePatterns ? compileGlobs(excludePatterns) : null;
|
|
440
|
-
async function count(dir) {
|
|
441
|
-
let entries;
|
|
442
|
-
try {
|
|
443
|
-
entries = await sem.run(() => fs.readdir(dir, { withFileTypes: true }));
|
|
444
|
-
} catch {
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
const subs = [];
|
|
448
|
-
for (const e of entries) {
|
|
449
|
-
const relPath = path2.relative(rootDir, path2.join(dir, e.name));
|
|
450
|
-
if (isExcluded && isExcluded(relPath)) continue;
|
|
451
|
-
if (e.isDirectory()) {
|
|
452
|
-
if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
|
|
453
|
-
totalDirs++;
|
|
454
|
-
subs.push(count(path2.join(dir, e.name)));
|
|
455
|
-
} else if (e.isFile()) {
|
|
456
|
-
const ext = path2.extname(e.name).toLowerCase();
|
|
457
|
-
if (!SKIP_EXTENSIONS.has(ext)) totalFiles++;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
await Promise.all(subs);
|
|
461
|
-
}
|
|
462
|
-
await count(rootDir);
|
|
463
|
-
return { totalFiles, totalDirs };
|
|
464
|
-
}
|
|
465
|
-
async function countFilesInDir(dir, recursive = true) {
|
|
466
|
-
let count = 0;
|
|
467
|
-
const extraSkip = /* @__PURE__ */ new Set(["obj", "bin", "Debug", "Release", "TestResults"]);
|
|
468
|
-
async function walk(currentDir) {
|
|
469
|
-
let entries;
|
|
470
|
-
try {
|
|
471
|
-
entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
472
|
-
} catch {
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
const subs = [];
|
|
476
|
-
for (const e of entries) {
|
|
477
|
-
if (e.isDirectory()) {
|
|
478
|
-
if (!recursive) continue;
|
|
479
|
-
if (SKIP_DIRS.has(e.name) || extraSkip.has(e.name)) continue;
|
|
480
|
-
subs.push(walk(path2.join(currentDir, e.name)));
|
|
481
|
-
} else if (e.isFile()) {
|
|
482
|
-
const ext = path2.extname(e.name).toLowerCase();
|
|
483
|
-
if (!SKIP_EXTENSIONS.has(ext)) count++;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
await Promise.all(subs);
|
|
487
|
-
}
|
|
488
|
-
await walk(dir);
|
|
489
|
-
return count;
|
|
490
|
-
}
|
|
491
|
-
async function findFiles(rootDir, predicate) {
|
|
492
|
-
const results = [];
|
|
493
|
-
const cores = typeof os.availableParallelism === "function" ? os.availableParallelism() : os.cpus().length || 4;
|
|
494
|
-
const maxConcurrentReads = Math.max(8, Math.min(64, cores * 4));
|
|
495
|
-
const readDirSemaphore = new Semaphore(maxConcurrentReads);
|
|
496
|
-
async function walk(dir) {
|
|
497
|
-
let entries;
|
|
498
|
-
try {
|
|
499
|
-
entries = await readDirSemaphore.run(() => fs.readdir(dir, { withFileTypes: true }));
|
|
500
|
-
} catch {
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
const subDirectoryWalks = [];
|
|
504
|
-
for (const e of entries) {
|
|
505
|
-
if (e.isDirectory()) {
|
|
506
|
-
if (SKIP_DIRS.has(e.name)) continue;
|
|
507
|
-
subDirectoryWalks.push(walk(path2.join(dir, e.name)));
|
|
508
|
-
} else if (e.isFile() && predicate(e.name)) {
|
|
509
|
-
const ext = path2.extname(e.name).toLowerCase();
|
|
510
|
-
if (!SKIP_EXTENSIONS.has(ext)) results.push(path2.join(dir, e.name));
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
await Promise.all(subDirectoryWalks);
|
|
514
|
-
}
|
|
515
|
-
await walk(rootDir);
|
|
516
|
-
return results;
|
|
517
|
-
}
|
|
518
|
-
async function findPackageJsonFiles(rootDir) {
|
|
519
|
-
return findFiles(rootDir, (name) => name === "package.json");
|
|
520
|
-
}
|
|
521
|
-
async function findSolutionFiles(rootDir) {
|
|
522
|
-
return findFiles(rootDir, (name) => name.endsWith(".sln"));
|
|
523
|
-
}
|
|
524
|
-
async function findCsprojFiles(rootDir) {
|
|
525
|
-
return findFiles(rootDir, (name) => name.endsWith(".csproj"));
|
|
526
|
-
}
|
|
527
|
-
async function readJsonFile(filePath) {
|
|
528
|
-
const txt = await fs.readFile(filePath, "utf8");
|
|
529
|
-
return JSON.parse(txt);
|
|
530
|
-
}
|
|
531
|
-
async function readTextFile(filePath) {
|
|
532
|
-
return fs.readFile(filePath, "utf8");
|
|
533
|
-
}
|
|
534
|
-
async function pathExists(p) {
|
|
535
|
-
try {
|
|
536
|
-
await fs.access(p);
|
|
537
|
-
return true;
|
|
538
|
-
} catch {
|
|
539
|
-
return false;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
async function ensureDir(dir) {
|
|
543
|
-
await fs.mkdir(dir, { recursive: true });
|
|
544
|
-
}
|
|
545
|
-
async function writeJsonFile(filePath, data) {
|
|
546
|
-
await ensureDir(path2.dirname(filePath));
|
|
547
|
-
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
548
|
-
}
|
|
549
|
-
async function writeTextFile(filePath, content) {
|
|
550
|
-
await ensureDir(path2.dirname(filePath));
|
|
551
|
-
await fs.writeFile(filePath, content, "utf8");
|
|
552
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
FileCache,
|
|
3
|
+
Semaphore,
|
|
4
|
+
countFilesInDir,
|
|
5
|
+
ensureDir,
|
|
6
|
+
findCsprojFiles,
|
|
7
|
+
findFiles,
|
|
8
|
+
findPackageJsonFiles,
|
|
9
|
+
findSolutionFiles,
|
|
10
|
+
pathExists,
|
|
11
|
+
quickTreeCount,
|
|
12
|
+
readJsonFile,
|
|
13
|
+
readTextFile,
|
|
14
|
+
writeJsonFile,
|
|
15
|
+
writeTextFile
|
|
16
|
+
} from "./chunk-RNVZIZNL.js";
|
|
553
17
|
|
|
554
18
|
// src/scoring/drift-score.ts
|
|
555
19
|
import * as crypto from "crypto";
|
|
@@ -622,17 +86,27 @@ function eolScore(projects) {
|
|
|
622
86
|
else if (p.runtimeMajorsBehind >= 2) score = Math.min(score, 20);
|
|
623
87
|
else if (p.runtimeMajorsBehind >= 1) score = Math.min(score, 60);
|
|
624
88
|
}
|
|
89
|
+
if (p.type === "python" && p.runtimeMajorsBehind !== void 0) {
|
|
90
|
+
if (p.runtimeMajorsBehind >= 6) score = Math.min(score, 0);
|
|
91
|
+
else if (p.runtimeMajorsBehind >= 4) score = Math.min(score, 20);
|
|
92
|
+
else if (p.runtimeMajorsBehind >= 2) score = Math.min(score, 60);
|
|
93
|
+
}
|
|
94
|
+
if (p.type === "java" && p.runtimeMajorsBehind !== void 0) {
|
|
95
|
+
if (p.runtimeMajorsBehind >= 10) score = Math.min(score, 0);
|
|
96
|
+
else if (p.runtimeMajorsBehind >= 4) score = Math.min(score, 30);
|
|
97
|
+
else if (p.runtimeMajorsBehind >= 1) score = Math.min(score, 70);
|
|
98
|
+
}
|
|
625
99
|
}
|
|
626
100
|
return score;
|
|
627
101
|
}
|
|
628
102
|
function computeDriftScore(projects) {
|
|
629
103
|
const rs = runtimeScore(projects);
|
|
630
|
-
const
|
|
104
|
+
const fs6 = frameworkScore(projects);
|
|
631
105
|
const ds = dependencyScore(projects);
|
|
632
106
|
const es = eolScore(projects);
|
|
633
107
|
const components = [
|
|
634
108
|
{ score: rs, weight: 0.25 },
|
|
635
|
-
{ score:
|
|
109
|
+
{ score: fs6, weight: 0.25 },
|
|
636
110
|
{ score: ds, weight: 0.3 },
|
|
637
111
|
{ score: es, weight: 0.2 }
|
|
638
112
|
];
|
|
@@ -643,7 +117,7 @@ function computeDriftScore(projects) {
|
|
|
643
117
|
riskLevel: "low",
|
|
644
118
|
components: {
|
|
645
119
|
runtimeScore: Math.round(rs ?? 100),
|
|
646
|
-
frameworkScore: Math.round(
|
|
120
|
+
frameworkScore: Math.round(fs6 ?? 100),
|
|
647
121
|
dependencyScore: Math.round(ds ?? 100),
|
|
648
122
|
eolScore: Math.round(es ?? 100)
|
|
649
123
|
}
|
|
@@ -661,7 +135,7 @@ function computeDriftScore(projects) {
|
|
|
661
135
|
else riskLevel = "high";
|
|
662
136
|
const measured = [];
|
|
663
137
|
if (rs !== null) measured.push("runtime");
|
|
664
|
-
if (
|
|
138
|
+
if (fs6 !== null) measured.push("framework");
|
|
665
139
|
if (ds !== null) measured.push("dependency");
|
|
666
140
|
if (es !== null) measured.push("eol");
|
|
667
141
|
return {
|
|
@@ -669,7 +143,7 @@ function computeDriftScore(projects) {
|
|
|
669
143
|
riskLevel,
|
|
670
144
|
components: {
|
|
671
145
|
runtimeScore: Math.round(rs ?? 100),
|
|
672
|
-
frameworkScore: Math.round(
|
|
146
|
+
frameworkScore: Math.round(fs6 ?? 100),
|
|
673
147
|
dependencyScore: Math.round(ds ?? 100),
|
|
674
148
|
eolScore: Math.round(es ?? 100)
|
|
675
149
|
},
|
|
@@ -684,17 +158,19 @@ function generateFindings(projects, config) {
|
|
|
684
158
|
const findings = [];
|
|
685
159
|
for (const project of projects) {
|
|
686
160
|
if (project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind >= 3) {
|
|
161
|
+
const runtimeLabel = project.type === "node" ? "Node.js" : project.type === "dotnet" ? ".NET" : project.type === "python" ? "Python" : project.type === "java" ? "Java" : project.type;
|
|
687
162
|
findings.push({
|
|
688
163
|
ruleId: "vibgrate/runtime-eol",
|
|
689
164
|
level: "error",
|
|
690
|
-
message: `${
|
|
165
|
+
message: `${runtimeLabel} runtime "${project.runtime}" is ${project.runtimeMajorsBehind} major versions behind (latest: ${project.runtimeLatest}). Likely at or past EOL.`,
|
|
691
166
|
location: project.path
|
|
692
167
|
});
|
|
693
168
|
} else if (project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind >= 2) {
|
|
169
|
+
const runtimeLabel = project.type === "node" ? "Node.js" : project.type === "dotnet" ? ".NET" : project.type === "python" ? "Python" : project.type === "java" ? "Java" : project.type;
|
|
694
170
|
findings.push({
|
|
695
171
|
ruleId: "vibgrate/runtime-lag",
|
|
696
172
|
level: "warning",
|
|
697
|
-
message: `${
|
|
173
|
+
message: `${runtimeLabel} runtime "${project.runtime}" is ${project.runtimeMajorsBehind} major versions behind (latest: ${project.runtimeLatest}).`,
|
|
698
174
|
location: project.path
|
|
699
175
|
});
|
|
700
176
|
}
|
|
@@ -1544,7 +1020,7 @@ function toSarifResult(finding) {
|
|
|
1544
1020
|
|
|
1545
1021
|
// src/commands/dsn.ts
|
|
1546
1022
|
import * as crypto2 from "crypto";
|
|
1547
|
-
import * as
|
|
1023
|
+
import * as path from "path";
|
|
1548
1024
|
import { Command } from "commander";
|
|
1549
1025
|
import chalk2 from "chalk";
|
|
1550
1026
|
var REGION_HOSTS = {
|
|
@@ -1589,7 +1065,7 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
|
|
|
1589
1065
|
console.log(chalk2.dim("Set this as VIBGRATE_DSN in your CI environment."));
|
|
1590
1066
|
console.log(chalk2.dim("The secret must be registered on your Vibgrate ingest API."));
|
|
1591
1067
|
if (opts.write) {
|
|
1592
|
-
const writePath =
|
|
1068
|
+
const writePath = path.resolve(opts.write);
|
|
1593
1069
|
await writeTextFile(writePath, dsn + "\n");
|
|
1594
1070
|
console.log("");
|
|
1595
1071
|
console.log(chalk2.green("\u2714") + ` DSN written to ${opts.write}`);
|
|
@@ -1599,7 +1075,7 @@ dsnCommand.command("create").description("Create a new DSN token").option("--ing
|
|
|
1599
1075
|
|
|
1600
1076
|
// src/commands/push.ts
|
|
1601
1077
|
import * as crypto3 from "crypto";
|
|
1602
|
-
import * as
|
|
1078
|
+
import * as path2 from "path";
|
|
1603
1079
|
import { Command as Command2 } from "commander";
|
|
1604
1080
|
import chalk3 from "chalk";
|
|
1605
1081
|
function parseDsn(dsn) {
|
|
@@ -1629,7 +1105,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
|
|
|
1629
1105
|
if (opts.strict) process.exit(1);
|
|
1630
1106
|
return;
|
|
1631
1107
|
}
|
|
1632
|
-
const filePath =
|
|
1108
|
+
const filePath = path2.resolve(opts.file);
|
|
1633
1109
|
if (!await pathExists(filePath)) {
|
|
1634
1110
|
console.error(chalk3.red(`Scan artifact not found: ${filePath}`));
|
|
1635
1111
|
console.error(chalk3.dim('Run "vibgrate scan" first.'));
|
|
@@ -1687,14 +1163,14 @@ import { Command as Command3 } from "commander";
|
|
|
1687
1163
|
import chalk6 from "chalk";
|
|
1688
1164
|
|
|
1689
1165
|
// src/scanners/node-scanner.ts
|
|
1690
|
-
import * as
|
|
1166
|
+
import * as path3 from "path";
|
|
1691
1167
|
import * as semver2 from "semver";
|
|
1692
1168
|
|
|
1693
1169
|
// src/utils/timeout.ts
|
|
1694
1170
|
async function withTimeout(promise, ms) {
|
|
1695
1171
|
let timer;
|
|
1696
|
-
const timeout = new Promise((
|
|
1697
|
-
timer = setTimeout(() =>
|
|
1172
|
+
const timeout = new Promise((resolve8) => {
|
|
1173
|
+
timer = setTimeout(() => resolve8({ ok: false }), ms);
|
|
1698
1174
|
});
|
|
1699
1175
|
try {
|
|
1700
1176
|
const result = await Promise.race([
|
|
@@ -1719,7 +1195,7 @@ function maxStable(versions) {
|
|
|
1719
1195
|
return stable.sort(semver.rcompare)[0] ?? null;
|
|
1720
1196
|
}
|
|
1721
1197
|
async function npmViewJson(args, cwd) {
|
|
1722
|
-
return new Promise((
|
|
1198
|
+
return new Promise((resolve8, reject) => {
|
|
1723
1199
|
const child = spawn("npm", ["view", ...args, "--json"], {
|
|
1724
1200
|
cwd,
|
|
1725
1201
|
shell: true,
|
|
@@ -1737,13 +1213,13 @@ async function npmViewJson(args, cwd) {
|
|
|
1737
1213
|
}
|
|
1738
1214
|
const trimmed = out.trim();
|
|
1739
1215
|
if (!trimmed) {
|
|
1740
|
-
|
|
1216
|
+
resolve8(null);
|
|
1741
1217
|
return;
|
|
1742
1218
|
}
|
|
1743
1219
|
try {
|
|
1744
|
-
|
|
1220
|
+
resolve8(JSON.parse(trimmed));
|
|
1745
1221
|
} catch {
|
|
1746
|
-
|
|
1222
|
+
resolve8(trimmed.replace(/^"|"$/g, ""));
|
|
1747
1223
|
}
|
|
1748
1224
|
});
|
|
1749
1225
|
});
|
|
@@ -1911,7 +1387,7 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
|
1911
1387
|
results.push(result.value);
|
|
1912
1388
|
packageNameToPath.set(result.value.name, result.value.path);
|
|
1913
1389
|
} else {
|
|
1914
|
-
const relPath =
|
|
1390
|
+
const relPath = path3.relative(rootDir, path3.dirname(pjPath));
|
|
1915
1391
|
if (cache) {
|
|
1916
1392
|
cache.addStuckPath(relPath || ".");
|
|
1917
1393
|
}
|
|
@@ -1942,8 +1418,8 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
|
|
|
1942
1418
|
}
|
|
1943
1419
|
async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
1944
1420
|
const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
|
|
1945
|
-
const absProjectPath =
|
|
1946
|
-
const projectPath =
|
|
1421
|
+
const absProjectPath = path3.dirname(packageJsonPath);
|
|
1422
|
+
const projectPath = path3.relative(rootDir, absProjectPath) || ".";
|
|
1947
1423
|
const nodeEngine = pj.engines?.node ?? void 0;
|
|
1948
1424
|
let runtimeLatest;
|
|
1949
1425
|
let runtimeMajorsBehind;
|
|
@@ -2030,7 +1506,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
2030
1506
|
return {
|
|
2031
1507
|
type: "node",
|
|
2032
1508
|
path: projectPath,
|
|
2033
|
-
name: pj.name ??
|
|
1509
|
+
name: pj.name ?? path3.basename(absProjectPath),
|
|
2034
1510
|
runtime: nodeEngine,
|
|
2035
1511
|
runtimeLatest,
|
|
2036
1512
|
runtimeMajorsBehind,
|
|
@@ -2042,7 +1518,8 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
|
|
|
2042
1518
|
}
|
|
2043
1519
|
|
|
2044
1520
|
// src/scanners/dotnet-scanner.ts
|
|
2045
|
-
import * as
|
|
1521
|
+
import * as path4 from "path";
|
|
1522
|
+
import * as semver3 from "semver";
|
|
2046
1523
|
import { XMLParser } from "fast-xml-parser";
|
|
2047
1524
|
var parser = new XMLParser({
|
|
2048
1525
|
ignoreAttributes: false,
|
|
@@ -2243,7 +1720,7 @@ function parseCsproj(xml, filePath) {
|
|
|
2243
1720
|
const parsed = parser.parse(xml);
|
|
2244
1721
|
const project = parsed?.Project;
|
|
2245
1722
|
if (!project) {
|
|
2246
|
-
return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName:
|
|
1723
|
+
return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName: path4.basename(filePath, ".csproj") };
|
|
2247
1724
|
}
|
|
2248
1725
|
const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
|
|
2249
1726
|
const targetFrameworks = [];
|
|
@@ -2280,22 +1757,22 @@ function parseCsproj(xml, filePath) {
|
|
|
2280
1757
|
targetFrameworks: [...new Set(targetFrameworks)],
|
|
2281
1758
|
packageReferences,
|
|
2282
1759
|
projectReferences,
|
|
2283
|
-
projectName:
|
|
1760
|
+
projectName: path4.basename(filePath, ".csproj")
|
|
2284
1761
|
};
|
|
2285
1762
|
}
|
|
2286
|
-
async function scanDotnetProjects(rootDir, cache) {
|
|
1763
|
+
async function scanDotnetProjects(rootDir, nugetCache, cache) {
|
|
2287
1764
|
const csprojFiles = cache ? await cache.findCsprojFiles(rootDir) : await findCsprojFiles(rootDir);
|
|
2288
1765
|
const slnFiles = cache ? await cache.findSolutionFiles(rootDir) : await findSolutionFiles(rootDir);
|
|
2289
1766
|
const slnCsprojPaths = /* @__PURE__ */ new Set();
|
|
2290
1767
|
for (const slnPath of slnFiles) {
|
|
2291
1768
|
try {
|
|
2292
1769
|
const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
|
|
2293
|
-
const slnDir =
|
|
1770
|
+
const slnDir = path4.dirname(slnPath);
|
|
2294
1771
|
const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
|
|
2295
1772
|
let match;
|
|
2296
1773
|
while ((match = projectRegex.exec(slnContent)) !== null) {
|
|
2297
1774
|
if (match[1]) {
|
|
2298
|
-
const csprojPath =
|
|
1775
|
+
const csprojPath = path4.resolve(slnDir, match[1].replace(/\\/g, "/"));
|
|
2299
1776
|
slnCsprojPaths.add(csprojPath);
|
|
2300
1777
|
}
|
|
2301
1778
|
}
|
|
@@ -2307,12 +1784,12 @@ async function scanDotnetProjects(rootDir, cache) {
|
|
|
2307
1784
|
const STUCK_TIMEOUT_MS = 6e4;
|
|
2308
1785
|
for (const csprojPath of allCsprojFiles) {
|
|
2309
1786
|
try {
|
|
2310
|
-
const scanPromise = scanOneCsproj(csprojPath, rootDir, cache);
|
|
1787
|
+
const scanPromise = scanOneCsproj(csprojPath, rootDir, nugetCache, cache);
|
|
2311
1788
|
const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
|
|
2312
1789
|
if (result.ok) {
|
|
2313
1790
|
results.push(result.value);
|
|
2314
1791
|
} else {
|
|
2315
|
-
const relPath =
|
|
1792
|
+
const relPath = path4.relative(rootDir, path4.dirname(csprojPath));
|
|
2316
1793
|
if (cache) {
|
|
2317
1794
|
cache.addStuckPath(relPath || ".");
|
|
2318
1795
|
}
|
|
@@ -2325,44 +1802,90 @@ async function scanDotnetProjects(rootDir, cache) {
|
|
|
2325
1802
|
}
|
|
2326
1803
|
return results;
|
|
2327
1804
|
}
|
|
2328
|
-
async function scanOneCsproj(csprojPath, rootDir, cache) {
|
|
1805
|
+
async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
|
|
2329
1806
|
const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
|
|
2330
1807
|
const data = parseCsproj(xml, csprojPath);
|
|
2331
|
-
const csprojDir =
|
|
1808
|
+
const csprojDir = path4.dirname(csprojPath);
|
|
2332
1809
|
const primaryTfm = data.targetFrameworks[0];
|
|
2333
1810
|
let runtimeMajorsBehind;
|
|
2334
1811
|
let targetFramework = primaryTfm;
|
|
2335
1812
|
if (primaryTfm) {
|
|
2336
|
-
const
|
|
2337
|
-
if (
|
|
2338
|
-
runtimeMajorsBehind = Math.max(0, LATEST_DOTNET_MAJOR -
|
|
2339
|
-
}
|
|
2340
|
-
}
|
|
2341
|
-
const dependencies =
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
1813
|
+
const major5 = parseTfmMajor(primaryTfm);
|
|
1814
|
+
if (major5 !== null) {
|
|
1815
|
+
runtimeMajorsBehind = Math.max(0, LATEST_DOTNET_MAJOR - major5);
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
const dependencies = [];
|
|
1819
|
+
const bucketsMut = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: 0 };
|
|
1820
|
+
if (nugetCache) {
|
|
1821
|
+
const metaPromises = data.packageReferences.map(async (ref) => {
|
|
1822
|
+
const meta = await nugetCache.get(ref.name);
|
|
1823
|
+
return { ref, meta };
|
|
1824
|
+
});
|
|
1825
|
+
const resolved = await Promise.all(metaPromises);
|
|
1826
|
+
for (const { ref, meta } of resolved) {
|
|
1827
|
+
const resolvedVersion = semver3.valid(ref.version) ? ref.version : null;
|
|
1828
|
+
const latestStable = meta.latestStableOverall;
|
|
1829
|
+
let majorsBehind = null;
|
|
1830
|
+
let drift = "unknown";
|
|
1831
|
+
if (resolvedVersion && latestStable) {
|
|
1832
|
+
const currentMajor = semver3.major(resolvedVersion);
|
|
1833
|
+
const latestMajor = semver3.major(latestStable);
|
|
1834
|
+
majorsBehind = latestMajor - currentMajor;
|
|
1835
|
+
if (majorsBehind === 0) {
|
|
1836
|
+
drift = semver3.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
|
|
1837
|
+
} else if (majorsBehind > 0) {
|
|
1838
|
+
drift = "major-behind";
|
|
1839
|
+
} else {
|
|
1840
|
+
drift = "current";
|
|
1841
|
+
}
|
|
1842
|
+
if (majorsBehind <= 0) bucketsMut.current++;
|
|
1843
|
+
else if (majorsBehind === 1) bucketsMut.oneBehind++;
|
|
1844
|
+
else bucketsMut.twoPlusBehind++;
|
|
1845
|
+
} else {
|
|
1846
|
+
bucketsMut.unknown++;
|
|
1847
|
+
}
|
|
1848
|
+
dependencies.push({
|
|
1849
|
+
package: ref.name,
|
|
1850
|
+
section: "dependencies",
|
|
1851
|
+
currentSpec: ref.version,
|
|
1852
|
+
resolvedVersion,
|
|
1853
|
+
latestStable,
|
|
1854
|
+
majorsBehind,
|
|
1855
|
+
drift
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
} else {
|
|
1859
|
+
for (const ref of data.packageReferences) {
|
|
1860
|
+
dependencies.push({
|
|
1861
|
+
package: ref.name,
|
|
1862
|
+
section: "dependencies",
|
|
1863
|
+
currentSpec: ref.version,
|
|
1864
|
+
resolvedVersion: ref.version,
|
|
1865
|
+
latestStable: null,
|
|
1866
|
+
majorsBehind: null,
|
|
1867
|
+
drift: "unknown"
|
|
1868
|
+
});
|
|
1869
|
+
bucketsMut.unknown++;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
2351
1872
|
const frameworks = [];
|
|
1873
|
+
const depLookup = new Map(dependencies.map((d) => [d.package, d]));
|
|
2352
1874
|
for (const ref of data.packageReferences) {
|
|
2353
1875
|
if (ref.name in KNOWN_DOTNET_FRAMEWORKS) {
|
|
1876
|
+
const resolved = depLookup.get(ref.name);
|
|
2354
1877
|
frameworks.push({
|
|
2355
1878
|
name: KNOWN_DOTNET_FRAMEWORKS[ref.name],
|
|
2356
|
-
currentVersion: ref.version,
|
|
2357
|
-
latestVersion: null,
|
|
2358
|
-
majorsBehind: null
|
|
1879
|
+
currentVersion: resolved?.resolvedVersion ?? ref.version,
|
|
1880
|
+
latestVersion: resolved?.latestStable ?? null,
|
|
1881
|
+
majorsBehind: resolved?.majorsBehind ?? null
|
|
2359
1882
|
});
|
|
2360
1883
|
}
|
|
2361
1884
|
}
|
|
2362
1885
|
const projectReferences = data.projectReferences.map((refPath) => {
|
|
2363
|
-
const absRefPath =
|
|
2364
|
-
const relRefPath =
|
|
2365
|
-
const refName =
|
|
1886
|
+
const absRefPath = path4.resolve(csprojDir, refPath);
|
|
1887
|
+
const relRefPath = path4.relative(rootDir, path4.dirname(absRefPath));
|
|
1888
|
+
const refName = path4.basename(absRefPath, ".csproj");
|
|
2366
1889
|
return {
|
|
2367
1890
|
path: relRefPath || ".",
|
|
2368
1891
|
name: refName,
|
|
@@ -2374,10 +1897,16 @@ async function scanOneCsproj(csprojPath, rootDir, cache) {
|
|
|
2374
1897
|
fileCount = await countFilesInDir(csprojDir);
|
|
2375
1898
|
} catch {
|
|
2376
1899
|
}
|
|
2377
|
-
|
|
1900
|
+
dependencies.sort((a, b) => {
|
|
1901
|
+
const order = { "major-behind": 0, "minor-behind": 1, "current": 2, "unknown": 3 };
|
|
1902
|
+
const diff = (order[a.drift] ?? 9) - (order[b.drift] ?? 9);
|
|
1903
|
+
if (diff !== 0) return diff;
|
|
1904
|
+
return a.package.localeCompare(b.package);
|
|
1905
|
+
});
|
|
1906
|
+
const buckets = bucketsMut;
|
|
2378
1907
|
return {
|
|
2379
1908
|
type: "dotnet",
|
|
2380
|
-
path:
|
|
1909
|
+
path: path4.relative(rootDir, csprojDir) || ".",
|
|
2381
1910
|
name: data.projectName,
|
|
2382
1911
|
targetFramework,
|
|
2383
1912
|
runtime: primaryTfm,
|
|
@@ -2391,9 +1920,957 @@ async function scanOneCsproj(csprojPath, rootDir, cache) {
|
|
|
2391
1920
|
};
|
|
2392
1921
|
}
|
|
2393
1922
|
|
|
1923
|
+
// src/scanners/python-scanner.ts
|
|
1924
|
+
import * as path5 from "path";
|
|
1925
|
+
import * as semver4 from "semver";
|
|
1926
|
+
var KNOWN_PYTHON_FRAMEWORKS = {
|
|
1927
|
+
// ── Web Frameworks ──
|
|
1928
|
+
"django": "Django",
|
|
1929
|
+
"flask": "Flask",
|
|
1930
|
+
"fastapi": "FastAPI",
|
|
1931
|
+
"starlette": "Starlette",
|
|
1932
|
+
"tornado": "Tornado",
|
|
1933
|
+
"bottle": "Bottle",
|
|
1934
|
+
"sanic": "Sanic",
|
|
1935
|
+
"falcon": "Falcon",
|
|
1936
|
+
"aiohttp": "aiohttp",
|
|
1937
|
+
"quart": "Quart",
|
|
1938
|
+
"litestar": "Litestar",
|
|
1939
|
+
"robyn": "Robyn",
|
|
1940
|
+
// ── ORM & Database ──
|
|
1941
|
+
"sqlalchemy": "SQLAlchemy",
|
|
1942
|
+
"django-rest-framework": "DRF",
|
|
1943
|
+
"djangorestframework": "DRF",
|
|
1944
|
+
"peewee": "Peewee",
|
|
1945
|
+
"tortoise-orm": "Tortoise ORM",
|
|
1946
|
+
"sqlmodel": "SQLModel",
|
|
1947
|
+
"pony": "Pony ORM",
|
|
1948
|
+
"alembic": "Alembic",
|
|
1949
|
+
"psycopg2": "psycopg2",
|
|
1950
|
+
"psycopg2-binary": "psycopg2",
|
|
1951
|
+
"psycopg": "psycopg3",
|
|
1952
|
+
"asyncpg": "asyncpg",
|
|
1953
|
+
"pymongo": "PyMongo",
|
|
1954
|
+
"motor": "Motor",
|
|
1955
|
+
"redis": "redis-py",
|
|
1956
|
+
"celery": "Celery",
|
|
1957
|
+
"boto3": "AWS SDK (boto3)",
|
|
1958
|
+
"botocore": "AWS SDK (botocore)",
|
|
1959
|
+
// ── Data Science & ML ──
|
|
1960
|
+
"numpy": "NumPy",
|
|
1961
|
+
"pandas": "pandas",
|
|
1962
|
+
"scipy": "SciPy",
|
|
1963
|
+
"scikit-learn": "scikit-learn",
|
|
1964
|
+
"tensorflow": "TensorFlow",
|
|
1965
|
+
"torch": "PyTorch",
|
|
1966
|
+
"keras": "Keras",
|
|
1967
|
+
"matplotlib": "Matplotlib",
|
|
1968
|
+
"seaborn": "Seaborn",
|
|
1969
|
+
"plotly": "Plotly",
|
|
1970
|
+
"polars": "Polars",
|
|
1971
|
+
"dask": "Dask",
|
|
1972
|
+
"xgboost": "XGBoost",
|
|
1973
|
+
"lightgbm": "LightGBM",
|
|
1974
|
+
"transformers": "Transformers (HF)",
|
|
1975
|
+
"langchain": "LangChain",
|
|
1976
|
+
"openai": "OpenAI SDK",
|
|
1977
|
+
// ── Testing ──
|
|
1978
|
+
"pytest": "pytest",
|
|
1979
|
+
"unittest2": "unittest2",
|
|
1980
|
+
"nose2": "nose2",
|
|
1981
|
+
"tox": "tox",
|
|
1982
|
+
"hypothesis": "Hypothesis",
|
|
1983
|
+
"factory-boy": "factory_boy",
|
|
1984
|
+
"faker": "Faker",
|
|
1985
|
+
"coverage": "Coverage.py",
|
|
1986
|
+
"responses": "responses",
|
|
1987
|
+
"httpx": "HTTPX",
|
|
1988
|
+
// ── Async & Tasks ──
|
|
1989
|
+
"uvicorn": "Uvicorn",
|
|
1990
|
+
"gunicorn": "Gunicorn",
|
|
1991
|
+
"hypercorn": "Hypercorn",
|
|
1992
|
+
"dramatiq": "Dramatiq",
|
|
1993
|
+
"rq": "RQ",
|
|
1994
|
+
"huey": "Huey",
|
|
1995
|
+
// ── Auth & Security ──
|
|
1996
|
+
"pyjwt": "PyJWT",
|
|
1997
|
+
"authlib": "Authlib",
|
|
1998
|
+
"python-jose": "python-jose",
|
|
1999
|
+
"passlib": "Passlib",
|
|
2000
|
+
"cryptography": "cryptography",
|
|
2001
|
+
// ── Serialization & Validation ──
|
|
2002
|
+
"pydantic": "Pydantic",
|
|
2003
|
+
"marshmallow": "Marshmallow",
|
|
2004
|
+
"attrs": "attrs",
|
|
2005
|
+
"cerberus": "Cerberus",
|
|
2006
|
+
"msgpack": "msgpack",
|
|
2007
|
+
"protobuf": "protobuf",
|
|
2008
|
+
// ── HTTP Clients ──
|
|
2009
|
+
"requests": "Requests",
|
|
2010
|
+
"urllib3": "urllib3",
|
|
2011
|
+
// ── DevOps & Infrastructure ──
|
|
2012
|
+
"ansible": "Ansible",
|
|
2013
|
+
"fabric": "Fabric",
|
|
2014
|
+
"invoke": "Invoke",
|
|
2015
|
+
"paramiko": "Paramiko",
|
|
2016
|
+
// ── Linting & Formatting ──
|
|
2017
|
+
"black": "Black",
|
|
2018
|
+
"ruff": "Ruff",
|
|
2019
|
+
"flake8": "Flake8",
|
|
2020
|
+
"mypy": "mypy",
|
|
2021
|
+
"pylint": "Pylint",
|
|
2022
|
+
"isort": "isort",
|
|
2023
|
+
"bandit": "Bandit",
|
|
2024
|
+
// ── Logging & Observability ──
|
|
2025
|
+
"structlog": "structlog",
|
|
2026
|
+
"loguru": "Loguru",
|
|
2027
|
+
"sentry-sdk": "Sentry SDK",
|
|
2028
|
+
"opentelemetry-api": "OpenTelemetry",
|
|
2029
|
+
"prometheus-client": "prometheus-client"
|
|
2030
|
+
};
|
|
2031
|
+
var LATEST_PYTHON_MINOR = { major: 3, minor: 13 };
|
|
2032
|
+
function parseRequirementLine(line) {
|
|
2033
|
+
const trimmed = line.trim();
|
|
2034
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) return null;
|
|
2035
|
+
const withoutMarkers = trimmed.split(";")[0].trim();
|
|
2036
|
+
const match = withoutMarkers.match(/^([A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]?)(?:\[.*?\])?\s*(.*)?$/);
|
|
2037
|
+
if (!match) return null;
|
|
2038
|
+
const rawName = match[1];
|
|
2039
|
+
const spec = (match[2] ?? "").trim();
|
|
2040
|
+
const normalisedName = rawName.toLowerCase().replace(/[_.]+/g, "-");
|
|
2041
|
+
return { name: rawName, spec, normalisedName };
|
|
2042
|
+
}
|
|
2043
|
+
function extractPinnedVersion(spec) {
|
|
2044
|
+
const match = spec.match(/^==\s*([^\s,;]+)/);
|
|
2045
|
+
return match?.[1] ?? null;
|
|
2046
|
+
}
|
|
2047
|
+
function pep440ToSemver(ver) {
|
|
2048
|
+
let v = ver.replace(/^[vV]/, "").trim();
|
|
2049
|
+
if (/(?:a\d|b\d|rc\d|alpha|beta|dev|post)/i.test(v)) return null;
|
|
2050
|
+
const parts = v.split(".");
|
|
2051
|
+
while (parts.length < 3) parts.push("0");
|
|
2052
|
+
v = parts.slice(0, 3).join(".");
|
|
2053
|
+
return semver4.valid(v);
|
|
2054
|
+
}
|
|
2055
|
+
function parseRequirementsTxt(content) {
|
|
2056
|
+
const deps = [];
|
|
2057
|
+
for (const line of content.split("\n")) {
|
|
2058
|
+
const dep = parseRequirementLine(line);
|
|
2059
|
+
if (dep) deps.push(dep);
|
|
2060
|
+
}
|
|
2061
|
+
return deps;
|
|
2062
|
+
}
|
|
2063
|
+
function parsePyprojectToml(content) {
|
|
2064
|
+
const result = { dependencies: [] };
|
|
2065
|
+
const nameMatch = content.match(/^\s*name\s*=\s*"([^"]+)"/m);
|
|
2066
|
+
if (nameMatch) result.projectName = nameMatch[1];
|
|
2067
|
+
const pyVerMatch = content.match(/^\s*requires-python\s*=\s*"([^"]+)"/m);
|
|
2068
|
+
if (pyVerMatch) result.pythonVersion = pyVerMatch[1];
|
|
2069
|
+
const depsBlockMatch = content.match(/^\s*dependencies\s*=\s*\[([\s\S]*?)\]/m);
|
|
2070
|
+
if (depsBlockMatch) {
|
|
2071
|
+
const block = depsBlockMatch[1];
|
|
2072
|
+
const lineRegex = /"([^"]+)"/g;
|
|
2073
|
+
let m;
|
|
2074
|
+
while ((m = lineRegex.exec(block)) !== null) {
|
|
2075
|
+
const dep = parseRequirementLine(m[1]);
|
|
2076
|
+
if (dep) result.dependencies.push(dep);
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
const poetrySection = content.match(/\[tool\.poetry\.dependencies\]([\s\S]*?)(?=\n\s*\[|\n*$)/);
|
|
2080
|
+
if (poetrySection) {
|
|
2081
|
+
const lines = poetrySection[1].split("\n");
|
|
2082
|
+
for (const line of lines) {
|
|
2083
|
+
const kv = line.match(/^\s*([A-Za-z0-9][-A-Za-z0-9_.]*)\s*=\s*(?:"([^"]+)"|{.*?version\s*=\s*"([^"]+)".*?})/);
|
|
2084
|
+
if (kv) {
|
|
2085
|
+
const name = kv[1];
|
|
2086
|
+
if (name.toLowerCase() === "python") {
|
|
2087
|
+
result.pythonVersion = kv[2] ?? kv[3] ?? void 0;
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
const ver = kv[2] ?? kv[3] ?? "";
|
|
2091
|
+
const normalisedName = name.toLowerCase().replace(/[_.]+/g, "-");
|
|
2092
|
+
result.dependencies.push({ name, spec: ver ? `==${ver}` : "", normalisedName });
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
return result;
|
|
2097
|
+
}
|
|
2098
|
+
function parsePipfile(content) {
|
|
2099
|
+
const deps = [];
|
|
2100
|
+
const packagesMatch = content.match(/\[packages\]([\s\S]*?)(?=\n\s*\[|\n*$)/);
|
|
2101
|
+
if (packagesMatch) {
|
|
2102
|
+
const lines = packagesMatch[1].split("\n");
|
|
2103
|
+
for (const line of lines) {
|
|
2104
|
+
const kv = line.match(/^\s*([A-Za-z0-9][-A-Za-z0-9_.]*)\s*=\s*(?:"([^"]+)"|{.*?version\s*=\s*"([^"]+)".*?}|\*|"[*]")/);
|
|
2105
|
+
if (kv) {
|
|
2106
|
+
const name = kv[1];
|
|
2107
|
+
const ver = kv[2] ?? kv[3] ?? "";
|
|
2108
|
+
const normalisedName = name.toLowerCase().replace(/[_.]+/g, "-");
|
|
2109
|
+
deps.push({ name, spec: ver || "*", normalisedName });
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
const devMatch = content.match(/\[dev-packages\]([\s\S]*?)(?=\n\s*\[|\n*$)/);
|
|
2114
|
+
if (devMatch) {
|
|
2115
|
+
const lines = devMatch[1].split("\n");
|
|
2116
|
+
for (const line of lines) {
|
|
2117
|
+
const kv = line.match(/^\s*([A-Za-z0-9][-A-Za-z0-9_.]*)\s*=\s*(?:"([^"]+)"|{.*?version\s*=\s*"([^"]+)".*?}|\*|"[*]")/);
|
|
2118
|
+
if (kv) {
|
|
2119
|
+
const name = kv[1];
|
|
2120
|
+
const ver = kv[2] ?? kv[3] ?? "";
|
|
2121
|
+
const normalisedName = name.toLowerCase().replace(/[_.]+/g, "-");
|
|
2122
|
+
deps.push({ name, spec: ver || "*", normalisedName });
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
return deps;
|
|
2127
|
+
}
|
|
2128
|
+
function parseSetupCfg(content) {
|
|
2129
|
+
const result = { deps: [] };
|
|
2130
|
+
const metadataSection = content.match(/\[metadata\]([\s\S]*?)(?=\n\s*\[|\n*$)/);
|
|
2131
|
+
if (metadataSection) {
|
|
2132
|
+
const nameMatch = metadataSection[1].match(/^\s*name\s*=\s*(.+)$/m);
|
|
2133
|
+
if (nameMatch) result.name = nameMatch[1].trim();
|
|
2134
|
+
}
|
|
2135
|
+
const optionsSection = content.match(/\[options\]([\s\S]*?)(?=\n\s*\[|\n*$)/);
|
|
2136
|
+
if (optionsSection) {
|
|
2137
|
+
const pyReqMatch = optionsSection[1].match(/^\s*python_requires\s*=\s*(.+)$/m);
|
|
2138
|
+
if (pyReqMatch) result.pythonVersion = pyReqMatch[1].trim();
|
|
2139
|
+
const installReqMatch = optionsSection[1].match(/install_requires\s*=\s*\n((?:\s+.*\n?)*)/);
|
|
2140
|
+
if (installReqMatch) {
|
|
2141
|
+
const block = installReqMatch[1];
|
|
2142
|
+
for (const line of block.split("\n")) {
|
|
2143
|
+
const dep = parseRequirementLine(line);
|
|
2144
|
+
if (dep) result.deps.push(dep);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
return result;
|
|
2149
|
+
}
|
|
2150
|
+
var PYTHON_MANIFEST_FILES = /* @__PURE__ */ new Set([
|
|
2151
|
+
"requirements.txt",
|
|
2152
|
+
"requirements-dev.txt",
|
|
2153
|
+
"requirements_dev.txt",
|
|
2154
|
+
"requirements-test.txt",
|
|
2155
|
+
"dev-requirements.txt",
|
|
2156
|
+
"pyproject.toml",
|
|
2157
|
+
"setup.py",
|
|
2158
|
+
"setup.cfg",
|
|
2159
|
+
"Pipfile"
|
|
2160
|
+
]);
|
|
2161
|
+
async function scanPythonProjects(rootDir, pypiCache, cache) {
|
|
2162
|
+
const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name)) : await findPythonManifests(rootDir);
|
|
2163
|
+
const projectDirs = /* @__PURE__ */ new Map();
|
|
2164
|
+
for (const f of manifestFiles) {
|
|
2165
|
+
const dir = path5.dirname(f);
|
|
2166
|
+
if (!projectDirs.has(dir)) projectDirs.set(dir, []);
|
|
2167
|
+
projectDirs.get(dir).push(f);
|
|
2168
|
+
}
|
|
2169
|
+
const results = [];
|
|
2170
|
+
const STUCK_TIMEOUT_MS = 6e4;
|
|
2171
|
+
for (const [dir, files] of projectDirs) {
|
|
2172
|
+
try {
|
|
2173
|
+
const scanPromise = scanOnePythonProject(dir, files, rootDir, pypiCache, cache);
|
|
2174
|
+
const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
|
|
2175
|
+
if (result.ok) {
|
|
2176
|
+
results.push(result.value);
|
|
2177
|
+
} else {
|
|
2178
|
+
const relPath = path5.relative(rootDir, dir);
|
|
2179
|
+
if (cache) cache.addStuckPath(relPath || ".");
|
|
2180
|
+
console.error(`Timeout scanning Python project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
2181
|
+
}
|
|
2182
|
+
} catch (e) {
|
|
2183
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2184
|
+
console.error(`Error scanning Python project ${dir}: ${msg}`);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return results;
|
|
2188
|
+
}
|
|
2189
|
+
async function findPythonManifests(rootDir) {
|
|
2190
|
+
const { findFiles: findFiles2 } = await import("./fs-Q63DRR7L.js");
|
|
2191
|
+
return findFiles2(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name));
|
|
2192
|
+
}
|
|
2193
|
+
async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cache) {
|
|
2194
|
+
const relDir = path5.relative(rootDir, dir) || ".";
|
|
2195
|
+
let projectName = path5.basename(dir === rootDir ? rootDir : dir);
|
|
2196
|
+
let pythonVersion;
|
|
2197
|
+
const allDeps = /* @__PURE__ */ new Map();
|
|
2198
|
+
for (const f of manifestFiles) {
|
|
2199
|
+
const fileName = path5.basename(f);
|
|
2200
|
+
const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
|
|
2201
|
+
if (fileName === "pyproject.toml") {
|
|
2202
|
+
const parsed = parsePyprojectToml(content);
|
|
2203
|
+
if (parsed.projectName) projectName = parsed.projectName;
|
|
2204
|
+
if (parsed.pythonVersion) pythonVersion = parsed.pythonVersion;
|
|
2205
|
+
for (const dep of parsed.dependencies) {
|
|
2206
|
+
if (!allDeps.has(dep.normalisedName)) allDeps.set(dep.normalisedName, dep);
|
|
2207
|
+
}
|
|
2208
|
+
} else if (fileName === "setup.cfg") {
|
|
2209
|
+
const parsed = parseSetupCfg(content);
|
|
2210
|
+
if (parsed.name) projectName = parsed.name;
|
|
2211
|
+
if (parsed.pythonVersion && !pythonVersion) pythonVersion = parsed.pythonVersion;
|
|
2212
|
+
for (const dep of parsed.deps) {
|
|
2213
|
+
if (!allDeps.has(dep.normalisedName)) allDeps.set(dep.normalisedName, dep);
|
|
2214
|
+
}
|
|
2215
|
+
} else if (fileName === "Pipfile") {
|
|
2216
|
+
for (const dep of parsePipfile(content)) {
|
|
2217
|
+
if (!allDeps.has(dep.normalisedName)) allDeps.set(dep.normalisedName, dep);
|
|
2218
|
+
}
|
|
2219
|
+
} else if (fileName.startsWith("requirements") && fileName.endsWith(".txt")) {
|
|
2220
|
+
for (const dep of parseRequirementsTxt(content)) {
|
|
2221
|
+
if (!allDeps.has(dep.normalisedName)) allDeps.set(dep.normalisedName, dep);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
let runtimeMajorsBehind;
|
|
2226
|
+
let runtimeLatest;
|
|
2227
|
+
if (pythonVersion) {
|
|
2228
|
+
const verMatch = pythonVersion.match(/(\d+)\.(\d+)/);
|
|
2229
|
+
if (verMatch) {
|
|
2230
|
+
const reqMajor = parseInt(verMatch[1], 10);
|
|
2231
|
+
const reqMinor = parseInt(verMatch[2], 10);
|
|
2232
|
+
if (reqMajor === LATEST_PYTHON_MINOR.major) {
|
|
2233
|
+
runtimeMajorsBehind = Math.max(0, LATEST_PYTHON_MINOR.minor - reqMinor);
|
|
2234
|
+
} else if (reqMajor < LATEST_PYTHON_MINOR.major) {
|
|
2235
|
+
runtimeMajorsBehind = LATEST_PYTHON_MINOR.minor + (LATEST_PYTHON_MINOR.major - reqMajor) * 10;
|
|
2236
|
+
}
|
|
2237
|
+
runtimeLatest = `${LATEST_PYTHON_MINOR.major}.${LATEST_PYTHON_MINOR.minor}`;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const dependencies = [];
|
|
2241
|
+
const frameworks = [];
|
|
2242
|
+
const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: 0 };
|
|
2243
|
+
const depEntries = [...allDeps.values()];
|
|
2244
|
+
const metaPromises = depEntries.map(async (dep) => {
|
|
2245
|
+
const meta = await pypiCache.get(dep.normalisedName);
|
|
2246
|
+
return { dep, meta };
|
|
2247
|
+
});
|
|
2248
|
+
const resolved = await Promise.all(metaPromises);
|
|
2249
|
+
for (const { dep, meta } of resolved) {
|
|
2250
|
+
const pinnedVersion = extractPinnedVersion(dep.spec);
|
|
2251
|
+
const resolvedVersion = pinnedVersion ? pep440ToSemver(pinnedVersion) : null;
|
|
2252
|
+
const latestStable = meta.latestStableOverall;
|
|
2253
|
+
let majorsBehind = null;
|
|
2254
|
+
let drift = "unknown";
|
|
2255
|
+
if (resolvedVersion && latestStable) {
|
|
2256
|
+
const currentMajor = semver4.major(resolvedVersion);
|
|
2257
|
+
const latestMajor = semver4.major(latestStable);
|
|
2258
|
+
majorsBehind = latestMajor - currentMajor;
|
|
2259
|
+
if (majorsBehind === 0) {
|
|
2260
|
+
drift = semver4.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
|
|
2261
|
+
} else if (majorsBehind > 0) {
|
|
2262
|
+
drift = "major-behind";
|
|
2263
|
+
} else {
|
|
2264
|
+
drift = "current";
|
|
2265
|
+
}
|
|
2266
|
+
if (majorsBehind <= 0) buckets.current++;
|
|
2267
|
+
else if (majorsBehind === 1) buckets.oneBehind++;
|
|
2268
|
+
else buckets.twoPlusBehind++;
|
|
2269
|
+
} else {
|
|
2270
|
+
buckets.unknown++;
|
|
2271
|
+
}
|
|
2272
|
+
dependencies.push({
|
|
2273
|
+
package: dep.name,
|
|
2274
|
+
section: "dependencies",
|
|
2275
|
+
currentSpec: dep.spec || "*",
|
|
2276
|
+
resolvedVersion,
|
|
2277
|
+
latestStable,
|
|
2278
|
+
majorsBehind,
|
|
2279
|
+
drift
|
|
2280
|
+
});
|
|
2281
|
+
if (dep.normalisedName in KNOWN_PYTHON_FRAMEWORKS) {
|
|
2282
|
+
frameworks.push({
|
|
2283
|
+
name: KNOWN_PYTHON_FRAMEWORKS[dep.normalisedName],
|
|
2284
|
+
currentVersion: resolvedVersion,
|
|
2285
|
+
latestVersion: latestStable,
|
|
2286
|
+
majorsBehind
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
dependencies.sort((a, b) => {
|
|
2291
|
+
const order = { "major-behind": 0, "minor-behind": 1, "current": 2, "unknown": 3 };
|
|
2292
|
+
const diff = (order[a.drift] ?? 9) - (order[b.drift] ?? 9);
|
|
2293
|
+
if (diff !== 0) return diff;
|
|
2294
|
+
return a.package.localeCompare(b.package);
|
|
2295
|
+
});
|
|
2296
|
+
let fileCount;
|
|
2297
|
+
try {
|
|
2298
|
+
fileCount = await countFilesInDir(dir);
|
|
2299
|
+
} catch {
|
|
2300
|
+
}
|
|
2301
|
+
return {
|
|
2302
|
+
type: "python",
|
|
2303
|
+
path: relDir,
|
|
2304
|
+
name: projectName,
|
|
2305
|
+
runtime: pythonVersion,
|
|
2306
|
+
runtimeLatest,
|
|
2307
|
+
runtimeMajorsBehind,
|
|
2308
|
+
frameworks,
|
|
2309
|
+
dependencies,
|
|
2310
|
+
dependencyAgeBuckets: buckets,
|
|
2311
|
+
fileCount
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// src/scanners/java-scanner.ts
|
|
2316
|
+
import * as path6 from "path";
|
|
2317
|
+
import * as semver5 from "semver";
|
|
2318
|
+
import { XMLParser as XMLParser2 } from "fast-xml-parser";
|
|
2319
|
+
var parser2 = new XMLParser2({
|
|
2320
|
+
ignoreAttributes: false,
|
|
2321
|
+
attributeNamePrefix: "@_"
|
|
2322
|
+
});
|
|
2323
|
+
var KNOWN_JAVA_FRAMEWORKS = {
|
|
2324
|
+
// ── Spring ──
|
|
2325
|
+
"org.springframework.boot:spring-boot-starter-web": "Spring Boot Web",
|
|
2326
|
+
"org.springframework.boot:spring-boot-starter": "Spring Boot",
|
|
2327
|
+
"org.springframework.boot:spring-boot-starter-data-jpa": "Spring Data JPA",
|
|
2328
|
+
"org.springframework.boot:spring-boot-starter-security": "Spring Security",
|
|
2329
|
+
"org.springframework.boot:spring-boot-starter-webflux": "Spring WebFlux",
|
|
2330
|
+
"org.springframework.boot:spring-boot-starter-actuator": "Spring Actuator",
|
|
2331
|
+
"org.springframework.boot:spring-boot-starter-test": "Spring Boot Test",
|
|
2332
|
+
"org.springframework:spring-core": "Spring Framework",
|
|
2333
|
+
"org.springframework:spring-web": "Spring Web",
|
|
2334
|
+
"org.springframework:spring-webmvc": "Spring MVC",
|
|
2335
|
+
"org.springframework.cloud:spring-cloud-starter-netflix-eureka-client": "Spring Cloud Eureka",
|
|
2336
|
+
"org.springframework.cloud:spring-cloud-starter-gateway": "Spring Cloud Gateway",
|
|
2337
|
+
"org.springframework.kafka:spring-kafka": "Spring Kafka",
|
|
2338
|
+
// ── Jakarta / Java EE ──
|
|
2339
|
+
"jakarta.platform:jakarta.jakartaee-api": "Jakarta EE",
|
|
2340
|
+
"jakarta.servlet:jakarta.servlet-api": "Jakarta Servlet",
|
|
2341
|
+
"javax.servlet:javax.servlet-api": "Java Servlet (Legacy)",
|
|
2342
|
+
// ── Micronaut ──
|
|
2343
|
+
"io.micronaut:micronaut-core": "Micronaut",
|
|
2344
|
+
"io.micronaut:micronaut-http-server-netty": "Micronaut HTTP",
|
|
2345
|
+
// ── Quarkus ──
|
|
2346
|
+
"io.quarkus:quarkus-core": "Quarkus",
|
|
2347
|
+
"io.quarkus:quarkus-resteasy": "Quarkus RESTEasy",
|
|
2348
|
+
"io.quarkus:quarkus-resteasy-reactive": "Quarkus RESTEasy Reactive",
|
|
2349
|
+
// ── Vert.x ──
|
|
2350
|
+
"io.vertx:vertx-core": "Vert.x",
|
|
2351
|
+
"io.vertx:vertx-web": "Vert.x Web",
|
|
2352
|
+
// ── ORM & Database ──
|
|
2353
|
+
"org.hibernate.orm:hibernate-core": "Hibernate",
|
|
2354
|
+
"org.hibernate:hibernate-core": "Hibernate",
|
|
2355
|
+
"org.mybatis:mybatis": "MyBatis",
|
|
2356
|
+
"org.mybatis.spring.boot:mybatis-spring-boot-starter": "MyBatis Spring Boot",
|
|
2357
|
+
"org.jooq:jooq": "jOOQ",
|
|
2358
|
+
"org.flywaydb:flyway-core": "Flyway",
|
|
2359
|
+
"org.liquibase:liquibase-core": "Liquibase",
|
|
2360
|
+
"com.zaxxer:HikariCP": "HikariCP",
|
|
2361
|
+
"org.postgresql:postgresql": "PostgreSQL JDBC",
|
|
2362
|
+
"com.mysql:mysql-connector-j": "MySQL Connector",
|
|
2363
|
+
"mysql:mysql-connector-java": "MySQL Connector (Legacy)",
|
|
2364
|
+
"com.oracle.database.jdbc:ojdbc11": "Oracle JDBC",
|
|
2365
|
+
"org.mongodb:mongodb-driver-sync": "MongoDB Driver",
|
|
2366
|
+
"io.lettuce:lettuce-core": "Lettuce (Redis)",
|
|
2367
|
+
"redis.clients:jedis": "Jedis (Redis)",
|
|
2368
|
+
// ── Messaging ──
|
|
2369
|
+
"org.apache.kafka:kafka-clients": "Apache Kafka",
|
|
2370
|
+
"com.rabbitmq:amqp-client": "RabbitMQ Client",
|
|
2371
|
+
"software.amazon.awssdk:sqs": "AWS SQS",
|
|
2372
|
+
"software.amazon.awssdk:sns": "AWS SNS",
|
|
2373
|
+
// ── HTTP & API ──
|
|
2374
|
+
"com.squareup.okhttp3:okhttp": "OkHttp",
|
|
2375
|
+
"com.squareup.retrofit2:retrofit": "Retrofit",
|
|
2376
|
+
"org.apache.httpcomponents.client5:httpclient5": "Apache HttpClient 5",
|
|
2377
|
+
"io.grpc:grpc-netty": "gRPC",
|
|
2378
|
+
"com.graphql-java:graphql-java": "GraphQL Java",
|
|
2379
|
+
"com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter": "Netflix DGS",
|
|
2380
|
+
// ── JSON ──
|
|
2381
|
+
"com.fasterxml.jackson.core:jackson-databind": "Jackson",
|
|
2382
|
+
"com.google.code.gson:gson": "Gson",
|
|
2383
|
+
// ── Testing ──
|
|
2384
|
+
"junit:junit": "JUnit 4",
|
|
2385
|
+
"org.junit.jupiter:junit-jupiter": "JUnit 5",
|
|
2386
|
+
"org.junit.jupiter:junit-jupiter-api": "JUnit 5",
|
|
2387
|
+
"org.mockito:mockito-core": "Mockito",
|
|
2388
|
+
"org.assertj:assertj-core": "AssertJ",
|
|
2389
|
+
"org.testcontainers:testcontainers": "Testcontainers",
|
|
2390
|
+
"io.rest-assured:rest-assured": "REST Assured",
|
|
2391
|
+
"org.awaitility:awaitility": "Awaitility",
|
|
2392
|
+
"com.tngtech.archunit:archunit-junit5": "ArchUnit",
|
|
2393
|
+
"org.hamcrest:hamcrest": "Hamcrest",
|
|
2394
|
+
// ── Logging ──
|
|
2395
|
+
"org.slf4j:slf4j-api": "SLF4J",
|
|
2396
|
+
"ch.qos.logback:logback-classic": "Logback",
|
|
2397
|
+
"org.apache.logging.log4j:log4j-core": "Log4j2",
|
|
2398
|
+
// ── Build Plugins (tracked as frameworks) ──
|
|
2399
|
+
"org.projectlombok:lombok": "Lombok",
|
|
2400
|
+
"org.mapstruct:mapstruct": "MapStruct",
|
|
2401
|
+
// ── Cloud SDKs ──
|
|
2402
|
+
"software.amazon.awssdk:bom": "AWS SDK v2",
|
|
2403
|
+
"com.google.cloud:google-cloud-bom": "Google Cloud SDK",
|
|
2404
|
+
"com.azure:azure-sdk-bom": "Azure SDK",
|
|
2405
|
+
// ── Security ──
|
|
2406
|
+
"io.jsonwebtoken:jjwt-api": "JJWT",
|
|
2407
|
+
"com.nimbusds:nimbus-jose-jwt": "Nimbus JOSE+JWT",
|
|
2408
|
+
// ── Observability ──
|
|
2409
|
+
"io.micrometer:micrometer-core": "Micrometer",
|
|
2410
|
+
"io.opentelemetry:opentelemetry-api": "OpenTelemetry",
|
|
2411
|
+
"io.prometheus:simpleclient": "Prometheus Client",
|
|
2412
|
+
// ── Reactive ──
|
|
2413
|
+
"io.projectreactor:reactor-core": "Project Reactor",
|
|
2414
|
+
"io.reactivex.rxjava3:rxjava": "RxJava 3"
|
|
2415
|
+
};
|
|
2416
|
+
var LATEST_JAVA_LTS = 21;
|
|
2417
|
+
function parsePom(xml, filePath) {
|
|
2418
|
+
const parsed = parser2.parse(xml);
|
|
2419
|
+
const project = parsed?.project;
|
|
2420
|
+
if (!project) {
|
|
2421
|
+
return {
|
|
2422
|
+
artifactId: path6.basename(path6.dirname(filePath)),
|
|
2423
|
+
dependencies: [],
|
|
2424
|
+
modules: [],
|
|
2425
|
+
properties: {}
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
const properties = {};
|
|
2429
|
+
if (project.properties && typeof project.properties === "object") {
|
|
2430
|
+
for (const [key, val] of Object.entries(project.properties)) {
|
|
2431
|
+
if (typeof val === "string" || typeof val === "number") {
|
|
2432
|
+
properties[key] = String(val);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
let javaVersion;
|
|
2437
|
+
const javaProps = [
|
|
2438
|
+
"java.version",
|
|
2439
|
+
"maven.compiler.source",
|
|
2440
|
+
"maven.compiler.target",
|
|
2441
|
+
"maven.compiler.release",
|
|
2442
|
+
"java.source.version"
|
|
2443
|
+
];
|
|
2444
|
+
for (const prop of javaProps) {
|
|
2445
|
+
if (properties[prop]) {
|
|
2446
|
+
javaVersion = String(properties[prop]);
|
|
2447
|
+
break;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
const depContainer = project.dependencies;
|
|
2451
|
+
const rawDeps = depContainer?.dependency ? Array.isArray(depContainer.dependency) ? depContainer.dependency : [depContainer.dependency] : [];
|
|
2452
|
+
const dependencies = rawDeps.filter((d) => d.groupId && d.artifactId).map((d) => ({
|
|
2453
|
+
groupId: resolveProperty(String(d.groupId ?? ""), properties),
|
|
2454
|
+
artifactId: resolveProperty(String(d.artifactId ?? ""), properties),
|
|
2455
|
+
version: resolveProperty(String(d.version ?? ""), properties),
|
|
2456
|
+
scope: d.scope ? String(d.scope) : void 0
|
|
2457
|
+
}));
|
|
2458
|
+
const mgmt = project.dependencyManagement?.dependencies;
|
|
2459
|
+
const mgmtDeps = mgmt?.dependency ? Array.isArray(mgmt.dependency) ? mgmt.dependency : [mgmt.dependency] : [];
|
|
2460
|
+
for (const d of mgmtDeps) {
|
|
2461
|
+
if (d.groupId && d.artifactId && d.version) {
|
|
2462
|
+
dependencies.push({
|
|
2463
|
+
groupId: resolveProperty(String(d.groupId), properties),
|
|
2464
|
+
artifactId: resolveProperty(String(d.artifactId), properties),
|
|
2465
|
+
version: resolveProperty(String(d.version), properties),
|
|
2466
|
+
scope: d.scope ? String(d.scope) : void 0
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
const rawModules = project.modules?.module ? Array.isArray(project.modules.module) ? project.modules.module : [project.modules.module] : [];
|
|
2471
|
+
const modules = rawModules.map(String);
|
|
2472
|
+
let parent;
|
|
2473
|
+
if (project.parent?.groupId && project.parent?.artifactId) {
|
|
2474
|
+
parent = {
|
|
2475
|
+
groupId: String(project.parent.groupId),
|
|
2476
|
+
artifactId: String(project.parent.artifactId),
|
|
2477
|
+
version: String(project.parent.version ?? "")
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
return {
|
|
2481
|
+
groupId: project.groupId ? String(project.groupId) : parent?.groupId,
|
|
2482
|
+
artifactId: String(project.artifactId ?? path6.basename(path6.dirname(filePath))),
|
|
2483
|
+
version: project.version ? String(project.version) : parent?.version,
|
|
2484
|
+
packaging: project.packaging ? String(project.packaging) : void 0,
|
|
2485
|
+
javaVersion,
|
|
2486
|
+
dependencies,
|
|
2487
|
+
modules,
|
|
2488
|
+
parent,
|
|
2489
|
+
properties
|
|
2490
|
+
};
|
|
2491
|
+
}
|
|
2492
|
+
function resolveProperty(value, properties) {
|
|
2493
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, key) => properties[key] ?? `\${${key}}`);
|
|
2494
|
+
}
|
|
2495
|
+
function parseGradleBuild(content, filePath) {
|
|
2496
|
+
const deps = [];
|
|
2497
|
+
const projectName = path6.basename(path6.dirname(filePath));
|
|
2498
|
+
let javaVersion;
|
|
2499
|
+
const compatMatch = content.match(/(?:sourceCompatibility|targetCompatibility|javaVersion)\s*[=:]\s*['"]?(?:JavaVersion\.VERSION_)?(\d+)['"]?/);
|
|
2500
|
+
if (compatMatch) javaVersion = compatMatch[1];
|
|
2501
|
+
const toolchainMatch = content.match(/JavaLanguageVersion\.of\((\d+)\)/);
|
|
2502
|
+
if (toolchainMatch) javaVersion = toolchainMatch[1];
|
|
2503
|
+
const depRegex = /(?:implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly|annotationProcessor|kapt)\s*(?:\(?\s*)?['"]([^'"]+)['"]/g;
|
|
2504
|
+
let match;
|
|
2505
|
+
while ((match = depRegex.exec(content)) !== null) {
|
|
2506
|
+
const parts = match[1].split(":");
|
|
2507
|
+
if (parts.length >= 2) {
|
|
2508
|
+
deps.push({
|
|
2509
|
+
groupId: parts[0],
|
|
2510
|
+
artifactId: parts[1],
|
|
2511
|
+
version: parts[2] ?? "",
|
|
2512
|
+
configuration: match[0].split(/\s/)[0]
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
const kotlinDepRegex = /(?:implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly|annotationProcessor|kapt)\s*\(\s*"([^"]+)"\s*\)/g;
|
|
2517
|
+
while ((match = kotlinDepRegex.exec(content)) !== null) {
|
|
2518
|
+
const parts = match[1].split(":");
|
|
2519
|
+
if (parts.length >= 2) {
|
|
2520
|
+
const key = `${parts[0]}:${parts[1]}`;
|
|
2521
|
+
if (!deps.some((d) => `${d.groupId}:${d.artifactId}` === key)) {
|
|
2522
|
+
deps.push({
|
|
2523
|
+
groupId: parts[0],
|
|
2524
|
+
artifactId: parts[1],
|
|
2525
|
+
version: parts[2] ?? "",
|
|
2526
|
+
configuration: match[0].split(/\s/)[0]
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
const platformRegex = /(?:implementation|api)\s*(?:\(?\s*)?platform\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
2532
|
+
while ((match = platformRegex.exec(content)) !== null) {
|
|
2533
|
+
const parts = match[1].split(":");
|
|
2534
|
+
if (parts.length >= 2) {
|
|
2535
|
+
deps.push({
|
|
2536
|
+
groupId: parts[0],
|
|
2537
|
+
artifactId: parts[1],
|
|
2538
|
+
version: parts[2] ?? "",
|
|
2539
|
+
configuration: "platform"
|
|
2540
|
+
});
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
return { javaVersion, dependencies: deps, projectName };
|
|
2544
|
+
}
|
|
2545
|
+
function mavenToSemver(ver) {
|
|
2546
|
+
let v = ver.trim();
|
|
2547
|
+
if (!v || v.includes("$")) return null;
|
|
2548
|
+
if (/(?:-SNAPSHOT|-alpha|-beta|-rc|-M\d|-CR\d)/i.test(v)) return null;
|
|
2549
|
+
v = v.replace(/\.(?:RELEASE|Final|GA)$/i, "");
|
|
2550
|
+
const parts = v.split(".");
|
|
2551
|
+
while (parts.length < 3) parts.push("0");
|
|
2552
|
+
v = parts.slice(0, 3).join(".");
|
|
2553
|
+
return semver5.valid(v);
|
|
2554
|
+
}
|
|
2555
|
+
var JAVA_MANIFEST_FILES = /* @__PURE__ */ new Set(["pom.xml", "build.gradle", "build.gradle.kts"]);
|
|
2556
|
+
async function scanJavaProjects(rootDir, mavenCache, cache) {
|
|
2557
|
+
const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => JAVA_MANIFEST_FILES.has(name)) : await findJavaManifests(rootDir);
|
|
2558
|
+
const projectDirs = /* @__PURE__ */ new Map();
|
|
2559
|
+
for (const f of manifestFiles) {
|
|
2560
|
+
const dir = path6.dirname(f);
|
|
2561
|
+
if (!projectDirs.has(dir)) projectDirs.set(dir, []);
|
|
2562
|
+
projectDirs.get(dir).push(f);
|
|
2563
|
+
}
|
|
2564
|
+
const results = [];
|
|
2565
|
+
const STUCK_TIMEOUT_MS = 6e4;
|
|
2566
|
+
for (const [dir, files] of projectDirs) {
|
|
2567
|
+
try {
|
|
2568
|
+
const scanPromise = scanOneJavaProject(dir, files, rootDir, mavenCache, cache);
|
|
2569
|
+
const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
|
|
2570
|
+
if (result.ok) {
|
|
2571
|
+
results.push(result.value);
|
|
2572
|
+
} else {
|
|
2573
|
+
const relPath = path6.relative(rootDir, dir);
|
|
2574
|
+
if (cache) cache.addStuckPath(relPath || ".");
|
|
2575
|
+
console.error(`Timeout scanning Java project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
|
|
2576
|
+
}
|
|
2577
|
+
} catch (e) {
|
|
2578
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2579
|
+
console.error(`Error scanning Java project ${dir}: ${msg}`);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
const projectPathMap = /* @__PURE__ */ new Map();
|
|
2583
|
+
for (const p of results) {
|
|
2584
|
+
projectPathMap.set(p.name, p.path);
|
|
2585
|
+
}
|
|
2586
|
+
return results;
|
|
2587
|
+
}
|
|
2588
|
+
async function findJavaManifests(rootDir) {
|
|
2589
|
+
const { findFiles: findFiles2 } = await import("./fs-Q63DRR7L.js");
|
|
2590
|
+
return findFiles2(rootDir, (name) => JAVA_MANIFEST_FILES.has(name));
|
|
2591
|
+
}
|
|
2592
|
+
async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache) {
|
|
2593
|
+
const relDir = path6.relative(rootDir, dir) || ".";
|
|
2594
|
+
let projectName = path6.basename(dir === rootDir ? rootDir : dir);
|
|
2595
|
+
let javaVersion;
|
|
2596
|
+
const allDeps = /* @__PURE__ */ new Map();
|
|
2597
|
+
const projectReferences = [];
|
|
2598
|
+
for (const f of manifestFiles) {
|
|
2599
|
+
const fileName = path6.basename(f);
|
|
2600
|
+
const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
|
|
2601
|
+
if (fileName === "pom.xml") {
|
|
2602
|
+
const pom = parsePom(content, f);
|
|
2603
|
+
if (pom.artifactId) projectName = pom.artifactId;
|
|
2604
|
+
if (pom.javaVersion) javaVersion = pom.javaVersion;
|
|
2605
|
+
for (const dep of pom.dependencies) {
|
|
2606
|
+
const key = `${dep.groupId}:${dep.artifactId}`;
|
|
2607
|
+
if (!allDeps.has(key) && dep.version && !dep.version.includes("${")) {
|
|
2608
|
+
allDeps.set(key, dep);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
for (const mod of pom.modules) {
|
|
2612
|
+
projectReferences.push({
|
|
2613
|
+
path: path6.join(relDir, mod),
|
|
2614
|
+
name: mod,
|
|
2615
|
+
refType: "project"
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
} else if (fileName === "build.gradle" || fileName === "build.gradle.kts") {
|
|
2619
|
+
const gradle = parseGradleBuild(content, f);
|
|
2620
|
+
if (gradle.projectName) projectName = gradle.projectName;
|
|
2621
|
+
if (gradle.javaVersion) javaVersion = gradle.javaVersion;
|
|
2622
|
+
for (const dep of gradle.dependencies) {
|
|
2623
|
+
const key = `${dep.groupId}:${dep.artifactId}`;
|
|
2624
|
+
if (!allDeps.has(key) && dep.version) {
|
|
2625
|
+
allDeps.set(key, dep);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
let runtimeMajorsBehind;
|
|
2631
|
+
let runtimeLatest;
|
|
2632
|
+
if (javaVersion) {
|
|
2633
|
+
const jvMatch = javaVersion.match(/^(1\.)?(\d+)/);
|
|
2634
|
+
if (jvMatch) {
|
|
2635
|
+
const major5 = jvMatch[1] ? parseInt(jvMatch[2], 10) : parseInt(jvMatch[2], 10);
|
|
2636
|
+
runtimeMajorsBehind = Math.max(0, LATEST_JAVA_LTS - major5);
|
|
2637
|
+
runtimeLatest = String(LATEST_JAVA_LTS);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
const dependencies = [];
|
|
2641
|
+
const frameworks = [];
|
|
2642
|
+
const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: 0 };
|
|
2643
|
+
const depEntries = [...allDeps.entries()];
|
|
2644
|
+
const metaPromises = depEntries.map(async ([key, dep]) => {
|
|
2645
|
+
const meta = await mavenCache.get(dep.groupId, dep.artifactId);
|
|
2646
|
+
return { key, dep, meta };
|
|
2647
|
+
});
|
|
2648
|
+
const resolved = await Promise.all(metaPromises);
|
|
2649
|
+
for (const { key, dep, meta } of resolved) {
|
|
2650
|
+
const resolvedVersion = mavenToSemver(dep.version);
|
|
2651
|
+
const latestStable = meta.latestStableOverall;
|
|
2652
|
+
let majorsBehind = null;
|
|
2653
|
+
let drift = "unknown";
|
|
2654
|
+
if (resolvedVersion && latestStable) {
|
|
2655
|
+
const currentMajor = semver5.major(resolvedVersion);
|
|
2656
|
+
const latestMajor = semver5.major(latestStable);
|
|
2657
|
+
majorsBehind = latestMajor - currentMajor;
|
|
2658
|
+
if (majorsBehind === 0) {
|
|
2659
|
+
drift = semver5.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
|
|
2660
|
+
} else if (majorsBehind > 0) {
|
|
2661
|
+
drift = "major-behind";
|
|
2662
|
+
} else {
|
|
2663
|
+
drift = "current";
|
|
2664
|
+
}
|
|
2665
|
+
if (majorsBehind <= 0) buckets.current++;
|
|
2666
|
+
else if (majorsBehind === 1) buckets.oneBehind++;
|
|
2667
|
+
else buckets.twoPlusBehind++;
|
|
2668
|
+
} else {
|
|
2669
|
+
buckets.unknown++;
|
|
2670
|
+
}
|
|
2671
|
+
dependencies.push({
|
|
2672
|
+
package: key,
|
|
2673
|
+
section: "dependencies",
|
|
2674
|
+
currentSpec: dep.version,
|
|
2675
|
+
resolvedVersion,
|
|
2676
|
+
latestStable,
|
|
2677
|
+
majorsBehind,
|
|
2678
|
+
drift
|
|
2679
|
+
});
|
|
2680
|
+
if (key in KNOWN_JAVA_FRAMEWORKS) {
|
|
2681
|
+
frameworks.push({
|
|
2682
|
+
name: KNOWN_JAVA_FRAMEWORKS[key],
|
|
2683
|
+
currentVersion: resolvedVersion,
|
|
2684
|
+
latestVersion: latestStable,
|
|
2685
|
+
majorsBehind
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
dependencies.sort((a, b) => {
|
|
2690
|
+
const order = { "major-behind": 0, "minor-behind": 1, "current": 2, "unknown": 3 };
|
|
2691
|
+
const diff = (order[a.drift] ?? 9) - (order[b.drift] ?? 9);
|
|
2692
|
+
if (diff !== 0) return diff;
|
|
2693
|
+
return a.package.localeCompare(b.package);
|
|
2694
|
+
});
|
|
2695
|
+
let fileCount;
|
|
2696
|
+
try {
|
|
2697
|
+
fileCount = await countFilesInDir(dir);
|
|
2698
|
+
} catch {
|
|
2699
|
+
}
|
|
2700
|
+
return {
|
|
2701
|
+
type: "java",
|
|
2702
|
+
path: relDir,
|
|
2703
|
+
name: projectName,
|
|
2704
|
+
runtime: javaVersion ? `Java ${javaVersion}` : void 0,
|
|
2705
|
+
runtimeLatest: String(LATEST_JAVA_LTS),
|
|
2706
|
+
runtimeMajorsBehind,
|
|
2707
|
+
targetFramework: javaVersion ? `Java ${javaVersion}` : void 0,
|
|
2708
|
+
frameworks,
|
|
2709
|
+
dependencies,
|
|
2710
|
+
dependencyAgeBuckets: buckets,
|
|
2711
|
+
projectReferences: projectReferences.length > 0 ? projectReferences : void 0,
|
|
2712
|
+
fileCount
|
|
2713
|
+
};
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
// src/scanners/nuget-cache.ts
|
|
2717
|
+
import * as semver6 from "semver";
|
|
2718
|
+
var NuGetCache = class {
|
|
2719
|
+
constructor(sem) {
|
|
2720
|
+
this.sem = sem;
|
|
2721
|
+
}
|
|
2722
|
+
meta = /* @__PURE__ */ new Map();
|
|
2723
|
+
baseUrl = "https://api.nuget.org/v3-flatcontainer";
|
|
2724
|
+
get(pkg2) {
|
|
2725
|
+
const existing = this.meta.get(pkg2);
|
|
2726
|
+
if (existing) return existing;
|
|
2727
|
+
const p = this.sem.run(async () => {
|
|
2728
|
+
try {
|
|
2729
|
+
const url = `${this.baseUrl}/${pkg2.toLowerCase()}/index.json`;
|
|
2730
|
+
const response = await fetch(url, {
|
|
2731
|
+
signal: AbortSignal.timeout(1e4),
|
|
2732
|
+
headers: { "Accept": "application/json" }
|
|
2733
|
+
});
|
|
2734
|
+
if (!response.ok) {
|
|
2735
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2736
|
+
}
|
|
2737
|
+
const data = await response.json();
|
|
2738
|
+
const allVersions = data.versions ?? [];
|
|
2739
|
+
const stableVersions = allVersions.filter((v) => {
|
|
2740
|
+
const parsed = semver6.valid(v);
|
|
2741
|
+
return parsed && semver6.prerelease(v) === null;
|
|
2742
|
+
});
|
|
2743
|
+
const sorted = [...stableVersions].sort(semver6.rcompare);
|
|
2744
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
2745
|
+
return {
|
|
2746
|
+
latest: latestStableOverall,
|
|
2747
|
+
stableVersions,
|
|
2748
|
+
latestStableOverall
|
|
2749
|
+
};
|
|
2750
|
+
} catch {
|
|
2751
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
this.meta.set(pkg2, p);
|
|
2755
|
+
return p;
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
|
|
2759
|
+
// src/scanners/pypi-cache.ts
|
|
2760
|
+
import * as semver7 from "semver";
|
|
2761
|
+
function pep440ToSemver2(ver) {
|
|
2762
|
+
let v = ver.replace(/^[vV]/, "").trim();
|
|
2763
|
+
if (/(?:a|b|rc|alpha|beta|dev|post)\d*/i.test(v)) return null;
|
|
2764
|
+
const parts = v.split(".");
|
|
2765
|
+
while (parts.length < 3) parts.push("0");
|
|
2766
|
+
v = parts.slice(0, 3).join(".");
|
|
2767
|
+
return semver7.valid(v);
|
|
2768
|
+
}
|
|
2769
|
+
var PyPICache = class {
|
|
2770
|
+
constructor(sem) {
|
|
2771
|
+
this.sem = sem;
|
|
2772
|
+
}
|
|
2773
|
+
meta = /* @__PURE__ */ new Map();
|
|
2774
|
+
get(pkg2) {
|
|
2775
|
+
const existing = this.meta.get(pkg2);
|
|
2776
|
+
if (existing) return existing;
|
|
2777
|
+
const p = this.sem.run(async () => {
|
|
2778
|
+
try {
|
|
2779
|
+
const url = `https://pypi.org/pypi/${encodeURIComponent(pkg2)}/json`;
|
|
2780
|
+
const response = await fetch(url, {
|
|
2781
|
+
signal: AbortSignal.timeout(1e4),
|
|
2782
|
+
headers: { "Accept": "application/json" }
|
|
2783
|
+
});
|
|
2784
|
+
if (!response.ok) {
|
|
2785
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2786
|
+
}
|
|
2787
|
+
const data = await response.json();
|
|
2788
|
+
const allVersionKeys = Object.keys(data.releases ?? {});
|
|
2789
|
+
const stableVersions = [];
|
|
2790
|
+
for (const ver of allVersionKeys) {
|
|
2791
|
+
const sv = pep440ToSemver2(ver);
|
|
2792
|
+
if (sv) stableVersions.push(sv);
|
|
2793
|
+
}
|
|
2794
|
+
const pypiLatest = data.info?.version ?? null;
|
|
2795
|
+
const pypiLatestSemver = pypiLatest ? pep440ToSemver2(pypiLatest) : null;
|
|
2796
|
+
const sorted = [...stableVersions].sort(semver7.rcompare);
|
|
2797
|
+
const latestStableOverall = sorted[0] ?? pypiLatestSemver ?? null;
|
|
2798
|
+
return {
|
|
2799
|
+
latest: pypiLatestSemver ?? latestStableOverall,
|
|
2800
|
+
stableVersions,
|
|
2801
|
+
latestStableOverall
|
|
2802
|
+
};
|
|
2803
|
+
} catch {
|
|
2804
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
this.meta.set(pkg2, p);
|
|
2808
|
+
return p;
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2811
|
+
|
|
2812
|
+
// src/scanners/maven-cache.ts
|
|
2813
|
+
import * as semver8 from "semver";
|
|
2814
|
+
function mavenToSemver2(ver) {
|
|
2815
|
+
let v = ver.trim();
|
|
2816
|
+
if (/(?:-SNAPSHOT|-alpha|-beta|-rc|-M\d|-CR\d)/i.test(v)) return null;
|
|
2817
|
+
v = v.replace(/\.(?:RELEASE|Final|GA)$/i, "");
|
|
2818
|
+
const parts = v.split(".");
|
|
2819
|
+
while (parts.length < 3) parts.push("0");
|
|
2820
|
+
v = parts.slice(0, 3).join(".");
|
|
2821
|
+
return semver8.valid(v);
|
|
2822
|
+
}
|
|
2823
|
+
var MavenCache = class {
|
|
2824
|
+
constructor(sem) {
|
|
2825
|
+
this.sem = sem;
|
|
2826
|
+
}
|
|
2827
|
+
meta = /* @__PURE__ */ new Map();
|
|
2828
|
+
/**
|
|
2829
|
+
* Get metadata for a Maven artifact.
|
|
2830
|
+
* @param groupId Maven group ID (e.g. "org.springframework.boot")
|
|
2831
|
+
* @param artifactId Maven artifact ID (e.g. "spring-boot-starter-web")
|
|
2832
|
+
*/
|
|
2833
|
+
get(groupId, artifactId) {
|
|
2834
|
+
const key = `${groupId}:${artifactId}`;
|
|
2835
|
+
const existing = this.meta.get(key);
|
|
2836
|
+
if (existing) return existing;
|
|
2837
|
+
const p = this.sem.run(async () => {
|
|
2838
|
+
try {
|
|
2839
|
+
const url = `https://search.maven.org/solrsearch/select?q=g:%22${encodeURIComponent(groupId)}%22+AND+a:%22${encodeURIComponent(artifactId)}%22&core=gav&rows=100&wt=json`;
|
|
2840
|
+
const response = await fetch(url, {
|
|
2841
|
+
signal: AbortSignal.timeout(1e4),
|
|
2842
|
+
headers: { "Accept": "application/json" }
|
|
2843
|
+
});
|
|
2844
|
+
if (!response.ok) {
|
|
2845
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2846
|
+
}
|
|
2847
|
+
const data = await response.json();
|
|
2848
|
+
const docs = data.response?.docs ?? [];
|
|
2849
|
+
const allVersions = docs.map((d) => d.v).filter((v) => typeof v === "string");
|
|
2850
|
+
const stableVersions = [];
|
|
2851
|
+
for (const ver of allVersions) {
|
|
2852
|
+
const sv = mavenToSemver2(ver);
|
|
2853
|
+
if (sv) stableVersions.push(sv);
|
|
2854
|
+
}
|
|
2855
|
+
const sorted = [...stableVersions].sort(semver8.rcompare);
|
|
2856
|
+
const latestStableOverall = sorted[0] ?? null;
|
|
2857
|
+
return {
|
|
2858
|
+
latest: latestStableOverall,
|
|
2859
|
+
stableVersions,
|
|
2860
|
+
latestStableOverall
|
|
2861
|
+
};
|
|
2862
|
+
} catch {
|
|
2863
|
+
return { latest: null, stableVersions: [], latestStableOverall: null };
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
this.meta.set(key, p);
|
|
2867
|
+
return p;
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2870
|
+
|
|
2394
2871
|
// src/config.ts
|
|
2395
2872
|
import * as path7 from "path";
|
|
2396
|
-
import * as
|
|
2873
|
+
import * as fs from "fs/promises";
|
|
2397
2874
|
var CONFIG_FILES = [
|
|
2398
2875
|
"vibgrate.config.ts",
|
|
2399
2876
|
"vibgrate.config.js",
|
|
@@ -2469,7 +2946,7 @@ const config: VibgrateConfig = {
|
|
|
2469
2946
|
|
|
2470
2947
|
export default config;
|
|
2471
2948
|
`;
|
|
2472
|
-
await
|
|
2949
|
+
await fs.writeFile(configPath, content, "utf8");
|
|
2473
2950
|
return configPath;
|
|
2474
2951
|
}
|
|
2475
2952
|
async function appendExcludePatterns(rootDir, newPatterns) {
|
|
@@ -2482,7 +2959,7 @@ async function appendExcludePatterns(rootDir, newPatterns) {
|
|
|
2482
2959
|
const existing2 = Array.isArray(cfg.exclude) ? cfg.exclude : [];
|
|
2483
2960
|
const merged2 = [.../* @__PURE__ */ new Set([...existing2, ...newPatterns])];
|
|
2484
2961
|
cfg.exclude = merged2;
|
|
2485
|
-
await
|
|
2962
|
+
await fs.writeFile(jsonPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
2486
2963
|
return true;
|
|
2487
2964
|
} catch {
|
|
2488
2965
|
}
|
|
@@ -2500,8 +2977,8 @@ async function appendExcludePatterns(rootDir, newPatterns) {
|
|
|
2500
2977
|
}
|
|
2501
2978
|
const merged = [.../* @__PURE__ */ new Set([...existing, ...newPatterns])];
|
|
2502
2979
|
try {
|
|
2503
|
-
await
|
|
2504
|
-
await
|
|
2980
|
+
await fs.mkdir(vibgrateDir, { recursive: true });
|
|
2981
|
+
await fs.writeFile(sidecarPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2505
2982
|
return true;
|
|
2506
2983
|
} catch {
|
|
2507
2984
|
return false;
|
|
@@ -2510,7 +2987,7 @@ async function appendExcludePatterns(rootDir, newPatterns) {
|
|
|
2510
2987
|
|
|
2511
2988
|
// src/utils/vcs.ts
|
|
2512
2989
|
import * as path8 from "path";
|
|
2513
|
-
import * as
|
|
2990
|
+
import * as fs2 from "fs/promises";
|
|
2514
2991
|
async function detectVcs(rootDir) {
|
|
2515
2992
|
try {
|
|
2516
2993
|
return await detectGit(rootDir);
|
|
@@ -2526,7 +3003,7 @@ async function detectGit(rootDir) {
|
|
|
2526
3003
|
const headPath = path8.join(gitDir, "HEAD");
|
|
2527
3004
|
let headContent;
|
|
2528
3005
|
try {
|
|
2529
|
-
headContent = (await
|
|
3006
|
+
headContent = (await fs2.readFile(headPath, "utf8")).trim();
|
|
2530
3007
|
} catch {
|
|
2531
3008
|
return { type: "unknown" };
|
|
2532
3009
|
}
|
|
@@ -2552,12 +3029,12 @@ async function findGitDir(startDir) {
|
|
|
2552
3029
|
while (dir !== root) {
|
|
2553
3030
|
const gitPath = path8.join(dir, ".git");
|
|
2554
3031
|
try {
|
|
2555
|
-
const
|
|
2556
|
-
if (
|
|
3032
|
+
const stat3 = await fs2.stat(gitPath);
|
|
3033
|
+
if (stat3.isDirectory()) {
|
|
2557
3034
|
return gitPath;
|
|
2558
3035
|
}
|
|
2559
|
-
if (
|
|
2560
|
-
const content = (await
|
|
3036
|
+
if (stat3.isFile()) {
|
|
3037
|
+
const content = (await fs2.readFile(gitPath, "utf8")).trim();
|
|
2561
3038
|
if (content.startsWith("gitdir: ")) {
|
|
2562
3039
|
const resolved = path8.resolve(dir, content.slice(8));
|
|
2563
3040
|
return resolved;
|
|
@@ -2572,7 +3049,7 @@ async function findGitDir(startDir) {
|
|
|
2572
3049
|
async function resolveRef(gitDir, refPath) {
|
|
2573
3050
|
const loosePath = path8.join(gitDir, refPath);
|
|
2574
3051
|
try {
|
|
2575
|
-
const sha = (await
|
|
3052
|
+
const sha = (await fs2.readFile(loosePath, "utf8")).trim();
|
|
2576
3053
|
if (/^[0-9a-f]{40}$/i.test(sha)) {
|
|
2577
3054
|
return sha;
|
|
2578
3055
|
}
|
|
@@ -2580,7 +3057,7 @@ async function resolveRef(gitDir, refPath) {
|
|
|
2580
3057
|
}
|
|
2581
3058
|
const packedPath = path8.join(gitDir, "packed-refs");
|
|
2582
3059
|
try {
|
|
2583
|
-
const packed = await
|
|
3060
|
+
const packed = await fs2.readFile(packedPath, "utf8");
|
|
2584
3061
|
for (const line of packed.split("\n")) {
|
|
2585
3062
|
if (line.startsWith("#") || line.startsWith("^")) continue;
|
|
2586
3063
|
const parts = line.trim().split(" ");
|
|
@@ -2974,14 +3451,14 @@ var ScanProgress = class {
|
|
|
2974
3451
|
};
|
|
2975
3452
|
|
|
2976
3453
|
// src/ui/scan-history.ts
|
|
2977
|
-
import * as
|
|
3454
|
+
import * as fs3 from "fs/promises";
|
|
2978
3455
|
import * as path9 from "path";
|
|
2979
3456
|
var HISTORY_FILENAME = "scan_history.json";
|
|
2980
3457
|
var MAX_RECORDS = 10;
|
|
2981
3458
|
async function loadScanHistory(rootDir) {
|
|
2982
3459
|
const filePath = path9.join(rootDir, ".vibgrate", HISTORY_FILENAME);
|
|
2983
3460
|
try {
|
|
2984
|
-
const txt = await
|
|
3461
|
+
const txt = await fs3.readFile(filePath, "utf8");
|
|
2985
3462
|
const data = JSON.parse(txt);
|
|
2986
3463
|
if (data.version === 1 && Array.isArray(data.records)) {
|
|
2987
3464
|
return data;
|
|
@@ -3006,8 +3483,8 @@ async function saveScanHistory(rootDir, record) {
|
|
|
3006
3483
|
history = { version: 1, records: [record] };
|
|
3007
3484
|
}
|
|
3008
3485
|
try {
|
|
3009
|
-
await
|
|
3010
|
-
await
|
|
3486
|
+
await fs3.mkdir(dir, { recursive: true });
|
|
3487
|
+
await fs3.writeFile(filePath, JSON.stringify(history, null, 2) + "\n", "utf8");
|
|
3011
3488
|
} catch {
|
|
3012
3489
|
}
|
|
3013
3490
|
}
|
|
@@ -4407,9 +4884,9 @@ function scanBreakingChangeExposure(projects) {
|
|
|
4407
4884
|
}
|
|
4408
4885
|
|
|
4409
4886
|
// src/scanners/file-hotspots.ts
|
|
4410
|
-
import * as
|
|
4887
|
+
import * as fs4 from "fs/promises";
|
|
4411
4888
|
import * as path14 from "path";
|
|
4412
|
-
var
|
|
4889
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
4413
4890
|
"node_modules",
|
|
4414
4891
|
".git",
|
|
4415
4892
|
".wrangler",
|
|
@@ -4428,7 +4905,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
|
4428
4905
|
".output",
|
|
4429
4906
|
".svelte-kit"
|
|
4430
4907
|
]);
|
|
4431
|
-
var
|
|
4908
|
+
var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4432
4909
|
".map",
|
|
4433
4910
|
".lock",
|
|
4434
4911
|
".png",
|
|
@@ -4454,15 +4931,15 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4454
4931
|
for (const entry of entries) {
|
|
4455
4932
|
if (!entry.isFile) continue;
|
|
4456
4933
|
const ext = path14.extname(entry.name).toLowerCase();
|
|
4457
|
-
if (
|
|
4934
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
4458
4935
|
const depth = entry.relPath.split(path14.sep).length - 1;
|
|
4459
4936
|
if (depth > maxDepth) maxDepth = depth;
|
|
4460
4937
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
4461
4938
|
try {
|
|
4462
|
-
const
|
|
4939
|
+
const stat3 = await fs4.stat(entry.absPath);
|
|
4463
4940
|
allFiles.push({
|
|
4464
4941
|
path: entry.relPath,
|
|
4465
|
-
bytes:
|
|
4942
|
+
bytes: stat3.size
|
|
4466
4943
|
});
|
|
4467
4944
|
} catch {
|
|
4468
4945
|
}
|
|
@@ -4472,7 +4949,7 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4472
4949
|
if (depth > maxDepth) maxDepth = depth;
|
|
4473
4950
|
let entries;
|
|
4474
4951
|
try {
|
|
4475
|
-
const dirents = await
|
|
4952
|
+
const dirents = await fs4.readdir(dir, { withFileTypes: true });
|
|
4476
4953
|
entries = dirents.map((d) => ({
|
|
4477
4954
|
name: d.name,
|
|
4478
4955
|
isDirectory: d.isDirectory(),
|
|
@@ -4483,17 +4960,17 @@ async function scanFileHotspots(rootDir, cache) {
|
|
|
4483
4960
|
}
|
|
4484
4961
|
for (const e of entries) {
|
|
4485
4962
|
if (e.isDirectory) {
|
|
4486
|
-
if (
|
|
4963
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
4487
4964
|
await walk(path14.join(dir, e.name), depth + 1);
|
|
4488
4965
|
} else if (e.isFile) {
|
|
4489
4966
|
const ext = path14.extname(e.name).toLowerCase();
|
|
4490
|
-
if (
|
|
4967
|
+
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
4491
4968
|
extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
|
|
4492
4969
|
try {
|
|
4493
|
-
const
|
|
4970
|
+
const stat3 = await fs4.stat(path14.join(dir, e.name));
|
|
4494
4971
|
allFiles.push({
|
|
4495
4972
|
path: path14.relative(rootDir, path14.join(dir, e.name)),
|
|
4496
|
-
bytes:
|
|
4973
|
+
bytes: stat3.size
|
|
4497
4974
|
});
|
|
4498
4975
|
} catch {
|
|
4499
4976
|
}
|
|
@@ -4583,7 +5060,7 @@ var SECRET_HEURISTICS = [
|
|
|
4583
5060
|
{ detector: "private-key", pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
|
|
4584
5061
|
{ detector: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
|
|
4585
5062
|
];
|
|
4586
|
-
var defaultRunner = (command, args) => new Promise((
|
|
5063
|
+
var defaultRunner = (command, args) => new Promise((resolve8, reject) => {
|
|
4587
5064
|
const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4588
5065
|
let stdout = "";
|
|
4589
5066
|
let stderr = "";
|
|
@@ -4595,7 +5072,7 @@ var defaultRunner = (command, args) => new Promise((resolve9, reject) => {
|
|
|
4595
5072
|
});
|
|
4596
5073
|
child.on("error", reject);
|
|
4597
5074
|
child.on("close", (code) => {
|
|
4598
|
-
|
|
5075
|
+
resolve8({ stdout, stderr, exitCode: code ?? 1 });
|
|
4599
5076
|
});
|
|
4600
5077
|
});
|
|
4601
5078
|
function compareSemver(a, b) {
|
|
@@ -5116,7 +5593,7 @@ function scanServiceDependencies(projects) {
|
|
|
5116
5593
|
|
|
5117
5594
|
// src/scanners/architecture.ts
|
|
5118
5595
|
import * as path17 from "path";
|
|
5119
|
-
import * as
|
|
5596
|
+
import * as fs5 from "fs/promises";
|
|
5120
5597
|
var ARCHETYPE_SIGNALS = [
|
|
5121
5598
|
// Meta-frameworks (highest priority — they imply routing patterns)
|
|
5122
5599
|
{ packages: ["next", "@next/core"], archetype: "nextjs", weight: 10 },
|
|
@@ -5424,7 +5901,7 @@ async function walkSourceFiles(rootDir, cache) {
|
|
|
5424
5901
|
async function walk(dir) {
|
|
5425
5902
|
let entries;
|
|
5426
5903
|
try {
|
|
5427
|
-
entries = await
|
|
5904
|
+
entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
5428
5905
|
} catch {
|
|
5429
5906
|
return;
|
|
5430
5907
|
}
|
|
@@ -5905,7 +6382,7 @@ var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
5905
6382
|
".env"
|
|
5906
6383
|
]);
|
|
5907
6384
|
async function runSemgrep(args, cwd, stdin) {
|
|
5908
|
-
return new Promise((
|
|
6385
|
+
return new Promise((resolve8, reject) => {
|
|
5909
6386
|
const child = spawn3("semgrep", args, {
|
|
5910
6387
|
cwd,
|
|
5911
6388
|
shell: true,
|
|
@@ -5921,7 +6398,7 @@ async function runSemgrep(args, cwd, stdin) {
|
|
|
5921
6398
|
});
|
|
5922
6399
|
child.on("error", reject);
|
|
5923
6400
|
child.on("close", (code) => {
|
|
5924
|
-
|
|
6401
|
+
resolve8({ code: code ?? 1, stdout, stderr });
|
|
5925
6402
|
});
|
|
5926
6403
|
if (stdin !== void 0) child.stdin.write(stdin);
|
|
5927
6404
|
child.stdin.end();
|
|
@@ -6061,7 +6538,7 @@ var SECURITY_TOOLS = [
|
|
|
6061
6538
|
];
|
|
6062
6539
|
var IS_WIN = process.platform === "win32";
|
|
6063
6540
|
function runCommand(cmd, args) {
|
|
6064
|
-
return new Promise((
|
|
6541
|
+
return new Promise((resolve8) => {
|
|
6065
6542
|
const child = spawn4(cmd, args, {
|
|
6066
6543
|
stdio: ["ignore", "pipe", "pipe"],
|
|
6067
6544
|
shell: IS_WIN
|
|
@@ -6075,8 +6552,8 @@ function runCommand(cmd, args) {
|
|
|
6075
6552
|
child.stderr.on("data", (d) => {
|
|
6076
6553
|
stderr += d.toString();
|
|
6077
6554
|
});
|
|
6078
|
-
child.on("error", () =>
|
|
6079
|
-
child.on("close", (code) =>
|
|
6555
|
+
child.on("error", () => resolve8({ exitCode: 127, stdout, stderr }));
|
|
6556
|
+
child.on("close", (code) => resolve8({ exitCode: code ?? 1, stdout, stderr }));
|
|
6080
6557
|
});
|
|
6081
6558
|
}
|
|
6082
6559
|
async function commandExists(command) {
|
|
@@ -6149,6 +6626,9 @@ async function runScan(rootDir, opts) {
|
|
|
6149
6626
|
const config = await loadConfig(rootDir);
|
|
6150
6627
|
const sem = new Semaphore(opts.concurrency);
|
|
6151
6628
|
const npmCache = new NpmCache(rootDir, sem);
|
|
6629
|
+
const nugetCache = new NuGetCache(sem);
|
|
6630
|
+
const pypiCache = new PyPICache(sem);
|
|
6631
|
+
const mavenCache = new MavenCache(sem);
|
|
6152
6632
|
const fileCache = new FileCache();
|
|
6153
6633
|
const excludePatterns = config.exclude ?? [];
|
|
6154
6634
|
fileCache.setExcludePatterns(excludePatterns);
|
|
@@ -6163,6 +6643,8 @@ async function runScan(rootDir, opts) {
|
|
|
6163
6643
|
{ id: "walk", label: "Indexing files", weight: 8 },
|
|
6164
6644
|
{ id: "node", label: "Scanning Node projects", weight: 4 },
|
|
6165
6645
|
{ id: "dotnet", label: "Scanning .NET projects", weight: 2 },
|
|
6646
|
+
{ id: "python", label: "Scanning Python projects", weight: 3 },
|
|
6647
|
+
{ id: "java", label: "Scanning Java projects", weight: 3 },
|
|
6166
6648
|
...scanners !== false ? [
|
|
6167
6649
|
...scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
|
|
6168
6650
|
...scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
|
|
@@ -6232,7 +6714,7 @@ async function runScan(rootDir, opts) {
|
|
|
6232
6714
|
progress.addProjects(nodeProjects.length);
|
|
6233
6715
|
progress.completeStep("node", `${nodeProjects.length} project${nodeProjects.length !== 1 ? "s" : ""}`, nodeProjects.length);
|
|
6234
6716
|
progress.startStep("dotnet");
|
|
6235
|
-
const dotnetProjects = await scanDotnetProjects(rootDir, fileCache);
|
|
6717
|
+
const dotnetProjects = await scanDotnetProjects(rootDir, nugetCache, fileCache);
|
|
6236
6718
|
for (const p of dotnetProjects) {
|
|
6237
6719
|
progress.addDependencies(p.dependencies.length);
|
|
6238
6720
|
progress.addFrameworks(p.frameworks.length);
|
|
@@ -6240,7 +6722,25 @@ async function runScan(rootDir, opts) {
|
|
|
6240
6722
|
filesScanned += dotnetProjects.length;
|
|
6241
6723
|
progress.addProjects(dotnetProjects.length);
|
|
6242
6724
|
progress.completeStep("dotnet", `${dotnetProjects.length} project${dotnetProjects.length !== 1 ? "s" : ""}`, dotnetProjects.length);
|
|
6243
|
-
|
|
6725
|
+
progress.startStep("python");
|
|
6726
|
+
const pythonProjects = await scanPythonProjects(rootDir, pypiCache, fileCache);
|
|
6727
|
+
for (const p of pythonProjects) {
|
|
6728
|
+
progress.addDependencies(p.dependencies.length);
|
|
6729
|
+
progress.addFrameworks(p.frameworks.length);
|
|
6730
|
+
}
|
|
6731
|
+
filesScanned += pythonProjects.length;
|
|
6732
|
+
progress.addProjects(pythonProjects.length);
|
|
6733
|
+
progress.completeStep("python", `${pythonProjects.length} project${pythonProjects.length !== 1 ? "s" : ""}`, pythonProjects.length);
|
|
6734
|
+
progress.startStep("java");
|
|
6735
|
+
const javaProjects = await scanJavaProjects(rootDir, mavenCache, fileCache);
|
|
6736
|
+
for (const p of javaProjects) {
|
|
6737
|
+
progress.addDependencies(p.dependencies.length);
|
|
6738
|
+
progress.addFrameworks(p.frameworks.length);
|
|
6739
|
+
}
|
|
6740
|
+
filesScanned += javaProjects.length;
|
|
6741
|
+
progress.addProjects(javaProjects.length);
|
|
6742
|
+
progress.completeStep("java", `${javaProjects.length} project${javaProjects.length !== 1 ? "s" : ""}`, javaProjects.length);
|
|
6743
|
+
const allProjects = [...nodeProjects, ...dotnetProjects, ...pythonProjects, ...javaProjects];
|
|
6244
6744
|
const dsn = opts.dsn || process.env.VIBGRATE_DSN;
|
|
6245
6745
|
const parsedDsn = dsn ? parseDsn(dsn) : null;
|
|
6246
6746
|
const workspaceId = parsedDsn?.workspaceId;
|
|
@@ -6686,10 +7186,6 @@ Failing: findings detected at warn level or above.`));
|
|
|
6686
7186
|
});
|
|
6687
7187
|
|
|
6688
7188
|
export {
|
|
6689
|
-
readJsonFile,
|
|
6690
|
-
pathExists,
|
|
6691
|
-
ensureDir,
|
|
6692
|
-
writeJsonFile,
|
|
6693
7189
|
writeDefaultConfig,
|
|
6694
7190
|
computeDriftScore,
|
|
6695
7191
|
generateFindings,
|