@vibgrate/cli 0.1.2 → 0.1.4

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.
@@ -0,0 +1,3900 @@
1
+ // src/utils/fs.ts
2
+ import * as fs from "fs/promises";
3
+ import * as path from "path";
4
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
5
+ "node_modules",
6
+ ".git",
7
+ ".next",
8
+ "dist",
9
+ "build",
10
+ "out",
11
+ ".turbo",
12
+ ".cache",
13
+ "coverage",
14
+ "bin",
15
+ "obj",
16
+ ".vs",
17
+ "packages",
18
+ "TestResults"
19
+ ]);
20
+ async function findFiles(rootDir, predicate) {
21
+ const results = [];
22
+ async function walk(dir) {
23
+ let entries;
24
+ try {
25
+ const dirents = await fs.readdir(dir, { withFileTypes: true });
26
+ entries = dirents.map((d) => ({
27
+ name: d.name,
28
+ isDirectory: d.isDirectory(),
29
+ isFile: d.isFile()
30
+ }));
31
+ } catch {
32
+ return;
33
+ }
34
+ for (const e of entries) {
35
+ if (e.isDirectory) {
36
+ if (SKIP_DIRS.has(e.name)) continue;
37
+ await walk(path.join(dir, e.name));
38
+ } else if (e.isFile && predicate(e.name)) {
39
+ results.push(path.join(dir, e.name));
40
+ }
41
+ }
42
+ }
43
+ await walk(rootDir);
44
+ return results;
45
+ }
46
+ async function findPackageJsonFiles(rootDir) {
47
+ return findFiles(rootDir, (name) => name === "package.json");
48
+ }
49
+ async function findSolutionFiles(rootDir) {
50
+ return findFiles(rootDir, (name) => name.endsWith(".sln"));
51
+ }
52
+ async function findCsprojFiles(rootDir) {
53
+ return findFiles(rootDir, (name) => name.endsWith(".csproj"));
54
+ }
55
+ async function readJsonFile(filePath) {
56
+ const txt = await fs.readFile(filePath, "utf8");
57
+ return JSON.parse(txt);
58
+ }
59
+ async function readTextFile(filePath) {
60
+ return fs.readFile(filePath, "utf8");
61
+ }
62
+ async function pathExists(p) {
63
+ try {
64
+ await fs.access(p);
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+ async function ensureDir(dir) {
71
+ await fs.mkdir(dir, { recursive: true });
72
+ }
73
+ async function writeJsonFile(filePath, data) {
74
+ await ensureDir(path.dirname(filePath));
75
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
76
+ }
77
+ async function writeTextFile(filePath, content) {
78
+ await ensureDir(path.dirname(filePath));
79
+ await fs.writeFile(filePath, content, "utf8");
80
+ }
81
+
82
+ // src/scoring/drift-score.ts
83
+ var DEFAULT_THRESHOLDS = {
84
+ failOnError: {
85
+ eolDays: 180,
86
+ frameworkMajorLag: 3,
87
+ dependencyTwoPlusPercent: 50
88
+ },
89
+ warn: {
90
+ frameworkMajorLag: 2,
91
+ dependencyTwoPlusPercent: 30
92
+ }
93
+ };
94
+ function clamp(val, min, max) {
95
+ return Math.min(max, Math.max(min, val));
96
+ }
97
+ function runtimeScore(projects) {
98
+ if (projects.length === 0) return null;
99
+ const lags = projects.map((p) => p.runtimeMajorsBehind).filter((v) => v !== void 0);
100
+ if (lags.length === 0) return null;
101
+ const maxLag = Math.max(...lags);
102
+ if (maxLag === 0) return 100;
103
+ if (maxLag === 1) return 80;
104
+ if (maxLag === 2) return 50;
105
+ if (maxLag === 3) return 20;
106
+ return 0;
107
+ }
108
+ function frameworkScore(projects) {
109
+ const allFrameworks = projects.flatMap((p) => p.frameworks);
110
+ if (allFrameworks.length === 0) return null;
111
+ const lags = allFrameworks.map((f) => f.majorsBehind).filter((v) => v !== null);
112
+ if (lags.length === 0) return null;
113
+ const maxLag = Math.max(...lags);
114
+ const avgLag = lags.reduce((a, b) => a + b, 0) / lags.length;
115
+ const maxPenalty = Math.min(maxLag * 20, 100);
116
+ const avgPenalty = Math.min(avgLag * 15, 100);
117
+ return clamp(100 - (maxPenalty * 0.6 + avgPenalty * 0.4), 0, 100);
118
+ }
119
+ function dependencyScore(projects) {
120
+ let totalCurrent = 0;
121
+ let totalOne = 0;
122
+ let totalTwo = 0;
123
+ let totalUnknown = 0;
124
+ for (const p of projects) {
125
+ totalCurrent += p.dependencyAgeBuckets.current;
126
+ totalOne += p.dependencyAgeBuckets.oneBehind;
127
+ totalTwo += p.dependencyAgeBuckets.twoPlusBehind;
128
+ totalUnknown += p.dependencyAgeBuckets.unknown;
129
+ }
130
+ const total = totalCurrent + totalOne + totalTwo;
131
+ if (total === 0) return null;
132
+ const currentPct = totalCurrent / total;
133
+ const onePct = totalOne / total;
134
+ const twoPct = totalTwo / total;
135
+ return clamp(Math.round(currentPct * 100 - onePct * 10 - twoPct * 40), 0, 100);
136
+ }
137
+ function eolScore(projects) {
138
+ const hasRuntimeData = projects.some((p) => p.runtimeMajorsBehind !== void 0);
139
+ if (!hasRuntimeData) return null;
140
+ let score = 100;
141
+ for (const p of projects) {
142
+ if (p.type === "node" && p.runtimeMajorsBehind !== void 0) {
143
+ if (p.runtimeMajorsBehind >= 3) score = Math.min(score, 0);
144
+ else if (p.runtimeMajorsBehind >= 2) score = Math.min(score, 30);
145
+ else if (p.runtimeMajorsBehind >= 1) score = Math.min(score, 70);
146
+ }
147
+ if (p.type === "dotnet" && p.runtimeMajorsBehind !== void 0) {
148
+ if (p.runtimeMajorsBehind >= 3) score = Math.min(score, 0);
149
+ else if (p.runtimeMajorsBehind >= 2) score = Math.min(score, 20);
150
+ else if (p.runtimeMajorsBehind >= 1) score = Math.min(score, 60);
151
+ }
152
+ }
153
+ return score;
154
+ }
155
+ function computeDriftScore(projects) {
156
+ const rs = runtimeScore(projects);
157
+ const fs5 = frameworkScore(projects);
158
+ const ds = dependencyScore(projects);
159
+ const es = eolScore(projects);
160
+ const components = [
161
+ { score: rs, weight: 0.25 },
162
+ { score: fs5, weight: 0.25 },
163
+ { score: ds, weight: 0.3 },
164
+ { score: es, weight: 0.2 }
165
+ ];
166
+ const active = components.filter((c) => c.score !== null);
167
+ if (active.length === 0) {
168
+ return {
169
+ score: 100,
170
+ riskLevel: "low",
171
+ components: {
172
+ runtimeScore: rs ?? 100,
173
+ frameworkScore: fs5 ?? 100,
174
+ dependencyScore: ds ?? 100,
175
+ eolScore: es ?? 100
176
+ }
177
+ };
178
+ }
179
+ const totalActiveWeight = active.reduce((sum, c) => sum + c.weight, 0);
180
+ let score = 0;
181
+ for (const c of active) {
182
+ score += c.score * (c.weight / totalActiveWeight);
183
+ }
184
+ score = Math.round(score);
185
+ let riskLevel;
186
+ if (score >= 70) riskLevel = "low";
187
+ else if (score >= 40) riskLevel = "moderate";
188
+ else riskLevel = "high";
189
+ const measured = [];
190
+ if (rs !== null) measured.push("runtime");
191
+ if (fs5 !== null) measured.push("framework");
192
+ if (ds !== null) measured.push("dependency");
193
+ if (es !== null) measured.push("eol");
194
+ return {
195
+ score,
196
+ riskLevel,
197
+ components: {
198
+ runtimeScore: rs ?? 100,
199
+ frameworkScore: fs5 ?? 100,
200
+ dependencyScore: ds ?? 100,
201
+ eolScore: es ?? 100
202
+ },
203
+ measured
204
+ };
205
+ }
206
+ function generateFindings(projects, config) {
207
+ const thresholds = {
208
+ failOnError: { ...DEFAULT_THRESHOLDS.failOnError, ...config?.thresholds?.failOnError },
209
+ warn: { ...DEFAULT_THRESHOLDS.warn, ...config?.thresholds?.warn }
210
+ };
211
+ const findings = [];
212
+ for (const project of projects) {
213
+ if (project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind >= 3) {
214
+ findings.push({
215
+ ruleId: "vibgrate/runtime-eol",
216
+ level: "error",
217
+ message: `${project.type === "node" ? "Node.js" : ".NET"} runtime "${project.runtime}" is ${project.runtimeMajorsBehind} major versions behind (latest: ${project.runtimeLatest}). Likely at or past EOL.`,
218
+ location: project.path
219
+ });
220
+ } else if (project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind >= 2) {
221
+ findings.push({
222
+ ruleId: "vibgrate/runtime-lag",
223
+ level: "warning",
224
+ message: `${project.type === "node" ? "Node.js" : ".NET"} runtime "${project.runtime}" is ${project.runtimeMajorsBehind} major versions behind (latest: ${project.runtimeLatest}).`,
225
+ location: project.path
226
+ });
227
+ }
228
+ for (const fw of project.frameworks) {
229
+ if (fw.majorsBehind !== null && thresholds.failOnError.frameworkMajorLag !== void 0 && fw.majorsBehind >= thresholds.failOnError.frameworkMajorLag) {
230
+ findings.push({
231
+ ruleId: "vibgrate/framework-major-lag",
232
+ level: "error",
233
+ message: `${fw.name} is ${fw.majorsBehind} major versions behind (current: ${fw.currentVersion}, latest: ${fw.latestVersion}).`,
234
+ location: project.path
235
+ });
236
+ } else if (fw.majorsBehind !== null && thresholds.warn.frameworkMajorLag !== void 0 && fw.majorsBehind >= thresholds.warn.frameworkMajorLag) {
237
+ findings.push({
238
+ ruleId: "vibgrate/framework-major-lag",
239
+ level: "warning",
240
+ message: `${fw.name} is ${fw.majorsBehind} major versions behind (current: ${fw.currentVersion}, latest: ${fw.latestVersion}).`,
241
+ location: project.path
242
+ });
243
+ }
244
+ }
245
+ const totalDeps = project.dependencyAgeBuckets.current + project.dependencyAgeBuckets.oneBehind + project.dependencyAgeBuckets.twoPlusBehind;
246
+ if (totalDeps > 0) {
247
+ const twoPlusPct = project.dependencyAgeBuckets.twoPlusBehind / totalDeps * 100;
248
+ if (thresholds.failOnError.dependencyTwoPlusPercent !== void 0 && twoPlusPct >= thresholds.failOnError.dependencyTwoPlusPercent) {
249
+ findings.push({
250
+ ruleId: "vibgrate/dependency-rot",
251
+ level: "error",
252
+ message: `${Math.round(twoPlusPct)}% of dependencies are 2+ major versions behind in ${project.name}.`,
253
+ location: project.path
254
+ });
255
+ } else if (thresholds.warn.dependencyTwoPlusPercent !== void 0 && twoPlusPct >= thresholds.warn.dependencyTwoPlusPercent) {
256
+ findings.push({
257
+ ruleId: "vibgrate/dependency-rot",
258
+ level: "warning",
259
+ message: `${Math.round(twoPlusPct)}% of dependencies are 2+ major versions behind in ${project.name}.`,
260
+ location: project.path
261
+ });
262
+ }
263
+ }
264
+ for (const dep of project.dependencies) {
265
+ if (dep.majorsBehind !== null && dep.majorsBehind >= 3) {
266
+ findings.push({
267
+ ruleId: "vibgrate/dependency-major-lag",
268
+ level: "error",
269
+ message: `${dep.package} is ${dep.majorsBehind} major versions behind (spec: ${dep.currentSpec}, latest: ${dep.latestStable}).`,
270
+ location: project.path
271
+ });
272
+ }
273
+ }
274
+ }
275
+ return findings;
276
+ }
277
+
278
+ // src/formatters/text.ts
279
+ import chalk from "chalk";
280
+ function formatText(artifact) {
281
+ const lines = [];
282
+ lines.push("");
283
+ lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
284
+ lines.push(chalk.bold.cyan("\u2551 Vibgrate Drift Report \u2551"));
285
+ lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
286
+ lines.push("");
287
+ const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
288
+ lines.push(chalk.bold(" Drift Score: ") + scoreColor.bold(`${artifact.drift.score}/100`));
289
+ lines.push(chalk.bold(" Risk Level: ") + riskBadge(artifact.drift.riskLevel));
290
+ lines.push(chalk.bold(" Projects: ") + `${artifact.projects.length}`);
291
+ if (artifact.vcs) {
292
+ const vcsParts = [artifact.vcs.type];
293
+ if (artifact.vcs.branch) vcsParts.push(artifact.vcs.branch);
294
+ if (artifact.vcs.shortSha) vcsParts.push(chalk.dim(artifact.vcs.shortSha));
295
+ lines.push(chalk.bold(" VCS: ") + vcsParts.join(" "));
296
+ }
297
+ lines.push("");
298
+ const m = new Set(artifact.drift.measured ?? ["runtime", "framework", "dependency", "eol"]);
299
+ lines.push(chalk.bold.underline(" Score Breakdown"));
300
+ lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) : chalk.dim("n/a")}`);
301
+ lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) : chalk.dim("n/a")}`);
302
+ lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) : chalk.dim("n/a")}`);
303
+ lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) : chalk.dim("n/a")}`);
304
+ lines.push("");
305
+ for (const project of artifact.projects) {
306
+ lines.push(chalk.bold(` \u2500\u2500 ${project.name} `) + chalk.dim(`(${project.type}) ${project.path}`));
307
+ if (project.runtime) {
308
+ const behindStr = project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind > 0 ? chalk.yellow(` (${project.runtimeMajorsBehind} major${project.runtimeMajorsBehind > 1 ? "s" : ""} behind)`) : chalk.green(" (current)");
309
+ lines.push(` Runtime: ${project.runtime}${behindStr}`);
310
+ }
311
+ if (project.targetFramework) {
312
+ lines.push(` Target: ${project.targetFramework}`);
313
+ }
314
+ if (project.frameworks.length > 0) {
315
+ lines.push(" Frameworks:");
316
+ for (const fw of project.frameworks) {
317
+ const lag = fw.majorsBehind !== null ? fw.majorsBehind === 0 ? chalk.green("current") : chalk.yellow(`${fw.majorsBehind} behind`) : chalk.dim("unknown");
318
+ lines.push(` ${fw.name}: ${fw.currentVersion ?? "?"} \u2192 ${fw.latestVersion ?? "?"} (${lag})`);
319
+ }
320
+ }
321
+ const b = project.dependencyAgeBuckets;
322
+ const total = b.current + b.oneBehind + b.twoPlusBehind + b.unknown;
323
+ if (total > 0) {
324
+ lines.push(" Dependencies:");
325
+ lines.push(` ${chalk.green(`${b.current} current`)} ${chalk.yellow(`${b.oneBehind} 1-behind`)} ${chalk.red(`${b.twoPlusBehind} 2+ behind`)} ${chalk.dim(`${b.unknown} unknown`)}`);
326
+ }
327
+ lines.push("");
328
+ }
329
+ if (artifact.delta !== void 0) {
330
+ const deltaStr = artifact.delta > 0 ? chalk.green(`+${artifact.delta}`) : artifact.delta < 0 ? chalk.red(`${artifact.delta}`) : chalk.dim("0");
331
+ lines.push(chalk.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
332
+ lines.push("");
333
+ }
334
+ if (artifact.extended) {
335
+ lines.push(...formatExtended(artifact.extended));
336
+ }
337
+ if (artifact.findings.length > 0) {
338
+ const errors = artifact.findings.filter((f) => f.level === "error");
339
+ const warnings = artifact.findings.filter((f) => f.level === "warning");
340
+ const notes = artifact.findings.filter((f) => f.level === "note");
341
+ const summary = [
342
+ errors.length > 0 ? chalk.red(`${errors.length} error${errors.length !== 1 ? "s" : ""}`) : "",
343
+ warnings.length > 0 ? chalk.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}`) : "",
344
+ notes.length > 0 ? chalk.blue(`${notes.length} note${notes.length !== 1 ? "s" : ""}`) : ""
345
+ ].filter(Boolean).join(chalk.dim(", "));
346
+ lines.push(chalk.bold.underline(` Findings`) + chalk.dim(` (${summary})`));
347
+ for (const f of artifact.findings) {
348
+ const icon = f.level === "error" ? chalk.red("\u2716") : f.level === "warning" ? chalk.yellow("\u26A0") : chalk.blue("\u2139");
349
+ lines.push(` ${icon} ${f.message}`);
350
+ lines.push(chalk.dim(` ${f.ruleId} in ${f.location}`));
351
+ }
352
+ lines.push("");
353
+ }
354
+ const actions = generatePriorityActions(artifact);
355
+ if (actions.length > 0) {
356
+ lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
357
+ lines.push(chalk.bold.cyan("\u2551 Top Priority Actions \u2551"));
358
+ lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
359
+ lines.push("");
360
+ for (let i = 0; i < actions.length; i++) {
361
+ const a = actions[i];
362
+ const num = chalk.bold.cyan(` ${i + 1}.`);
363
+ lines.push(`${num} ${chalk.bold(a.title)}`);
364
+ lines.push(chalk.dim(` ${a.explanation}`));
365
+ if (a.impact) lines.push(` Impact: ${chalk.green(a.impact)}`);
366
+ lines.push("");
367
+ }
368
+ }
369
+ lines.push(chalk.dim(` Scanned at ${artifact.timestamp}`));
370
+ lines.push("");
371
+ return lines.join("\n");
372
+ }
373
+ function riskBadge(level) {
374
+ switch (level) {
375
+ case "low":
376
+ return chalk.bgGreen.black(" LOW ");
377
+ case "moderate":
378
+ return chalk.bgYellow.black(" MODERATE ");
379
+ case "high":
380
+ return chalk.bgRed.white(" HIGH ");
381
+ default:
382
+ return level;
383
+ }
384
+ }
385
+ function scoreBar(score) {
386
+ const width = 20;
387
+ const filled = Math.round(score / 100 * width);
388
+ const empty = width - filled;
389
+ const color = score >= 70 ? chalk.green : score >= 40 ? chalk.yellow : chalk.red;
390
+ return color("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + ` ${Math.round(score)}`;
391
+ }
392
+ var CATEGORY_LABELS = {
393
+ frontend: "Frontend",
394
+ metaFrameworks: "Meta-frameworks",
395
+ bundlers: "Bundlers",
396
+ css: "CSS / UI",
397
+ backend: "Backend",
398
+ orm: "ORM / Database",
399
+ testing: "Testing",
400
+ lintFormat: "Lint & Format",
401
+ apiMessaging: "API & Messaging",
402
+ observability: "Observability",
403
+ payment: "Payment",
404
+ auth: "Auth",
405
+ email: "Email",
406
+ cloud: "Cloud",
407
+ databases: "Databases",
408
+ messaging: "Messaging",
409
+ crm: "CRM & Comms",
410
+ storage: "Storage",
411
+ search: "Search & AI"
412
+ };
413
+ function formatExtended(ext) {
414
+ const lines = [];
415
+ if (ext.toolingInventory) {
416
+ const inv = ext.toolingInventory;
417
+ const categories = Object.entries(inv).filter(([, items]) => items.length > 0);
418
+ if (categories.length > 0) {
419
+ lines.push(chalk.bold.underline(" Tech Stack"));
420
+ for (const [cat, items] of categories) {
421
+ const label = CATEGORY_LABELS[cat] ?? cat;
422
+ const names = items.map((i) => chalk.white(i.name)).join(chalk.dim(", "));
423
+ lines.push(` ${chalk.cyan(label)}: ${names}`);
424
+ }
425
+ lines.push("");
426
+ }
427
+ }
428
+ if (ext.serviceDependencies) {
429
+ const svc = ext.serviceDependencies;
430
+ const categories = Object.entries(svc).filter(([, items]) => items.length > 0);
431
+ if (categories.length > 0) {
432
+ lines.push(chalk.bold.underline(" Services & Integrations"));
433
+ for (const [cat, items] of categories) {
434
+ const label = CATEGORY_LABELS[cat] ?? cat;
435
+ const names = items.map((i) => {
436
+ const ver = i.version ? chalk.dim(` ${i.version}`) : "";
437
+ return chalk.white(i.name) + ver;
438
+ }).join(chalk.dim(", "));
439
+ lines.push(` ${chalk.cyan(label)}: ${names}`);
440
+ }
441
+ lines.push("");
442
+ }
443
+ }
444
+ if (ext.breakingChangeExposure) {
445
+ const bc = ext.breakingChangeExposure;
446
+ if (bc.deprecatedPackages.length > 0 || bc.legacyPolyfills.length > 0) {
447
+ lines.push(chalk.bold.underline(" Breaking Change Exposure"));
448
+ const exposureColor = bc.exposureScore >= 40 ? chalk.red : bc.exposureScore >= 20 ? chalk.yellow : chalk.green;
449
+ lines.push(` Exposure Score: ${exposureColor.bold(`${bc.exposureScore}/100`)}`);
450
+ if (bc.deprecatedPackages.length > 0) {
451
+ lines.push(` ${chalk.red("Deprecated")}: ${bc.deprecatedPackages.map((p) => chalk.dim(p)).join(", ")}`);
452
+ }
453
+ if (bc.legacyPolyfills.length > 0) {
454
+ lines.push(` ${chalk.yellow("Polyfills")}: ${bc.legacyPolyfills.map((p) => chalk.dim(p)).join(", ")}`);
455
+ }
456
+ if (bc.peerConflictsDetected) {
457
+ lines.push(` ${chalk.red("\u26A0")} Peer dependency conflicts detected`);
458
+ }
459
+ lines.push("");
460
+ }
461
+ }
462
+ if (ext.tsModernity && ext.tsModernity.typescriptVersion) {
463
+ const ts = ext.tsModernity;
464
+ lines.push(chalk.bold.underline(" TypeScript"));
465
+ const parts = [];
466
+ parts.push(`v${ts.typescriptVersion}`);
467
+ if (ts.strict === true) parts.push(chalk.green("strict \u2714"));
468
+ else if (ts.strict === false) parts.push(chalk.yellow("strict \u2716"));
469
+ if (ts.moduleType) parts.push(ts.moduleType.toUpperCase());
470
+ if (ts.target) parts.push(`target: ${ts.target}`);
471
+ lines.push(` ${parts.join(chalk.dim(" \xB7 "))}`);
472
+ lines.push("");
473
+ }
474
+ if (ext.buildDeploy) {
475
+ const bd = ext.buildDeploy;
476
+ const hasSomething = bd.ci.length > 0 || bd.docker.dockerfileCount > 0 || bd.packageManagers.length > 0;
477
+ if (hasSomething) {
478
+ lines.push(chalk.bold.underline(" Build & Deploy"));
479
+ if (bd.ci.length > 0) lines.push(` CI: ${bd.ci.join(", ")}`);
480
+ if (bd.docker.dockerfileCount > 0) {
481
+ lines.push(` Docker: ${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""} (${bd.docker.baseImages.join(", ")})`);
482
+ }
483
+ if (bd.packageManagers.length > 0) lines.push(` Package Managers: ${bd.packageManagers.join(", ")}`);
484
+ if (bd.monorepoTools.length > 0) lines.push(` Monorepo: ${bd.monorepoTools.join(", ")}`);
485
+ if (bd.iac.length > 0) lines.push(` IaC: ${bd.iac.join(", ")}`);
486
+ lines.push("");
487
+ }
488
+ }
489
+ if (ext.securityPosture) {
490
+ const sec = ext.securityPosture;
491
+ lines.push(chalk.bold.underline(" Security Posture"));
492
+ const checks = [];
493
+ checks.push(sec.lockfilePresent ? chalk.green("Lockfile \u2714") : chalk.red("Lockfile \u2716"));
494
+ checks.push(sec.gitignoreCoversEnv ? chalk.green(".env \u2714") : chalk.red(".env \u2716"));
495
+ checks.push(sec.gitignoreCoversNodeModules ? chalk.green("node_modules \u2714") : chalk.yellow("node_modules \u2716"));
496
+ if (sec.multipleLockfileTypes) checks.push(chalk.yellow("Multiple lockfiles \u26A0"));
497
+ if (sec.envFilesTracked) checks.push(chalk.red("Env files tracked \u2716"));
498
+ lines.push(` ${checks.join(chalk.dim(" \xB7 "))}`);
499
+ lines.push("");
500
+ }
501
+ if (ext.platformMatrix) {
502
+ const pm = ext.platformMatrix;
503
+ if (pm.nativeModules.length > 0 || pm.dockerBaseImages.length > 0) {
504
+ lines.push(chalk.bold.underline(" Platform"));
505
+ if (pm.nativeModules.length > 0) {
506
+ lines.push(` Native modules: ${pm.nativeModules.map((m) => chalk.dim(m)).join(", ")}`);
507
+ }
508
+ if (pm.osAssumptions.length > 0) {
509
+ lines.push(` OS assumptions: ${pm.osAssumptions.join(", ")}`);
510
+ }
511
+ lines.push("");
512
+ }
513
+ }
514
+ if (ext.dependencyGraph) {
515
+ const dg = ext.dependencyGraph;
516
+ if (dg.lockfileType) {
517
+ lines.push(chalk.bold.underline(" Dependency Graph"));
518
+ lines.push(` ${dg.lockfileType}: ${chalk.white(`${dg.totalUnique}`)} unique, ${chalk.white(`${dg.totalInstalled}`)} installed`);
519
+ if (dg.duplicatedPackages.length > 0) {
520
+ lines.push(` ${chalk.yellow(`${dg.duplicatedPackages.length} duplicated`)} packages`);
521
+ }
522
+ if (dg.phantomDependencies.length > 0) {
523
+ lines.push(` ${chalk.red(`${dg.phantomDependencies.length} phantom`)} dependencies`);
524
+ }
525
+ lines.push("");
526
+ }
527
+ }
528
+ return lines;
529
+ }
530
+ function generatePriorityActions(artifact) {
531
+ const actions = [];
532
+ const eolProjects = artifact.projects.filter(
533
+ (p) => p.runtimeMajorsBehind !== void 0 && p.runtimeMajorsBehind >= 3
534
+ );
535
+ if (eolProjects.length > 0) {
536
+ const names = eolProjects.map((p) => p.name).join(", ");
537
+ const runtimes = eolProjects.map((p) => `${p.runtime} \u2192 ${p.runtimeLatest}`).join(", ");
538
+ actions.push({
539
+ title: `Upgrade EOL runtime${eolProjects.length > 1 ? "s" : ""} in ${names}`,
540
+ explanation: `${runtimes}. End-of-life runtimes no longer receive security patches and block ecosystem upgrades.`,
541
+ impact: `+${Math.min(eolProjects.length * 10, 30)} points (runtime & EOL scores)`,
542
+ severity: 100
543
+ });
544
+ }
545
+ const severeFrameworks = [];
546
+ for (const p of artifact.projects) {
547
+ for (const fw of p.frameworks) {
548
+ if (fw.majorsBehind !== null && fw.majorsBehind >= 3) {
549
+ severeFrameworks.push({ name: fw.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}`, behind: fw.majorsBehind, project: p.name });
550
+ }
551
+ }
552
+ }
553
+ if (severeFrameworks.length > 0) {
554
+ const worst = severeFrameworks.sort((a, b) => b.behind - a.behind)[0];
555
+ const others = severeFrameworks.length > 1 ? ` (+${severeFrameworks.length - 1} more)` : "";
556
+ actions.push({
557
+ title: `Upgrade ${worst.name} ${worst.fw} in ${worst.project}${others}`,
558
+ explanation: `${worst.behind} major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.`,
559
+ impact: `+5\u201315 points (framework score)`,
560
+ severity: 90
561
+ });
562
+ }
563
+ for (const p of artifact.projects) {
564
+ const b = p.dependencyAgeBuckets;
565
+ const total = b.current + b.oneBehind + b.twoPlusBehind;
566
+ if (total === 0) continue;
567
+ const twoPlusPct = Math.round(b.twoPlusBehind / total * 100);
568
+ if (twoPlusPct >= 40) {
569
+ actions.push({
570
+ title: `Reduce dependency rot in ${p.name} (${twoPlusPct}% severely outdated)`,
571
+ explanation: `${b.twoPlusBehind} of ${total} dependencies are 2+ majors behind. Run \`npm outdated\` and prioritise packages with known CVEs or breaking API changes.`,
572
+ impact: `+5\u201310 points (dependency score)`,
573
+ severity: 80 + twoPlusPct / 10
574
+ });
575
+ }
576
+ }
577
+ const twoMajorFrameworks = [];
578
+ for (const p of artifact.projects) {
579
+ for (const fw of p.frameworks) {
580
+ if (fw.majorsBehind === 2) {
581
+ twoMajorFrameworks.push({ name: fw.name, project: p.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}` });
582
+ }
583
+ }
584
+ }
585
+ const uniqueTwo = [...new Map(twoMajorFrameworks.map((f) => [f.name, f])).values()];
586
+ if (uniqueTwo.length > 0) {
587
+ const list = uniqueTwo.slice(0, 3).map((f) => `${f.name} (${f.fw})`).join(", ");
588
+ const moreCount = uniqueTwo.length > 3 ? ` +${uniqueTwo.length - 3} more` : "";
589
+ actions.push({
590
+ title: `Plan major framework upgrades: ${list}${moreCount}`,
591
+ explanation: `These frameworks are 2 major versions behind. Create upgrade tickets and check migration guides \u2014 the gap will widen with each new release.`,
592
+ impact: `+5\u201310 points (framework score)`,
593
+ severity: 60
594
+ });
595
+ }
596
+ if (artifact.extended?.breakingChangeExposure) {
597
+ const bc = artifact.extended.breakingChangeExposure;
598
+ const total = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
599
+ if (total > 0) {
600
+ const items = [...bc.deprecatedPackages, ...bc.legacyPolyfills].slice(0, 5).join(", ");
601
+ const moreCount = total > 5 ? ` +${total - 5} more` : "";
602
+ actions.push({
603
+ title: `Replace deprecated/legacy packages: ${items}${moreCount}`,
604
+ explanation: `${total} package${total !== 1 ? "s" : ""} are deprecated or legacy polyfills. These receive no updates and may have known vulnerabilities.`,
605
+ severity: 55
606
+ });
607
+ }
608
+ }
609
+ if (artifact.extended?.dependencyGraph) {
610
+ const dg = artifact.extended.dependencyGraph;
611
+ const phantomCount = dg.phantomDependencies.length;
612
+ if (phantomCount >= 10) {
613
+ let detail = `Packages used in code but not declared in package.json. These rely on transitive installs and can break unpredictably when other packages update.`;
614
+ const details = dg.phantomDependencyDetails;
615
+ if (details && details.length > 0) {
616
+ const byPath = /* @__PURE__ */ new Map();
617
+ for (const d of details) {
618
+ if (!byPath.has(d.sourcePath)) byPath.set(d.sourcePath, []);
619
+ byPath.get(d.sourcePath).push({ package: d.package, spec: d.spec });
620
+ }
621
+ const pathLines = [];
622
+ let shown = 0;
623
+ for (const [srcPath, pkgs] of byPath) {
624
+ if (shown >= 10) break;
625
+ pathLines.push(`
626
+ ./${srcPath}`);
627
+ for (const pkg2 of pkgs) {
628
+ if (shown >= 10) break;
629
+ pathLines.push(` ${pkg2.package}: ${pkg2.spec}`);
630
+ shown++;
631
+ }
632
+ }
633
+ const remaining = phantomCount - shown;
634
+ detail += pathLines.join("");
635
+ if (remaining > 0) detail += `
636
+ ... and ${remaining} more`;
637
+ }
638
+ actions.push({
639
+ title: `Fix ${phantomCount} phantom dependencies`,
640
+ explanation: detail,
641
+ severity: 45
642
+ });
643
+ }
644
+ }
645
+ if (artifact.extended?.securityPosture) {
646
+ const sec = artifact.extended.securityPosture;
647
+ if (sec.envFilesTracked || !sec.lockfilePresent) {
648
+ const issues = [];
649
+ if (sec.envFilesTracked) issues.push(".env files are tracked in git");
650
+ if (!sec.lockfilePresent) issues.push("no lockfile found");
651
+ actions.push({
652
+ title: `Fix security posture: ${issues.join(", ")}`,
653
+ explanation: sec.envFilesTracked ? "Environment files may contain secrets. Add them to .gitignore and rotate any exposed credentials immediately." : "Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.",
654
+ severity: 95
655
+ });
656
+ }
657
+ }
658
+ if (artifact.extended?.dependencyGraph) {
659
+ const dupes = artifact.extended.dependencyGraph.duplicatedPackages;
660
+ const highImpactDupes = dupes.filter((d) => d.versions.length >= 3);
661
+ if (highImpactDupes.length >= 3) {
662
+ const names = highImpactDupes.slice(0, 4).map((d) => `${d.name} (${d.versions.length}v)`).join(", ");
663
+ actions.push({
664
+ title: `Deduplicate heavily-versioned packages`,
665
+ explanation: `${highImpactDupes.length} packages have 3+ versions installed: ${names}. Run \`npm dedupe\` to reduce bundle size and install time.`,
666
+ severity: 35
667
+ });
668
+ }
669
+ }
670
+ actions.sort((a, b) => b.severity - a.severity);
671
+ return actions.slice(0, 5);
672
+ }
673
+
674
+ // src/formatters/sarif.ts
675
+ function formatSarif(artifact) {
676
+ const rules = buildRules(artifact.findings);
677
+ const results = artifact.findings.map((f) => toSarifResult(f));
678
+ return {
679
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
680
+ version: "2.1.0",
681
+ runs: [
682
+ {
683
+ tool: {
684
+ driver: {
685
+ name: "vibgrate",
686
+ version: artifact.vibgrateVersion,
687
+ informationUri: "https://vibgrate.com",
688
+ rules
689
+ }
690
+ },
691
+ results,
692
+ invocations: [
693
+ {
694
+ executionSuccessful: true,
695
+ startTimeUtc: artifact.timestamp
696
+ }
697
+ ]
698
+ }
699
+ ]
700
+ };
701
+ }
702
+ function buildRules(findings) {
703
+ const ruleIds = [...new Set(findings.map((f) => f.ruleId))];
704
+ return ruleIds.map((id) => {
705
+ const descriptions = {
706
+ "vibgrate/runtime-eol": {
707
+ id: "vibgrate/runtime-eol",
708
+ shortDescription: { text: "Runtime at or past end-of-life" },
709
+ helpUri: "https://vibgrate.com/rules/runtime-eol"
710
+ },
711
+ "vibgrate/runtime-lag": {
712
+ id: "vibgrate/runtime-lag",
713
+ shortDescription: { text: "Runtime major version lag" },
714
+ helpUri: "https://vibgrate.com/rules/runtime-lag"
715
+ },
716
+ "vibgrate/framework-major-lag": {
717
+ id: "vibgrate/framework-major-lag",
718
+ shortDescription: { text: "Framework major version behind latest" },
719
+ helpUri: "https://vibgrate.com/rules/framework-major-lag"
720
+ },
721
+ "vibgrate/dependency-rot": {
722
+ id: "vibgrate/dependency-rot",
723
+ shortDescription: { text: "High percentage of outdated dependencies" },
724
+ helpUri: "https://vibgrate.com/rules/dependency-rot"
725
+ },
726
+ "vibgrate/dependency-major-lag": {
727
+ id: "vibgrate/dependency-major-lag",
728
+ shortDescription: { text: "Individual dependency severely behind" },
729
+ helpUri: "https://vibgrate.com/rules/dependency-major-lag"
730
+ }
731
+ };
732
+ return descriptions[id] ?? {
733
+ id,
734
+ shortDescription: { text: id },
735
+ helpUri: "https://vibgrate.com"
736
+ };
737
+ });
738
+ }
739
+ function toSarifResult(finding) {
740
+ return {
741
+ ruleId: finding.ruleId,
742
+ level: finding.level === "error" ? "error" : finding.level === "warning" ? "warning" : "note",
743
+ message: { text: finding.message },
744
+ locations: [
745
+ {
746
+ physicalLocation: {
747
+ artifactLocation: {
748
+ uri: finding.location
749
+ }
750
+ }
751
+ }
752
+ ]
753
+ };
754
+ }
755
+
756
+ // src/version.ts
757
+ import { createRequire } from "module";
758
+ var require2 = createRequire(import.meta.url);
759
+ var pkg = require2("../package.json");
760
+ var VERSION = pkg.version;
761
+
762
+ // src/commands/scan.ts
763
+ import * as path12 from "path";
764
+ import { Command } from "commander";
765
+ import chalk3 from "chalk";
766
+
767
+ // src/scanners/node-scanner.ts
768
+ import * as path2 from "path";
769
+ import * as semver2 from "semver";
770
+
771
+ // src/scanners/npm-cache.ts
772
+ import { spawn } from "child_process";
773
+ import * as semver from "semver";
774
+ function stableOnly(versions) {
775
+ return versions.filter((v) => semver.valid(v) && semver.prerelease(v) === null);
776
+ }
777
+ function maxStable(versions) {
778
+ const stable = stableOnly(versions);
779
+ if (stable.length === 0) return null;
780
+ return stable.sort(semver.rcompare)[0] ?? null;
781
+ }
782
+ async function npmViewJson(args, cwd) {
783
+ return new Promise((resolve4, reject) => {
784
+ const child = spawn("npm", ["view", ...args, "--json"], {
785
+ cwd,
786
+ stdio: ["ignore", "pipe", "pipe"]
787
+ });
788
+ let out = "";
789
+ let err = "";
790
+ child.stdout.on("data", (d) => out += String(d));
791
+ child.stderr.on("data", (d) => err += String(d));
792
+ child.on("error", reject);
793
+ child.on("close", (code) => {
794
+ if (code !== 0) {
795
+ reject(new Error(`npm view ${args.join(" ")} failed (code=${code}): ${err.trim()}`));
796
+ return;
797
+ }
798
+ const trimmed = out.trim();
799
+ if (!trimmed) {
800
+ resolve4(null);
801
+ return;
802
+ }
803
+ try {
804
+ resolve4(JSON.parse(trimmed));
805
+ } catch {
806
+ resolve4(trimmed.replace(/^"|"$/g, ""));
807
+ }
808
+ });
809
+ });
810
+ }
811
+ var NpmCache = class {
812
+ constructor(cwd, sem) {
813
+ this.cwd = cwd;
814
+ this.sem = sem;
815
+ }
816
+ meta = /* @__PURE__ */ new Map();
817
+ get(pkg2) {
818
+ const existing = this.meta.get(pkg2);
819
+ if (existing) return existing;
820
+ const p = this.sem.run(async () => {
821
+ let latest = null;
822
+ try {
823
+ const dist = await npmViewJson([pkg2, "dist-tags"], this.cwd);
824
+ if (dist && typeof dist === "object" && typeof dist.latest === "string") {
825
+ latest = dist.latest;
826
+ }
827
+ } catch {
828
+ }
829
+ let versions = [];
830
+ try {
831
+ const v = await npmViewJson([pkg2, "versions"], this.cwd);
832
+ if (Array.isArray(v)) versions = v.map(String);
833
+ else if (typeof v === "string") versions = [v];
834
+ } catch {
835
+ }
836
+ const stable = stableOnly(versions);
837
+ const latestStableOverall = maxStable(stable);
838
+ if (!latest && latestStableOverall) latest = latestStableOverall;
839
+ return { latest, stableVersions: stable, latestStableOverall };
840
+ });
841
+ this.meta.set(pkg2, p);
842
+ return p;
843
+ }
844
+ };
845
+ function isSemverSpec(spec) {
846
+ const s = spec.trim();
847
+ if (!s) return false;
848
+ if (s.startsWith("workspace:")) return false;
849
+ if (s.startsWith("file:")) return false;
850
+ if (s.startsWith("link:")) return false;
851
+ if (s.startsWith("git+")) return false;
852
+ if (s.includes("://")) return false;
853
+ if (s.startsWith("github:")) return false;
854
+ if (s === "*" || s.toLowerCase() === "latest") return true;
855
+ return semver.validRange(s) !== null;
856
+ }
857
+
858
+ // src/scanners/node-scanner.ts
859
+ var KNOWN_FRAMEWORKS = {
860
+ // ── Frontend ──
861
+ "react": "React",
862
+ "react-dom": "React DOM",
863
+ "vue": "Vue",
864
+ "@angular/core": "Angular",
865
+ "svelte": "Svelte",
866
+ "solid-js": "Solid",
867
+ "preact": "Preact",
868
+ "lit": "Lit",
869
+ "qwik": "Qwik",
870
+ "htmx.org": "htmx",
871
+ "alpinejs": "Alpine.js",
872
+ "stimulus": "Stimulus",
873
+ // ── Meta-frameworks ──
874
+ "next": "Next.js",
875
+ "nuxt": "Nuxt",
876
+ "@remix-run/react": "Remix",
877
+ "@remix-run/node": "Remix (Node)",
878
+ "gatsby": "Gatsby",
879
+ "astro": "Astro",
880
+ "@sveltejs/kit": "SvelteKit",
881
+ "@analogjs/platform": "Analog",
882
+ "@tanstack/start": "TanStack Start",
883
+ // ── Backend ──
884
+ "express": "Express",
885
+ "fastify": "Fastify",
886
+ "@nestjs/core": "NestJS",
887
+ "hono": "Hono",
888
+ "koa": "Koa",
889
+ "@hapi/hapi": "Hapi",
890
+ "restify": "Restify",
891
+ "@elysiajs/eden": "Elysia",
892
+ "elysia": "Elysia",
893
+ "@adonisjs/core": "AdonisJS",
894
+ "moleculer": "Moleculer",
895
+ "@feathersjs/feathers": "Feathers",
896
+ "sails": "Sails",
897
+ // ── Language & Runtime ──
898
+ "typescript": "TypeScript",
899
+ // ── State Management ──
900
+ "redux": "Redux",
901
+ "@reduxjs/toolkit": "Redux Toolkit",
902
+ "zustand": "Zustand",
903
+ "mobx": "MobX",
904
+ "jotai": "Jotai",
905
+ "recoil": "Recoil",
906
+ "pinia": "Pinia",
907
+ "vuex": "Vuex",
908
+ "@tanstack/react-query": "TanStack Query",
909
+ "swr": "SWR",
910
+ "xstate": "XState",
911
+ "@ngrx/store": "NgRx",
912
+ // ── ORM & Database ──
913
+ "prisma": "Prisma",
914
+ "drizzle-orm": "Drizzle",
915
+ "typeorm": "TypeORM",
916
+ "sequelize": "Sequelize",
917
+ "@mikro-orm/core": "MikroORM",
918
+ "mongoose": "Mongoose",
919
+ "knex": "Knex",
920
+ "kysely": "Kysely",
921
+ "objection": "Objection.js",
922
+ // ── Bundlers ──
923
+ "vite": "Vite",
924
+ "webpack": "webpack",
925
+ "rollup": "Rollup",
926
+ "esbuild": "esbuild",
927
+ "parcel": "Parcel",
928
+ "turbo": "Turbo",
929
+ "tsup": "tsup",
930
+ "@swc/core": "SWC",
931
+ "bun": "Bun",
932
+ // ── Testing ──
933
+ "vitest": "Vitest",
934
+ "jest": "Jest",
935
+ "@playwright/test": "Playwright",
936
+ "cypress": "Cypress",
937
+ "mocha": "Mocha",
938
+ "ava": "AVA",
939
+ "storybook": "Storybook",
940
+ "@storybook/react": "Storybook"
941
+ };
942
+ async function scanNodeProjects(rootDir, npmCache) {
943
+ const packageJsonFiles = await findPackageJsonFiles(rootDir);
944
+ const results = [];
945
+ for (const pjPath of packageJsonFiles) {
946
+ try {
947
+ const scan = await scanOnePackageJson(pjPath, rootDir, npmCache);
948
+ results.push(scan);
949
+ } catch (e) {
950
+ const msg = e instanceof Error ? e.message : String(e);
951
+ console.error(`Error scanning ${pjPath}: ${msg}`);
952
+ }
953
+ }
954
+ return results;
955
+ }
956
+ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache) {
957
+ const pj = await readJsonFile(packageJsonPath);
958
+ const absProjectPath = path2.dirname(packageJsonPath);
959
+ const projectPath = path2.relative(rootDir, absProjectPath) || ".";
960
+ const nodeEngine = pj.engines?.node ?? void 0;
961
+ let runtimeLatest;
962
+ let runtimeMajorsBehind;
963
+ if (nodeEngine) {
964
+ const latestLtsMajor = 22;
965
+ const parsed = semver2.minVersion(nodeEngine);
966
+ if (parsed) {
967
+ const currentMajor = semver2.major(parsed);
968
+ runtimeLatest = `${latestLtsMajor}.0.0`;
969
+ runtimeMajorsBehind = Math.max(0, latestLtsMajor - currentMajor);
970
+ }
971
+ }
972
+ const sections = [
973
+ { name: "dependencies", deps: pj.dependencies },
974
+ { name: "devDependencies", deps: pj.devDependencies },
975
+ { name: "peerDependencies", deps: pj.peerDependencies },
976
+ { name: "optionalDependencies", deps: pj.optionalDependencies }
977
+ ];
978
+ const dependencies = [];
979
+ const frameworks = [];
980
+ const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: 0 };
981
+ const depEntries = [];
982
+ for (const s of sections) {
983
+ if (!s.deps) continue;
984
+ for (const [pkg2, spec] of Object.entries(s.deps)) {
985
+ if (!isSemverSpec(spec)) continue;
986
+ depEntries.push({ pkg: pkg2, section: s.name, spec });
987
+ }
988
+ }
989
+ const metaPromises = depEntries.map(async (entry) => {
990
+ const meta = await npmCache.get(entry.pkg);
991
+ return { ...entry, meta };
992
+ });
993
+ const resolved = await Promise.all(metaPromises);
994
+ for (const { pkg: pkg2, section, spec, meta } of resolved) {
995
+ const resolvedVersion = meta.stableVersions.length > 0 ? semver2.maxSatisfying(meta.stableVersions, spec) ?? null : null;
996
+ const latestStable = meta.latestStableOverall;
997
+ let majorsBehind = null;
998
+ let drift = "unknown";
999
+ if (resolvedVersion && latestStable) {
1000
+ const currentMajor = semver2.major(resolvedVersion);
1001
+ const latestMajor = semver2.major(latestStable);
1002
+ majorsBehind = latestMajor - currentMajor;
1003
+ if (majorsBehind === 0) {
1004
+ drift = semver2.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
1005
+ } else {
1006
+ drift = "major-behind";
1007
+ }
1008
+ if (majorsBehind === 0) buckets.current++;
1009
+ else if (majorsBehind === 1) buckets.oneBehind++;
1010
+ else buckets.twoPlusBehind++;
1011
+ } else {
1012
+ buckets.unknown++;
1013
+ }
1014
+ dependencies.push({
1015
+ package: pkg2,
1016
+ section,
1017
+ currentSpec: spec,
1018
+ resolvedVersion,
1019
+ latestStable,
1020
+ majorsBehind,
1021
+ drift
1022
+ });
1023
+ if (pkg2 in KNOWN_FRAMEWORKS) {
1024
+ frameworks.push({
1025
+ name: KNOWN_FRAMEWORKS[pkg2],
1026
+ currentVersion: resolvedVersion,
1027
+ latestVersion: latestStable,
1028
+ majorsBehind
1029
+ });
1030
+ }
1031
+ }
1032
+ dependencies.sort((a, b) => {
1033
+ const order = { "major-behind": 0, "minor-behind": 1, "current": 2, "unknown": 3 };
1034
+ const diff = (order[a.drift] ?? 9) - (order[b.drift] ?? 9);
1035
+ if (diff !== 0) return diff;
1036
+ return a.package.localeCompare(b.package);
1037
+ });
1038
+ return {
1039
+ type: "node",
1040
+ path: projectPath,
1041
+ name: pj.name ?? path2.basename(absProjectPath),
1042
+ runtime: nodeEngine,
1043
+ runtimeLatest,
1044
+ runtimeMajorsBehind,
1045
+ frameworks,
1046
+ dependencies,
1047
+ dependencyAgeBuckets: buckets
1048
+ };
1049
+ }
1050
+
1051
+ // src/scanners/dotnet-scanner.ts
1052
+ import * as path3 from "path";
1053
+ import { XMLParser } from "fast-xml-parser";
1054
+ var parser = new XMLParser({
1055
+ ignoreAttributes: false,
1056
+ attributeNamePrefix: "@_"
1057
+ });
1058
+ var KNOWN_DOTNET_FRAMEWORKS = {
1059
+ // ── ASP.NET Core & Web ──
1060
+ "Microsoft.AspNetCore.App": "ASP.NET Core",
1061
+ "Microsoft.AspNetCore.Mvc": "ASP.NET Core MVC",
1062
+ "Microsoft.AspNetCore.Components": "Blazor",
1063
+ "Microsoft.AspNetCore.Components.WebAssembly": "Blazor WASM",
1064
+ "Microsoft.AspNetCore.SignalR": "SignalR",
1065
+ "Microsoft.AspNetCore.OData": "OData",
1066
+ "Microsoft.AspNetCore.Identity": "ASP.NET Identity",
1067
+ "Microsoft.AspNetCore.Authentication.JwtBearer": "JWT Bearer Auth",
1068
+ "Microsoft.AspNetCore.Diagnostics.HealthChecks": "Health Checks",
1069
+ "Swashbuckle.AspNetCore": "Swashbuckle",
1070
+ "NSwag.AspNetCore": "NSwag",
1071
+ // ── Entity Framework & Data Access ──
1072
+ "Microsoft.EntityFrameworkCore": "EF Core",
1073
+ "Microsoft.EntityFrameworkCore.SqlServer": "EF Core SQL Server",
1074
+ "Microsoft.EntityFrameworkCore.Sqlite": "EF Core SQLite",
1075
+ "Microsoft.EntityFrameworkCore.Design": "EF Core Design",
1076
+ "Microsoft.EntityFrameworkCore.Tools": "EF Core Tools",
1077
+ "Npgsql.EntityFrameworkCore.PostgreSQL": "EF Core PostgreSQL",
1078
+ "Pomelo.EntityFrameworkCore.MySql": "EF Core MySQL (Pomelo)",
1079
+ "MongoDB.EntityFrameworkCore": "EF Core MongoDB",
1080
+ "Dapper": "Dapper",
1081
+ "Dapper.Contrib": "Dapper Contrib",
1082
+ "NHibernate": "NHibernate",
1083
+ "Npgsql": "Npgsql",
1084
+ "MySqlConnector": "MySqlConnector",
1085
+ "MongoDB.Driver": "MongoDB Driver",
1086
+ "StackExchange.Redis": "StackExchange.Redis",
1087
+ "Microsoft.Data.SqlClient": "SqlClient",
1088
+ "Oracle.ManagedDataAccess.Core": "Oracle (Managed)",
1089
+ "Cassandra": "Cassandra Driver",
1090
+ "Neo4j.Driver": "Neo4j Driver",
1091
+ "Marten": "Marten",
1092
+ "LiteDB": "LiteDB",
1093
+ "RavenDB.Client": "RavenDB",
1094
+ // ── Hosting & Configuration ──
1095
+ "Microsoft.Extensions.Hosting": ".NET Hosting",
1096
+ "Microsoft.Extensions.DependencyInjection": ".NET DI",
1097
+ "Microsoft.Extensions.Configuration": ".NET Configuration",
1098
+ "Microsoft.Extensions.Logging": ".NET Logging",
1099
+ "Microsoft.Extensions.Options": ".NET Options",
1100
+ "Microsoft.Extensions.Caching.Memory": ".NET Memory Cache",
1101
+ "Microsoft.Extensions.Caching.StackExchangeRedis": ".NET Redis Cache",
1102
+ "Microsoft.Extensions.Http": ".NET HttpClientFactory",
1103
+ "Microsoft.Extensions.Resilience": ".NET Resilience",
1104
+ // ── CQRS & Mediator ──
1105
+ "MediatR": "MediatR",
1106
+ "Wolverine": "Wolverine",
1107
+ "Brighter": "Brighter",
1108
+ // ── Mapping ──
1109
+ "AutoMapper": "AutoMapper",
1110
+ "Mapster": "Mapster",
1111
+ "Riok.Mapperly": "Mapperly",
1112
+ // ── Validation ──
1113
+ "FluentValidation": "FluentValidation",
1114
+ "FluentValidation.AspNetCore": "FluentValidation ASP.NET",
1115
+ // ── Serialization ──
1116
+ "Newtonsoft.Json": "Newtonsoft.Json",
1117
+ "System.Text.Json": "System.Text.Json",
1118
+ "MessagePack": "MessagePack",
1119
+ "protobuf-net": "protobuf-net",
1120
+ "CsvHelper": "CsvHelper",
1121
+ // ── Logging & Observability ──
1122
+ "Serilog": "Serilog",
1123
+ "Serilog.AspNetCore": "Serilog ASP.NET",
1124
+ "Serilog.Sinks.Console": "Serilog Console",
1125
+ "Serilog.Sinks.Seq": "Serilog Seq",
1126
+ "Serilog.Sinks.File": "Serilog File",
1127
+ "Serilog.Sinks.Elasticsearch": "Serilog Elasticsearch",
1128
+ "NLog": "NLog",
1129
+ "NLog.Web.AspNetCore": "NLog ASP.NET",
1130
+ "log4net": "log4net",
1131
+ "OpenTelemetry": "OpenTelemetry",
1132
+ "OpenTelemetry.Extensions.Hosting": "OpenTelemetry Hosting",
1133
+ "OpenTelemetry.Instrumentation.AspNetCore": "OpenTelemetry ASP.NET",
1134
+ "OpenTelemetry.Exporter.Prometheus": "OpenTelemetry Prometheus",
1135
+ "OpenTelemetry.Exporter.Jaeger": "OpenTelemetry Jaeger",
1136
+ "OpenTelemetry.Exporter.OpenTelemetryProtocol": "OpenTelemetry OTLP",
1137
+ "App.Metrics": "App.Metrics",
1138
+ "prometheus-net": "Prometheus.NET",
1139
+ "Elastic.Apm": "Elastic APM",
1140
+ // ── Testing ──
1141
+ "xunit": "xUnit",
1142
+ "xunit.runner.visualstudio": "xUnit Runner",
1143
+ "NUnit": "NUnit",
1144
+ "NUnit3TestAdapter": "NUnit Adapter",
1145
+ "MSTest.TestFramework": "MSTest",
1146
+ "MSTest.TestAdapter": "MSTest Adapter",
1147
+ "Moq": "Moq",
1148
+ "NSubstitute": "NSubstitute",
1149
+ "FakeItEasy": "FakeItEasy",
1150
+ "FluentAssertions": "FluentAssertions",
1151
+ "Shouldly": "Shouldly",
1152
+ "Bogus": "Bogus",
1153
+ "AutoFixture": "AutoFixture",
1154
+ "WireMock.Net": "WireMock.Net",
1155
+ "Testcontainers": "Testcontainers",
1156
+ "Respawn": "Respawn",
1157
+ "BenchmarkDotNet": "BenchmarkDotNet",
1158
+ "coverlet.collector": "Coverlet",
1159
+ "SpecFlow": "SpecFlow",
1160
+ "TUnit": "TUnit",
1161
+ "Verify.Xunit": "Verify",
1162
+ "Snapshooter": "Snapshooter",
1163
+ // ── Messaging & Event Bus ──
1164
+ "MassTransit": "MassTransit",
1165
+ "MassTransit.RabbitMQ": "MassTransit RabbitMQ",
1166
+ "MassTransit.Azure.ServiceBus.Core": "MassTransit Azure SB",
1167
+ "NServiceBus": "NServiceBus",
1168
+ "RabbitMQ.Client": "RabbitMQ Client",
1169
+ "Confluent.Kafka": "Confluent Kafka",
1170
+ "Azure.Messaging.ServiceBus": "Azure Service Bus",
1171
+ "Azure.Messaging.EventHubs": "Azure Event Hubs",
1172
+ "Amazon.SQS": "AWS SQS",
1173
+ "Amazon.SNS": "AWS SNS",
1174
+ "Rebus": "Rebus",
1175
+ "EasyNetQ": "EasyNetQ",
1176
+ "SlimMessageBus": "SlimMessageBus",
1177
+ "CAP": "DotNetCore.CAP",
1178
+ // ── Cloud SDKs ──
1179
+ "AWSSDK.Core": "AWS SDK Core",
1180
+ "AWSSDK.S3": "AWS SDK S3",
1181
+ "AWSSDK.SQS": "AWS SDK SQS",
1182
+ "AWSSDK.DynamoDBv2": "AWS SDK DynamoDB",
1183
+ "AWSSDK.Lambda": "AWS SDK Lambda",
1184
+ "AWSSDK.SecretsManager": "AWS SDK Secrets Manager",
1185
+ "AWSSDK.CloudWatch": "AWS SDK CloudWatch",
1186
+ "Azure.Storage.Blobs": "Azure Blob Storage",
1187
+ "Azure.Identity": "Azure Identity",
1188
+ "Azure.Security.KeyVault.Secrets": "Azure Key Vault",
1189
+ "Azure.Cosmos": "Azure Cosmos DB",
1190
+ "Microsoft.Azure.Functions.Worker": "Azure Functions",
1191
+ "Google.Cloud.Storage.V1": "GCP Storage",
1192
+ "Google.Cloud.PubSub.V1": "GCP Pub/Sub",
1193
+ "Google.Cloud.Firestore": "GCP Firestore",
1194
+ // ── Auth & Identity ──
1195
+ "Microsoft.Identity.Web": "Microsoft Identity Web",
1196
+ "Microsoft.Identity.Client": "MSAL",
1197
+ "IdentityServer4": "IdentityServer4",
1198
+ "Duende.IdentityServer": "Duende IdentityServer",
1199
+ "Microsoft.AspNetCore.Authentication.OpenIdConnect": "OpenID Connect",
1200
+ "IdentityModel": "IdentityModel",
1201
+ // ── HTTP & API ──
1202
+ "Refit": "Refit",
1203
+ "RestSharp": "RestSharp",
1204
+ "Flurl.Http": "Flurl",
1205
+ "Polly": "Polly",
1206
+ "Polly.Extensions.Http": "Polly HTTP",
1207
+ "Microsoft.Extensions.Http.Polly": "HttpClient Polly",
1208
+ "Grpc.AspNetCore": "gRPC ASP.NET",
1209
+ "Grpc.Net.Client": "gRPC Client",
1210
+ "GraphQL.Server.All": "GraphQL Server",
1211
+ "HotChocolate.AspNetCore": "Hot Chocolate (GraphQL)",
1212
+ // ── Background Processing ──
1213
+ "Hangfire": "Hangfire",
1214
+ "Hangfire.Core": "Hangfire Core",
1215
+ "Hangfire.AspNetCore": "Hangfire ASP.NET",
1216
+ "Quartz": "Quartz.NET",
1217
+ "Quartz.Extensions.Hosting": "Quartz.NET Hosting",
1218
+ "Coravel": "Coravel",
1219
+ // ── File & Document ──
1220
+ "EPPlus": "EPPlus",
1221
+ "ClosedXML": "ClosedXML",
1222
+ "iTextSharp": "iTextSharp",
1223
+ "QuestPDF": "QuestPDF",
1224
+ "ImageSharp": "ImageSharp",
1225
+ "SixLabors.ImageSharp": "ImageSharp",
1226
+ // ── Feature Flags & Config ──
1227
+ "LaunchDarkly.ServerSdk": "LaunchDarkly",
1228
+ "Microsoft.FeatureManagement": "Feature Management",
1229
+ "Microsoft.FeatureManagement.AspNetCore": "Feature Management ASP.NET",
1230
+ // ── Microservices & Distributed ──
1231
+ "Dapr.Client": "Dapr",
1232
+ "Steeltoe.Discovery.ClientCore": "Steeltoe",
1233
+ "Ocelot": "Ocelot (API Gateway)",
1234
+ "Yarp.ReverseProxy": "YARP",
1235
+ // ── Real-time ──
1236
+ "Microsoft.AspNetCore.SignalR.Client": "SignalR Client"
1237
+ };
1238
+ var LATEST_DOTNET_MAJOR = 9;
1239
+ function parseTfmMajor(tfm) {
1240
+ const match = tfm.match(/^net(\d+)\.\d+$/);
1241
+ if (match?.[1]) return parseInt(match[1], 10);
1242
+ const coreMatch = tfm.match(/^netcoreapp(\d+)\.\d+$/);
1243
+ if (coreMatch?.[1]) return parseInt(coreMatch[1], 10);
1244
+ if (tfm.startsWith("netstandard")) return null;
1245
+ const fxMatch = tfm.match(/^net(\d)(\d+)?$/);
1246
+ if (fxMatch) return null;
1247
+ return null;
1248
+ }
1249
+ function parseCsproj(xml, filePath) {
1250
+ const parsed = parser.parse(xml);
1251
+ const project = parsed?.Project;
1252
+ if (!project) {
1253
+ return { targetFrameworks: [], packageReferences: [], projectName: path3.basename(filePath, ".csproj") };
1254
+ }
1255
+ const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
1256
+ const targetFrameworks = [];
1257
+ for (const pg of propertyGroups) {
1258
+ if (pg.TargetFramework) {
1259
+ targetFrameworks.push(String(pg.TargetFramework));
1260
+ }
1261
+ if (pg.TargetFrameworks) {
1262
+ const tfms = String(pg.TargetFrameworks).split(";").map((s) => s.trim()).filter(Boolean);
1263
+ targetFrameworks.push(...tfms);
1264
+ }
1265
+ }
1266
+ const itemGroups = Array.isArray(project.ItemGroup) ? project.ItemGroup : project.ItemGroup ? [project.ItemGroup] : [];
1267
+ const packageReferences = [];
1268
+ for (const ig of itemGroups) {
1269
+ const refs = Array.isArray(ig.PackageReference) ? ig.PackageReference : ig.PackageReference ? [ig.PackageReference] : [];
1270
+ for (const ref of refs) {
1271
+ const name = ref["@_Include"] ?? ref["@_include"] ?? "";
1272
+ const version = ref["@_Version"] ?? ref["@_version"] ?? ref.Version ?? "";
1273
+ if (name && version) {
1274
+ packageReferences.push({ name: String(name), version: String(version) });
1275
+ }
1276
+ }
1277
+ }
1278
+ return {
1279
+ targetFrameworks: [...new Set(targetFrameworks)],
1280
+ packageReferences,
1281
+ projectName: path3.basename(filePath, ".csproj")
1282
+ };
1283
+ }
1284
+ async function scanDotnetProjects(rootDir) {
1285
+ const csprojFiles = await findCsprojFiles(rootDir);
1286
+ const slnFiles = await findSolutionFiles(rootDir);
1287
+ const slnCsprojPaths = /* @__PURE__ */ new Set();
1288
+ for (const slnPath of slnFiles) {
1289
+ try {
1290
+ const slnContent = await readTextFile(slnPath);
1291
+ const slnDir = path3.dirname(slnPath);
1292
+ const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
1293
+ let match;
1294
+ while ((match = projectRegex.exec(slnContent)) !== null) {
1295
+ if (match[1]) {
1296
+ const csprojPath = path3.resolve(slnDir, match[1].replace(/\\/g, "/"));
1297
+ slnCsprojPaths.add(csprojPath);
1298
+ }
1299
+ }
1300
+ } catch {
1301
+ }
1302
+ }
1303
+ const allCsprojFiles = /* @__PURE__ */ new Set([...csprojFiles, ...slnCsprojPaths]);
1304
+ const results = [];
1305
+ for (const csprojPath of allCsprojFiles) {
1306
+ try {
1307
+ const scan = await scanOneCsproj(csprojPath, rootDir);
1308
+ results.push(scan);
1309
+ } catch (e) {
1310
+ const msg = e instanceof Error ? e.message : String(e);
1311
+ console.error(`Error scanning ${csprojPath}: ${msg}`);
1312
+ }
1313
+ }
1314
+ return results;
1315
+ }
1316
+ async function scanOneCsproj(csprojPath, rootDir) {
1317
+ const xml = await readTextFile(csprojPath);
1318
+ const data = parseCsproj(xml, csprojPath);
1319
+ const primaryTfm = data.targetFrameworks[0];
1320
+ let runtimeMajorsBehind;
1321
+ let targetFramework = primaryTfm;
1322
+ if (primaryTfm) {
1323
+ const major2 = parseTfmMajor(primaryTfm);
1324
+ if (major2 !== null) {
1325
+ runtimeMajorsBehind = Math.max(0, LATEST_DOTNET_MAJOR - major2);
1326
+ }
1327
+ }
1328
+ const dependencies = data.packageReferences.map((ref) => ({
1329
+ package: ref.name,
1330
+ section: "dependencies",
1331
+ currentSpec: ref.version,
1332
+ resolvedVersion: ref.version,
1333
+ latestStable: null,
1334
+ // NuGet lookup not implemented in v1
1335
+ majorsBehind: null,
1336
+ drift: "unknown"
1337
+ }));
1338
+ const frameworks = [];
1339
+ for (const ref of data.packageReferences) {
1340
+ if (ref.name in KNOWN_DOTNET_FRAMEWORKS) {
1341
+ frameworks.push({
1342
+ name: KNOWN_DOTNET_FRAMEWORKS[ref.name],
1343
+ currentVersion: ref.version,
1344
+ latestVersion: null,
1345
+ majorsBehind: null
1346
+ });
1347
+ }
1348
+ }
1349
+ const buckets = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: dependencies.length };
1350
+ return {
1351
+ type: "dotnet",
1352
+ path: path3.relative(rootDir, path3.dirname(csprojPath)) || ".",
1353
+ name: data.projectName,
1354
+ targetFramework,
1355
+ runtime: primaryTfm,
1356
+ runtimeLatest: `net${LATEST_DOTNET_MAJOR}.0`,
1357
+ runtimeMajorsBehind,
1358
+ frameworks,
1359
+ dependencies,
1360
+ dependencyAgeBuckets: buckets
1361
+ };
1362
+ }
1363
+
1364
+ // src/utils/semaphore.ts
1365
+ var Semaphore = class {
1366
+ available;
1367
+ queue = [];
1368
+ constructor(max) {
1369
+ this.available = max;
1370
+ }
1371
+ async run(fn) {
1372
+ await this.acquire();
1373
+ try {
1374
+ return await fn();
1375
+ } finally {
1376
+ this.release();
1377
+ }
1378
+ }
1379
+ acquire() {
1380
+ if (this.available > 0) {
1381
+ this.available--;
1382
+ return Promise.resolve();
1383
+ }
1384
+ return new Promise((resolve4) => this.queue.push(resolve4));
1385
+ }
1386
+ release() {
1387
+ const next = this.queue.shift();
1388
+ if (next) next();
1389
+ else this.available++;
1390
+ }
1391
+ };
1392
+
1393
+ // src/config.ts
1394
+ import * as path4 from "path";
1395
+ import * as fs2 from "fs/promises";
1396
+ var CONFIG_FILES = [
1397
+ "vibgrate.config.ts",
1398
+ "vibgrate.config.js",
1399
+ "vibgrate.config.json"
1400
+ ];
1401
+ var DEFAULT_CONFIG = {
1402
+ exclude: [],
1403
+ thresholds: {
1404
+ failOnError: {
1405
+ eolDays: 180,
1406
+ frameworkMajorLag: 3,
1407
+ dependencyTwoPlusPercent: 50
1408
+ },
1409
+ warn: {
1410
+ frameworkMajorLag: 2,
1411
+ dependencyTwoPlusPercent: 30
1412
+ }
1413
+ }
1414
+ };
1415
+ async function loadConfig(rootDir) {
1416
+ for (const file of CONFIG_FILES) {
1417
+ const configPath = path4.join(rootDir, file);
1418
+ if (await pathExists(configPath)) {
1419
+ if (file.endsWith(".json")) {
1420
+ const txt = await readTextFile(configPath);
1421
+ return { ...DEFAULT_CONFIG, ...JSON.parse(txt) };
1422
+ }
1423
+ try {
1424
+ const mod = await import(configPath);
1425
+ return { ...DEFAULT_CONFIG, ...mod.default ?? mod };
1426
+ } catch {
1427
+ }
1428
+ }
1429
+ }
1430
+ return DEFAULT_CONFIG;
1431
+ }
1432
+ async function writeDefaultConfig(rootDir) {
1433
+ const configPath = path4.join(rootDir, "vibgrate.config.ts");
1434
+ const content = `import type { VibgrateConfig } from '@vibgrate/cli';
1435
+
1436
+ const config: VibgrateConfig = {
1437
+ // exclude: ['legacy/**'],
1438
+ thresholds: {
1439
+ failOnError: {
1440
+ eolDays: 180,
1441
+ frameworkMajorLag: 3,
1442
+ dependencyTwoPlusPercent: 50,
1443
+ },
1444
+ warn: {
1445
+ frameworkMajorLag: 2,
1446
+ dependencyTwoPlusPercent: 30,
1447
+ },
1448
+ },
1449
+ };
1450
+
1451
+ export default config;
1452
+ `;
1453
+ await fs2.writeFile(configPath, content, "utf8");
1454
+ return configPath;
1455
+ }
1456
+
1457
+ // src/utils/vcs.ts
1458
+ import * as path5 from "path";
1459
+ import * as fs3 from "fs/promises";
1460
+ async function detectVcs(rootDir) {
1461
+ try {
1462
+ return await detectGit(rootDir);
1463
+ } catch {
1464
+ return { type: "unknown" };
1465
+ }
1466
+ }
1467
+ async function detectGit(rootDir) {
1468
+ const gitDir = await findGitDir(rootDir);
1469
+ if (!gitDir) {
1470
+ return { type: "unknown" };
1471
+ }
1472
+ const headPath = path5.join(gitDir, "HEAD");
1473
+ let headContent;
1474
+ try {
1475
+ headContent = (await fs3.readFile(headPath, "utf8")).trim();
1476
+ } catch {
1477
+ return { type: "unknown" };
1478
+ }
1479
+ let sha;
1480
+ let branch;
1481
+ if (headContent.startsWith("ref: ")) {
1482
+ const refPath = headContent.slice(5);
1483
+ branch = refPath.startsWith("refs/heads/") ? refPath.slice(11) : refPath;
1484
+ sha = await resolveRef(gitDir, refPath);
1485
+ } else if (/^[0-9a-f]{40}$/i.test(headContent)) {
1486
+ sha = headContent;
1487
+ }
1488
+ return {
1489
+ type: "git",
1490
+ sha: sha ?? void 0,
1491
+ shortSha: sha ? sha.slice(0, 7) : void 0,
1492
+ branch: branch ?? void 0
1493
+ };
1494
+ }
1495
+ async function findGitDir(startDir) {
1496
+ let dir = path5.resolve(startDir);
1497
+ const root = path5.parse(dir).root;
1498
+ while (dir !== root) {
1499
+ const gitPath = path5.join(dir, ".git");
1500
+ try {
1501
+ const stat3 = await fs3.stat(gitPath);
1502
+ if (stat3.isDirectory()) {
1503
+ return gitPath;
1504
+ }
1505
+ if (stat3.isFile()) {
1506
+ const content = (await fs3.readFile(gitPath, "utf8")).trim();
1507
+ if (content.startsWith("gitdir: ")) {
1508
+ const resolved = path5.resolve(dir, content.slice(8));
1509
+ return resolved;
1510
+ }
1511
+ }
1512
+ } catch {
1513
+ }
1514
+ dir = path5.dirname(dir);
1515
+ }
1516
+ return null;
1517
+ }
1518
+ async function resolveRef(gitDir, refPath) {
1519
+ const loosePath = path5.join(gitDir, refPath);
1520
+ try {
1521
+ const sha = (await fs3.readFile(loosePath, "utf8")).trim();
1522
+ if (/^[0-9a-f]{40}$/i.test(sha)) {
1523
+ return sha;
1524
+ }
1525
+ } catch {
1526
+ }
1527
+ const packedPath = path5.join(gitDir, "packed-refs");
1528
+ try {
1529
+ const packed = await fs3.readFile(packedPath, "utf8");
1530
+ for (const line of packed.split("\n")) {
1531
+ if (line.startsWith("#") || line.startsWith("^")) continue;
1532
+ const parts = line.trim().split(" ");
1533
+ if (parts.length >= 2 && parts[1] === refPath) {
1534
+ return parts[0];
1535
+ }
1536
+ }
1537
+ } catch {
1538
+ }
1539
+ return void 0;
1540
+ }
1541
+
1542
+ // src/ui/progress.ts
1543
+ import chalk2 from "chalk";
1544
+ var ROBOT = [
1545
+ chalk2.cyan(" \u256D\u2500\u2500\u2500\u256E") + chalk2.greenBright("\u279C"),
1546
+ chalk2.cyan(" \u256D\u2524") + chalk2.greenBright("\u25C9 \u25C9") + chalk2.cyan("\u251C\u256E"),
1547
+ chalk2.cyan(" \u2570\u2524") + chalk2.dim("\u2500\u2500\u2500") + chalk2.cyan("\u251C\u256F"),
1548
+ chalk2.cyan(" \u2570\u2500\u2500\u2500\u256F")
1549
+ ];
1550
+ var BRAND = [
1551
+ chalk2.bold.white(" V I B G R A T E"),
1552
+ chalk2.dim(" Drift Intelligence Engine")
1553
+ ];
1554
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1555
+ var ScanProgress = class {
1556
+ steps = [];
1557
+ stats = {
1558
+ projects: 0,
1559
+ dependencies: 0,
1560
+ frameworks: 0,
1561
+ findings: { warnings: 0, errors: 0, notes: 0 }
1562
+ };
1563
+ spinnerFrame = 0;
1564
+ timer = null;
1565
+ lastLineCount = 0;
1566
+ startTime = Date.now();
1567
+ isTTY;
1568
+ rootDir = "";
1569
+ constructor(rootDir) {
1570
+ this.isTTY = process.stderr.isTTY ?? false;
1571
+ this.rootDir = rootDir;
1572
+ }
1573
+ /** Register all steps up front */
1574
+ setSteps(steps) {
1575
+ this.steps = steps.map((s) => ({ ...s, status: "pending" }));
1576
+ if (this.isTTY) {
1577
+ this.startSpinner();
1578
+ }
1579
+ this.render();
1580
+ }
1581
+ /** Mark a step as active (currently running) */
1582
+ startStep(id) {
1583
+ const step = this.steps.find((s) => s.id === id);
1584
+ if (step) {
1585
+ step.status = "active";
1586
+ step.detail = void 0;
1587
+ step.count = void 0;
1588
+ }
1589
+ this.render();
1590
+ }
1591
+ /** Mark a step as completed */
1592
+ completeStep(id, detail, count) {
1593
+ const step = this.steps.find((s) => s.id === id);
1594
+ if (step) {
1595
+ step.status = "done";
1596
+ step.detail = detail;
1597
+ step.count = count;
1598
+ }
1599
+ this.render();
1600
+ }
1601
+ /** Mark a step as skipped */
1602
+ skipStep(id) {
1603
+ const step = this.steps.find((s) => s.id === id);
1604
+ if (step) {
1605
+ step.status = "skipped";
1606
+ step.detail = "disabled";
1607
+ }
1608
+ this.render();
1609
+ }
1610
+ /** Update live stats */
1611
+ updateStats(partial) {
1612
+ Object.assign(this.stats, partial);
1613
+ this.render();
1614
+ }
1615
+ /** Increment stats */
1616
+ addProjects(n) {
1617
+ this.stats.projects += n;
1618
+ this.render();
1619
+ }
1620
+ addDependencies(n) {
1621
+ this.stats.dependencies += n;
1622
+ this.render();
1623
+ }
1624
+ addFrameworks(n) {
1625
+ this.stats.frameworks += n;
1626
+ this.render();
1627
+ }
1628
+ addFindings(warnings, errors, notes) {
1629
+ this.stats.findings.warnings += warnings;
1630
+ this.stats.findings.errors += errors;
1631
+ this.stats.findings.notes += notes;
1632
+ this.render();
1633
+ }
1634
+ /** Stop the progress display and clear it */
1635
+ finish() {
1636
+ if (this.timer) {
1637
+ clearInterval(this.timer);
1638
+ this.timer = null;
1639
+ }
1640
+ if (this.isTTY) {
1641
+ this.clearLines();
1642
+ }
1643
+ const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
1644
+ const doneCount = this.steps.filter((s) => s.status === "done").length;
1645
+ process.stderr.write(
1646
+ chalk2.dim(` \u2714 ${doneCount} scanners completed in ${elapsed}s
1647
+
1648
+ `)
1649
+ );
1650
+ }
1651
+ // ── Internal rendering ──
1652
+ startSpinner() {
1653
+ this.timer = setInterval(() => {
1654
+ this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length;
1655
+ this.render();
1656
+ }, 80);
1657
+ }
1658
+ clearLines() {
1659
+ if (this.lastLineCount > 0) {
1660
+ process.stderr.write(`\x1B[${this.lastLineCount}A`);
1661
+ for (let i = 0; i < this.lastLineCount; i++) {
1662
+ process.stderr.write("\x1B[2K\n");
1663
+ }
1664
+ process.stderr.write(`\x1B[${this.lastLineCount}A`);
1665
+ }
1666
+ }
1667
+ render() {
1668
+ if (!this.isTTY) {
1669
+ this.renderCI();
1670
+ return;
1671
+ }
1672
+ this.clearLines();
1673
+ const lines = [];
1674
+ lines.push("");
1675
+ lines.push(` ${ROBOT[0]} ${BRAND[0]}`);
1676
+ lines.push(` ${ROBOT[1]} ${BRAND[1]}`);
1677
+ lines.push(` ${ROBOT[2]}`);
1678
+ lines.push(` ${ROBOT[3]} ${chalk2.dim(this.rootDir)}`);
1679
+ lines.push("");
1680
+ const totalSteps = this.steps.length;
1681
+ const doneSteps = this.steps.filter((s) => s.status === "done" || s.status === "skipped").length;
1682
+ const pct = totalSteps > 0 ? Math.round(doneSteps / totalSteps * 100) : 0;
1683
+ const barWidth = 30;
1684
+ const filled = Math.round(doneSteps / Math.max(totalSteps, 1) * barWidth);
1685
+ const bar = chalk2.greenBright("\u2501".repeat(filled)) + chalk2.dim("\u254C".repeat(barWidth - filled));
1686
+ const elapsed = ((Date.now() - this.startTime) / 1e3).toFixed(1);
1687
+ lines.push(` ${bar} ${chalk2.bold.white(`${pct}%`)} ${chalk2.dim(`${elapsed}s`)}`);
1688
+ lines.push("");
1689
+ for (const step of this.steps) {
1690
+ lines.push(this.renderStep(step));
1691
+ }
1692
+ lines.push("");
1693
+ lines.push(this.renderStats());
1694
+ lines.push("");
1695
+ const output = lines.join("\n") + "\n";
1696
+ process.stderr.write(output);
1697
+ this.lastLineCount = lines.length;
1698
+ }
1699
+ renderStep(step) {
1700
+ const spinner = SPINNER_FRAMES[this.spinnerFrame];
1701
+ let icon;
1702
+ let label;
1703
+ let detail = "";
1704
+ switch (step.status) {
1705
+ case "done":
1706
+ icon = chalk2.green("\u2714");
1707
+ label = chalk2.white(step.label);
1708
+ break;
1709
+ case "active":
1710
+ icon = chalk2.cyan(spinner);
1711
+ label = chalk2.bold.white(step.label);
1712
+ break;
1713
+ case "skipped":
1714
+ icon = chalk2.dim("\u25CC");
1715
+ label = chalk2.dim.strikethrough(step.label);
1716
+ break;
1717
+ default:
1718
+ icon = chalk2.dim("\u25CB");
1719
+ label = chalk2.dim(step.label);
1720
+ break;
1721
+ }
1722
+ if (step.detail) {
1723
+ detail = chalk2.dim(` \xB7 ${step.detail}`);
1724
+ }
1725
+ if (step.count !== void 0 && step.count > 0) {
1726
+ detail += chalk2.cyan(` (${step.count})`);
1727
+ }
1728
+ return ` ${icon} ${label}${detail}`;
1729
+ }
1730
+ renderStats() {
1731
+ const p = this.stats.projects;
1732
+ const d = this.stats.dependencies;
1733
+ const f = this.stats.frameworks;
1734
+ const w = this.stats.findings.warnings;
1735
+ const e = this.stats.findings.errors;
1736
+ const n = this.stats.findings.notes;
1737
+ const parts = [
1738
+ chalk2.bold.white(` ${p}`) + chalk2.dim(` project${p !== 1 ? "s" : ""}`),
1739
+ chalk2.white(`${d}`) + chalk2.dim(` dep${d !== 1 ? "s" : ""}`),
1740
+ chalk2.white(`${f}`) + chalk2.dim(` framework${f !== 1 ? "s" : ""}`)
1741
+ ];
1742
+ const findingParts = [];
1743
+ if (e > 0) findingParts.push(chalk2.red(`${e} \u2716`));
1744
+ if (w > 0) findingParts.push(chalk2.yellow(`${w} \u26A0`));
1745
+ if (n > 0) findingParts.push(chalk2.blue(`${n} \u2139`));
1746
+ if (findingParts.length > 0) {
1747
+ parts.push(findingParts.join(chalk2.dim(" \xB7 ")));
1748
+ }
1749
+ return ` ${chalk2.dim("\u2503")} ${parts.join(chalk2.dim(" \u2502 "))}`;
1750
+ }
1751
+ /** Simple CI-friendly output (no ANSI rewriting) */
1752
+ lastCIStep = null;
1753
+ renderCI() {
1754
+ const active = this.steps.find((s) => s.status === "active");
1755
+ if (active && active.id !== this.lastCIStep) {
1756
+ this.lastCIStep = active.id;
1757
+ process.stderr.write(` \u25C9 ${active.label}...
1758
+ `);
1759
+ }
1760
+ for (const step of this.steps) {
1761
+ if (step.status === "done" && step.id !== this.lastCIStep) {
1762
+ }
1763
+ }
1764
+ }
1765
+ };
1766
+
1767
+ // src/scanners/platform-matrix.ts
1768
+ import * as path6 from "path";
1769
+ var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
1770
+ // Image / media processing
1771
+ "sharp",
1772
+ // libvips native bindings
1773
+ "canvas",
1774
+ // node-canvas (Cairo native)
1775
+ "jimp",
1776
+ // pure JS but optionally uses native
1777
+ "@napi-rs/image",
1778
+ // NAPI-RS image processing
1779
+ "imagemagick",
1780
+ // ImageMagick bindings
1781
+ "gm",
1782
+ // GraphicsMagick
1783
+ "fluent-ffmpeg",
1784
+ // FFmpeg bindings
1785
+ "ffmpeg-static",
1786
+ // bundled FFmpeg binary
1787
+ "puppeteer",
1788
+ // ships Chromium binary
1789
+ "playwright",
1790
+ // ships browser binaries
1791
+ "playwright-core",
1792
+ // browser binaries
1793
+ // Cryptography / security
1794
+ "bcrypt",
1795
+ // native bcrypt
1796
+ "argon2",
1797
+ // native argon2 hashing
1798
+ "sodium-native",
1799
+ // libsodium bindings
1800
+ "libsodium-wrappers",
1801
+ // libsodium WASM/native
1802
+ "node-forge",
1803
+ // mostly JS but linked to native in some envs
1804
+ "ssh2",
1805
+ // native SSH bindings (optional)
1806
+ "keytar",
1807
+ // OS keychain native bindings
1808
+ // Database drivers
1809
+ "better-sqlite3",
1810
+ // native SQLite
1811
+ "sqlite3",
1812
+ // native SQLite
1813
+ "pg-native",
1814
+ // PostgreSQL native bindings
1815
+ "oracledb",
1816
+ // Oracle native client
1817
+ "odbc",
1818
+ // native ODBC
1819
+ "ibm_db",
1820
+ // IBM DB2 native
1821
+ "couchbase",
1822
+ // native Couchbase SDK
1823
+ "rocksdb",
1824
+ // native RocksDB store
1825
+ "leveldown",
1826
+ // native LevelDB
1827
+ "lmdb",
1828
+ // native LMDB bindings
1829
+ // Compilation & build tools
1830
+ "node-gyp",
1831
+ // native build tool itself
1832
+ "node-pre-gyp",
1833
+ // native binary distribution
1834
+ "@mapbox/node-pre-gyp",
1835
+ // native binary distribution
1836
+ "prebuild",
1837
+ // prebuilt native bindings
1838
+ "prebuild-install",
1839
+ // prebuilt native installer
1840
+ "esbuild",
1841
+ // platform-specific Go binary
1842
+ "@swc/core",
1843
+ // platform-specific Rust binary
1844
+ "@rspack/core",
1845
+ // platform-specific Rust binary
1846
+ "@biomejs/biome",
1847
+ // platform-specific Rust binary
1848
+ "node-sass",
1849
+ // deprecated, native libsass
1850
+ "sass-embedded",
1851
+ // Dart Sass embedded binary
1852
+ "turbo",
1853
+ // Turborepo Go/Rust binary
1854
+ "@vercel/nft",
1855
+ // native file tracing (optional)
1856
+ // System / hardware access
1857
+ "fsevents",
1858
+ // macOS-only file watching
1859
+ "cpu-features",
1860
+ // CPU instruction detection
1861
+ "deasync",
1862
+ // native event loop control
1863
+ "usb",
1864
+ // USB device access
1865
+ "serialport",
1866
+ // serial port access
1867
+ "node-hid",
1868
+ // HID device access
1869
+ "i2c-bus",
1870
+ // I2C bus access
1871
+ "spi-device",
1872
+ // SPI bus access
1873
+ "node-bluetooth",
1874
+ // Bluetooth
1875
+ "mdns",
1876
+ // mDNS/Bonjour
1877
+ // Compression
1878
+ "snappy",
1879
+ // native Snappy compression
1880
+ "zstd-napi",
1881
+ // native Zstandard
1882
+ "lz4",
1883
+ // native LZ4
1884
+ "brotli",
1885
+ // native Brotli (older, node has built-in)
1886
+ // Regex / text
1887
+ "re2",
1888
+ // native RE2 regex engine
1889
+ "oniguruma",
1890
+ // native Oniguruma regex
1891
+ "vscode-oniguruma",
1892
+ // native Oniguruma (VS Code)
1893
+ "tree-sitter",
1894
+ // native parser generator
1895
+ "node-tree-sitter",
1896
+ // native Tree-sitter
1897
+ // XML / HTML
1898
+ "libxmljs",
1899
+ // native libxml2
1900
+ "libxmljs2",
1901
+ // native libxml2
1902
+ "node-expat",
1903
+ // native Expat XML parser
1904
+ "htmlparser2",
1905
+ // mostly JS, optional native
1906
+ // Networking / IPC
1907
+ "@grpc/grpc-js",
1908
+ // gRPC (JS but has native dep for HTTP/2)
1909
+ "grpc",
1910
+ // deprecated native gRPC
1911
+ "zeromq",
1912
+ // native ZeroMQ
1913
+ "nanomsg",
1914
+ // native nanomsg
1915
+ "unix-dgram",
1916
+ // native Unix sockets
1917
+ // Observability
1918
+ "dtrace-provider",
1919
+ // native DTrace
1920
+ "v8-profiler-next",
1921
+ // native V8 profiler
1922
+ "heapdump",
1923
+ // native V8 heap dump
1924
+ // Misc
1925
+ "farmhash",
1926
+ // native FarmHash
1927
+ "xxhash",
1928
+ // native xxHash
1929
+ "xxhash-addon",
1930
+ // native xxHash
1931
+ "iconv",
1932
+ // native iconv
1933
+ "ref-napi",
1934
+ // native FFI
1935
+ "ffi-napi",
1936
+ // native FFI
1937
+ "node-pty",
1938
+ // native pseudo-terminal
1939
+ "robotjs",
1940
+ // native desktop automation
1941
+ "electron",
1942
+ // ships Chromium + Node binary
1943
+ "xdg-open",
1944
+ // OS-specific
1945
+ "windows-process-tree"
1946
+ // Windows-only
1947
+ ]);
1948
+ var OS_PATTERNS = [
1949
+ { pattern: /\bcmd\.exe\b|\.bat\b|\.cmd\b/i, label: "windows-scripts" },
1950
+ { pattern: /\bpowershell\b|\bpwsh\b/i, label: "powershell" },
1951
+ { pattern: /\bbash\b|#!\/bin\/bash/i, label: "bash-scripts" },
1952
+ { pattern: /\\\\/g, label: "backslash-paths" }
1953
+ ];
1954
+ async function scanPlatformMatrix(rootDir) {
1955
+ const result = {
1956
+ dotnetTargetFrameworks: [],
1957
+ nativeModules: [],
1958
+ osAssumptions: [],
1959
+ dockerBaseImages: [],
1960
+ nodeVersionFiles: []
1961
+ };
1962
+ const pkgFiles = await findPackageJsonFiles(rootDir);
1963
+ const allDeps = /* @__PURE__ */ new Set();
1964
+ const osAssumptions = /* @__PURE__ */ new Set();
1965
+ for (const pjPath of pkgFiles) {
1966
+ try {
1967
+ const pj = await readJsonFile(pjPath);
1968
+ if (pj.engines?.node && !result.nodeEngines) result.nodeEngines = pj.engines.node;
1969
+ if (pj.engines?.npm && !result.npmEngines) result.npmEngines = pj.engines.npm;
1970
+ if (pj.engines?.pnpm && !result.pnpmEngines) {
1971
+ result.pnpmEngines = pj.engines.pnpm;
1972
+ }
1973
+ for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) {
1974
+ const deps = pj[section];
1975
+ if (deps) {
1976
+ for (const name of Object.keys(deps)) {
1977
+ allDeps.add(name);
1978
+ }
1979
+ }
1980
+ }
1981
+ const scripts = pj.scripts;
1982
+ if (scripts && typeof scripts === "object") {
1983
+ for (const val of Object.values(scripts)) {
1984
+ if (typeof val !== "string") continue;
1985
+ const firstToken = val.split(/\s/)[0] ?? "";
1986
+ for (const { pattern, label } of OS_PATTERNS) {
1987
+ if (pattern.test(firstToken)) {
1988
+ osAssumptions.add(label);
1989
+ }
1990
+ }
1991
+ }
1992
+ }
1993
+ } catch {
1994
+ }
1995
+ }
1996
+ for (const dep of allDeps) {
1997
+ if (NATIVE_MODULE_PACKAGES.has(dep)) {
1998
+ result.nativeModules.push(dep);
1999
+ }
2000
+ }
2001
+ result.nativeModules.sort();
2002
+ result.osAssumptions = [...osAssumptions].sort();
2003
+ const csprojFiles = await findFiles(rootDir, (name) => name.endsWith(".csproj"));
2004
+ const tfms = /* @__PURE__ */ new Set();
2005
+ for (const csprojPath of csprojFiles) {
2006
+ try {
2007
+ const xml = await readTextFile(csprojPath);
2008
+ const tfMatch = xml.match(/<TargetFramework>(.*?)<\/TargetFramework>/);
2009
+ if (tfMatch?.[1]) tfms.add(tfMatch[1]);
2010
+ const tfsMatch = xml.match(/<TargetFrameworks>(.*?)<\/TargetFrameworks>/);
2011
+ if (tfsMatch?.[1]) {
2012
+ for (const tfm of tfsMatch[1].split(";")) {
2013
+ if (tfm.trim()) tfms.add(tfm.trim());
2014
+ }
2015
+ }
2016
+ } catch {
2017
+ }
2018
+ }
2019
+ result.dotnetTargetFrameworks = [...tfms].sort();
2020
+ const dockerfiles = await findFiles(
2021
+ rootDir,
2022
+ (name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
2023
+ );
2024
+ const baseImages = /* @__PURE__ */ new Set();
2025
+ for (const df of dockerfiles) {
2026
+ try {
2027
+ const content = await readTextFile(df);
2028
+ for (const line of content.split("\n")) {
2029
+ const trimmed = line.trim();
2030
+ if (/^FROM\s+/i.test(trimmed)) {
2031
+ const parts = trimmed.split(/\s+/);
2032
+ if (parts[1] && !parts[1].startsWith("--")) {
2033
+ baseImages.add(parts[1]);
2034
+ } else if (parts[1]?.startsWith("--")) {
2035
+ const imageIdx = parts[1].includes("=") ? 2 : 3;
2036
+ if (parts[imageIdx]) baseImages.add(parts[imageIdx]);
2037
+ }
2038
+ }
2039
+ }
2040
+ } catch {
2041
+ }
2042
+ }
2043
+ result.dockerBaseImages = [...baseImages].sort();
2044
+ for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
2045
+ if (await pathExists(path6.join(rootDir, file))) {
2046
+ result.nodeVersionFiles.push(file);
2047
+ }
2048
+ }
2049
+ return result;
2050
+ }
2051
+
2052
+ // src/scanners/dependency-risk.ts
2053
+ var DEPRECATED_PACKAGES = /* @__PURE__ */ new Set([
2054
+ "request",
2055
+ "node-sass",
2056
+ "tslint",
2057
+ "istanbul",
2058
+ "popper.js",
2059
+ "Left-pad",
2060
+ "left-pad",
2061
+ "bower",
2062
+ "grunt",
2063
+ "gulp",
2064
+ "coffee-script",
2065
+ "coffeescript",
2066
+ "merge",
2067
+ "nomnom",
2068
+ "optimist",
2069
+ "natives",
2070
+ "querystring",
2071
+ "domain-browser",
2072
+ "sys",
2073
+ "punycode"
2074
+ ]);
2075
+ var NATIVE_MODULE_PACKAGES2 = /* @__PURE__ */ new Set([
2076
+ "sharp",
2077
+ "canvas",
2078
+ "bcrypt",
2079
+ "node-gyp",
2080
+ "fsevents",
2081
+ "better-sqlite3",
2082
+ "sqlite3",
2083
+ "leveldown",
2084
+ "sodium-native",
2085
+ "node-sass",
2086
+ "argon2",
2087
+ "usb",
2088
+ "serialport",
2089
+ "re2",
2090
+ "libxmljs",
2091
+ "libxmljs2",
2092
+ "cpu-features",
2093
+ "deasync",
2094
+ "farmhash",
2095
+ "grpc",
2096
+ "@grpc/grpc-js"
2097
+ ]);
2098
+ function scanDependencyRisk(projects) {
2099
+ const deprecated = /* @__PURE__ */ new Set();
2100
+ const nativeModules = /* @__PURE__ */ new Set();
2101
+ let totalDeps = 0;
2102
+ for (const project of projects) {
2103
+ for (const dep of project.dependencies) {
2104
+ totalDeps++;
2105
+ if (DEPRECATED_PACKAGES.has(dep.package)) {
2106
+ deprecated.add(dep.package);
2107
+ }
2108
+ if (NATIVE_MODULE_PACKAGES2.has(dep.package)) {
2109
+ nativeModules.add(dep.package);
2110
+ }
2111
+ }
2112
+ }
2113
+ return {
2114
+ deprecatedPackages: [...deprecated].sort(),
2115
+ nativeModulePackages: [...nativeModules].sort(),
2116
+ totalDependencies: totalDeps
2117
+ };
2118
+ }
2119
+
2120
+ // src/scanners/dependency-graph.ts
2121
+ import * as path7 from "path";
2122
+ function parsePnpmLock(content) {
2123
+ const entries = [];
2124
+ const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
2125
+ let match;
2126
+ while ((match = regex.exec(content)) !== null) {
2127
+ if (match[1] && match[2]) {
2128
+ entries.push({ name: match[1], version: match[2] });
2129
+ }
2130
+ }
2131
+ return entries;
2132
+ }
2133
+ function parseNpmLock(content) {
2134
+ const entries = [];
2135
+ try {
2136
+ const lock = JSON.parse(content);
2137
+ if (lock.packages && typeof lock.packages === "object") {
2138
+ for (const [key, value] of Object.entries(lock.packages)) {
2139
+ if (key === "") continue;
2140
+ const v = value;
2141
+ if (v.version) {
2142
+ const name = key.replace(/^node_modules\//, "").replace(/.*node_modules\//, "");
2143
+ entries.push({ name, version: v.version });
2144
+ }
2145
+ }
2146
+ } else if (lock.dependencies && typeof lock.dependencies === "object") {
2147
+ let walkDeps2 = function(deps) {
2148
+ for (const [name, data] of Object.entries(deps)) {
2149
+ if (data.version) entries.push({ name, version: data.version });
2150
+ if (data.dependencies) walkDeps2(data.dependencies);
2151
+ }
2152
+ };
2153
+ var walkDeps = walkDeps2;
2154
+ walkDeps2(lock.dependencies);
2155
+ }
2156
+ } catch {
2157
+ }
2158
+ return entries;
2159
+ }
2160
+ function parseYarnLock(content) {
2161
+ const entries = [];
2162
+ const regex = /^"?(@?[^\s"@]+)@[^:]+:\s*\n\s+version\s+"([^"]+)"/gm;
2163
+ let match;
2164
+ while ((match = regex.exec(content)) !== null) {
2165
+ if (match[1] && match[2]) {
2166
+ entries.push({ name: match[1], version: match[2] });
2167
+ }
2168
+ }
2169
+ return entries;
2170
+ }
2171
+ async function scanDependencyGraph(rootDir) {
2172
+ const result = {
2173
+ lockfileType: null,
2174
+ totalUnique: 0,
2175
+ totalInstalled: 0,
2176
+ duplicatedPackages: [],
2177
+ phantomDependencies: []
2178
+ };
2179
+ let entries = [];
2180
+ const pnpmLock = path7.join(rootDir, "pnpm-lock.yaml");
2181
+ const npmLock = path7.join(rootDir, "package-lock.json");
2182
+ const yarnLock = path7.join(rootDir, "yarn.lock");
2183
+ if (await pathExists(pnpmLock)) {
2184
+ result.lockfileType = "pnpm";
2185
+ const content = await readTextFile(pnpmLock);
2186
+ entries = parsePnpmLock(content);
2187
+ } else if (await pathExists(npmLock)) {
2188
+ result.lockfileType = "npm";
2189
+ const content = await readTextFile(npmLock);
2190
+ entries = parseNpmLock(content);
2191
+ } else if (await pathExists(yarnLock)) {
2192
+ result.lockfileType = "yarn";
2193
+ const content = await readTextFile(yarnLock);
2194
+ entries = parseYarnLock(content);
2195
+ }
2196
+ if (entries.length === 0) return result;
2197
+ const versionMap = /* @__PURE__ */ new Map();
2198
+ for (const entry of entries) {
2199
+ const existing = versionMap.get(entry.name);
2200
+ if (existing) {
2201
+ existing.add(entry.version);
2202
+ } else {
2203
+ versionMap.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
2204
+ }
2205
+ }
2206
+ result.totalInstalled = entries.length;
2207
+ result.totalUnique = versionMap.size;
2208
+ const duplicated = [];
2209
+ for (const [name, versions] of versionMap) {
2210
+ if (versions.size > 1) {
2211
+ duplicated.push({
2212
+ name,
2213
+ versions: [...versions].sort(),
2214
+ consumers: versions.size
2215
+ });
2216
+ }
2217
+ }
2218
+ duplicated.sort((a, b) => b.versions.length - a.versions.length || a.name.localeCompare(b.name));
2219
+ result.duplicatedPackages = duplicated;
2220
+ const lockedNames = new Set(versionMap.keys());
2221
+ const pkgFiles = await findPackageJsonFiles(rootDir);
2222
+ const phantoms = /* @__PURE__ */ new Set();
2223
+ const phantomDetails = [];
2224
+ for (const pjPath of pkgFiles) {
2225
+ try {
2226
+ const pj = await readJsonFile(pjPath);
2227
+ const relPath = path7.relative(rootDir, pjPath);
2228
+ for (const section of ["dependencies", "devDependencies"]) {
2229
+ const deps = pj[section];
2230
+ if (!deps) continue;
2231
+ for (const [name, version] of Object.entries(deps)) {
2232
+ const ver = typeof version === "string" ? version : "";
2233
+ if (!lockedNames.has(name) && !ver.startsWith("workspace:")) {
2234
+ phantoms.add(name);
2235
+ phantomDetails.push({ package: name, spec: ver, sourcePath: relPath });
2236
+ }
2237
+ }
2238
+ }
2239
+ } catch {
2240
+ }
2241
+ }
2242
+ result.phantomDependencies = [...phantoms].sort();
2243
+ result.phantomDependencyDetails = phantomDetails.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath) || a.package.localeCompare(b.package));
2244
+ return result;
2245
+ }
2246
+
2247
+ // src/scanners/tooling-inventory.ts
2248
+ var CATEGORIES = {
2249
+ frontend: {
2250
+ "react": "React",
2251
+ "react-dom": "React DOM",
2252
+ "vue": "Vue",
2253
+ "@angular/core": "Angular",
2254
+ "svelte": "Svelte",
2255
+ "solid-js": "Solid",
2256
+ "qwik": "Qwik",
2257
+ "htmx.org": "htmx",
2258
+ "preact": "Preact",
2259
+ "lit": "Lit",
2260
+ "alpinejs": "Alpine.js",
2261
+ "stimulus": "Stimulus",
2262
+ "petite-vue": "petite-vue",
2263
+ "mithril": "Mithril",
2264
+ "inferno": "Inferno",
2265
+ "hyperapp": "Hyperapp",
2266
+ "marko": "Marko",
2267
+ "@builder.io/qwik": "Qwik (Builder)",
2268
+ "million": "Million",
2269
+ "ember-source": "Ember"
2270
+ },
2271
+ metaFrameworks: {
2272
+ "next": "Next.js",
2273
+ "nuxt": "Nuxt",
2274
+ "@analogjs/platform": "Analog",
2275
+ "astro": "Astro",
2276
+ "@remix-run/react": "Remix",
2277
+ "gatsby": "Gatsby",
2278
+ "@sveltejs/kit": "SvelteKit",
2279
+ "@tanstack/start": "TanStack Start",
2280
+ "@adonisjs/core": "AdonisJS",
2281
+ "@redwoodjs/core": "RedwoodJS",
2282
+ "blitz": "Blitz.js",
2283
+ "@solidjs/start": "SolidStart",
2284
+ "fresh": "Fresh (Deno)"
2285
+ },
2286
+ bundlers: {
2287
+ "vite": "Vite",
2288
+ "webpack": "webpack",
2289
+ "rollup": "Rollup",
2290
+ "esbuild": "esbuild",
2291
+ "parcel": "Parcel",
2292
+ "turbo": "Turbo",
2293
+ "tsup": "tsup",
2294
+ "unbuild": "unbuild",
2295
+ "@swc/core": "SWC",
2296
+ "bun": "Bun",
2297
+ "pkg": "pkg",
2298
+ "ncc": "ncc",
2299
+ "@vercel/ncc": "Vercel ncc",
2300
+ "microbundle": "microbundle",
2301
+ "tsc-watch": "tsc-watch",
2302
+ "ts-node": "ts-node",
2303
+ "tsx": "tsx",
2304
+ "jiti": "jiti",
2305
+ "@rspack/core": "Rspack",
2306
+ "farm": "Farm"
2307
+ },
2308
+ css: {
2309
+ "tailwindcss": "Tailwind CSS",
2310
+ "@mui/material": "MUI",
2311
+ "vuetify": "Vuetify",
2312
+ "bootstrap": "Bootstrap",
2313
+ "styled-components": "styled-components",
2314
+ "@emotion/react": "Emotion",
2315
+ "@chakra-ui/react": "Chakra UI",
2316
+ "sass": "Sass",
2317
+ "@mantine/core": "Mantine",
2318
+ "antd": "Ant Design",
2319
+ "@radix-ui/react-slot": "Radix UI",
2320
+ "@headlessui/react": "Headless UI",
2321
+ "daisyui": "DaisyUI",
2322
+ "@shadcn/ui": "shadcn/ui",
2323
+ "primereact": "PrimeReact",
2324
+ "primevue": "PrimeVue",
2325
+ "@nextui-org/react": "NextUI",
2326
+ "@ariakit/react": "Ariakit",
2327
+ "windstitch": "Windstitch",
2328
+ "vanilla-extract": "vanilla-extract",
2329
+ "@vanilla-extract/css": "vanilla-extract",
2330
+ "linaria": "Linaria",
2331
+ "stylex": "StyleX",
2332
+ "@stylexjs/stylex": "StyleX",
2333
+ "unocss": "UnoCSS",
2334
+ "postcss": "PostCSS",
2335
+ "autoprefixer": "Autoprefixer",
2336
+ "lightningcss": "Lightning CSS",
2337
+ "less": "Less",
2338
+ "stylus": "Stylus",
2339
+ "open-props": "Open Props",
2340
+ "@ark-ui/react": "Ark UI"
2341
+ },
2342
+ backend: {
2343
+ "express": "Express",
2344
+ "fastify": "Fastify",
2345
+ "@nestjs/core": "NestJS",
2346
+ "hono": "Hono",
2347
+ "koa": "Koa",
2348
+ "@hapi/hapi": "Hapi",
2349
+ "restify": "Restify",
2350
+ "elysia": "Elysia",
2351
+ "@elysiajs/eden": "Elysia Eden",
2352
+ "moleculer": "Moleculer",
2353
+ "@feathersjs/feathers": "Feathers",
2354
+ "sails": "Sails",
2355
+ "micro": "Micro",
2356
+ "polka": "Polka",
2357
+ "h3": "h3",
2358
+ "nitro": "Nitro",
2359
+ "@trpc/server": "tRPC Server",
2360
+ "@trpc/client": "tRPC Client",
2361
+ "middy": "Middy (Lambda)",
2362
+ "serverless": "Serverless Framework",
2363
+ "aws-lambda": "AWS Lambda",
2364
+ "@aws-sdk/client-lambda": "AWS Lambda SDK",
2365
+ "@cloudflare/workers-types": "Cloudflare Workers",
2366
+ "wrangler": "Wrangler"
2367
+ },
2368
+ orm: {
2369
+ "prisma": "Prisma",
2370
+ "@prisma/client": "Prisma Client",
2371
+ "drizzle-orm": "Drizzle",
2372
+ "typeorm": "TypeORM",
2373
+ "sequelize": "Sequelize",
2374
+ "knex": "Knex",
2375
+ "pg": "pg (PostgreSQL)",
2376
+ "mysql2": "mysql2",
2377
+ "mongodb": "MongoDB",
2378
+ "ioredis": "ioredis",
2379
+ "redis": "Redis",
2380
+ "better-sqlite3": "better-sqlite3",
2381
+ "@mikro-orm/core": "MikroORM",
2382
+ "mongoose": "Mongoose",
2383
+ "mssql": "mssql",
2384
+ "kysely": "Kysely",
2385
+ "objection": "Objection.js",
2386
+ "@planetscale/database": "PlanetScale",
2387
+ "@neondatabase/serverless": "Neon",
2388
+ "@libsql/client": "libSQL (Turso)",
2389
+ "@electric-sql/pglite": "PGlite",
2390
+ "sql.js": "sql.js",
2391
+ "oracledb": "Oracle DB",
2392
+ "cassandra-driver": "Cassandra",
2393
+ "neo4j-driver": "Neo4j",
2394
+ "@upstash/redis": "Upstash Redis",
2395
+ "@upstash/kafka": "Upstash Kafka",
2396
+ "dynamoose": "Dynamoose",
2397
+ "fauna": "Fauna",
2398
+ "faunadb": "FaunaDB",
2399
+ "@clickhouse/client": "ClickHouse",
2400
+ "influx": "InfluxDB",
2401
+ "slonik": "Slonik",
2402
+ "massive": "Massive.js"
2403
+ },
2404
+ testing: {
2405
+ "vitest": "Vitest",
2406
+ "jest": "Jest",
2407
+ "mocha": "Mocha",
2408
+ "@playwright/test": "Playwright",
2409
+ "cypress": "Cypress",
2410
+ "@testing-library/react": "Testing Library (React)",
2411
+ "@testing-library/vue": "Testing Library (Vue)",
2412
+ "@testing-library/svelte": "Testing Library (Svelte)",
2413
+ "@testing-library/jest-dom": "Testing Library DOM",
2414
+ "@testing-library/user-event": "Testing Library User Event",
2415
+ "chai": "Chai",
2416
+ "ava": "AVA",
2417
+ "tap": "node-tap",
2418
+ "supertest": "Supertest",
2419
+ "storybook": "Storybook",
2420
+ "@storybook/react": "Storybook (React)",
2421
+ "@storybook/vue3": "Storybook (Vue)",
2422
+ "msw": "Mock Service Worker",
2423
+ "nock": "Nock",
2424
+ "sinon": "Sinon",
2425
+ "faker": "Faker",
2426
+ "@faker-js/faker": "Faker.js",
2427
+ "testcontainers": "Testcontainers",
2428
+ "pact": "Pact",
2429
+ "@pact-foundation/pact": "Pact",
2430
+ "k6": "k6",
2431
+ "artillery": "Artillery",
2432
+ "autocannon": "Autocannon",
2433
+ "puppeteer": "Puppeteer",
2434
+ "webdriverio": "WebdriverIO",
2435
+ "nightwatch": "Nightwatch",
2436
+ "detox": "Detox (Mobile)",
2437
+ "jest-image-snapshot": "Image Snapshot",
2438
+ "happy-dom": "happy-dom",
2439
+ "jsdom": "jsdom",
2440
+ "c8": "c8 (coverage)",
2441
+ "nyc": "nyc (coverage)",
2442
+ "@vitest/coverage-v8": "Vitest Coverage"
2443
+ },
2444
+ lintFormat: {
2445
+ "eslint": "ESLint",
2446
+ "prettier": "Prettier",
2447
+ "stylelint": "Stylelint",
2448
+ "@biomejs/biome": "Biome",
2449
+ "oxlint": "oxlint",
2450
+ "tslint": "TSLint",
2451
+ "@typescript-eslint/parser": "typescript-eslint",
2452
+ "eslint-config-next": "ESLint Next.js",
2453
+ "eslint-config-prettier": "ESLint Prettier",
2454
+ "eslint-plugin-react": "ESLint React",
2455
+ "eslint-plugin-vue": "ESLint Vue",
2456
+ "husky": "Husky",
2457
+ "lint-staged": "lint-staged",
2458
+ "commitlint": "commitlint",
2459
+ "@commitlint/cli": "commitlint",
2460
+ "lefthook": "Lefthook",
2461
+ "cspell": "CSpell",
2462
+ "markdownlint": "markdownlint",
2463
+ "depcheck": "depcheck",
2464
+ "knip": "Knip",
2465
+ "madge": "Madge",
2466
+ "publint": "publint",
2467
+ "arethetypeswrong": "Are the Types Wrong",
2468
+ "sort-package-json": "sort-package-json"
2469
+ },
2470
+ apiMessaging: {
2471
+ "graphql": "GraphQL",
2472
+ "@grpc/grpc-js": "gRPC",
2473
+ "@trpc/server": "tRPC",
2474
+ "@connectrpc/connect": "Connect",
2475
+ "socket.io": "Socket.IO",
2476
+ "ws": "WebSocket (ws)",
2477
+ "@apollo/server": "Apollo Server",
2478
+ "@apollo/client": "Apollo Client",
2479
+ "urql": "URQL",
2480
+ "graphql-yoga": "GraphQL Yoga",
2481
+ "mercurius": "Mercurius",
2482
+ "type-graphql": "TypeGraphQL",
2483
+ "nexus": "Nexus",
2484
+ "pothos": "Pothos",
2485
+ "@graphql-codegen/cli": "GraphQL Codegen",
2486
+ "kafkajs": "Kafka.js",
2487
+ "amqplib": "AMQP (RabbitMQ)",
2488
+ "bullmq": "BullMQ",
2489
+ "bull": "Bull",
2490
+ "bee-queue": "Bee Queue",
2491
+ "agenda": "Agenda",
2492
+ "pg-boss": "pg-boss",
2493
+ "@temporalio/client": "Temporal",
2494
+ "inngest": "Inngest",
2495
+ "trigger.dev": "Trigger.dev",
2496
+ "nats": "NATS",
2497
+ "mqtt": "MQTT",
2498
+ "zeromq": "ZeroMQ",
2499
+ "sse-channel": "SSE Channel"
2500
+ },
2501
+ observability: {
2502
+ "@sentry/node": "Sentry (Node)",
2503
+ "@sentry/browser": "Sentry (Browser)",
2504
+ "@sentry/react": "Sentry (React)",
2505
+ "@sentry/nextjs": "Sentry (Next.js)",
2506
+ "@opentelemetry/api": "OpenTelemetry API",
2507
+ "@opentelemetry/sdk-node": "OpenTelemetry SDK",
2508
+ "@opentelemetry/auto-instrumentations-node": "OpenTelemetry Auto",
2509
+ "applicationinsights": "Application Insights",
2510
+ "pino": "Pino",
2511
+ "winston": "Winston",
2512
+ "dd-trace": "Datadog",
2513
+ "newrelic": "New Relic",
2514
+ "bunyan": "Bunyan",
2515
+ "@axiomhq/pino": "Axiom (Pino)",
2516
+ "loglevel": "loglevel",
2517
+ "consola": "consola",
2518
+ "@logtail/node": "Logtail",
2519
+ "elastic-apm-node": "Elastic APM",
2520
+ "prom-client": "Prometheus Client",
2521
+ "@google-cloud/trace-agent": "GCP Trace",
2522
+ "@google-cloud/logging": "GCP Logging",
2523
+ "lightstep-tracer": "Lightstep",
2524
+ "roarr": "Roarr",
2525
+ "cls-hooked": "CLS Hooked",
2526
+ "@baselime/node-opentelemetry": "Baselime",
2527
+ "highlight.run": "Highlight",
2528
+ "posthog-node": "PostHog",
2529
+ "@amplitude/node": "Amplitude",
2530
+ "mixpanel": "Mixpanel",
2531
+ "@segment/analytics-node": "Segment"
2532
+ }
2533
+ };
2534
+ function scanToolingInventory(projects) {
2535
+ const result = {
2536
+ frontend: [],
2537
+ metaFrameworks: [],
2538
+ bundlers: [],
2539
+ css: [],
2540
+ backend: [],
2541
+ orm: [],
2542
+ testing: [],
2543
+ lintFormat: [],
2544
+ apiMessaging: [],
2545
+ observability: []
2546
+ };
2547
+ const packageVersions = /* @__PURE__ */ new Map();
2548
+ for (const project of projects) {
2549
+ for (const dep of project.dependencies) {
2550
+ if (!packageVersions.has(dep.package)) {
2551
+ packageVersions.set(dep.package, dep.resolvedVersion);
2552
+ }
2553
+ }
2554
+ }
2555
+ for (const [category, packages] of Object.entries(CATEGORIES)) {
2556
+ const items = [];
2557
+ for (const [pkg2, displayName] of Object.entries(packages)) {
2558
+ if (packageVersions.has(pkg2)) {
2559
+ items.push({
2560
+ name: displayName,
2561
+ package: pkg2,
2562
+ version: packageVersions.get(pkg2) ?? null
2563
+ });
2564
+ }
2565
+ }
2566
+ items.sort((a, b) => a.name.localeCompare(b.name));
2567
+ result[category] = items;
2568
+ }
2569
+ return result;
2570
+ }
2571
+
2572
+ // src/scanners/build-deploy.ts
2573
+ import * as path8 from "path";
2574
+ var CI_FILES = {
2575
+ ".github/workflows": "github-actions",
2576
+ ".gitlab-ci.yml": "gitlab-ci",
2577
+ "azure-pipelines.yml": "azure-devops",
2578
+ "bitbucket-pipelines.yml": "bitbucket-pipelines",
2579
+ "Jenkinsfile": "jenkins",
2580
+ ".circleci/config.yml": "circleci",
2581
+ ".travis.yml": "travis-ci"
2582
+ };
2583
+ var RELEASE_PACKAGES = /* @__PURE__ */ new Set([
2584
+ "semantic-release",
2585
+ "@changesets/cli",
2586
+ "standard-version",
2587
+ "release-it",
2588
+ "auto",
2589
+ "lerna"
2590
+ ]);
2591
+ var RELEASE_FILES = {
2592
+ ".changeset": "changesets",
2593
+ ".releaserc": "semantic-release",
2594
+ ".releaserc.json": "semantic-release",
2595
+ ".releaserc.yml": "semantic-release",
2596
+ "release.config.js": "semantic-release",
2597
+ "release.config.cjs": "semantic-release",
2598
+ "GitVersion.yml": "gitversion"
2599
+ };
2600
+ var MONOREPO_FILES = {
2601
+ "pnpm-workspace.yaml": "pnpm-workspaces",
2602
+ "lerna.json": "lerna",
2603
+ "nx.json": "nx",
2604
+ "turbo.json": "turbo",
2605
+ "rush.json": "rush"
2606
+ };
2607
+ var IAC_EXTENSIONS = {
2608
+ ".tf": "terraform",
2609
+ ".bicep": "bicep"
2610
+ };
2611
+ async function scanBuildDeploy(rootDir) {
2612
+ const result = {
2613
+ ci: [],
2614
+ ciWorkflowCount: 0,
2615
+ docker: { dockerfileCount: 0, baseImages: [] },
2616
+ iac: [],
2617
+ releaseTooling: [],
2618
+ packageManagers: [],
2619
+ monorepoTools: []
2620
+ };
2621
+ const ciSystems = /* @__PURE__ */ new Set();
2622
+ for (const [file, system] of Object.entries(CI_FILES)) {
2623
+ const fullPath = path8.join(rootDir, file);
2624
+ if (await pathExists(fullPath)) {
2625
+ ciSystems.add(system);
2626
+ }
2627
+ }
2628
+ const ghWorkflowDir = path8.join(rootDir, ".github", "workflows");
2629
+ if (await pathExists(ghWorkflowDir)) {
2630
+ try {
2631
+ const files = await findFiles(
2632
+ ghWorkflowDir,
2633
+ (name) => name.endsWith(".yml") || name.endsWith(".yaml")
2634
+ );
2635
+ result.ciWorkflowCount = files.length;
2636
+ } catch {
2637
+ }
2638
+ }
2639
+ result.ci = [...ciSystems].sort();
2640
+ const dockerfiles = await findFiles(
2641
+ rootDir,
2642
+ (name) => name === "Dockerfile" || name.startsWith("Dockerfile.")
2643
+ );
2644
+ result.docker.dockerfileCount = dockerfiles.length;
2645
+ const baseImages = /* @__PURE__ */ new Set();
2646
+ for (const df of dockerfiles) {
2647
+ try {
2648
+ const content = await readTextFile(df);
2649
+ for (const line of content.split("\n")) {
2650
+ const trimmed = line.trim();
2651
+ if (/^FROM\s+/i.test(trimmed)) {
2652
+ const parts = trimmed.split(/\s+/);
2653
+ let imageIdx = 1;
2654
+ if (parts[1]?.startsWith("--")) {
2655
+ imageIdx = parts[1].includes("=") ? 2 : 3;
2656
+ }
2657
+ if (parts[imageIdx]) {
2658
+ baseImages.add(parts[imageIdx]);
2659
+ }
2660
+ }
2661
+ }
2662
+ } catch {
2663
+ }
2664
+ }
2665
+ result.docker.baseImages = [...baseImages].sort();
2666
+ const iacSystems = /* @__PURE__ */ new Set();
2667
+ for (const [ext, system] of Object.entries(IAC_EXTENSIONS)) {
2668
+ const files = await findFiles(rootDir, (name) => name.endsWith(ext));
2669
+ if (files.length > 0) iacSystems.add(system);
2670
+ }
2671
+ const cfnFiles = await findFiles(
2672
+ rootDir,
2673
+ (name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
2674
+ );
2675
+ if (cfnFiles.length > 0) iacSystems.add("cloudformation");
2676
+ if (await pathExists(path8.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
2677
+ result.iac = [...iacSystems].sort();
2678
+ const releaseTools = /* @__PURE__ */ new Set();
2679
+ for (const [file, tool] of Object.entries(RELEASE_FILES)) {
2680
+ if (await pathExists(path8.join(rootDir, file))) releaseTools.add(tool);
2681
+ }
2682
+ const pkgFiles = await findPackageJsonFiles(rootDir);
2683
+ for (const pjPath of pkgFiles) {
2684
+ try {
2685
+ const pj = await readJsonFile(pjPath);
2686
+ for (const section of ["dependencies", "devDependencies"]) {
2687
+ const deps = pj[section];
2688
+ if (!deps) continue;
2689
+ for (const name of Object.keys(deps)) {
2690
+ if (RELEASE_PACKAGES.has(name)) releaseTools.add(name);
2691
+ }
2692
+ }
2693
+ } catch {
2694
+ }
2695
+ }
2696
+ result.releaseTooling = [...releaseTools].sort();
2697
+ const lockfileMap = {
2698
+ "pnpm-lock.yaml": "pnpm",
2699
+ "package-lock.json": "npm",
2700
+ "yarn.lock": "yarn",
2701
+ "bun.lockb": "bun"
2702
+ };
2703
+ const managers = /* @__PURE__ */ new Set();
2704
+ for (const [file, manager] of Object.entries(lockfileMap)) {
2705
+ if (await pathExists(path8.join(rootDir, file))) managers.add(manager);
2706
+ }
2707
+ result.packageManagers = [...managers].sort();
2708
+ const monoTools = /* @__PURE__ */ new Set();
2709
+ for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
2710
+ if (await pathExists(path8.join(rootDir, file))) monoTools.add(tool);
2711
+ }
2712
+ result.monorepoTools = [...monoTools].sort();
2713
+ return result;
2714
+ }
2715
+
2716
+ // src/scanners/ts-modernity.ts
2717
+ import * as path9 from "path";
2718
+ async function scanTsModernity(rootDir) {
2719
+ const result = {
2720
+ typescriptVersion: null,
2721
+ strict: null,
2722
+ noImplicitAny: null,
2723
+ strictNullChecks: null,
2724
+ module: null,
2725
+ moduleResolution: null,
2726
+ target: null,
2727
+ moduleType: null,
2728
+ exportsField: false
2729
+ };
2730
+ const pkgFiles = await findPackageJsonFiles(rootDir);
2731
+ let hasEsm = false;
2732
+ let hasCjs = false;
2733
+ for (const pjPath of pkgFiles) {
2734
+ try {
2735
+ const pj = await readJsonFile(pjPath);
2736
+ if (!result.typescriptVersion) {
2737
+ const tsVer = pj.devDependencies?.["typescript"] ?? pj.dependencies?.["typescript"];
2738
+ if (tsVer) {
2739
+ result.typescriptVersion = tsVer.replace(/^[\^~>=<\s]+/, "");
2740
+ }
2741
+ }
2742
+ const typeField = pj.type;
2743
+ if (typeField === "module") hasEsm = true;
2744
+ else if (typeField === "commonjs") hasCjs = true;
2745
+ else if (!typeField) hasCjs = true;
2746
+ if (pj.exports) {
2747
+ result.exportsField = true;
2748
+ }
2749
+ } catch {
2750
+ }
2751
+ }
2752
+ if (hasEsm && hasCjs) result.moduleType = "mixed";
2753
+ else if (hasEsm) result.moduleType = "esm";
2754
+ else if (hasCjs) result.moduleType = "cjs";
2755
+ let tsConfigPath = path9.join(rootDir, "tsconfig.json");
2756
+ if (!await pathExists(tsConfigPath)) {
2757
+ const tsConfigs = await findFiles(rootDir, (name) => name === "tsconfig.json");
2758
+ if (tsConfigs.length > 0) {
2759
+ tsConfigPath = tsConfigs[0];
2760
+ } else {
2761
+ return result;
2762
+ }
2763
+ }
2764
+ try {
2765
+ const raw = await readTextFile(tsConfigPath);
2766
+ const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/,(\s*[}\]])/g, "$1");
2767
+ const tsConfig = JSON.parse(stripped);
2768
+ const co = tsConfig?.compilerOptions;
2769
+ if (co) {
2770
+ if (typeof co.strict === "boolean") result.strict = co.strict;
2771
+ if (typeof co.noImplicitAny === "boolean") result.noImplicitAny = co.noImplicitAny;
2772
+ if (typeof co.strictNullChecks === "boolean") result.strictNullChecks = co.strictNullChecks;
2773
+ if (typeof co.module === "string") result.module = co.module;
2774
+ if (typeof co.moduleResolution === "string") result.moduleResolution = co.moduleResolution;
2775
+ if (typeof co.target === "string") result.target = co.target;
2776
+ }
2777
+ } catch {
2778
+ }
2779
+ return result;
2780
+ }
2781
+
2782
+ // src/scanners/breaking-change.ts
2783
+ var DEPRECATED_PACKAGES2 = /* @__PURE__ */ new Set([
2784
+ // Fully deprecated / archived
2785
+ "request",
2786
+ // deprecated 2020, use undici/node fetch
2787
+ "request-promise",
2788
+ // deprecated with request
2789
+ "request-promise-native",
2790
+ // deprecated with request
2791
+ "moment",
2792
+ // deprecated, use date-fns / luxon / Temporal
2793
+ "node-sass",
2794
+ // deprecated, use sass (Dart Sass)
2795
+ "tslint",
2796
+ // archived, use eslint + typescript-eslint
2797
+ "aws-sdk",
2798
+ // AWS SDK v2 EOL, use @aws-sdk/* v3
2799
+ "babel-core",
2800
+ // replaced by @babel/core
2801
+ "babel-preset-env",
2802
+ // replaced by @babel/preset-env
2803
+ "babel-preset-react",
2804
+ // replaced by @babel/preset-react
2805
+ "babel-loader",
2806
+ // 7.x deprecated, must pair with @babel/core
2807
+ "core-js",
2808
+ // v2 deprecated (v3 ok but signals old config)
2809
+ "istanbul",
2810
+ // replaced by nyc / c8
2811
+ "istanbul-instrumenter-loader",
2812
+ // deprecated with istanbul
2813
+ "left-pad",
2814
+ // meme package, use String.padStart
2815
+ "popper.js",
2816
+ // deprecated, use @popperjs/core
2817
+ "create-react-class",
2818
+ // deprecated React pattern
2819
+ "react-addons-css-transition-group",
2820
+ // deprecated React addon
2821
+ "react-addons-test-utils",
2822
+ // use react-dom/test-utils
2823
+ "@types/express-serve-static-core",
2824
+ // now shipped with express
2825
+ "enzyme",
2826
+ // unmaintained, use Testing Library or Vitest
2827
+ "enzyme-adapter-react-16",
2828
+ // unmaintained
2829
+ "enzyme-adapter-react-17",
2830
+ // unmaintained
2831
+ "react-hot-loader",
2832
+ // deprecated, use React Fast Refresh
2833
+ "react-loadable",
2834
+ // unmaintained, use React.lazy
2835
+ "react-router-dom-v5-compat",
2836
+ // migration shim
2837
+ "redux-thunk",
2838
+ // bundled into @reduxjs/toolkit since v1.5
2839
+ "redux-saga",
2840
+ // declining, consider RTK Query
2841
+ "recompose",
2842
+ // archived, use hooks
2843
+ "classnames",
2844
+ // works but clsx is faster & smaller
2845
+ "glamor",
2846
+ // deprecated CSS-in-JS
2847
+ "radium",
2848
+ // deprecated CSS-in-JS
2849
+ "material-ui",
2850
+ // replaced by @mui/material
2851
+ "@material-ui/core",
2852
+ // replaced by @mui/material
2853
+ // Unmaintained / abandoned
2854
+ "bower",
2855
+ // dead package manager
2856
+ "grunt",
2857
+ // superseded by npm scripts / modern bundlers
2858
+ "gulp",
2859
+ // declining, modern bundlers preferred
2860
+ "browserify",
2861
+ // superseded by modern bundlers
2862
+ "coffee-script",
2863
+ // CoffeeScript 1.x deprecated
2864
+ "coffeescript",
2865
+ // declining
2866
+ "jade",
2867
+ // renamed to pug
2868
+ "nomnom",
2869
+ // deprecated CLI parser
2870
+ "optimist",
2871
+ // deprecated CLI parser, use yargs/commander
2872
+ "minimist",
2873
+ // unmaintained, use mri or parseArgs
2874
+ "colors",
2875
+ // supply chain compromised, use chalk/picocolors
2876
+ "faker",
2877
+ // supply chain compromised, use @faker-js/faker
2878
+ "event-stream",
2879
+ // supply chain incident
2880
+ "ua-parser-js",
2881
+ // had supply chain incident
2882
+ "caniuse-db",
2883
+ // replaced by caniuse-lite
2884
+ "circular-json",
2885
+ // deprecated, use flatted
2886
+ "mkdirp",
2887
+ // Node has fs.mkdir recursive since v10
2888
+ "rimraf",
2889
+ // Node has fs.rm recursive since v14
2890
+ "glob",
2891
+ // consider fast-glob or Node fs.glob
2892
+ "swig",
2893
+ // abandoned template engine
2894
+ "dustjs-linkedin",
2895
+ // abandoned template engine
2896
+ "hogan.js",
2897
+ // abandoned template engine
2898
+ "passport-local-mongoose",
2899
+ // low maintenance
2900
+ // Known breaking-change magnets
2901
+ "@angular/http",
2902
+ // removed in Angular 15+
2903
+ "rxjs-compat",
2904
+ // migration shim for RxJS 5→6
2905
+ "protractor",
2906
+ // deprecated by Angular team
2907
+ "karma",
2908
+ // deprecated, Vitest/Jest preferred
2909
+ "karma-jasmine",
2910
+ // deprecated with Karma
2911
+ "jasmine"
2912
+ // declining in usage
2913
+ ]);
2914
+ var LEGACY_POLYFILLS = /* @__PURE__ */ new Set([
2915
+ // Built-in fetch & web APIs (Node 18+)
2916
+ "node-fetch",
2917
+ // native fetch since Node 18
2918
+ "cross-fetch",
2919
+ // native fetch since Node 18
2920
+ "isomorphic-fetch",
2921
+ // native fetch since Node 18
2922
+ "whatwg-fetch",
2923
+ // native fetch since Node 18
2924
+ "abort-controller",
2925
+ // native AbortController since Node 15
2926
+ "form-data",
2927
+ // native FormData since Node 18
2928
+ "formdata-polyfill",
2929
+ // native FormData since Node 18
2930
+ "web-streams-polyfill",
2931
+ // native ReadableStream since Node 18
2932
+ "whatwg-url",
2933
+ // native URL since Node 10
2934
+ "url-parse",
2935
+ // native URL since Node 10
2936
+ "domexception",
2937
+ // native DOMException since Node 17
2938
+ "abortcontroller-polyfill",
2939
+ // native since Node 15
2940
+ // Built-in Node modules shimmed for browser
2941
+ "querystring",
2942
+ // URLSearchParams preferred, native in Node
2943
+ "string_decoder",
2944
+ // native TextDecoder preferred
2945
+ "buffer",
2946
+ // native Buffer in Node, Uint8Array in browser
2947
+ "events",
2948
+ // native EventTarget preferred
2949
+ "path-browserify",
2950
+ // browser shim
2951
+ "stream-browserify",
2952
+ // browser shim
2953
+ "stream-http",
2954
+ // browser shim
2955
+ "https-browserify",
2956
+ // browser shim
2957
+ "os-browserify",
2958
+ // browser shim
2959
+ "crypto-browserify",
2960
+ // native Web Crypto API
2961
+ "assert",
2962
+ // native assert in Node, console.assert in browser
2963
+ "util",
2964
+ // Node native, deprecations in browser bundles
2965
+ "process",
2966
+ // shimmed, usually unnecessary
2967
+ "timers-browserify",
2968
+ // native timers
2969
+ "tty-browserify",
2970
+ // rarely needed
2971
+ "vm-browserify",
2972
+ // rarely needed
2973
+ "domain-browser",
2974
+ // domains deprecated in Node
2975
+ "punycode",
2976
+ // native in URL since Node 10
2977
+ "readable-stream",
2978
+ // Node streams polyfill, usually unnecessary
2979
+ // ES / language polyfills for ES2015+ (Node 18+ has all)
2980
+ "es6-promise",
2981
+ // native Promise since Node 4
2982
+ "promise-polyfill",
2983
+ // native Promise
2984
+ "es6-symbol",
2985
+ // native Symbol since Node 4
2986
+ "es6-map",
2987
+ // native Map since Node 4
2988
+ "es6-set",
2989
+ // native Set since Node 4
2990
+ "es6-weak-map",
2991
+ // native WeakMap since Node 4
2992
+ "es6-iterator",
2993
+ // native iterators
2994
+ "object-assign",
2995
+ // native Object.assign since Node 4
2996
+ "object.assign",
2997
+ // same
2998
+ "array.prototype.find",
2999
+ // native since ES2015
3000
+ "array.prototype.findindex",
3001
+ // native since ES2015
3002
+ "array.prototype.flat",
3003
+ // native since Node 11
3004
+ "array.prototype.flatmap",
3005
+ // native since Node 11
3006
+ "array-includes",
3007
+ // native Array.includes since Node 6
3008
+ "string.prototype.startswith",
3009
+ // native since ES2015
3010
+ "string.prototype.endswith",
3011
+ // native since ES2015
3012
+ "string.prototype.includes",
3013
+ // native since ES2015
3014
+ "string.prototype.padstart",
3015
+ // native since Node 8
3016
+ "string.prototype.padend",
3017
+ // native since Node 8
3018
+ "string.prototype.matchall",
3019
+ // native since Node 12
3020
+ "string.prototype.replaceall",
3021
+ // native since Node 15
3022
+ "string.prototype.trimstart",
3023
+ // native since Node 10
3024
+ "string.prototype.trimend",
3025
+ // native since Node 10
3026
+ "string.prototype.at",
3027
+ // native since Node 16.6
3028
+ "object.entries",
3029
+ // native since Node 7
3030
+ "object.values",
3031
+ // native since Node 7
3032
+ "object.fromentries",
3033
+ // native since Node 12
3034
+ "globalthis",
3035
+ // native globalThis since Node 12
3036
+ "symbol-observable",
3037
+ // TC39 withdrawn
3038
+ "setimmediate",
3039
+ // Node-only, setTimeout(fn, 0) preferred
3040
+ "regenerator-runtime",
3041
+ // async/await native since Node 8
3042
+ "@babel/polyfill",
3043
+ // deprecated, use core-js directly
3044
+ "whatwg-encoding",
3045
+ // native TextEncoder/TextDecoder
3046
+ "text-encoding",
3047
+ // native TextEncoder/TextDecoder since Node 11
3048
+ "encoding",
3049
+ // native TextEncoder/TextDecoder
3050
+ "unorm",
3051
+ // native String.normalize since Node 0.12
3052
+ "number.isnan",
3053
+ // native Number.isNaN since Node 0.12
3054
+ "is-nan",
3055
+ // native Number.isNaN
3056
+ "has-symbols",
3057
+ // native Symbol detection
3058
+ "has",
3059
+ // native Object.hasOwn since Node 16.9
3060
+ "hasown",
3061
+ // shim for Object.hasOwn
3062
+ "safe-buffer",
3063
+ // Buffer.from available since Node 5.10
3064
+ "safer-buffer"
3065
+ // Buffer.from available since Node 5.10
3066
+ ]);
3067
+ function scanBreakingChangeExposure(projects) {
3068
+ const deprecated = /* @__PURE__ */ new Set();
3069
+ const legacyPolyfills = /* @__PURE__ */ new Set();
3070
+ let peerConflictsDetected = false;
3071
+ const allDeps = /* @__PURE__ */ new Set();
3072
+ for (const project of projects) {
3073
+ for (const dep of project.dependencies) {
3074
+ allDeps.add(dep.package);
3075
+ if (DEPRECATED_PACKAGES2.has(dep.package)) {
3076
+ deprecated.add(dep.package);
3077
+ }
3078
+ if (LEGACY_POLYFILLS.has(dep.package)) {
3079
+ legacyPolyfills.add(dep.package);
3080
+ }
3081
+ if (dep.section === "peerDependencies" && dep.majorsBehind !== null && dep.majorsBehind >= 2) {
3082
+ peerConflictsDetected = true;
3083
+ }
3084
+ }
3085
+ }
3086
+ let score = 0;
3087
+ score += Math.min(deprecated.size * 10, 40);
3088
+ score += Math.min(legacyPolyfills.size * 5, 30);
3089
+ score += peerConflictsDetected ? 20 : 0;
3090
+ score = Math.min(score, 100);
3091
+ return {
3092
+ deprecatedPackages: [...deprecated].sort(),
3093
+ legacyPolyfills: [...legacyPolyfills].sort(),
3094
+ peerConflictsDetected,
3095
+ exposureScore: score
3096
+ };
3097
+ }
3098
+
3099
+ // src/scanners/file-hotspots.ts
3100
+ import * as fs4 from "fs/promises";
3101
+ import * as path10 from "path";
3102
+ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
3103
+ "node_modules",
3104
+ ".git",
3105
+ ".next",
3106
+ "dist",
3107
+ "build",
3108
+ "out",
3109
+ ".turbo",
3110
+ ".cache",
3111
+ "coverage",
3112
+ "bin",
3113
+ "obj",
3114
+ ".vs",
3115
+ "TestResults",
3116
+ ".nuxt",
3117
+ ".output",
3118
+ ".svelte-kit"
3119
+ ]);
3120
+ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
3121
+ ".map",
3122
+ ".lock",
3123
+ ".png",
3124
+ ".jpg",
3125
+ ".jpeg",
3126
+ ".gif",
3127
+ ".ico",
3128
+ ".svg",
3129
+ ".woff",
3130
+ ".woff2",
3131
+ ".ttf",
3132
+ ".eot",
3133
+ ".mp4",
3134
+ ".webm"
3135
+ ]);
3136
+ async function scanFileHotspots(rootDir) {
3137
+ const extensionCounts = {};
3138
+ const allFiles = [];
3139
+ let maxDepth = 0;
3140
+ async function walk(dir, depth) {
3141
+ if (depth > maxDepth) maxDepth = depth;
3142
+ let entries;
3143
+ try {
3144
+ const dirents = await fs4.readdir(dir, { withFileTypes: true });
3145
+ entries = dirents.map((d) => ({
3146
+ name: d.name,
3147
+ isDirectory: d.isDirectory(),
3148
+ isFile: d.isFile()
3149
+ }));
3150
+ } catch {
3151
+ return;
3152
+ }
3153
+ for (const e of entries) {
3154
+ if (e.isDirectory) {
3155
+ if (SKIP_DIRS2.has(e.name)) continue;
3156
+ await walk(path10.join(dir, e.name), depth + 1);
3157
+ } else if (e.isFile) {
3158
+ const ext = path10.extname(e.name).toLowerCase();
3159
+ if (SKIP_EXTENSIONS.has(ext)) continue;
3160
+ extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
3161
+ try {
3162
+ const stat3 = await fs4.stat(path10.join(dir, e.name));
3163
+ allFiles.push({
3164
+ path: path10.relative(rootDir, path10.join(dir, e.name)),
3165
+ bytes: stat3.size
3166
+ });
3167
+ } catch {
3168
+ }
3169
+ }
3170
+ }
3171
+ }
3172
+ await walk(rootDir, 0);
3173
+ allFiles.sort((a, b) => b.bytes - a.bytes);
3174
+ const largestFiles = allFiles.slice(0, 20);
3175
+ return {
3176
+ fileCountByExtension: extensionCounts,
3177
+ largestFiles,
3178
+ totalFiles: allFiles.length,
3179
+ maxDirectoryDepth: maxDepth,
3180
+ mostUsedPackages: []
3181
+ // Filled in by caller with project data
3182
+ };
3183
+ }
3184
+
3185
+ // src/scanners/security-posture.ts
3186
+ import * as path11 from "path";
3187
+ var LOCKFILES = {
3188
+ "pnpm-lock.yaml": "pnpm",
3189
+ "package-lock.json": "npm",
3190
+ "yarn.lock": "yarn",
3191
+ "bun.lockb": "bun",
3192
+ "packages.lock.json": "nuget"
3193
+ };
3194
+ async function scanSecurityPosture(rootDir) {
3195
+ const result = {
3196
+ lockfilePresent: false,
3197
+ multipleLockfileTypes: false,
3198
+ gitignoreCoversEnv: false,
3199
+ gitignoreCoversNodeModules: false,
3200
+ envFilesTracked: false,
3201
+ lockfileTypes: []
3202
+ };
3203
+ const foundLockfiles = [];
3204
+ for (const [file, type] of Object.entries(LOCKFILES)) {
3205
+ if (await pathExists(path11.join(rootDir, file))) {
3206
+ foundLockfiles.push(type);
3207
+ }
3208
+ }
3209
+ result.lockfilePresent = foundLockfiles.length > 0;
3210
+ result.multipleLockfileTypes = foundLockfiles.length > 1;
3211
+ result.lockfileTypes = foundLockfiles.sort();
3212
+ const gitignorePath = path11.join(rootDir, ".gitignore");
3213
+ if (await pathExists(gitignorePath)) {
3214
+ try {
3215
+ const content = await readTextFile(gitignorePath);
3216
+ const lines = content.split("\n").map((l) => l.trim());
3217
+ result.gitignoreCoversEnv = lines.some(
3218
+ (line) => line === ".env" || line === ".env*" || line === ".env.*" || line === ".env.local" || line === "*.env"
3219
+ );
3220
+ result.gitignoreCoversNodeModules = lines.some(
3221
+ (line) => line === "node_modules" || line === "node_modules/" || line === "/node_modules"
3222
+ );
3223
+ } catch {
3224
+ }
3225
+ }
3226
+ for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
3227
+ if (await pathExists(path11.join(rootDir, envFile))) {
3228
+ if (!result.gitignoreCoversEnv) {
3229
+ result.envFilesTracked = true;
3230
+ break;
3231
+ }
3232
+ }
3233
+ }
3234
+ return result;
3235
+ }
3236
+
3237
+ // src/scanners/service-dependencies.ts
3238
+ var SERVICE_CATEGORIES = {
3239
+ payment: {
3240
+ "stripe": "Stripe",
3241
+ "@stripe/stripe-js": "Stripe.js",
3242
+ "@stripe/react-stripe-js": "Stripe React",
3243
+ "braintree": "Braintree",
3244
+ "braintree-web": "Braintree Web",
3245
+ "@paypal/checkout-server-sdk": "PayPal",
3246
+ "@paypal/react-paypal-js": "PayPal React",
3247
+ "@paypal/paypal-js": "PayPal.js",
3248
+ "square": "Square",
3249
+ "razorpay": "Razorpay",
3250
+ "adyen-api-library": "Adyen",
3251
+ "@adyen/api-library": "Adyen",
3252
+ "@adyen/adyen-web": "Adyen Web",
3253
+ "mollie-api-node": "Mollie",
3254
+ "@lemonsqueezy/lemonsqueezy.js": "Lemon Squeezy",
3255
+ "paddle-sdk": "Paddle",
3256
+ "@paddle/paddle-node-sdk": "Paddle",
3257
+ "coinbase-commerce-node": "Coinbase Commerce",
3258
+ "gocardless-nodejs": "GoCardless",
3259
+ "klarna-checkout": "Klarna",
3260
+ "@recurly/recurly-js": "Recurly",
3261
+ "chargebee": "Chargebee"
3262
+ },
3263
+ auth: {
3264
+ "@auth0/auth0-spa-js": "Auth0 SPA",
3265
+ "@auth0/auth0-react": "Auth0 React",
3266
+ "@auth0/nextjs-auth0": "Auth0 Next.js",
3267
+ "auth0": "Auth0",
3268
+ "@clerk/clerk-sdk-node": "Clerk (Node)",
3269
+ "@clerk/nextjs": "Clerk (Next.js)",
3270
+ "@clerk/clerk-react": "Clerk (React)",
3271
+ "firebase-admin": "Firebase Admin",
3272
+ "firebase": "Firebase",
3273
+ "@firebase/auth": "Firebase Auth",
3274
+ "passport": "Passport.js",
3275
+ "passport-local": "Passport Local",
3276
+ "passport-google-oauth20": "Passport Google",
3277
+ "passport-github2": "Passport GitHub",
3278
+ "next-auth": "NextAuth",
3279
+ "@auth/core": "Auth.js",
3280
+ "@supabase/supabase-js": "Supabase",
3281
+ "@supabase/auth-helpers-nextjs": "Supabase Auth (Next.js)",
3282
+ "@supabase/ssr": "Supabase SSR",
3283
+ "jsonwebtoken": "JWT",
3284
+ "jose": "JOSE",
3285
+ "@okta/okta-auth-js": "Okta",
3286
+ "@okta/okta-react": "Okta React",
3287
+ "oidc-client-ts": "OIDC Client",
3288
+ "@casl/ability": "CASL",
3289
+ "casbin": "Casbin",
3290
+ "keycloak-js": "Keycloak",
3291
+ "@keycloak/keycloak-admin-client": "Keycloak Admin",
3292
+ "lucia": "Lucia Auth",
3293
+ "@lucia-auth/adapter-prisma": "Lucia (Prisma)",
3294
+ "arctic": "Arctic (OAuth)",
3295
+ "better-auth": "Better Auth",
3296
+ "grant": "Grant (OAuth)",
3297
+ "@workos-inc/node": "WorkOS",
3298
+ "stytch": "Stytch",
3299
+ "frontegg": "Frontegg",
3300
+ "@descope/node-sdk": "Descope",
3301
+ "kinde-auth-nextjs": "Kinde"
3302
+ },
3303
+ email: {
3304
+ "@sendgrid/mail": "SendGrid",
3305
+ "@sendgrid/client": "SendGrid Client",
3306
+ "nodemailer": "Nodemailer",
3307
+ "twilio": "Twilio",
3308
+ "@aws-sdk/client-ses": "AWS SES",
3309
+ "@aws-sdk/client-sesv2": "AWS SES v2",
3310
+ "postmark": "Postmark",
3311
+ "mailgun.js": "Mailgun",
3312
+ "resend": "Resend",
3313
+ "@react-email/components": "React Email",
3314
+ "react-email": "React Email",
3315
+ "mjml": "MJML",
3316
+ "email-templates": "Email Templates",
3317
+ "@mailchimp/mailchimp_marketing": "Mailchimp",
3318
+ "@mailchimp/mailchimp_transactional": "Mailchimp Transactional",
3319
+ "plunk-node": "Plunk",
3320
+ "brevo": "Brevo (Sendinblue)",
3321
+ "sib-api-v3-sdk": "Brevo v3",
3322
+ "@customer.io/node": "Customer.io",
3323
+ "sparkpost": "SparkPost",
3324
+ "mandrill-api": "Mandrill",
3325
+ "mail-listener5": "Mail Listener",
3326
+ "@azure/communication-email": "Azure Email"
3327
+ },
3328
+ cloud: {
3329
+ // AWS
3330
+ "aws-sdk": "AWS SDK v2",
3331
+ "@aws-sdk/client-s3": "AWS S3",
3332
+ "@aws-sdk/client-lambda": "AWS Lambda",
3333
+ "@aws-sdk/client-dynamodb": "AWS DynamoDB",
3334
+ "@aws-sdk/client-ec2": "AWS EC2",
3335
+ "@aws-sdk/client-ecs": "AWS ECS",
3336
+ "@aws-sdk/client-ecr": "AWS ECR",
3337
+ "@aws-sdk/client-cloudformation": "AWS CloudFormation",
3338
+ "@aws-sdk/client-cloudwatch": "AWS CloudWatch",
3339
+ "@aws-sdk/client-cloudfront": "AWS CloudFront",
3340
+ "@aws-sdk/client-iam": "AWS IAM",
3341
+ "@aws-sdk/client-secrets-manager": "AWS Secrets Manager",
3342
+ "@aws-sdk/client-ssm": "AWS SSM",
3343
+ "@aws-sdk/client-sts": "AWS STS",
3344
+ "@aws-sdk/client-cognito-identity-provider": "AWS Cognito",
3345
+ "@aws-sdk/client-route-53": "AWS Route 53",
3346
+ "@aws-sdk/client-step-functions": "AWS Step Functions",
3347
+ "@aws-sdk/client-eventbridge": "AWS EventBridge",
3348
+ "@aws-sdk/client-kinesis": "AWS Kinesis",
3349
+ "@aws-sdk/client-kms": "AWS KMS",
3350
+ "@aws-sdk/client-athena": "AWS Athena",
3351
+ "@aws-sdk/client-bedrock-runtime": "AWS Bedrock",
3352
+ "@aws-sdk/lib-dynamodb": "AWS DynamoDB Doc",
3353
+ // Azure
3354
+ "@azure/storage-blob": "Azure Blob",
3355
+ "@azure/identity": "Azure Identity",
3356
+ "@azure/cosmos": "Azure Cosmos DB",
3357
+ "@azure/keyvault-secrets": "Azure Key Vault",
3358
+ "@azure/service-bus": "Azure Service Bus",
3359
+ "@azure/event-hubs": "Azure Event Hubs",
3360
+ "@azure/functions": "Azure Functions",
3361
+ "@azure/ai-form-recognizer": "Azure Form Recognizer",
3362
+ "@azure/openai": "Azure OpenAI",
3363
+ "@azure/app-configuration": "Azure App Config",
3364
+ "@azure/monitor-opentelemetry": "Azure Monitor OTel",
3365
+ "@azure/container-registry": "Azure Container Registry",
3366
+ // GCP
3367
+ "@google-cloud/storage": "GCP Storage",
3368
+ "@google-cloud/functions-framework": "GCP Functions",
3369
+ "@google-cloud/bigquery": "GCP BigQuery",
3370
+ "@google-cloud/firestore": "GCP Firestore",
3371
+ "@google-cloud/pubsub": "GCP Pub/Sub",
3372
+ "@google-cloud/secret-manager": "GCP Secret Manager",
3373
+ "@google-cloud/tasks": "GCP Cloud Tasks",
3374
+ "@google-cloud/run": "GCP Cloud Run",
3375
+ "@google-cloud/logging": "GCP Logging",
3376
+ "@google-cloud/translate": "GCP Translate",
3377
+ "@google-cloud/vertexai": "GCP Vertex AI",
3378
+ // Multi-cloud / PaaS
3379
+ "@vercel/sdk": "Vercel",
3380
+ "@vercel/kv": "Vercel KV",
3381
+ "@vercel/blob": "Vercel Blob",
3382
+ "@vercel/postgres": "Vercel Postgres",
3383
+ "@vercel/edge-config": "Vercel Edge Config",
3384
+ "@netlify/functions": "Netlify Functions",
3385
+ "@cloudflare/workers-types": "Cloudflare Workers",
3386
+ "wrangler": "Wrangler",
3387
+ "@railway/cli": "Railway",
3388
+ "@fly/apps": "Fly.io",
3389
+ "heroku-client": "Heroku",
3390
+ "@pulumi/pulumi": "Pulumi",
3391
+ "@pulumi/aws": "Pulumi AWS",
3392
+ "cdktf": "CDK for Terraform",
3393
+ "aws-cdk-lib": "AWS CDK",
3394
+ "sst": "SST"
3395
+ },
3396
+ databases: {
3397
+ "pg": "PostgreSQL",
3398
+ "postgres": "Postgres.js",
3399
+ "mysql2": "MySQL",
3400
+ "mysql": "MySQL (legacy)",
3401
+ "mongodb": "MongoDB",
3402
+ "ioredis": "Redis (ioredis)",
3403
+ "redis": "Redis",
3404
+ "better-sqlite3": "SQLite",
3405
+ "sql.js": "sql.js",
3406
+ "@prisma/client": "Prisma",
3407
+ "drizzle-orm": "Drizzle",
3408
+ "typeorm": "TypeORM",
3409
+ "sequelize": "Sequelize",
3410
+ "knex": "Knex",
3411
+ "kysely": "Kysely",
3412
+ "objection": "Objection.js",
3413
+ "slonik": "Slonik",
3414
+ "massive": "Massive.js",
3415
+ "mongoose": "Mongoose",
3416
+ "@mikro-orm/core": "MikroORM",
3417
+ "mssql": "SQL Server",
3418
+ "oracledb": "Oracle",
3419
+ "cassandra-driver": "Cassandra",
3420
+ "neo4j-driver": "Neo4j",
3421
+ "arangojs": "ArangoDB",
3422
+ "@clickhouse/client": "ClickHouse",
3423
+ "influx": "InfluxDB",
3424
+ "@libsql/client": "libSQL (Turso)",
3425
+ "@neondatabase/serverless": "Neon",
3426
+ "@planetscale/database": "PlanetScale",
3427
+ "@electric-sql/pglite": "PGlite",
3428
+ "@upstash/redis": "Upstash Redis",
3429
+ "@upstash/kafka": "Upstash Kafka",
3430
+ "@upstash/qstash": "Upstash QStash",
3431
+ "@upstash/vector": "Upstash Vector",
3432
+ "dynamoose": "Dynamoose",
3433
+ "fauna": "Fauna",
3434
+ "faunadb": "FaunaDB",
3435
+ "@supabase/postgrest-js": "Supabase PostgREST",
3436
+ "couchbase": "Couchbase",
3437
+ "couchdb-nano": "CouchDB",
3438
+ "duckdb": "DuckDB",
3439
+ "@tidbcloud/serverless": "TiDB Serverless"
3440
+ },
3441
+ messaging: {
3442
+ "@aws-sdk/client-sqs": "AWS SQS",
3443
+ "@aws-sdk/client-sns": "AWS SNS",
3444
+ "amqplib": "RabbitMQ",
3445
+ "kafkajs": "Kafka",
3446
+ "bullmq": "BullMQ",
3447
+ "bull": "Bull",
3448
+ "@google-cloud/pubsub": "GCP Pub/Sub",
3449
+ "bee-queue": "Bee Queue",
3450
+ "pg-boss": "pg-boss",
3451
+ "agenda": "Agenda",
3452
+ "@azure/service-bus": "Azure Service Bus",
3453
+ "@azure/event-hubs": "Azure Event Hubs",
3454
+ "nats": "NATS",
3455
+ "mqtt": "MQTT",
3456
+ "zeromq": "ZeroMQ",
3457
+ "@temporalio/client": "Temporal Client",
3458
+ "@temporalio/worker": "Temporal Worker",
3459
+ "inngest": "Inngest",
3460
+ "@trigger.dev/sdk": "Trigger.dev",
3461
+ "@defer/client": "Defer",
3462
+ "graphile-worker": "Graphile Worker",
3463
+ "@quirrel/quirrel": "Quirrel",
3464
+ "rhea": "AMQP 1.0 (rhea)",
3465
+ "stompit": "STOMP",
3466
+ "@eventstore/db-client": "EventStoreDB",
3467
+ "@aws-sdk/client-eventbridge": "AWS EventBridge",
3468
+ "@aws-sdk/client-kinesis": "AWS Kinesis",
3469
+ "@aws-sdk/client-firehose": "AWS Firehose"
3470
+ },
3471
+ observability: {
3472
+ "@sentry/node": "Sentry (Node)",
3473
+ "@sentry/browser": "Sentry (Browser)",
3474
+ "@sentry/react": "Sentry (React)",
3475
+ "@sentry/nextjs": "Sentry (Next.js)",
3476
+ "@sentry/vue": "Sentry (Vue)",
3477
+ "@sentry/angular": "Sentry (Angular)",
3478
+ "@opentelemetry/api": "OpenTelemetry API",
3479
+ "@opentelemetry/sdk-node": "OpenTelemetry SDK",
3480
+ "@opentelemetry/auto-instrumentations-node": "OpenTelemetry Auto",
3481
+ "applicationinsights": "App Insights",
3482
+ "dd-trace": "Datadog",
3483
+ "@datadog/browser-rum": "Datadog RUM",
3484
+ "newrelic": "New Relic",
3485
+ "@newrelic/browser-agent": "New Relic Browser",
3486
+ "pino": "Pino",
3487
+ "winston": "Winston",
3488
+ "bunyan": "Bunyan",
3489
+ "consola": "Consola",
3490
+ "loglevel": "loglevel",
3491
+ "roarr": "Roarr",
3492
+ "prom-client": "Prometheus",
3493
+ "elastic-apm-node": "Elastic APM",
3494
+ "@logtail/node": "Logtail",
3495
+ "@axiomhq/pino": "Axiom (Pino)",
3496
+ "@axiomhq/winston": "Axiom (Winston)",
3497
+ "@baselime/node-opentelemetry": "Baselime",
3498
+ "highlight.run": "Highlight",
3499
+ "@google-cloud/trace-agent": "GCP Trace",
3500
+ "@google-cloud/logging": "GCP Logging",
3501
+ "@azure/monitor-opentelemetry": "Azure Monitor",
3502
+ "lightstep-tracer": "Lightstep",
3503
+ "honeycomb-beeline": "Honeycomb",
3504
+ "@honeycombio/opentelemetry-node": "Honeycomb OTel",
3505
+ "posthog-node": "PostHog",
3506
+ "@amplitude/node": "Amplitude",
3507
+ "mixpanel": "Mixpanel",
3508
+ "@segment/analytics-node": "Segment",
3509
+ "analytics-node": "Segment (legacy)",
3510
+ "@rudderstack/rudder-sdk-node": "RudderStack",
3511
+ "heap-api": "Heap",
3512
+ "@fullstory/browser": "FullStory",
3513
+ "@logrocket/react": "LogRocket",
3514
+ "logrocket": "LogRocket",
3515
+ "hotjar-js": "Hotjar",
3516
+ "@clarity-ai/clarity": "Microsoft Clarity"
3517
+ },
3518
+ crm: {
3519
+ "xero-node": "Xero",
3520
+ "hubspot-api-client": "HubSpot",
3521
+ "@hubspot/api-client": "HubSpot",
3522
+ "@slack/web-api": "Slack Web API",
3523
+ "@slack/bolt": "Slack Bolt",
3524
+ "@slack/events-api": "Slack Events",
3525
+ "discord.js": "Discord",
3526
+ "intercom-client": "Intercom",
3527
+ "salesforce-sdk": "Salesforce",
3528
+ "jsforce": "JSforce (Salesforce)",
3529
+ "@zendesk/sell-client": "Zendesk",
3530
+ "node-zendesk": "Zendesk",
3531
+ "freshdesk-api": "Freshdesk",
3532
+ "pipedrive": "Pipedrive",
3533
+ "airtable": "Airtable",
3534
+ "@notionhq/client": "Notion",
3535
+ "@linear/sdk": "Linear",
3536
+ "jira-client": "Jira",
3537
+ "jira.js": "Jira.js",
3538
+ "@octokit/rest": "GitHub (Octokit)",
3539
+ "@octokit/core": "GitHub (Octokit Core)",
3540
+ "gitlab": "GitLab",
3541
+ "@gitbeaker/rest": "GitLab (Gitbeaker)",
3542
+ "clickup.js": "ClickUp",
3543
+ "asana": "Asana",
3544
+ "monday-sdk-js": "Monday.com",
3545
+ "telegram-bot-api": "Telegram",
3546
+ "telegraf": "Telegraf (Telegram)",
3547
+ "@microsoft/microsoft-graph-client": "Microsoft Graph",
3548
+ "googleapis": "Google APIs",
3549
+ "@google-cloud/aiplatform": "Google AI Platform"
3550
+ },
3551
+ storage: {
3552
+ "@aws-sdk/client-s3": "AWS S3",
3553
+ "@aws-sdk/s3-request-presigner": "AWS S3 Presigner",
3554
+ "@azure/storage-blob": "Azure Blob",
3555
+ "@azure/storage-queue": "Azure Storage Queue",
3556
+ "@azure/storage-file-share": "Azure File Share",
3557
+ "@google-cloud/storage": "GCP Storage",
3558
+ "minio": "MinIO",
3559
+ "@supabase/storage-js": "Supabase Storage",
3560
+ "cloudinary": "Cloudinary",
3561
+ "uploadthing": "UploadThing",
3562
+ "@uploadthing/react": "UploadThing React",
3563
+ "multer": "Multer",
3564
+ "multer-s3": "Multer S3",
3565
+ "formidable": "Formidable",
3566
+ "busboy": "Busboy",
3567
+ "@vercel/blob": "Vercel Blob",
3568
+ "imagekit": "ImageKit",
3569
+ "sharp": "Sharp (image)",
3570
+ "jimp": "Jimp (image)",
3571
+ "fluent-ffmpeg": "FFmpeg",
3572
+ "@google-cloud/vision": "GCP Vision",
3573
+ "@aws-sdk/client-rekognition": "AWS Rekognition",
3574
+ "file-type": "file-type",
3575
+ "@backblaze-b2/sdk": "Backblaze B2",
3576
+ "wasabi-sdk": "Wasabi"
3577
+ },
3578
+ search: {
3579
+ "@elastic/elasticsearch": "Elasticsearch",
3580
+ "algoliasearch": "Algolia",
3581
+ "@algolia/client-search": "Algolia Client",
3582
+ "react-instantsearch": "Algolia InstantSearch",
3583
+ "meilisearch": "Meilisearch",
3584
+ "typesense": "Typesense",
3585
+ "@opensearch-project/opensearch": "OpenSearch",
3586
+ "solr-client": "Solr",
3587
+ "lunr": "Lunr",
3588
+ "flexsearch": "FlexSearch",
3589
+ "fuse.js": "Fuse.js",
3590
+ "minisearch": "MiniSearch",
3591
+ "orama": "Orama",
3592
+ "@orama/orama": "Orama",
3593
+ "@trieve/trieve-ts-sdk": "Trieve",
3594
+ "pagefind": "Pagefind",
3595
+ // Vector / AI search
3596
+ "@pinecone-database/pinecone": "Pinecone",
3597
+ "chromadb": "Chroma",
3598
+ "@qdrant/js-client-rest": "Qdrant",
3599
+ "weaviate-ts-client": "Weaviate",
3600
+ "@zilliz/milvus2-sdk-node": "Milvus",
3601
+ "openai": "OpenAI",
3602
+ "@anthropic-ai/sdk": "Anthropic",
3603
+ "@langchain/core": "LangChain",
3604
+ "langchain": "LangChain",
3605
+ "@google/generative-ai": "Google Gemini",
3606
+ "cohere-ai": "Cohere",
3607
+ "replicate": "Replicate",
3608
+ "@huggingface/inference": "Hugging Face",
3609
+ "ai": "Vercel AI SDK",
3610
+ "@ai-sdk/openai": "AI SDK (OpenAI)"
3611
+ }
3612
+ };
3613
+ function scanServiceDependencies(projects) {
3614
+ const packageVersions = /* @__PURE__ */ new Map();
3615
+ for (const project of projects) {
3616
+ for (const dep of project.dependencies) {
3617
+ if (!packageVersions.has(dep.package)) {
3618
+ packageVersions.set(dep.package, dep.resolvedVersion);
3619
+ }
3620
+ }
3621
+ }
3622
+ const result = {
3623
+ payment: [],
3624
+ auth: [],
3625
+ email: [],
3626
+ cloud: [],
3627
+ databases: [],
3628
+ messaging: [],
3629
+ observability: [],
3630
+ crm: [],
3631
+ storage: [],
3632
+ search: []
3633
+ };
3634
+ for (const [category, packages] of Object.entries(SERVICE_CATEGORIES)) {
3635
+ const items = [];
3636
+ for (const [pkg2, displayName] of Object.entries(packages)) {
3637
+ if (packageVersions.has(pkg2)) {
3638
+ items.push({
3639
+ name: displayName,
3640
+ package: pkg2,
3641
+ version: packageVersions.get(pkg2) ?? null
3642
+ });
3643
+ }
3644
+ }
3645
+ items.sort((a, b) => a.name.localeCompare(b.name));
3646
+ result[category] = items;
3647
+ }
3648
+ return result;
3649
+ }
3650
+
3651
+ // src/commands/scan.ts
3652
+ async function runScan(rootDir, opts) {
3653
+ const config = await loadConfig(rootDir);
3654
+ const sem = new Semaphore(opts.concurrency);
3655
+ const npmCache = new NpmCache(rootDir, sem);
3656
+ const scanners = config.scanners;
3657
+ const progress = new ScanProgress(rootDir);
3658
+ const steps = [
3659
+ { id: "config", label: "Loading configuration" },
3660
+ { id: "vcs", label: "Detecting version control" },
3661
+ { id: "node", label: "Scanning Node projects" },
3662
+ { id: "dotnet", label: "Scanning .NET projects" },
3663
+ ...scanners !== false ? [
3664
+ ...scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
3665
+ ...scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
3666
+ ...scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
3667
+ ...scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
3668
+ ...scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
3669
+ ...scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
3670
+ ...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
3671
+ ...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
3672
+ ...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
3673
+ ...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : []
3674
+ ] : [],
3675
+ { id: "drift", label: "Computing drift score" },
3676
+ { id: "findings", label: "Generating findings" }
3677
+ ];
3678
+ progress.setSteps(steps);
3679
+ progress.completeStep("config", "loaded");
3680
+ progress.startStep("vcs");
3681
+ const vcs = await detectVcs(rootDir);
3682
+ const vcsDetail = vcs.type !== "unknown" ? `${vcs.type}${vcs.branch ? ` ${vcs.branch}` : ""}${vcs.shortSha ? ` @ ${vcs.shortSha}` : ""}` : "none detected";
3683
+ progress.completeStep("vcs", vcsDetail);
3684
+ progress.startStep("node");
3685
+ const nodeProjects = await scanNodeProjects(rootDir, npmCache);
3686
+ for (const p of nodeProjects) {
3687
+ progress.addDependencies(p.dependencies.length);
3688
+ progress.addFrameworks(p.frameworks.length);
3689
+ }
3690
+ progress.addProjects(nodeProjects.length);
3691
+ progress.completeStep("node", `${nodeProjects.length} project${nodeProjects.length !== 1 ? "s" : ""}`, nodeProjects.length);
3692
+ progress.startStep("dotnet");
3693
+ const dotnetProjects = await scanDotnetProjects(rootDir);
3694
+ for (const p of dotnetProjects) {
3695
+ progress.addDependencies(p.dependencies.length);
3696
+ progress.addFrameworks(p.frameworks.length);
3697
+ }
3698
+ progress.addProjects(dotnetProjects.length);
3699
+ progress.completeStep("dotnet", `${dotnetProjects.length} project${dotnetProjects.length !== 1 ? "s" : ""}`, dotnetProjects.length);
3700
+ const allProjects = [...nodeProjects, ...dotnetProjects];
3701
+ const extended = {};
3702
+ if (scanners !== false) {
3703
+ if (scanners?.platformMatrix?.enabled !== false) {
3704
+ progress.startStep("platform");
3705
+ extended.platformMatrix = await scanPlatformMatrix(rootDir);
3706
+ const nativeCount = extended.platformMatrix.nativeModules.length;
3707
+ const dockerCount = extended.platformMatrix.dockerBaseImages.length;
3708
+ const parts = [];
3709
+ if (nativeCount > 0) parts.push(`${nativeCount} native`);
3710
+ if (dockerCount > 0) parts.push(`${dockerCount} docker`);
3711
+ progress.completeStep("platform", parts.join(", ") || "clean", nativeCount + dockerCount);
3712
+ }
3713
+ if (scanners?.toolingInventory?.enabled !== false) {
3714
+ progress.startStep("tooling");
3715
+ extended.toolingInventory = scanToolingInventory(allProjects);
3716
+ const toolCount = Object.values(extended.toolingInventory).reduce((sum, arr) => sum + arr.length, 0);
3717
+ progress.completeStep("tooling", `${toolCount} tool${toolCount !== 1 ? "s" : ""} mapped`, toolCount);
3718
+ }
3719
+ if (scanners?.serviceDependencies?.enabled !== false) {
3720
+ progress.startStep("services");
3721
+ extended.serviceDependencies = scanServiceDependencies(allProjects);
3722
+ const svcCount = Object.values(extended.serviceDependencies).reduce((sum, arr) => sum + arr.length, 0);
3723
+ progress.completeStep("services", `${svcCount} service${svcCount !== 1 ? "s" : ""} detected`, svcCount);
3724
+ }
3725
+ if (scanners?.breakingChangeExposure?.enabled !== false) {
3726
+ progress.startStep("breaking");
3727
+ extended.breakingChangeExposure = scanBreakingChangeExposure(allProjects);
3728
+ const bc = extended.breakingChangeExposure;
3729
+ const bcTotal = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
3730
+ progress.completeStep(
3731
+ "breaking",
3732
+ bcTotal > 0 ? `${bc.deprecatedPackages.length} deprecated, ${bc.legacyPolyfills.length} polyfills` : "none found",
3733
+ bcTotal
3734
+ );
3735
+ }
3736
+ if (scanners?.securityPosture?.enabled !== false) {
3737
+ progress.startStep("security");
3738
+ extended.securityPosture = await scanSecurityPosture(rootDir);
3739
+ const sec = extended.securityPosture;
3740
+ const secDetail = sec.lockfilePresent ? `lockfile \u2714${sec.gitignoreCoversEnv ? " \xB7 .env \u2714" : " \xB7 .env \u2716"}` : "no lockfile";
3741
+ progress.completeStep("security", secDetail);
3742
+ }
3743
+ if (scanners?.buildDeploy?.enabled !== false) {
3744
+ progress.startStep("build");
3745
+ extended.buildDeploy = await scanBuildDeploy(rootDir);
3746
+ const bd = extended.buildDeploy;
3747
+ const bdParts = [];
3748
+ if (bd.ci.length > 0) bdParts.push(bd.ci.join(", "));
3749
+ if (bd.docker.dockerfileCount > 0) bdParts.push(`${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""}`);
3750
+ progress.completeStep("build", bdParts.join(" \xB7 ") || "none detected");
3751
+ }
3752
+ if (scanners?.tsModernity?.enabled !== false) {
3753
+ progress.startStep("ts");
3754
+ extended.tsModernity = await scanTsModernity(rootDir);
3755
+ const ts = extended.tsModernity;
3756
+ const tsParts = [];
3757
+ if (ts.typescriptVersion) tsParts.push(`v${ts.typescriptVersion}`);
3758
+ if (ts.strict === true) tsParts.push("strict");
3759
+ if (ts.moduleType) tsParts.push(ts.moduleType.toUpperCase());
3760
+ progress.completeStep("ts", tsParts.join(" \xB7 ") || "no tsconfig");
3761
+ }
3762
+ if (scanners?.fileHotspots?.enabled !== false) {
3763
+ progress.startStep("hotspots");
3764
+ extended.fileHotspots = await scanFileHotspots(rootDir);
3765
+ progress.completeStep("hotspots", `${extended.fileHotspots.totalFiles} files`, extended.fileHotspots.totalFiles);
3766
+ }
3767
+ if (scanners?.dependencyGraph?.enabled !== false) {
3768
+ progress.startStep("depgraph");
3769
+ extended.dependencyGraph = await scanDependencyGraph(rootDir);
3770
+ const dg = extended.dependencyGraph;
3771
+ const dgDetail = dg.lockfileType ? `${dg.lockfileType} \xB7 ${dg.totalUnique} unique` : "no lockfile";
3772
+ progress.completeStep("depgraph", dgDetail, dg.totalUnique);
3773
+ }
3774
+ if (scanners?.dependencyRisk?.enabled !== false) {
3775
+ progress.startStep("deprisk");
3776
+ extended.dependencyRisk = scanDependencyRisk(allProjects);
3777
+ const dr = extended.dependencyRisk;
3778
+ const drParts = [];
3779
+ if (dr.deprecatedPackages.length > 0) drParts.push(`${dr.deprecatedPackages.length} deprecated`);
3780
+ if (dr.nativeModulePackages.length > 0) drParts.push(`${dr.nativeModulePackages.length} native`);
3781
+ progress.completeStep("deprisk", drParts.join(", ") || "low risk");
3782
+ }
3783
+ }
3784
+ progress.startStep("drift");
3785
+ const drift = computeDriftScore(allProjects);
3786
+ progress.completeStep("drift", `${drift.score}/100 \u2014 ${drift.riskLevel} risk`);
3787
+ progress.startStep("findings");
3788
+ const findings = generateFindings(allProjects, config);
3789
+ const warnCount = findings.filter((f) => f.level === "warning").length;
3790
+ const errCount = findings.filter((f) => f.level === "error").length;
3791
+ const noteCount = findings.filter((f) => f.level === "note").length;
3792
+ progress.addFindings(warnCount, errCount, noteCount);
3793
+ const findingParts = [];
3794
+ if (errCount > 0) findingParts.push(`${errCount} error${errCount !== 1 ? "s" : ""}`);
3795
+ if (warnCount > 0) findingParts.push(`${warnCount} warning${warnCount !== 1 ? "s" : ""}`);
3796
+ if (noteCount > 0) findingParts.push(`${noteCount} note${noteCount !== 1 ? "s" : ""}`);
3797
+ progress.completeStep("findings", findingParts.join(", ") || "none");
3798
+ progress.finish();
3799
+ if (allProjects.length === 0) {
3800
+ console.log(chalk3.yellow("No projects found."));
3801
+ }
3802
+ const artifact = {
3803
+ schemaVersion: "1.0",
3804
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3805
+ vibgrateVersion: VERSION,
3806
+ rootPath: path12.basename(rootDir),
3807
+ ...vcs.type !== "unknown" ? { vcs } : {},
3808
+ projects: allProjects,
3809
+ drift,
3810
+ findings,
3811
+ ...Object.keys(extended).length > 0 ? { extended } : {}
3812
+ };
3813
+ if (opts.baseline) {
3814
+ const baselinePath = path12.resolve(opts.baseline);
3815
+ if (await pathExists(baselinePath)) {
3816
+ try {
3817
+ const baseline = await readJsonFile(baselinePath);
3818
+ artifact.baseline = baselinePath;
3819
+ artifact.delta = artifact.drift.score - baseline.drift.score;
3820
+ } catch {
3821
+ console.error(chalk3.yellow(`Warning: Could not read baseline file: ${baselinePath}`));
3822
+ }
3823
+ }
3824
+ }
3825
+ const vibgrateDir = path12.join(rootDir, ".vibgrate");
3826
+ await ensureDir(vibgrateDir);
3827
+ await writeJsonFile(path12.join(vibgrateDir, "scan_result.json"), artifact);
3828
+ if (opts.format === "json") {
3829
+ const jsonStr = JSON.stringify(artifact, null, 2);
3830
+ if (opts.out) {
3831
+ await writeTextFile(path12.resolve(opts.out), jsonStr);
3832
+ console.log(chalk3.green("\u2714") + ` JSON written to ${opts.out}`);
3833
+ } else {
3834
+ console.log(jsonStr);
3835
+ }
3836
+ } else if (opts.format === "sarif") {
3837
+ const sarif = formatSarif(artifact);
3838
+ const sarifStr = JSON.stringify(sarif, null, 2);
3839
+ if (opts.out) {
3840
+ await writeTextFile(path12.resolve(opts.out), sarifStr);
3841
+ console.log(chalk3.green("\u2714") + ` SARIF written to ${opts.out}`);
3842
+ } else {
3843
+ console.log(sarifStr);
3844
+ }
3845
+ } else {
3846
+ const text = formatText(artifact);
3847
+ console.log(text);
3848
+ if (opts.out) {
3849
+ await writeTextFile(path12.resolve(opts.out), text);
3850
+ }
3851
+ }
3852
+ return artifact;
3853
+ }
3854
+ var scanCommand = new Command("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").action(async (targetPath, opts) => {
3855
+ const rootDir = path12.resolve(targetPath);
3856
+ if (!await pathExists(rootDir)) {
3857
+ console.error(chalk3.red(`Path does not exist: ${rootDir}`));
3858
+ process.exit(1);
3859
+ }
3860
+ const scanOpts = {
3861
+ out: opts.out,
3862
+ format: opts.format || "text",
3863
+ failOn: opts.failOn,
3864
+ baseline: opts.baseline,
3865
+ changedOnly: opts.changedOnly,
3866
+ concurrency: parseInt(opts.concurrency, 10) || 8
3867
+ };
3868
+ const artifact = await runScan(rootDir, scanOpts);
3869
+ if (opts.failOn) {
3870
+ const hasErrors = artifact.findings.some((f) => f.level === "error");
3871
+ const hasWarnings = artifact.findings.some((f) => f.level === "warning");
3872
+ if (opts.failOn === "error" && hasErrors) {
3873
+ console.error(chalk3.red(`
3874
+ Failing: ${artifact.findings.filter((f) => f.level === "error").length} error finding(s) detected.`));
3875
+ process.exit(2);
3876
+ }
3877
+ if (opts.failOn === "warn" && (hasErrors || hasWarnings)) {
3878
+ console.error(chalk3.red(`
3879
+ Failing: findings detected at warn level or above.`));
3880
+ process.exit(2);
3881
+ }
3882
+ }
3883
+ });
3884
+
3885
+ export {
3886
+ readJsonFile,
3887
+ readTextFile,
3888
+ pathExists,
3889
+ ensureDir,
3890
+ writeJsonFile,
3891
+ writeTextFile,
3892
+ writeDefaultConfig,
3893
+ computeDriftScore,
3894
+ generateFindings,
3895
+ formatText,
3896
+ formatSarif,
3897
+ VERSION,
3898
+ runScan,
3899
+ scanCommand
3900
+ };