@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.
@@ -1,555 +1,19 @@
1
- // src/utils/fs.ts
2
- import * as fs from "fs/promises";
3
- import * as os from "os";
4
- import * as path2 from "path";
5
-
6
- // src/utils/semaphore.ts
7
- var Semaphore = class {
8
- available;
9
- queue = [];
10
- constructor(max) {
11
- this.available = max;
12
- }
13
- async run(fn) {
14
- await this.acquire();
15
- try {
16
- return await fn();
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 fs7 = frameworkScore(projects);
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: fs7, weight: 0.25 },
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(fs7 ?? 100),
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 (fs7 !== null) measured.push("framework");
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(fs7 ?? 100),
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: `${project.type === "node" ? "Node.js" : ".NET"} runtime "${project.runtime}" is ${project.runtimeMajorsBehind} major versions behind (latest: ${project.runtimeLatest}). Likely at or past EOL.`,
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: `${project.type === "node" ? "Node.js" : ".NET"} runtime "${project.runtime}" is ${project.runtimeMajorsBehind} major versions behind (latest: ${project.runtimeLatest}).`,
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 path3 from "path";
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 = path3.resolve(opts.write);
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 path4 from "path";
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 = path4.resolve(opts.file);
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 path5 from "path";
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((resolve9) => {
1697
- timer = setTimeout(() => resolve9({ ok: false }), ms);
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((resolve9, reject) => {
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
- resolve9(null);
1216
+ resolve8(null);
1741
1217
  return;
1742
1218
  }
1743
1219
  try {
1744
- resolve9(JSON.parse(trimmed));
1220
+ resolve8(JSON.parse(trimmed));
1745
1221
  } catch {
1746
- resolve9(trimmed.replace(/^"|"$/g, ""));
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 = path5.relative(rootDir, path5.dirname(pjPath));
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 = path5.dirname(packageJsonPath);
1946
- const projectPath = path5.relative(rootDir, absProjectPath) || ".";
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 ?? path5.basename(absProjectPath),
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 path6 from "path";
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: path6.basename(filePath, ".csproj") };
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: path6.basename(filePath, ".csproj")
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 = path6.dirname(slnPath);
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 = path6.resolve(slnDir, match[1].replace(/\\/g, "/"));
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 = path6.relative(rootDir, path6.dirname(csprojPath));
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 = path6.dirname(csprojPath);
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 major2 = parseTfmMajor(primaryTfm);
2337
- if (major2 !== null) {
2338
- runtimeMajorsBehind = Math.max(0, LATEST_DOTNET_MAJOR - major2);
2339
- }
2340
- }
2341
- const dependencies = data.packageReferences.map((ref) => ({
2342
- package: ref.name,
2343
- section: "dependencies",
2344
- currentSpec: ref.version,
2345
- resolvedVersion: ref.version,
2346
- latestStable: null,
2347
- // NuGet lookup not implemented in v1
2348
- majorsBehind: null,
2349
- drift: "unknown"
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 = path6.resolve(csprojDir, refPath);
2364
- const relRefPath = path6.relative(rootDir, path6.dirname(absRefPath));
2365
- const refName = path6.basename(absRefPath, ".csproj");
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
- const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: dependencies.length };
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: path6.relative(rootDir, csprojDir) || ".",
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 fs2 from "fs/promises";
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 fs2.writeFile(configPath, content, "utf8");
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 fs2.writeFile(jsonPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
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 fs2.mkdir(vibgrateDir, { recursive: true });
2504
- await fs2.writeFile(sidecarPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
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 fs3 from "fs/promises";
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 fs3.readFile(headPath, "utf8")).trim();
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 stat4 = await fs3.stat(gitPath);
2556
- if (stat4.isDirectory()) {
3032
+ const stat3 = await fs2.stat(gitPath);
3033
+ if (stat3.isDirectory()) {
2557
3034
  return gitPath;
2558
3035
  }
2559
- if (stat4.isFile()) {
2560
- const content = (await fs3.readFile(gitPath, "utf8")).trim();
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 fs3.readFile(loosePath, "utf8")).trim();
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 fs3.readFile(packedPath, "utf8");
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 fs4 from "fs/promises";
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 fs4.readFile(filePath, "utf8");
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 fs4.mkdir(dir, { recursive: true });
3010
- await fs4.writeFile(filePath, JSON.stringify(history, null, 2) + "\n", "utf8");
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 fs5 from "fs/promises";
4887
+ import * as fs4 from "fs/promises";
4411
4888
  import * as path14 from "path";
4412
- var SKIP_DIRS2 = /* @__PURE__ */ new Set([
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 SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([
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 (SKIP_EXTENSIONS2.has(ext)) continue;
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 stat4 = await fs5.stat(entry.absPath);
4939
+ const stat3 = await fs4.stat(entry.absPath);
4463
4940
  allFiles.push({
4464
4941
  path: entry.relPath,
4465
- bytes: stat4.size
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 fs5.readdir(dir, { withFileTypes: true });
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 (SKIP_DIRS2.has(e.name)) continue;
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 (SKIP_EXTENSIONS2.has(ext)) continue;
4967
+ if (SKIP_EXTENSIONS.has(ext)) continue;
4491
4968
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
4492
4969
  try {
4493
- const stat4 = await fs5.stat(path14.join(dir, e.name));
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: stat4.size
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((resolve9, reject) => {
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
- resolve9({ stdout, stderr, exitCode: code ?? 1 });
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 fs6 from "fs/promises";
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 fs6.readdir(dir, { withFileTypes: true });
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((resolve9, reject) => {
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
- resolve9({ code: code ?? 1, stdout, stderr });
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((resolve9) => {
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", () => resolve9({ exitCode: 127, stdout, stderr }));
6079
- child.on("close", (code) => resolve9({ exitCode: code ?? 1, stdout, stderr }));
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
- const allProjects = [...nodeProjects, ...dotnetProjects];
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,