@vibgrate/cli 1.0.45 → 1.0.47

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.
@@ -227,6 +227,10 @@ function computeProjectId(relativePath, projectName, workspaceId) {
227
227
  const input = workspaceId ? `${relativePath}:${projectName}:${workspaceId}` : `${relativePath}:${projectName}`;
228
228
  return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
229
229
  }
230
+ function computeSolutionId(relativePath, solutionName, workspaceId) {
231
+ const input = workspaceId ? `${relativePath}:${solutionName}:${workspaceId}` : `${relativePath}:${solutionName}`;
232
+ return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
233
+ }
230
234
 
231
235
  // src/version.ts
232
236
  import { createRequire } from "module";
@@ -315,6 +319,27 @@ function formatText(artifact) {
315
319
  if (artifact.extended?.architecture) {
316
320
  lines.push(...formatArchitectureDiagram(artifact.extended.architecture));
317
321
  }
322
+ if (artifact.relationshipDiagram?.mermaid) {
323
+ 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"));
324
+ lines.push(chalk.bold.cyan("\u2551 Project Relationship Diagram \u2551"));
325
+ 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"));
326
+ lines.push("");
327
+ lines.push(chalk.bold(" Mermaid"));
328
+ lines.push(chalk.dim(artifact.relationshipDiagram.mermaid));
329
+ lines.push("");
330
+ }
331
+ if (artifact.solutions && artifact.solutions.length > 0) {
332
+ 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"));
333
+ lines.push(chalk.bold.cyan("\u2551 Solution Drift Summary \u2551"));
334
+ 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"));
335
+ lines.push("");
336
+ for (const solution of artifact.solutions) {
337
+ const solScore = solution.drift?.score;
338
+ const color = typeof solScore === "number" ? solScore >= 70 ? chalk.green : solScore >= 40 ? chalk.yellow : chalk.red : chalk.dim;
339
+ lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk.dim("n/a")}`);
340
+ }
341
+ lines.push("");
342
+ }
318
343
  const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
319
344
  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"));
320
345
  lines.push(chalk.bold.cyan("\u2551 Drift Score Summary \u2551"));
@@ -469,6 +494,26 @@ function formatExtended(ext) {
469
494
  lines.push("");
470
495
  }
471
496
  }
497
+ if (ext.uiPurpose) {
498
+ const up = ext.uiPurpose;
499
+ lines.push(chalk.bold.underline(" Product Purpose Signals"));
500
+ lines.push(` Frameworks: ${up.detectedFrameworks.length > 0 ? up.detectedFrameworks.join(", ") : chalk.dim("unknown")}`);
501
+ lines.push(` Evidence: ${up.topEvidence.length}${up.capped ? chalk.dim(` of ${up.evidenceCount} (capped)`) : ""}`);
502
+ const top = up.topEvidence.slice(0, 8);
503
+ if (top.length > 0) {
504
+ lines.push(" Top Signals:");
505
+ for (const item of top) {
506
+ lines.push(` - [${item.kind}] ${item.value} ${chalk.dim(`(${item.file})`)}`);
507
+ }
508
+ }
509
+ if (up.unknownSignals.length > 0) {
510
+ lines.push(" Unknowns:");
511
+ for (const u of up.unknownSignals.slice(0, 4)) {
512
+ lines.push(` - ${chalk.yellow(u)}`);
513
+ }
514
+ }
515
+ lines.push("");
516
+ }
472
517
  if (ext.owaspCategoryMapping) {
473
518
  const ow = ext.owaspCategoryMapping;
474
519
  lines.push(chalk.bold.underline(" OWASP Category Mapping"));
@@ -565,125 +610,22 @@ function formatExtended(ext) {
565
610
  }
566
611
  return lines;
567
612
  }
568
- var LAYER_LABELS = {
569
- "presentation": "Presentation",
570
- "routing": "Routing",
571
- "middleware": "Middleware",
572
- "services": "Services",
573
- "domain": "Domain",
574
- "data-access": "Data Access",
575
- "infrastructure": "Infrastructure",
576
- "config": "Config",
577
- "shared": "Shared",
578
- "testing": "Testing"
579
- };
580
- var LAYER_ICONS = {
581
- "presentation": "\u{1F5A5}",
582
- "routing": "\u{1F500}",
583
- "middleware": "\u{1F517}",
584
- "services": "\u2699",
585
- "domain": "\u{1F48E}",
586
- "data-access": "\u{1F5C4}",
587
- "infrastructure": "\u{1F3D7}",
588
- "config": "\u2699",
589
- "shared": "\u{1F4E6}",
590
- "testing": "\u{1F9EA}"
591
- };
592
613
  function formatArchitectureDiagram(arch) {
593
614
  const lines = [];
594
615
  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"));
595
616
  lines.push(chalk.bold.cyan("\u2551 Architecture Layers \u2551"));
596
617
  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"));
597
618
  lines.push("");
598
- const archetypeDisplay = arch.archetype === "unknown" ? "Unknown" : arch.archetype;
599
- const confPct = Math.round(arch.archetypeConfidence * 100);
600
- lines.push(chalk.bold(" Archetype: ") + chalk.cyan.bold(archetypeDisplay) + chalk.dim(` (${confPct}% confidence)`));
601
- lines.push(chalk.dim(` ${arch.totalClassified} files classified \xB7 ${arch.unclassified} unclassified`));
619
+ lines.push(chalk.bold(" Archetype: ") + `${arch.archetype}` + chalk.dim(` (${Math.round(arch.archetypeConfidence * 100)}% confidence)`));
620
+ lines.push(` Files classified: ${arch.totalClassified}` + (arch.unclassified > 0 ? chalk.dim(` (${arch.unclassified} unclassified)`) : ""));
602
621
  lines.push("");
603
- const boxWidth = 44;
604
- const visibleLayers = arch.layers.filter((l) => l.fileCount > 0 || l.techStack.length > 0 || l.services.length > 0);
605
- if (visibleLayers.length === 0) {
606
- lines.push(chalk.dim(" No layers detected"));
607
- lines.push("");
608
- return lines;
609
- }
610
- for (let i = 0; i < visibleLayers.length; i++) {
611
- const layer = visibleLayers[i];
612
- const icon = LAYER_ICONS[layer.layer] ?? "\xB7";
613
- const label = LAYER_LABELS[layer.layer] ?? layer.layer;
614
- const hasScore = layer.packages.length > 0;
615
- const ds = layer.driftScore;
616
- const scoreColor = !hasScore ? chalk.dim : ds >= 70 ? chalk.green : ds >= 40 ? chalk.yellow : chalk.red;
617
- const riskBadgeStr = layer.riskLevel === "low" ? chalk.bgGreen.black(" LOW ") : layer.riskLevel === "moderate" ? chalk.bgYellow.black(" MOD ") : chalk.bgRed.white(" HIGH ");
618
- if (i === 0) {
619
- lines.push(chalk.cyan(` \u250C${"\u2500".repeat(boxWidth)}\u2510`));
620
- }
621
- const nameStr = `${icon} ${label}`;
622
- const scoreStr = hasScore ? `${layer.driftScore}/100` : "n/a";
623
- const fileSuffix = `${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""}`;
624
- const leftContent = ` ${nameStr}`;
625
- const rightContent = `${fileSuffix} ${scoreStr} `;
626
- const leftLen = nameStr.length + 2;
627
- const rightLen = rightContent.length;
628
- const padLen = Math.max(1, boxWidth - leftLen - rightLen);
629
- lines.push(
630
- chalk.cyan(" \u2502") + ` ${icon} ${chalk.bold(label)}` + " ".repeat(padLen) + chalk.dim(fileSuffix) + " " + scoreColor.bold(scoreStr) + " " + chalk.cyan("\u2502")
631
- );
632
- const barWidth = boxWidth - 8;
633
- if (hasScore) {
634
- const filled = Math.round(layer.driftScore / 100 * barWidth);
635
- const empty = barWidth - filled;
636
- lines.push(
637
- chalk.cyan(" \u2502") + " " + scoreColor("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + " " + chalk.cyan("\u2502")
638
- );
639
- } else {
640
- lines.push(
641
- chalk.cyan(" \u2502") + " " + chalk.dim("\xB7".repeat(barWidth)) + " " + chalk.cyan("\u2502")
642
- );
643
- }
644
- if (layer.techStack.length > 0) {
645
- const techNames = layer.techStack.slice(0, 6).map((t) => t.name);
646
- const moreCount = layer.techStack.length > 6 ? ` +${layer.techStack.length - 6}` : "";
647
- const techLine = `Tech: ${techNames.join(", ")}${moreCount}`;
648
- const truncated = techLine.length > boxWidth - 6 ? techLine.slice(0, boxWidth - 9) + "..." : techLine;
649
- const techPad = Math.max(0, boxWidth - truncated.length - 4);
650
- lines.push(
651
- chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(techPad) + chalk.cyan("\u2502")
652
- );
653
- }
654
- if (layer.services.length > 0) {
655
- const svcNames = layer.services.slice(0, 5).map((s) => s.name);
656
- const moreCount = layer.services.length > 5 ? ` +${layer.services.length - 5}` : "";
657
- const svcLine = `Services: ${svcNames.join(", ")}${moreCount}`;
658
- const truncated = svcLine.length > boxWidth - 6 ? svcLine.slice(0, boxWidth - 9) + "..." : svcLine;
659
- const svcPad = Math.max(0, boxWidth - truncated.length - 4);
660
- lines.push(
661
- chalk.cyan(" \u2502") + " " + chalk.dim(truncated) + " ".repeat(svcPad) + chalk.cyan("\u2502")
662
- );
663
- }
664
- const driftPkgs = layer.packages.filter((p) => p.majorsBehind !== null && p.majorsBehind > 0);
665
- if (driftPkgs.length > 0) {
666
- const worst = driftPkgs.sort((a, b) => (b.majorsBehind ?? 0) - (a.majorsBehind ?? 0));
667
- const shown = worst.slice(0, 3);
668
- const pkgStrs = shown.map((p) => {
669
- const color = (p.majorsBehind ?? 0) >= 2 ? chalk.red : chalk.yellow;
670
- return color(`${p.name} -${p.majorsBehind}`);
671
- });
672
- const moreCount = worst.length > 3 ? chalk.dim(` +${worst.length - 3}`) : "";
673
- const pkgLine = pkgStrs.join(chalk.dim(", ")) + moreCount;
674
- const roughLen = shown.map((p) => `${p.name} -${p.majorsBehind}`).join(", ").length + (worst.length > 3 ? ` +${worst.length - 3}`.length : 0);
675
- const pkgPad = Math.max(0, boxWidth - roughLen - 4);
676
- lines.push(
677
- chalk.cyan(" \u2502") + " " + pkgLine + " ".repeat(pkgPad) + chalk.cyan("\u2502")
678
- );
679
- }
680
- if (i < visibleLayers.length - 1) {
681
- lines.push(chalk.cyan(` \u251C${"\u2500".repeat(boxWidth)}\u2524`));
682
- } else {
683
- lines.push(chalk.cyan(` \u2514${"\u2500".repeat(boxWidth)}\u2518`));
622
+ if (arch.layers.length > 0) {
623
+ for (const layer of arch.layers) {
624
+ const risk = layer.riskLevel === "low" ? chalk.green("low") : layer.riskLevel === "moderate" ? chalk.yellow("moderate") : chalk.red("high");
625
+ lines.push(` ${chalk.bold(layer.layer)} ${layer.fileCount} file${layer.fileCount !== 1 ? "s" : ""} drift ${scoreBar(layer.driftScore)} risk ${risk}`);
684
626
  }
627
+ lines.push("");
685
628
  }
686
- lines.push("");
687
629
  return lines;
688
630
  }
689
631
  function generatePriorityActions(artifact) {
@@ -1158,19 +1100,19 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
1158
1100
  });
1159
1101
 
1160
1102
  // src/commands/scan.ts
1161
- import * as path20 from "path";
1103
+ import * as path21 from "path";
1162
1104
  import { Command as Command3 } from "commander";
1163
1105
  import chalk6 from "chalk";
1164
1106
 
1165
1107
  // src/scanners/node-scanner.ts
1166
- import * as path3 from "path";
1108
+ import * as path4 from "path";
1167
1109
  import * as semver2 from "semver";
1168
1110
 
1169
1111
  // src/utils/timeout.ts
1170
1112
  async function withTimeout(promise, ms) {
1171
1113
  let timer;
1172
- const timeout = new Promise((resolve8) => {
1173
- timer = setTimeout(() => resolve8({ ok: false }), ms);
1114
+ const timeout = new Promise((resolve10) => {
1115
+ timer = setTimeout(() => resolve10({ ok: false }), ms);
1174
1116
  });
1175
1117
  try {
1176
1118
  const result = await Promise.race([
@@ -1184,8 +1126,78 @@ async function withTimeout(promise, ms) {
1184
1126
  }
1185
1127
 
1186
1128
  // src/scanners/npm-cache.ts
1187
- import { spawn } from "child_process";
1129
+ import { spawn as spawn2 } from "child_process";
1188
1130
  import * as semver from "semver";
1131
+
1132
+ // src/package-version-manifest.ts
1133
+ import { mkdtemp, readFile, rm } from "fs/promises";
1134
+ import * as path3 from "path";
1135
+ import * as os from "os";
1136
+ import { spawn } from "child_process";
1137
+ function runCommand(cmd, args) {
1138
+ return new Promise((resolve10, reject) => {
1139
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1140
+ let out = "";
1141
+ let err = "";
1142
+ child.stdout.on("data", (d) => out += String(d));
1143
+ child.stderr.on("data", (d) => err += String(d));
1144
+ child.on("error", reject);
1145
+ child.on("close", (code) => {
1146
+ if (code !== 0) {
1147
+ reject(new Error(`${cmd} ${args.join(" ")} failed (code=${code}): ${err.trim()}`));
1148
+ return;
1149
+ }
1150
+ resolve10(out);
1151
+ });
1152
+ });
1153
+ }
1154
+ async function parseManifestText(text, source) {
1155
+ try {
1156
+ return JSON.parse(text);
1157
+ } catch {
1158
+ throw new Error(`Invalid JSON in package version manifest: ${source}`);
1159
+ }
1160
+ }
1161
+ async function loadManifestFromZip(zipPath) {
1162
+ const tmpDir = await mkdtemp(path3.join(os.tmpdir(), "vibgrate-manifest-"));
1163
+ try {
1164
+ await runCommand("unzip", ["-qq", zipPath, "-d", tmpDir]);
1165
+ const candidates = [
1166
+ path3.join(tmpDir, "package-versions.json"),
1167
+ path3.join(tmpDir, "manifest.json"),
1168
+ path3.join(tmpDir, "index.json")
1169
+ ];
1170
+ for (const candidate of candidates) {
1171
+ try {
1172
+ const text = await readFile(candidate, "utf8");
1173
+ return await parseManifestText(text, candidate);
1174
+ } catch {
1175
+ }
1176
+ }
1177
+ throw new Error("Zip must contain package-versions.json, manifest.json, or index.json");
1178
+ } finally {
1179
+ await rm(tmpDir, { recursive: true, force: true });
1180
+ }
1181
+ }
1182
+ async function loadPackageVersionManifest(filePath) {
1183
+ const resolved = path3.resolve(filePath);
1184
+ if (resolved.toLowerCase().endsWith(".zip")) {
1185
+ return loadManifestFromZip(resolved);
1186
+ }
1187
+ const text = await readFile(resolved, "utf8");
1188
+ return parseManifestText(text, resolved);
1189
+ }
1190
+ function getManifestEntry(manifest, ecosystem, packageName) {
1191
+ if (!manifest) return void 0;
1192
+ const table = manifest[ecosystem];
1193
+ if (!table) return void 0;
1194
+ if (ecosystem === "nuget") {
1195
+ return table[packageName.toLowerCase()] ?? table[packageName];
1196
+ }
1197
+ return table[packageName];
1198
+ }
1199
+
1200
+ // src/scanners/npm-cache.ts
1189
1201
  function stableOnly(versions) {
1190
1202
  return versions.filter((v) => semver.valid(v) && semver.prerelease(v) === null);
1191
1203
  }
@@ -1195,8 +1207,8 @@ function maxStable(versions) {
1195
1207
  return stable.sort(semver.rcompare)[0] ?? null;
1196
1208
  }
1197
1209
  async function npmViewJson(args, cwd) {
1198
- return new Promise((resolve8, reject) => {
1199
- const child = spawn("npm", ["view", ...args, "--json"], {
1210
+ return new Promise((resolve10, reject) => {
1211
+ const child = spawn2("npm", ["view", ...args, "--json"], {
1200
1212
  cwd,
1201
1213
  shell: true,
1202
1214
  stdio: ["ignore", "pipe", "pipe"]
@@ -1213,27 +1225,42 @@ async function npmViewJson(args, cwd) {
1213
1225
  }
1214
1226
  const trimmed = out.trim();
1215
1227
  if (!trimmed) {
1216
- resolve8(null);
1228
+ resolve10(null);
1217
1229
  return;
1218
1230
  }
1219
1231
  try {
1220
- resolve8(JSON.parse(trimmed));
1232
+ resolve10(JSON.parse(trimmed));
1221
1233
  } catch {
1222
- resolve8(trimmed.replace(/^"|"$/g, ""));
1234
+ resolve10(trimmed.replace(/^"|"$/g, ""));
1223
1235
  }
1224
1236
  });
1225
1237
  });
1226
1238
  }
1227
1239
  var NpmCache = class {
1228
- constructor(cwd, sem) {
1240
+ constructor(cwd, sem, manifest, offline = false) {
1229
1241
  this.cwd = cwd;
1230
1242
  this.sem = sem;
1243
+ this.manifest = manifest;
1244
+ this.offline = offline;
1231
1245
  }
1232
1246
  meta = /* @__PURE__ */ new Map();
1233
1247
  get(pkg2) {
1234
1248
  const existing = this.meta.get(pkg2);
1235
1249
  if (existing) return existing;
1236
1250
  const p = this.sem.run(async () => {
1251
+ const manifestEntry = getManifestEntry(this.manifest, "npm", pkg2);
1252
+ if (manifestEntry) {
1253
+ const stable2 = stableOnly(manifestEntry.versions ?? []);
1254
+ const latestStableOverall2 = maxStable(stable2);
1255
+ return {
1256
+ latest: manifestEntry.latest ?? latestStableOverall2,
1257
+ stableVersions: stable2,
1258
+ latestStableOverall: latestStableOverall2
1259
+ };
1260
+ }
1261
+ if (this.offline) {
1262
+ return { latest: null, stableVersions: [], latestStableOverall: null };
1263
+ }
1237
1264
  let latest = null;
1238
1265
  let versions = [];
1239
1266
  try {
@@ -1387,7 +1414,7 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
1387
1414
  results.push(result.value);
1388
1415
  packageNameToPath.set(result.value.name, result.value.path);
1389
1416
  } else {
1390
- const relPath = path3.relative(rootDir, path3.dirname(pjPath));
1417
+ const relPath = path4.relative(rootDir, path4.dirname(pjPath));
1391
1418
  if (cache) {
1392
1419
  cache.addStuckPath(relPath || ".");
1393
1420
  }
@@ -1418,8 +1445,8 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
1418
1445
  }
1419
1446
  async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1420
1447
  const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
1421
- const absProjectPath = path3.dirname(packageJsonPath);
1422
- const projectPath = path3.relative(rootDir, absProjectPath) || ".";
1448
+ const absProjectPath = path4.dirname(packageJsonPath);
1449
+ const projectPath = path4.relative(rootDir, absProjectPath) || ".";
1423
1450
  const nodeEngine = pj.engines?.node ?? void 0;
1424
1451
  let runtimeLatest;
1425
1452
  let runtimeMajorsBehind;
@@ -1506,7 +1533,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1506
1533
  return {
1507
1534
  type: "node",
1508
1535
  path: projectPath,
1509
- name: pj.name ?? path3.basename(absProjectPath),
1536
+ name: pj.name ?? path4.basename(absProjectPath),
1510
1537
  runtime: nodeEngine,
1511
1538
  runtimeLatest,
1512
1539
  runtimeMajorsBehind,
@@ -1518,7 +1545,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1518
1545
  }
1519
1546
 
1520
1547
  // src/scanners/dotnet-scanner.ts
1521
- import * as path4 from "path";
1548
+ import * as path5 from "path";
1522
1549
  import * as semver3 from "semver";
1523
1550
  import { XMLParser } from "fast-xml-parser";
1524
1551
  var parser = new XMLParser({
@@ -1720,7 +1747,7 @@ function parseCsproj(xml, filePath) {
1720
1747
  const parsed = parser.parse(xml);
1721
1748
  const project = parsed?.Project;
1722
1749
  if (!project) {
1723
- return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName: path4.basename(filePath, ".csproj") };
1750
+ return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName: path5.basename(filePath, ".csproj") };
1724
1751
  }
1725
1752
  const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
1726
1753
  const targetFrameworks = [];
@@ -1757,7 +1784,7 @@ function parseCsproj(xml, filePath) {
1757
1784
  targetFrameworks: [...new Set(targetFrameworks)],
1758
1785
  packageReferences,
1759
1786
  projectReferences,
1760
- projectName: path4.basename(filePath, ".csproj")
1787
+ projectName: path5.basename(filePath, ".csproj")
1761
1788
  };
1762
1789
  }
1763
1790
  async function scanDotnetProjects(rootDir, nugetCache, cache) {
@@ -1767,12 +1794,12 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
1767
1794
  for (const slnPath of slnFiles) {
1768
1795
  try {
1769
1796
  const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
1770
- const slnDir = path4.dirname(slnPath);
1797
+ const slnDir = path5.dirname(slnPath);
1771
1798
  const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
1772
1799
  let match;
1773
1800
  while ((match = projectRegex.exec(slnContent)) !== null) {
1774
1801
  if (match[1]) {
1775
- const csprojPath = path4.resolve(slnDir, match[1].replace(/\\/g, "/"));
1802
+ const csprojPath = path5.resolve(slnDir, match[1].replace(/\\/g, "/"));
1776
1803
  slnCsprojPaths.add(csprojPath);
1777
1804
  }
1778
1805
  }
@@ -1789,7 +1816,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
1789
1816
  if (result.ok) {
1790
1817
  results.push(result.value);
1791
1818
  } else {
1792
- const relPath = path4.relative(rootDir, path4.dirname(csprojPath));
1819
+ const relPath = path5.relative(rootDir, path5.dirname(csprojPath));
1793
1820
  if (cache) {
1794
1821
  cache.addStuckPath(relPath || ".");
1795
1822
  }
@@ -1805,7 +1832,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
1805
1832
  async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1806
1833
  const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
1807
1834
  const data = parseCsproj(xml, csprojPath);
1808
- const csprojDir = path4.dirname(csprojPath);
1835
+ const csprojDir = path5.dirname(csprojPath);
1809
1836
  const primaryTfm = data.targetFrameworks[0];
1810
1837
  let runtimeMajorsBehind;
1811
1838
  let targetFramework = primaryTfm;
@@ -1883,9 +1910,9 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1883
1910
  }
1884
1911
  }
1885
1912
  const projectReferences = data.projectReferences.map((refPath) => {
1886
- const absRefPath = path4.resolve(csprojDir, refPath);
1887
- const relRefPath = path4.relative(rootDir, path4.dirname(absRefPath));
1888
- const refName = path4.basename(absRefPath, ".csproj");
1913
+ const absRefPath = path5.resolve(csprojDir, refPath);
1914
+ const relRefPath = path5.relative(rootDir, path5.dirname(absRefPath));
1915
+ const refName = path5.basename(absRefPath, ".csproj");
1889
1916
  return {
1890
1917
  path: relRefPath || ".",
1891
1918
  name: refName,
@@ -1906,7 +1933,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1906
1933
  const buckets = bucketsMut;
1907
1934
  return {
1908
1935
  type: "dotnet",
1909
- path: path4.relative(rootDir, csprojDir) || ".",
1936
+ path: path5.relative(rootDir, csprojDir) || ".",
1910
1937
  name: data.projectName,
1911
1938
  targetFramework,
1912
1939
  runtime: primaryTfm,
@@ -1921,7 +1948,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1921
1948
  }
1922
1949
 
1923
1950
  // src/scanners/python-scanner.ts
1924
- import * as path5 from "path";
1951
+ import * as path6 from "path";
1925
1952
  import * as semver4 from "semver";
1926
1953
  var KNOWN_PYTHON_FRAMEWORKS = {
1927
1954
  // ── Web Frameworks ──
@@ -2162,7 +2189,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
2162
2189
  const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name)) : await findPythonManifests(rootDir);
2163
2190
  const projectDirs = /* @__PURE__ */ new Map();
2164
2191
  for (const f of manifestFiles) {
2165
- const dir = path5.dirname(f);
2192
+ const dir = path6.dirname(f);
2166
2193
  if (!projectDirs.has(dir)) projectDirs.set(dir, []);
2167
2194
  projectDirs.get(dir).push(f);
2168
2195
  }
@@ -2175,7 +2202,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
2175
2202
  if (result.ok) {
2176
2203
  results.push(result.value);
2177
2204
  } else {
2178
- const relPath = path5.relative(rootDir, dir);
2205
+ const relPath = path6.relative(rootDir, dir);
2179
2206
  if (cache) cache.addStuckPath(relPath || ".");
2180
2207
  console.error(`Timeout scanning Python project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
2181
2208
  }
@@ -2191,12 +2218,12 @@ async function findPythonManifests(rootDir) {
2191
2218
  return findFiles2(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name));
2192
2219
  }
2193
2220
  async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cache) {
2194
- const relDir = path5.relative(rootDir, dir) || ".";
2195
- let projectName = path5.basename(dir === rootDir ? rootDir : dir);
2221
+ const relDir = path6.relative(rootDir, dir) || ".";
2222
+ let projectName = path6.basename(dir === rootDir ? rootDir : dir);
2196
2223
  let pythonVersion;
2197
2224
  const allDeps = /* @__PURE__ */ new Map();
2198
2225
  for (const f of manifestFiles) {
2199
- const fileName = path5.basename(f);
2226
+ const fileName = path6.basename(f);
2200
2227
  const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
2201
2228
  if (fileName === "pyproject.toml") {
2202
2229
  const parsed = parsePyprojectToml(content);
@@ -2313,7 +2340,7 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
2313
2340
  }
2314
2341
 
2315
2342
  // src/scanners/java-scanner.ts
2316
- import * as path6 from "path";
2343
+ import * as path7 from "path";
2317
2344
  import * as semver5 from "semver";
2318
2345
  import { XMLParser as XMLParser2 } from "fast-xml-parser";
2319
2346
  var parser2 = new XMLParser2({
@@ -2419,7 +2446,7 @@ function parsePom(xml, filePath) {
2419
2446
  const project = parsed?.project;
2420
2447
  if (!project) {
2421
2448
  return {
2422
- artifactId: path6.basename(path6.dirname(filePath)),
2449
+ artifactId: path7.basename(path7.dirname(filePath)),
2423
2450
  dependencies: [],
2424
2451
  modules: [],
2425
2452
  properties: {}
@@ -2479,7 +2506,7 @@ function parsePom(xml, filePath) {
2479
2506
  }
2480
2507
  return {
2481
2508
  groupId: project.groupId ? String(project.groupId) : parent?.groupId,
2482
- artifactId: String(project.artifactId ?? path6.basename(path6.dirname(filePath))),
2509
+ artifactId: String(project.artifactId ?? path7.basename(path7.dirname(filePath))),
2483
2510
  version: project.version ? String(project.version) : parent?.version,
2484
2511
  packaging: project.packaging ? String(project.packaging) : void 0,
2485
2512
  javaVersion,
@@ -2494,7 +2521,7 @@ function resolveProperty(value, properties) {
2494
2521
  }
2495
2522
  function parseGradleBuild(content, filePath) {
2496
2523
  const deps = [];
2497
- const projectName = path6.basename(path6.dirname(filePath));
2524
+ const projectName = path7.basename(path7.dirname(filePath));
2498
2525
  let javaVersion;
2499
2526
  const compatMatch = content.match(/(?:sourceCompatibility|targetCompatibility|javaVersion)\s*[=:]\s*['"]?(?:JavaVersion\.VERSION_)?(\d+)['"]?/);
2500
2527
  if (compatMatch) javaVersion = compatMatch[1];
@@ -2557,7 +2584,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
2557
2584
  const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => JAVA_MANIFEST_FILES.has(name)) : await findJavaManifests(rootDir);
2558
2585
  const projectDirs = /* @__PURE__ */ new Map();
2559
2586
  for (const f of manifestFiles) {
2560
- const dir = path6.dirname(f);
2587
+ const dir = path7.dirname(f);
2561
2588
  if (!projectDirs.has(dir)) projectDirs.set(dir, []);
2562
2589
  projectDirs.get(dir).push(f);
2563
2590
  }
@@ -2570,7 +2597,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
2570
2597
  if (result.ok) {
2571
2598
  results.push(result.value);
2572
2599
  } else {
2573
- const relPath = path6.relative(rootDir, dir);
2600
+ const relPath = path7.relative(rootDir, dir);
2574
2601
  if (cache) cache.addStuckPath(relPath || ".");
2575
2602
  console.error(`Timeout scanning Java project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
2576
2603
  }
@@ -2590,13 +2617,13 @@ async function findJavaManifests(rootDir) {
2590
2617
  return findFiles2(rootDir, (name) => JAVA_MANIFEST_FILES.has(name));
2591
2618
  }
2592
2619
  async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache) {
2593
- const relDir = path6.relative(rootDir, dir) || ".";
2594
- let projectName = path6.basename(dir === rootDir ? rootDir : dir);
2620
+ const relDir = path7.relative(rootDir, dir) || ".";
2621
+ let projectName = path7.basename(dir === rootDir ? rootDir : dir);
2595
2622
  let javaVersion;
2596
2623
  const allDeps = /* @__PURE__ */ new Map();
2597
2624
  const projectReferences = [];
2598
2625
  for (const f of manifestFiles) {
2599
- const fileName = path6.basename(f);
2626
+ const fileName = path7.basename(f);
2600
2627
  const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
2601
2628
  if (fileName === "pom.xml") {
2602
2629
  const pom = parsePom(content, f);
@@ -2610,7 +2637,7 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
2610
2637
  }
2611
2638
  for (const mod of pom.modules) {
2612
2639
  projectReferences.push({
2613
- path: path6.join(relDir, mod),
2640
+ path: path7.join(relDir, mod),
2614
2641
  name: mod,
2615
2642
  refType: "project"
2616
2643
  });
@@ -2716,8 +2743,10 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
2716
2743
  // src/scanners/nuget-cache.ts
2717
2744
  import * as semver6 from "semver";
2718
2745
  var NuGetCache = class {
2719
- constructor(sem) {
2746
+ constructor(sem, manifest, offline = false) {
2720
2747
  this.sem = sem;
2748
+ this.manifest = manifest;
2749
+ this.offline = offline;
2721
2750
  }
2722
2751
  meta = /* @__PURE__ */ new Map();
2723
2752
  baseUrl = "https://api.nuget.org/v3-flatcontainer";
@@ -2725,6 +2754,23 @@ var NuGetCache = class {
2725
2754
  const existing = this.meta.get(pkg2);
2726
2755
  if (existing) return existing;
2727
2756
  const p = this.sem.run(async () => {
2757
+ const manifestEntry = getManifestEntry(this.manifest, "nuget", pkg2);
2758
+ if (manifestEntry) {
2759
+ const stableVersions = (manifestEntry.versions ?? []).filter((v) => {
2760
+ const parsed = semver6.valid(v);
2761
+ return parsed && semver6.prerelease(v) === null;
2762
+ });
2763
+ const sorted = [...stableVersions].sort(semver6.rcompare);
2764
+ const latestStableOverall = sorted[0] ?? null;
2765
+ return {
2766
+ latest: manifestEntry.latest ?? latestStableOverall,
2767
+ stableVersions,
2768
+ latestStableOverall
2769
+ };
2770
+ }
2771
+ if (this.offline) {
2772
+ return { latest: null, stableVersions: [], latestStableOverall: null };
2773
+ }
2728
2774
  try {
2729
2775
  const url = `${this.baseUrl}/${pkg2.toLowerCase()}/index.json`;
2730
2776
  const response = await fetch(url, {
@@ -2767,14 +2813,34 @@ function pep440ToSemver2(ver) {
2767
2813
  return semver7.valid(v);
2768
2814
  }
2769
2815
  var PyPICache = class {
2770
- constructor(sem) {
2816
+ constructor(sem, manifest, offline = false) {
2771
2817
  this.sem = sem;
2818
+ this.manifest = manifest;
2819
+ this.offline = offline;
2772
2820
  }
2773
2821
  meta = /* @__PURE__ */ new Map();
2774
2822
  get(pkg2) {
2775
2823
  const existing = this.meta.get(pkg2);
2776
2824
  if (existing) return existing;
2777
2825
  const p = this.sem.run(async () => {
2826
+ const manifestEntry = getManifestEntry(this.manifest, "pypi", pkg2);
2827
+ if (manifestEntry) {
2828
+ const stableVersions = [];
2829
+ for (const ver of manifestEntry.versions ?? []) {
2830
+ const sv = pep440ToSemver2(ver);
2831
+ if (sv) stableVersions.push(sv);
2832
+ }
2833
+ const sorted = [...stableVersions].sort(semver7.rcompare);
2834
+ const latestStableOverall = sorted[0] ?? null;
2835
+ return {
2836
+ latest: manifestEntry.latest ? pep440ToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
2837
+ stableVersions,
2838
+ latestStableOverall
2839
+ };
2840
+ }
2841
+ if (this.offline) {
2842
+ return { latest: null, stableVersions: [], latestStableOverall: null };
2843
+ }
2778
2844
  try {
2779
2845
  const url = `https://pypi.org/pypi/${encodeURIComponent(pkg2)}/json`;
2780
2846
  const response = await fetch(url, {
@@ -2821,8 +2887,10 @@ function mavenToSemver2(ver) {
2821
2887
  return semver8.valid(v);
2822
2888
  }
2823
2889
  var MavenCache = class {
2824
- constructor(sem) {
2890
+ constructor(sem, manifest, offline = false) {
2825
2891
  this.sem = sem;
2892
+ this.manifest = manifest;
2893
+ this.offline = offline;
2826
2894
  }
2827
2895
  meta = /* @__PURE__ */ new Map();
2828
2896
  /**
@@ -2835,6 +2903,25 @@ var MavenCache = class {
2835
2903
  const existing = this.meta.get(key);
2836
2904
  if (existing) return existing;
2837
2905
  const p = this.sem.run(async () => {
2906
+ const key2 = `${groupId}:${artifactId}`;
2907
+ const manifestEntry = getManifestEntry(this.manifest, "maven", key2);
2908
+ if (manifestEntry) {
2909
+ const stableVersions = [];
2910
+ for (const ver of manifestEntry.versions ?? []) {
2911
+ const sv = mavenToSemver2(ver);
2912
+ if (sv) stableVersions.push(sv);
2913
+ }
2914
+ const sorted = [...stableVersions].sort(semver8.rcompare);
2915
+ const latestStableOverall = sorted[0] ?? null;
2916
+ return {
2917
+ latest: manifestEntry.latest ? mavenToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
2918
+ stableVersions,
2919
+ latestStableOverall
2920
+ };
2921
+ }
2922
+ if (this.offline) {
2923
+ return { latest: null, stableVersions: [], latestStableOverall: null };
2924
+ }
2838
2925
  try {
2839
2926
  const url = `https://search.maven.org/solrsearch/select?q=g:%22${encodeURIComponent(groupId)}%22+AND+a:%22${encodeURIComponent(artifactId)}%22&core=gav&rows=100&wt=json`;
2840
2927
  const response = await fetch(url, {
@@ -2869,7 +2956,7 @@ var MavenCache = class {
2869
2956
  };
2870
2957
 
2871
2958
  // src/config.ts
2872
- import * as path7 from "path";
2959
+ import * as path8 from "path";
2873
2960
  import * as fs from "fs/promises";
2874
2961
  var CONFIG_FILES = [
2875
2962
  "vibgrate.config.ts",
@@ -2895,7 +2982,7 @@ var DEFAULT_CONFIG = {
2895
2982
  async function loadConfig(rootDir) {
2896
2983
  let config = DEFAULT_CONFIG;
2897
2984
  for (const file of CONFIG_FILES) {
2898
- const configPath = path7.join(rootDir, file);
2985
+ const configPath = path8.join(rootDir, file);
2899
2986
  if (await pathExists(configPath)) {
2900
2987
  if (file.endsWith(".json")) {
2901
2988
  const txt = await readTextFile(configPath);
@@ -2910,7 +2997,7 @@ async function loadConfig(rootDir) {
2910
2997
  }
2911
2998
  }
2912
2999
  }
2913
- const sidecarPath = path7.join(rootDir, ".vibgrate", "auto-excludes.json");
3000
+ const sidecarPath = path8.join(rootDir, ".vibgrate", "auto-excludes.json");
2914
3001
  if (await pathExists(sidecarPath)) {
2915
3002
  try {
2916
3003
  const txt = await readTextFile(sidecarPath);
@@ -2925,7 +3012,7 @@ async function loadConfig(rootDir) {
2925
3012
  return config;
2926
3013
  }
2927
3014
  async function writeDefaultConfig(rootDir) {
2928
- const configPath = path7.join(rootDir, "vibgrate.config.ts");
3015
+ const configPath = path8.join(rootDir, "vibgrate.config.ts");
2929
3016
  const content = `import type { VibgrateConfig } from '@vibgrate/cli';
2930
3017
 
2931
3018
  const config: VibgrateConfig = {
@@ -2951,7 +3038,7 @@ export default config;
2951
3038
  }
2952
3039
  async function appendExcludePatterns(rootDir, newPatterns) {
2953
3040
  if (newPatterns.length === 0) return false;
2954
- const jsonPath = path7.join(rootDir, "vibgrate.config.json");
3041
+ const jsonPath = path8.join(rootDir, "vibgrate.config.json");
2955
3042
  if (await pathExists(jsonPath)) {
2956
3043
  try {
2957
3044
  const txt = await readTextFile(jsonPath);
@@ -2964,8 +3051,8 @@ async function appendExcludePatterns(rootDir, newPatterns) {
2964
3051
  } catch {
2965
3052
  }
2966
3053
  }
2967
- const vibgrateDir = path7.join(rootDir, ".vibgrate");
2968
- const sidecarPath = path7.join(vibgrateDir, "auto-excludes.json");
3054
+ const vibgrateDir = path8.join(rootDir, ".vibgrate");
3055
+ const sidecarPath = path8.join(vibgrateDir, "auto-excludes.json");
2969
3056
  let existing = [];
2970
3057
  if (await pathExists(sidecarPath)) {
2971
3058
  try {
@@ -2986,7 +3073,7 @@ async function appendExcludePatterns(rootDir, newPatterns) {
2986
3073
  }
2987
3074
 
2988
3075
  // src/utils/vcs.ts
2989
- import * as path8 from "path";
3076
+ import * as path9 from "path";
2990
3077
  import * as fs2 from "fs/promises";
2991
3078
  async function detectVcs(rootDir) {
2992
3079
  try {
@@ -3000,7 +3087,7 @@ async function detectGit(rootDir) {
3000
3087
  if (!gitDir) {
3001
3088
  return { type: "unknown" };
3002
3089
  }
3003
- const headPath = path8.join(gitDir, "HEAD");
3090
+ const headPath = path9.join(gitDir, "HEAD");
3004
3091
  let headContent;
3005
3092
  try {
3006
3093
  headContent = (await fs2.readFile(headPath, "utf8")).trim();
@@ -3016,18 +3103,20 @@ async function detectGit(rootDir) {
3016
3103
  } else if (/^[0-9a-f]{40}$/i.test(headContent)) {
3017
3104
  sha = headContent;
3018
3105
  }
3106
+ const remoteUrl = await readGitRemoteUrl(gitDir);
3019
3107
  return {
3020
3108
  type: "git",
3021
3109
  sha: sha ?? void 0,
3022
3110
  shortSha: sha ? sha.slice(0, 7) : void 0,
3023
- branch: branch ?? void 0
3111
+ branch: branch ?? void 0,
3112
+ remoteUrl
3024
3113
  };
3025
3114
  }
3026
3115
  async function findGitDir(startDir) {
3027
- let dir = path8.resolve(startDir);
3028
- const root = path8.parse(dir).root;
3116
+ let dir = path9.resolve(startDir);
3117
+ const root = path9.parse(dir).root;
3029
3118
  while (dir !== root) {
3030
- const gitPath = path8.join(dir, ".git");
3119
+ const gitPath = path9.join(dir, ".git");
3031
3120
  try {
3032
3121
  const stat3 = await fs2.stat(gitPath);
3033
3122
  if (stat3.isDirectory()) {
@@ -3036,18 +3125,18 @@ async function findGitDir(startDir) {
3036
3125
  if (stat3.isFile()) {
3037
3126
  const content = (await fs2.readFile(gitPath, "utf8")).trim();
3038
3127
  if (content.startsWith("gitdir: ")) {
3039
- const resolved = path8.resolve(dir, content.slice(8));
3128
+ const resolved = path9.resolve(dir, content.slice(8));
3040
3129
  return resolved;
3041
3130
  }
3042
3131
  }
3043
3132
  } catch {
3044
3133
  }
3045
- dir = path8.dirname(dir);
3134
+ dir = path9.dirname(dir);
3046
3135
  }
3047
3136
  return null;
3048
3137
  }
3049
3138
  async function resolveRef(gitDir, refPath) {
3050
- const loosePath = path8.join(gitDir, refPath);
3139
+ const loosePath = path9.join(gitDir, refPath);
3051
3140
  try {
3052
3141
  const sha = (await fs2.readFile(loosePath, "utf8")).trim();
3053
3142
  if (/^[0-9a-f]{40}$/i.test(sha)) {
@@ -3055,7 +3144,7 @@ async function resolveRef(gitDir, refPath) {
3055
3144
  }
3056
3145
  } catch {
3057
3146
  }
3058
- const packedPath = path8.join(gitDir, "packed-refs");
3147
+ const packedPath = path9.join(gitDir, "packed-refs");
3059
3148
  try {
3060
3149
  const packed = await fs2.readFile(packedPath, "utf8");
3061
3150
  for (const line of packed.split("\n")) {
@@ -3069,6 +3158,39 @@ async function resolveRef(gitDir, refPath) {
3069
3158
  }
3070
3159
  return void 0;
3071
3160
  }
3161
+ async function readGitRemoteUrl(gitDir) {
3162
+ const configPath = await resolveGitConfigPath(gitDir);
3163
+ if (!configPath) return void 0;
3164
+ try {
3165
+ const config = await fs2.readFile(configPath, "utf8");
3166
+ const originBlock = config.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
3167
+ if (!originBlock) return void 0;
3168
+ const urlMatch = originBlock[1]?.match(/\n\s*url\s*=\s*(.+)\s*/);
3169
+ return urlMatch?.[1]?.trim();
3170
+ } catch {
3171
+ return void 0;
3172
+ }
3173
+ }
3174
+ async function resolveGitConfigPath(gitDir) {
3175
+ const directConfig = path9.join(gitDir, "config");
3176
+ try {
3177
+ const stat3 = await fs2.stat(directConfig);
3178
+ if (stat3.isFile()) return directConfig;
3179
+ } catch {
3180
+ }
3181
+ const commonDirFile = path9.join(gitDir, "commondir");
3182
+ try {
3183
+ const commonDir = (await fs2.readFile(commonDirFile, "utf8")).trim();
3184
+ if (!commonDir) return void 0;
3185
+ const resolvedCommonDir = path9.resolve(gitDir, commonDir);
3186
+ const commonConfig = path9.join(resolvedCommonDir, "config");
3187
+ const stat3 = await fs2.stat(commonConfig);
3188
+ if (stat3.isFile()) return commonConfig;
3189
+ } catch {
3190
+ return void 0;
3191
+ }
3192
+ return void 0;
3193
+ }
3072
3194
 
3073
3195
  // src/ui/progress.ts
3074
3196
  import chalk4 from "chalk";
@@ -3452,11 +3574,11 @@ var ScanProgress = class {
3452
3574
 
3453
3575
  // src/ui/scan-history.ts
3454
3576
  import * as fs3 from "fs/promises";
3455
- import * as path9 from "path";
3577
+ import * as path10 from "path";
3456
3578
  var HISTORY_FILENAME = "scan_history.json";
3457
3579
  var MAX_RECORDS = 10;
3458
3580
  async function loadScanHistory(rootDir) {
3459
- const filePath = path9.join(rootDir, ".vibgrate", HISTORY_FILENAME);
3581
+ const filePath = path10.join(rootDir, ".vibgrate", HISTORY_FILENAME);
3460
3582
  try {
3461
3583
  const txt = await fs3.readFile(filePath, "utf8");
3462
3584
  const data = JSON.parse(txt);
@@ -3469,8 +3591,8 @@ async function loadScanHistory(rootDir) {
3469
3591
  }
3470
3592
  }
3471
3593
  async function saveScanHistory(rootDir, record) {
3472
- const dir = path9.join(rootDir, ".vibgrate");
3473
- const filePath = path9.join(dir, HISTORY_FILENAME);
3594
+ const dir = path10.join(rootDir, ".vibgrate");
3595
+ const filePath = path10.join(dir, HISTORY_FILENAME);
3474
3596
  let history;
3475
3597
  const existing = await loadScanHistory(rootDir);
3476
3598
  if (existing) {
@@ -3534,7 +3656,7 @@ function estimateStepDurations(history, currentFileCount) {
3534
3656
  }
3535
3657
 
3536
3658
  // src/scanners/platform-matrix.ts
3537
- import * as path10 from "path";
3659
+ import * as path11 from "path";
3538
3660
  var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
3539
3661
  // Image / media processing
3540
3662
  "sharp",
@@ -3814,7 +3936,7 @@ async function scanPlatformMatrix(rootDir, cache) {
3814
3936
  }
3815
3937
  result.dockerBaseImages = [...baseImages].sort();
3816
3938
  for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
3817
- const exists = cache ? await cache.pathExists(path10.join(rootDir, file)) : await pathExists(path10.join(rootDir, file));
3939
+ const exists = cache ? await cache.pathExists(path11.join(rootDir, file)) : await pathExists(path11.join(rootDir, file));
3818
3940
  if (exists) {
3819
3941
  result.nodeVersionFiles.push(file);
3820
3942
  }
@@ -3891,7 +4013,7 @@ function scanDependencyRisk(projects) {
3891
4013
  }
3892
4014
 
3893
4015
  // src/scanners/dependency-graph.ts
3894
- import * as path11 from "path";
4016
+ import * as path12 from "path";
3895
4017
  function parsePnpmLock(content) {
3896
4018
  const entries = [];
3897
4019
  const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
@@ -3950,9 +4072,9 @@ async function scanDependencyGraph(rootDir, cache) {
3950
4072
  phantomDependencies: []
3951
4073
  };
3952
4074
  let entries = [];
3953
- const pnpmLock = path11.join(rootDir, "pnpm-lock.yaml");
3954
- const npmLock = path11.join(rootDir, "package-lock.json");
3955
- const yarnLock = path11.join(rootDir, "yarn.lock");
4075
+ const pnpmLock = path12.join(rootDir, "pnpm-lock.yaml");
4076
+ const npmLock = path12.join(rootDir, "package-lock.json");
4077
+ const yarnLock = path12.join(rootDir, "yarn.lock");
3956
4078
  const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
3957
4079
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3958
4080
  if (await _pathExists(pnpmLock)) {
@@ -3999,7 +4121,7 @@ async function scanDependencyGraph(rootDir, cache) {
3999
4121
  for (const pjPath of pkgFiles) {
4000
4122
  try {
4001
4123
  const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
4002
- const relPath = path11.relative(rootDir, pjPath);
4124
+ const relPath = path12.relative(rootDir, pjPath);
4003
4125
  for (const section of ["dependencies", "devDependencies"]) {
4004
4126
  const deps = pj[section];
4005
4127
  if (!deps) continue;
@@ -4345,7 +4467,7 @@ function scanToolingInventory(projects) {
4345
4467
  }
4346
4468
 
4347
4469
  // src/scanners/build-deploy.ts
4348
- import * as path12 from "path";
4470
+ import * as path13 from "path";
4349
4471
  var CI_FILES = {
4350
4472
  ".github/workflows": "github-actions",
4351
4473
  ".gitlab-ci.yml": "gitlab-ci",
@@ -4398,17 +4520,17 @@ async function scanBuildDeploy(rootDir, cache) {
4398
4520
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
4399
4521
  const ciSystems = /* @__PURE__ */ new Set();
4400
4522
  for (const [file, system] of Object.entries(CI_FILES)) {
4401
- const fullPath = path12.join(rootDir, file);
4523
+ const fullPath = path13.join(rootDir, file);
4402
4524
  if (await _pathExists(fullPath)) {
4403
4525
  ciSystems.add(system);
4404
4526
  }
4405
4527
  }
4406
- const ghWorkflowDir = path12.join(rootDir, ".github", "workflows");
4528
+ const ghWorkflowDir = path13.join(rootDir, ".github", "workflows");
4407
4529
  if (await _pathExists(ghWorkflowDir)) {
4408
4530
  try {
4409
4531
  if (cache) {
4410
4532
  const entries = await cache.walkDir(rootDir);
4411
- const ghPrefix = path12.relative(rootDir, ghWorkflowDir) + path12.sep;
4533
+ const ghPrefix = path13.relative(rootDir, ghWorkflowDir) + path13.sep;
4412
4534
  result.ciWorkflowCount = entries.filter(
4413
4535
  (e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
4414
4536
  ).length;
@@ -4459,11 +4581,11 @@ async function scanBuildDeploy(rootDir, cache) {
4459
4581
  (name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
4460
4582
  );
4461
4583
  if (cfnFiles.length > 0) iacSystems.add("cloudformation");
4462
- if (await _pathExists(path12.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
4584
+ if (await _pathExists(path13.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
4463
4585
  result.iac = [...iacSystems].sort();
4464
4586
  const releaseTools = /* @__PURE__ */ new Set();
4465
4587
  for (const [file, tool] of Object.entries(RELEASE_FILES)) {
4466
- if (await _pathExists(path12.join(rootDir, file))) releaseTools.add(tool);
4588
+ if (await _pathExists(path13.join(rootDir, file))) releaseTools.add(tool);
4467
4589
  }
4468
4590
  const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
4469
4591
  for (const pjPath of pkgFiles) {
@@ -4488,19 +4610,19 @@ async function scanBuildDeploy(rootDir, cache) {
4488
4610
  };
4489
4611
  const managers = /* @__PURE__ */ new Set();
4490
4612
  for (const [file, manager] of Object.entries(lockfileMap)) {
4491
- if (await _pathExists(path12.join(rootDir, file))) managers.add(manager);
4613
+ if (await _pathExists(path13.join(rootDir, file))) managers.add(manager);
4492
4614
  }
4493
4615
  result.packageManagers = [...managers].sort();
4494
4616
  const monoTools = /* @__PURE__ */ new Set();
4495
4617
  for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
4496
- if (await _pathExists(path12.join(rootDir, file))) monoTools.add(tool);
4618
+ if (await _pathExists(path13.join(rootDir, file))) monoTools.add(tool);
4497
4619
  }
4498
4620
  result.monorepoTools = [...monoTools].sort();
4499
4621
  return result;
4500
4622
  }
4501
4623
 
4502
4624
  // src/scanners/ts-modernity.ts
4503
- import * as path13 from "path";
4625
+ import * as path14 from "path";
4504
4626
  async function scanTsModernity(rootDir, cache) {
4505
4627
  const result = {
4506
4628
  typescriptVersion: null,
@@ -4538,7 +4660,7 @@ async function scanTsModernity(rootDir, cache) {
4538
4660
  if (hasEsm && hasCjs) result.moduleType = "mixed";
4539
4661
  else if (hasEsm) result.moduleType = "esm";
4540
4662
  else if (hasCjs) result.moduleType = "cjs";
4541
- let tsConfigPath = path13.join(rootDir, "tsconfig.json");
4663
+ let tsConfigPath = path14.join(rootDir, "tsconfig.json");
4542
4664
  const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
4543
4665
  if (!tsConfigExists) {
4544
4666
  const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
@@ -4885,7 +5007,7 @@ function scanBreakingChangeExposure(projects) {
4885
5007
 
4886
5008
  // src/scanners/file-hotspots.ts
4887
5009
  import * as fs4 from "fs/promises";
4888
- import * as path14 from "path";
5010
+ import * as path15 from "path";
4889
5011
  var SKIP_DIRS = /* @__PURE__ */ new Set([
4890
5012
  "node_modules",
4891
5013
  ".git",
@@ -4930,9 +5052,9 @@ async function scanFileHotspots(rootDir, cache) {
4930
5052
  const entries = await cache.walkDir(rootDir);
4931
5053
  for (const entry of entries) {
4932
5054
  if (!entry.isFile) continue;
4933
- const ext = path14.extname(entry.name).toLowerCase();
5055
+ const ext = path15.extname(entry.name).toLowerCase();
4934
5056
  if (SKIP_EXTENSIONS.has(ext)) continue;
4935
- const depth = entry.relPath.split(path14.sep).length - 1;
5057
+ const depth = entry.relPath.split(path15.sep).length - 1;
4936
5058
  if (depth > maxDepth) maxDepth = depth;
4937
5059
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
4938
5060
  try {
@@ -4961,15 +5083,15 @@ async function scanFileHotspots(rootDir, cache) {
4961
5083
  for (const e of entries) {
4962
5084
  if (e.isDirectory) {
4963
5085
  if (SKIP_DIRS.has(e.name)) continue;
4964
- await walk(path14.join(dir, e.name), depth + 1);
5086
+ await walk(path15.join(dir, e.name), depth + 1);
4965
5087
  } else if (e.isFile) {
4966
- const ext = path14.extname(e.name).toLowerCase();
5088
+ const ext = path15.extname(e.name).toLowerCase();
4967
5089
  if (SKIP_EXTENSIONS.has(ext)) continue;
4968
5090
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
4969
5091
  try {
4970
- const stat3 = await fs4.stat(path14.join(dir, e.name));
5092
+ const stat3 = await fs4.stat(path15.join(dir, e.name));
4971
5093
  allFiles.push({
4972
- path: path14.relative(rootDir, path14.join(dir, e.name)),
5094
+ path: path15.relative(rootDir, path15.join(dir, e.name)),
4973
5095
  bytes: stat3.size
4974
5096
  });
4975
5097
  } catch {
@@ -4992,7 +5114,7 @@ async function scanFileHotspots(rootDir, cache) {
4992
5114
  }
4993
5115
 
4994
5116
  // src/scanners/security-posture.ts
4995
- import * as path15 from "path";
5117
+ import * as path16 from "path";
4996
5118
  var LOCKFILES = {
4997
5119
  "pnpm-lock.yaml": "pnpm",
4998
5120
  "package-lock.json": "npm",
@@ -5013,14 +5135,14 @@ async function scanSecurityPosture(rootDir, cache) {
5013
5135
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
5014
5136
  const foundLockfiles = [];
5015
5137
  for (const [file, type] of Object.entries(LOCKFILES)) {
5016
- if (await _pathExists(path15.join(rootDir, file))) {
5138
+ if (await _pathExists(path16.join(rootDir, file))) {
5017
5139
  foundLockfiles.push(type);
5018
5140
  }
5019
5141
  }
5020
5142
  result.lockfilePresent = foundLockfiles.length > 0;
5021
5143
  result.multipleLockfileTypes = foundLockfiles.length > 1;
5022
5144
  result.lockfileTypes = foundLockfiles.sort();
5023
- const gitignorePath = path15.join(rootDir, ".gitignore");
5145
+ const gitignorePath = path16.join(rootDir, ".gitignore");
5024
5146
  if (await _pathExists(gitignorePath)) {
5025
5147
  try {
5026
5148
  const content = await _readTextFile(gitignorePath);
@@ -5035,7 +5157,7 @@ async function scanSecurityPosture(rootDir, cache) {
5035
5157
  }
5036
5158
  }
5037
5159
  for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
5038
- if (await _pathExists(path15.join(rootDir, envFile))) {
5160
+ if (await _pathExists(path16.join(rootDir, envFile))) {
5039
5161
  if (!result.gitignoreCoversEnv) {
5040
5162
  result.envFilesTracked = true;
5041
5163
  break;
@@ -5046,8 +5168,8 @@ async function scanSecurityPosture(rootDir, cache) {
5046
5168
  }
5047
5169
 
5048
5170
  // src/scanners/security-scanners.ts
5049
- import { spawn as spawn2 } from "child_process";
5050
- import * as path16 from "path";
5171
+ import { spawn as spawn3 } from "child_process";
5172
+ import * as path17 from "path";
5051
5173
  var TOOL_MATRIX = [
5052
5174
  { key: "semgrep", category: "sast", command: "semgrep", versionArgs: ["--version"], minRecommendedVersion: "1.75.0" },
5053
5175
  { key: "gitleaks", category: "secrets", command: "gitleaks", versionArgs: ["version"], minRecommendedVersion: "8.20.0" },
@@ -5060,8 +5182,8 @@ var SECRET_HEURISTICS = [
5060
5182
  { detector: "private-key", pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
5061
5183
  { detector: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
5062
5184
  ];
5063
- var defaultRunner = (command, args) => new Promise((resolve8, reject) => {
5064
- const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"] });
5185
+ var defaultRunner = (command, args) => new Promise((resolve10, reject) => {
5186
+ const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
5065
5187
  let stdout = "";
5066
5188
  let stderr = "";
5067
5189
  child.stdout.on("data", (d) => {
@@ -5072,7 +5194,7 @@ var defaultRunner = (command, args) => new Promise((resolve8, reject) => {
5072
5194
  });
5073
5195
  child.on("error", reject);
5074
5196
  child.on("close", (code) => {
5075
- resolve8({ stdout, stderr, exitCode: code ?? 1 });
5197
+ resolve10({ stdout, stderr, exitCode: code ?? 1 });
5076
5198
  });
5077
5199
  });
5078
5200
  function compareSemver(a, b) {
@@ -5142,7 +5264,7 @@ async function detectSecretHeuristics(rootDir, cache) {
5142
5264
  const findings = [];
5143
5265
  for (const entry of entries) {
5144
5266
  if (!entry.isFile) continue;
5145
- const ext = path16.extname(entry.name).toLowerCase();
5267
+ const ext = path17.extname(entry.name).toLowerCase();
5146
5268
  if (ext && [".png", ".jpg", ".jpeg", ".gif", ".zip", ".pdf"].includes(ext)) continue;
5147
5269
  const content = await cache.readTextFile(entry.absPath);
5148
5270
  if (!content || content.length > 3e5) continue;
@@ -5165,9 +5287,9 @@ async function scanSecurityScanners(rootDir, cache, runner = defaultRunner) {
5165
5287
  const [semgrep, gitleaks, trufflehog] = await Promise.all(TOOL_MATRIX.map((tool) => assessTool(tool, runner)));
5166
5288
  const heuristicFindings = await detectSecretHeuristics(rootDir, cache);
5167
5289
  const configFiles = {
5168
- semgrep: await cache.pathExists(path16.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path16.join(rootDir, ".semgrep.yaml")),
5169
- gitleaks: await cache.pathExists(path16.join(rootDir, ".gitleaks.toml")),
5170
- trufflehog: await cache.pathExists(path16.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path16.join(rootDir, ".trufflehog.yaml"))
5290
+ semgrep: await cache.pathExists(path17.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path17.join(rootDir, ".semgrep.yaml")),
5291
+ gitleaks: await cache.pathExists(path17.join(rootDir, ".gitleaks.toml")),
5292
+ trufflehog: await cache.pathExists(path17.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path17.join(rootDir, ".trufflehog.yaml"))
5171
5293
  };
5172
5294
  return {
5173
5295
  semgrep,
@@ -5592,7 +5714,7 @@ function scanServiceDependencies(projects) {
5592
5714
  }
5593
5715
 
5594
5716
  // src/scanners/architecture.ts
5595
- import * as path17 from "path";
5717
+ import * as path18 from "path";
5596
5718
  import * as fs5 from "fs/promises";
5597
5719
  var ARCHETYPE_SIGNALS = [
5598
5720
  // Meta-frameworks (highest priority — they imply routing patterns)
@@ -5891,9 +6013,9 @@ async function walkSourceFiles(rootDir, cache) {
5891
6013
  const entries = await cache.walkDir(rootDir);
5892
6014
  return entries.filter((e) => {
5893
6015
  if (!e.isFile) return false;
5894
- const name = path17.basename(e.absPath);
6016
+ const name = path18.basename(e.absPath);
5895
6017
  if (name.startsWith(".") && name !== ".") return false;
5896
- const ext = path17.extname(name);
6018
+ const ext = path18.extname(name);
5897
6019
  return SOURCE_EXTENSIONS.has(ext);
5898
6020
  }).map((e) => e.relPath);
5899
6021
  }
@@ -5907,15 +6029,15 @@ async function walkSourceFiles(rootDir, cache) {
5907
6029
  }
5908
6030
  for (const entry of entries) {
5909
6031
  if (entry.name.startsWith(".") && entry.name !== ".") continue;
5910
- const fullPath = path17.join(dir, entry.name);
6032
+ const fullPath = path18.join(dir, entry.name);
5911
6033
  if (entry.isDirectory()) {
5912
6034
  if (!IGNORE_DIRS.has(entry.name)) {
5913
6035
  await walk(fullPath);
5914
6036
  }
5915
6037
  } else if (entry.isFile()) {
5916
- const ext = path17.extname(entry.name);
6038
+ const ext = path18.extname(entry.name);
5917
6039
  if (SOURCE_EXTENSIONS.has(ext)) {
5918
- files.push(path17.relative(rootDir, fullPath));
6040
+ files.push(path18.relative(rootDir, fullPath));
5919
6041
  }
5920
6042
  }
5921
6043
  }
@@ -5939,7 +6061,7 @@ function classifyFile(filePath, archetype) {
5939
6061
  }
5940
6062
  }
5941
6063
  if (!bestMatch || bestMatch.confidence < 0.7) {
5942
- const baseName = path17.basename(filePath, path17.extname(filePath));
6064
+ const baseName = path18.basename(filePath, path18.extname(filePath));
5943
6065
  const cleanBase = baseName.replace(/\.(test|spec)$/, "");
5944
6066
  for (const rule of SUFFIX_RULES) {
5945
6067
  if (cleanBase.endsWith(rule.suffix)) {
@@ -6022,6 +6144,56 @@ function mapToolingToLayers(tooling, services, depsByLayer) {
6022
6144
  }
6023
6145
  return { layerTooling, layerServices };
6024
6146
  }
6147
+ function generateLayerFlowMermaid(layers) {
6148
+ const labels = {
6149
+ presentation: "Presentation",
6150
+ routing: "Routing",
6151
+ middleware: "Middleware",
6152
+ services: "Services",
6153
+ domain: "Domain",
6154
+ "data-access": "Data Access",
6155
+ infrastructure: "Infrastructure",
6156
+ config: "Config",
6157
+ shared: "Shared",
6158
+ testing: "Testing"
6159
+ };
6160
+ if (layers.length === 0) {
6161
+ return 'flowchart TD\n APP["Project"]';
6162
+ }
6163
+ const ordered = [...layers];
6164
+ const lines = ["flowchart TD"];
6165
+ for (let i = 0; i < ordered.length; i++) {
6166
+ const layer = ordered[i];
6167
+ lines.push(` L${i}["${labels[layer]}"]`);
6168
+ if (i > 0) lines.push(` L${i - 1} --> L${i}`);
6169
+ }
6170
+ return lines.join("\n");
6171
+ }
6172
+ async function buildProjectArchitectureMermaid(rootDir, project, archetype, cache) {
6173
+ const projectRoot = path18.resolve(rootDir, project.path || ".");
6174
+ const allFiles = await walkSourceFiles(projectRoot, cache);
6175
+ const layerSet = /* @__PURE__ */ new Set();
6176
+ for (const rel of allFiles) {
6177
+ const classification = classifyFile(rel, archetype);
6178
+ if (classification) {
6179
+ layerSet.add(classification.layer);
6180
+ }
6181
+ }
6182
+ const layerOrder = [
6183
+ "presentation",
6184
+ "routing",
6185
+ "middleware",
6186
+ "services",
6187
+ "domain",
6188
+ "data-access",
6189
+ "infrastructure",
6190
+ "config",
6191
+ "shared",
6192
+ "testing"
6193
+ ];
6194
+ const orderedLayers = layerOrder.filter((l) => layerSet.has(l));
6195
+ return generateLayerFlowMermaid(orderedLayers);
6196
+ }
6025
6197
  async function scanArchitecture(rootDir, projects, tooling, services, cache) {
6026
6198
  const { archetype, confidence: archetypeConfidence } = detectArchetype(projects);
6027
6199
  const sourceFiles = await walkSourceFiles(rootDir, cache);
@@ -6124,7 +6296,7 @@ async function scanArchitecture(rootDir, projects, tooling, services, cache) {
6124
6296
  }
6125
6297
 
6126
6298
  // src/scanners/code-quality.ts
6127
- import * as path18 from "path";
6299
+ import * as path19 from "path";
6128
6300
  import * as ts from "typescript";
6129
6301
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
6130
6302
  var DEFAULT_RESULT = {
@@ -6155,9 +6327,9 @@ async function scanCodeQuality(rootDir, cache) {
6155
6327
  continue;
6156
6328
  }
6157
6329
  if (!raw.trim()) continue;
6158
- const rel = normalizeModuleId(path18.relative(rootDir, filePath));
6330
+ const rel = normalizeModuleId(path19.relative(rootDir, filePath));
6159
6331
  const source = ts.createSourceFile(filePath, raw, ts.ScriptTarget.Latest, true);
6160
- const imports = collectLocalImports(source, path18.dirname(filePath), rootDir);
6332
+ const imports = collectLocalImports(source, path19.dirname(filePath), rootDir);
6161
6333
  depGraph.set(rel, imports);
6162
6334
  const fileMetrics = computeFileMetrics(source, raw);
6163
6335
  totalFunctions += fileMetrics.functionsAnalyzed;
@@ -6191,9 +6363,9 @@ async function scanCodeQuality(rootDir, cache) {
6191
6363
  async function findSourceFiles(rootDir, cache) {
6192
6364
  if (cache) {
6193
6365
  const entries = await cache.walkDir(rootDir);
6194
- return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path18.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
6366
+ return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path19.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
6195
6367
  }
6196
- const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path18.extname(name).toLowerCase()));
6368
+ const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path19.extname(name).toLowerCase()));
6197
6369
  return files;
6198
6370
  }
6199
6371
  function collectLocalImports(source, fileDir, rootDir) {
@@ -6214,8 +6386,8 @@ function collectLocalImports(source, fileDir, rootDir) {
6214
6386
  }
6215
6387
  function resolveLocalImport(specifier, fileDir, rootDir) {
6216
6388
  if (!specifier.startsWith(".")) return null;
6217
- const rawTarget = path18.resolve(fileDir, specifier);
6218
- const normalized = path18.relative(rootDir, rawTarget).replace(/\\/g, "/");
6389
+ const rawTarget = path19.resolve(fileDir, specifier);
6390
+ const normalized = path19.relative(rootDir, rawTarget).replace(/\\/g, "/");
6219
6391
  if (!normalized || normalized.startsWith("..")) return null;
6220
6392
  return normalizeModuleId(normalized);
6221
6393
  }
@@ -6356,8 +6528,8 @@ function visitEach(node, cb) {
6356
6528
  }
6357
6529
 
6358
6530
  // src/scanners/owasp-category-mapping.ts
6359
- import { spawn as spawn3 } from "child_process";
6360
- import * as path19 from "path";
6531
+ import { spawn as spawn4 } from "child_process";
6532
+ import * as path20 from "path";
6361
6533
  var OWASP_CONFIG = "p/owasp-top-ten";
6362
6534
  var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
6363
6535
  ".js",
@@ -6382,8 +6554,8 @@ var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
6382
6554
  ".env"
6383
6555
  ]);
6384
6556
  async function runSemgrep(args, cwd, stdin) {
6385
- return new Promise((resolve8, reject) => {
6386
- const child = spawn3("semgrep", args, {
6557
+ return new Promise((resolve10, reject) => {
6558
+ const child = spawn4("semgrep", args, {
6387
6559
  cwd,
6388
6560
  shell: true,
6389
6561
  stdio: ["pipe", "pipe", "pipe"]
@@ -6398,7 +6570,7 @@ async function runSemgrep(args, cwd, stdin) {
6398
6570
  });
6399
6571
  child.on("error", reject);
6400
6572
  child.on("close", (code) => {
6401
- resolve8({ code: code ?? 1, stdout, stderr });
6573
+ resolve10({ code: code ?? 1, stdout, stderr });
6402
6574
  });
6403
6575
  if (stdin !== void 0) child.stdin.write(stdin);
6404
6576
  child.stdin.end();
@@ -6436,7 +6608,7 @@ function parseFindings(results, rootDir) {
6436
6608
  const metadata = r.extra?.metadata;
6437
6609
  return {
6438
6610
  ruleId: r.check_id ?? "unknown",
6439
- path: r.path ? path19.relative(rootDir, path19.resolve(rootDir, r.path)) : "",
6611
+ path: r.path ? path20.relative(rootDir, path20.resolve(rootDir, r.path)) : "",
6440
6612
  line: r.start?.line ?? 1,
6441
6613
  endLine: r.end?.line,
6442
6614
  message: r.extra?.message ?? "Potential security issue",
@@ -6496,7 +6668,7 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
6496
6668
  }
6497
6669
  }
6498
6670
  const entries = cache ? await cache.walkDir(rootDir) : [];
6499
- const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path19.extname(e.name).toLowerCase()));
6671
+ const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path20.extname(e.name).toLowerCase()));
6500
6672
  const findings = [];
6501
6673
  const errors = [];
6502
6674
  let scannedFiles = 0;
@@ -6528,8 +6700,223 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
6528
6700
  };
6529
6701
  }
6530
6702
 
6703
+ // src/scanners/ui-purpose.ts
6704
+ var UI_EXTENSIONS = /* @__PURE__ */ new Set([
6705
+ ".tsx",
6706
+ ".jsx",
6707
+ ".ts",
6708
+ ".js",
6709
+ ".vue",
6710
+ ".svelte",
6711
+ ".html",
6712
+ ".mdx",
6713
+ ".json",
6714
+ ".yml",
6715
+ ".yaml"
6716
+ ]);
6717
+ var HIGH_SIGNAL_DEPENDENCIES = /* @__PURE__ */ new Set([
6718
+ "stripe",
6719
+ "posthog-js",
6720
+ "posthog-node",
6721
+ "@sentry/node",
6722
+ "@sentry/browser",
6723
+ "@sentry/react",
6724
+ "next-auth",
6725
+ "firebase",
6726
+ "auth0",
6727
+ "@auth0/auth0-react",
6728
+ "@supabase/supabase-js",
6729
+ "supabase",
6730
+ "clerk",
6731
+ "@clerk/clerk-js"
6732
+ ]);
6733
+ var GENERIC_LOW_SIGNAL = /* @__PURE__ */ new Set(["welcome", "home", "click here", "learn more", "submit", "cancel"]);
6734
+ async function scanUiPurpose(rootDir, fileCache, maxItems = 300) {
6735
+ const entries = await fileCache.walkDir(rootDir);
6736
+ const files = entries.filter((e) => e.isFile);
6737
+ const packageJsonEntry = files.find((e) => e.relPath === "package.json");
6738
+ let packageJson = {};
6739
+ if (packageJsonEntry) {
6740
+ try {
6741
+ packageJson = JSON.parse(await fileCache.readTextFile(packageJsonEntry.absPath));
6742
+ } catch {
6743
+ packageJson = {};
6744
+ }
6745
+ }
6746
+ const frameworks = detectFrameworks(packageJson);
6747
+ const items = [];
6748
+ for (const entry of files) {
6749
+ const ext = extension(entry.relPath);
6750
+ if (!UI_EXTENSIONS.has(ext)) continue;
6751
+ if (!isLikelyUiPath(entry.relPath)) {
6752
+ const routeHints2 = extractRouteHints(entry.relPath, frameworks);
6753
+ if (routeHints2.length > 0) {
6754
+ items.push(...routeHints2.map((r) => ({ ...r, file: entry.relPath })));
6755
+ }
6756
+ continue;
6757
+ }
6758
+ const routeHints = extractRouteHints(entry.relPath, frameworks);
6759
+ if (routeHints.length > 0) {
6760
+ items.push(...routeHints.map((r) => ({ ...r, file: entry.relPath })));
6761
+ }
6762
+ const src = await fileCache.readTextFile(entry.absPath);
6763
+ if (!src || src.length > 512e3) continue;
6764
+ const strings = extractUiStrings(src);
6765
+ for (const text of strings) {
6766
+ const kind = classifyString(text);
6767
+ const weight = scoreString(kind, text);
6768
+ items.push({ kind, value: text, file: entry.relPath, weight });
6769
+ }
6770
+ if (/(featureFlag|FEATURE_FLAG|launchDarkly|isFeatureEnabled|flags\.)/.test(src)) {
6771
+ items.push({ kind: "feature_flag", value: `flags in ${entry.relPath}`, file: entry.relPath, weight: 2 });
6772
+ }
6773
+ }
6774
+ const deps = getDependencies(packageJson);
6775
+ for (const [name, version] of deps) {
6776
+ if (HIGH_SIGNAL_DEPENDENCIES.has(name)) {
6777
+ items.push({ kind: "dependency", value: `${name}@${version}`, file: "package.json", weight: 4 });
6778
+ }
6779
+ }
6780
+ const deduped = dedupeByKindValue(items).sort((a, b) => b.weight - a.weight || a.value.localeCompare(b.value));
6781
+ const cappedItems = deduped.slice(0, maxItems);
6782
+ const unknownSignals = buildUnknowns(cappedItems);
6783
+ return {
6784
+ enabled: true,
6785
+ detectedFrameworks: frameworks,
6786
+ evidenceCount: deduped.length,
6787
+ capped: deduped.length > cappedItems.length,
6788
+ topEvidence: cappedItems,
6789
+ unknownSignals
6790
+ };
6791
+ }
6792
+ function extension(relPath) {
6793
+ const i = relPath.lastIndexOf(".");
6794
+ return i === -1 ? "" : relPath.slice(i).toLowerCase();
6795
+ }
6796
+ function isLikelyUiPath(relPath) {
6797
+ const lower = relPath.toLowerCase();
6798
+ return lower.startsWith("pages/") || lower.includes("/pages/") || lower.startsWith("app/") || lower.includes("/app/") || lower.startsWith("components/") || lower.includes("/components/") || lower.startsWith("ui/") || lower.includes("/ui/") || lower.startsWith("views/") || lower.includes("/views/") || lower.includes("/routes") || lower.startsWith("routes") || lower.includes("/router") || lower.startsWith("router") || lower.startsWith("locales/") || lower.includes("/locales/") || lower.startsWith("i18n/") || lower.includes("/i18n/") || lower.endsWith(".html") || lower.endsWith(".mdx");
6799
+ }
6800
+ function detectFrameworks(pkgJson) {
6801
+ const deps = {
6802
+ ...asRecord(pkgJson.dependencies),
6803
+ ...asRecord(pkgJson.devDependencies)
6804
+ };
6805
+ const hits = [];
6806
+ if (deps.next) hits.push("nextjs");
6807
+ if (deps.nuxt || deps.nuxt3) hits.push("nuxt");
6808
+ if (deps.react) hits.push("react");
6809
+ if (deps.vue) hits.push("vue");
6810
+ if (deps.svelte) hits.push("svelte");
6811
+ if (deps["@angular/core"]) hits.push("angular");
6812
+ return Array.from(new Set(hits));
6813
+ }
6814
+ function extractRouteHints(relPath, frameworks) {
6815
+ const items = [];
6816
+ if (frameworks.includes("nextjs")) {
6817
+ const m = relPath.match(/^(?:src\/)?(pages|app)\/(.+)\.(tsx|jsx|ts|js)$/);
6818
+ if (m) {
6819
+ let route = "/" + m[2].replace(/(^|\/)page$/i, "").replace(/(^|\/)route$/i, "").replace(/index$/i, "").replace(/\[(?:\.\.\.)?(.+?)\]/g, ":$1").replace(/\/+/g, "/");
6820
+ if (route !== "/" && route.endsWith("/")) route = route.slice(0, -1);
6821
+ items.push({ kind: "route", value: route || "/", weight: 5 });
6822
+ }
6823
+ }
6824
+ if (/(^|\/)(routes?|router)\.(ts|js|json)$/.test(relPath) || relPath.includes("/router/")) {
6825
+ items.push({ kind: "route", value: `route config: ${relPath}`, weight: 3 });
6826
+ }
6827
+ return items;
6828
+ }
6829
+ function extractUiStrings(src) {
6830
+ const out = [];
6831
+ const textNodeRegex = />\s*([A-Za-z0-9][^<>]{2,120}?)\s*</g;
6832
+ for (const m of src.matchAll(textNodeRegex)) {
6833
+ const s = normaliseText(m[1] ?? "");
6834
+ if (isUsefulString(s)) out.push(s);
6835
+ }
6836
+ const titleRegex = /<title>\s*([^<]{2,120})\s*<\/title>|title\s*[:=]\s*["'`](.{2,120}?)["'`]/g;
6837
+ for (const m of src.matchAll(titleRegex)) {
6838
+ const s = normaliseText((m[1] ?? m[2] ?? "").trim());
6839
+ if (isUsefulString(s)) out.push(s);
6840
+ }
6841
+ const attrRegex = /(?:aria-label|label|placeholder|alt)\s*=\s*["'`](.{2,120}?)["'`]/g;
6842
+ for (const m of src.matchAll(attrRegex)) {
6843
+ const s = normaliseText(m[1] ?? "");
6844
+ if (isUsefulString(s)) out.push(s);
6845
+ }
6846
+ const jsonValueRegex = /:\s*["'`](.{2,140}?)["'`]\s*[,\n]/g;
6847
+ for (const m of src.matchAll(jsonValueRegex)) {
6848
+ const s = normaliseText(m[1] ?? "");
6849
+ if (isUsefulString(s)) out.push(s);
6850
+ }
6851
+ return out;
6852
+ }
6853
+ function classifyString(s) {
6854
+ const lower = s.toLowerCase();
6855
+ if (/(pricing|plan|billing|subscription|trial|credit)/.test(lower)) return "copy";
6856
+ if (/(sign in|sign up|log in|register|invite|workspace|organization|sso|oauth)/.test(lower)) return "copy";
6857
+ if (/(dashboard|reports|settings|integrations|users|roles|permissions)/.test(lower)) return "heading";
6858
+ if (/(get started|start|scan|generate|export|run|deploy|upgrade|analy[sz]e)/.test(lower)) return "cta";
6859
+ if (/(overview|features|about|documentation|docs)/.test(lower)) return "title";
6860
+ if (/(menu|navigation|sidebar|breadcrumb)/.test(lower)) return "nav";
6861
+ return "copy";
6862
+ }
6863
+ function scoreString(kind, value) {
6864
+ const lower = value.toLowerCase();
6865
+ if (GENERIC_LOW_SIGNAL.has(lower)) return 0;
6866
+ let score = 1;
6867
+ if (kind === "route") score += 4;
6868
+ if (kind === "nav") score += 3;
6869
+ if (kind === "title") score += 2;
6870
+ if (kind === "heading") score += 2;
6871
+ if (kind === "cta") score += 2;
6872
+ if (/(pricing|billing|subscription|auth|security|integration)/.test(lower)) score += 2;
6873
+ if (/(dashboard|report|scan|workspace|project|repository)/.test(lower)) score += 1;
6874
+ return score;
6875
+ }
6876
+ function dedupeByKindValue(items) {
6877
+ const seen = /* @__PURE__ */ new Map();
6878
+ for (const item of items) {
6879
+ const key = `${item.kind}::${item.value.toLowerCase()}`;
6880
+ const prev = seen.get(key);
6881
+ if (!prev || item.weight > prev.weight) {
6882
+ seen.set(key, item);
6883
+ }
6884
+ }
6885
+ return Array.from(seen.values()).filter((i) => i.weight > 0);
6886
+ }
6887
+ function buildUnknowns(items) {
6888
+ const unknowns = [];
6889
+ const hasPricing = items.some((i) => /pricing|billing|subscription|trial|credit/i.test(i.value));
6890
+ const hasAuth = items.some((i) => /sign in|sign up|login|auth|sso|oauth|invite/i.test(i.value));
6891
+ const hasIntegrations = items.some((i) => /integration|webhook|api key|connector/i.test(i.value));
6892
+ const hasRoutes = items.some((i) => i.kind === "route");
6893
+ if (!hasPricing) unknowns.push("No pricing or billing evidence found.");
6894
+ if (!hasAuth) unknowns.push("No authentication or user access flow evidence found.");
6895
+ if (!hasIntegrations) unknowns.push("No integrations/connectors evidence found.");
6896
+ if (!hasRoutes) unknowns.push("No route structure evidence found.");
6897
+ return unknowns;
6898
+ }
6899
+ function asRecord(value) {
6900
+ if (!value || typeof value !== "object") return {};
6901
+ return value;
6902
+ }
6903
+ function getDependencies(pkgJson) {
6904
+ return Object.entries(asRecord(pkgJson.dependencies));
6905
+ }
6906
+ function normaliseText(s) {
6907
+ return s.replace(/\s+/g, " ").trim();
6908
+ }
6909
+ function isUsefulString(s) {
6910
+ if (!s || s.length < 3 || s.length > 160) return false;
6911
+ if (/^[0-9_./-]+$/.test(s)) return false;
6912
+ if (/^(true|false|null|undefined)$/i.test(s)) return false;
6913
+ if (/[<>{}]/.test(s)) return false;
6914
+ if (/function\s*\(|=>|console\.|import\s+|export\s+/.test(s)) return false;
6915
+ return true;
6916
+ }
6917
+
6531
6918
  // src/utils/tool-installer.ts
6532
- import { spawn as spawn4 } from "child_process";
6919
+ import { spawn as spawn5 } from "child_process";
6533
6920
  import chalk5 from "chalk";
6534
6921
  var SECURITY_TOOLS = [
6535
6922
  { name: "semgrep", command: "semgrep", brew: "semgrep", winget: null, scoop: null, pip: "semgrep" },
@@ -6537,9 +6924,9 @@ var SECURITY_TOOLS = [
6537
6924
  { name: "trufflehog", command: "trufflehog", brew: "trufflehog", winget: null, scoop: "trufflehog", pip: null }
6538
6925
  ];
6539
6926
  var IS_WIN = process.platform === "win32";
6540
- function runCommand(cmd, args) {
6541
- return new Promise((resolve8) => {
6542
- const child = spawn4(cmd, args, {
6927
+ function runCommand2(cmd, args) {
6928
+ return new Promise((resolve10) => {
6929
+ const child = spawn5(cmd, args, {
6543
6930
  stdio: ["ignore", "pipe", "pipe"],
6544
6931
  shell: IS_WIN
6545
6932
  // required for .cmd/.ps1 wrappers on Windows
@@ -6552,19 +6939,19 @@ function runCommand(cmd, args) {
6552
6939
  child.stderr.on("data", (d) => {
6553
6940
  stderr += d.toString();
6554
6941
  });
6555
- child.on("error", () => resolve8({ exitCode: 127, stdout, stderr }));
6556
- child.on("close", (code) => resolve8({ exitCode: code ?? 1, stdout, stderr }));
6942
+ child.on("error", () => resolve10({ exitCode: 127, stdout, stderr }));
6943
+ child.on("close", (code) => resolve10({ exitCode: code ?? 1, stdout, stderr }));
6557
6944
  });
6558
6945
  }
6559
6946
  async function commandExists(command) {
6560
6947
  const checker = IS_WIN ? "where" : "which";
6561
- const { exitCode } = await runCommand(checker, [command]);
6948
+ const { exitCode } = await runCommand2(checker, [command]);
6562
6949
  return exitCode === 0;
6563
6950
  }
6564
6951
  async function tryInstall(tool, strategies, log) {
6565
6952
  for (const { pm, args } of strategies) {
6566
6953
  log(chalk5.dim(` ${pm} ${args.join(" ")}\u2026`));
6567
- const { exitCode, stderr } = await runCommand(pm, args);
6954
+ const { exitCode, stderr } = await runCommand2(pm, args);
6568
6955
  if (exitCode === 0) {
6569
6956
  log(` ${chalk5.green("\u2714")} ${tool.name} installed via ${pm}`);
6570
6957
  return { ok: true, pm };
@@ -6620,20 +7007,163 @@ async function installMissingTools(log = (m) => process.stderr.write(m + "\n"))
6620
7007
  return result;
6621
7008
  }
6622
7009
 
7010
+ // src/utils/mermaid.ts
7011
+ function sanitizeId(input) {
7012
+ return input.replace(/[^a-zA-Z0-9_]/g, "_");
7013
+ }
7014
+ function escapeLabel(input) {
7015
+ return input.replace(/"/g, '\\"');
7016
+ }
7017
+ function scoreClass(score) {
7018
+ if (score === void 0 || Number.isNaN(score)) return "scoreUnknown";
7019
+ if (score >= 70) return "scoreHigh";
7020
+ if (score >= 40) return "scoreModerate";
7021
+ return "scoreLow";
7022
+ }
7023
+ function nodeLabel(project) {
7024
+ const score = project.drift?.score;
7025
+ const scoreText = typeof score === "number" ? ` (${score})` : " (n/a)";
7026
+ return `${project.name}${scoreText}`;
7027
+ }
7028
+ function buildDefs() {
7029
+ return [
7030
+ "classDef scoreHigh fill:#064e3b,stroke:#10b981,color:#d1fae5,stroke-width:2px",
7031
+ "classDef scoreModerate fill:#78350f,stroke:#f59e0b,color:#fef3c7,stroke-width:2px",
7032
+ "classDef scoreLow fill:#7f1d1d,stroke:#ef4444,color:#fee2e2,stroke-width:2px",
7033
+ "classDef scoreUnknown fill:#334155,stroke:#94a3b8,color:#e2e8f0,stroke-width:2px"
7034
+ ];
7035
+ }
7036
+ function generateWorkspaceRelationshipMermaid(projects) {
7037
+ const lines = ["flowchart LR"];
7038
+ const byPath = new Map(projects.map((p) => [p.path, p]));
7039
+ for (const project of projects) {
7040
+ const id = sanitizeId(project.projectId || project.path || project.name);
7041
+ lines.push(`${id}["${escapeLabel(nodeLabel(project))}"]`);
7042
+ lines.push(`class ${id} ${scoreClass(project.drift?.score)}`);
7043
+ }
7044
+ for (const project of projects) {
7045
+ const fromId = sanitizeId(project.projectId || project.path || project.name);
7046
+ for (const ref of project.projectReferences ?? []) {
7047
+ const target = byPath.get(ref.path);
7048
+ if (!target) continue;
7049
+ const toId = sanitizeId(target.projectId || target.path || target.name);
7050
+ lines.push(`${fromId} --> ${toId}`);
7051
+ }
7052
+ }
7053
+ lines.push(...buildDefs());
7054
+ return { mermaid: lines.join("\n") };
7055
+ }
7056
+ function generateProjectRelationshipMermaid(project, projects) {
7057
+ const lines = ["flowchart LR"];
7058
+ const byPath = new Map(projects.map((p) => [p.path, p]));
7059
+ const parents = projects.filter((p) => p.projectReferences?.some((r) => r.path === project.path));
7060
+ const children = (project.projectReferences ?? []).map((r) => byPath.get(r.path)).filter((p) => Boolean(p));
7061
+ const centerId = sanitizeId(project.projectId || project.path || project.name);
7062
+ lines.push(`${centerId}["${escapeLabel(nodeLabel(project))}"]`);
7063
+ lines.push(`class ${centerId} ${scoreClass(project.drift?.score)}`);
7064
+ for (const parent of parents) {
7065
+ const id = sanitizeId(parent.projectId || parent.path || parent.name);
7066
+ lines.push(`${id}["${escapeLabel(nodeLabel(parent))}"]`);
7067
+ lines.push(`class ${id} ${scoreClass(parent.drift?.score)}`);
7068
+ lines.push(`${id} --> ${centerId}`);
7069
+ }
7070
+ for (const child of children) {
7071
+ const id = sanitizeId(child.projectId || child.path || child.name);
7072
+ lines.push(`${id}["${escapeLabel(nodeLabel(child))}"]`);
7073
+ lines.push(`class ${id} ${scoreClass(child.drift?.score)}`);
7074
+ lines.push(`${centerId} --> ${id}`);
7075
+ }
7076
+ lines.push(...buildDefs());
7077
+ return { mermaid: lines.join("\n") };
7078
+ }
7079
+ function generateSolutionRelationshipMermaid(solution, projects) {
7080
+ const lines = ["flowchart TB"];
7081
+ const solutionNodeId = sanitizeId(solution.solutionId || solution.path || solution.name);
7082
+ const solutionScore = solution.drift?.score;
7083
+ const solutionScoreText = typeof solutionScore === "number" ? ` (${solutionScore})` : " (n/a)";
7084
+ lines.push(`${solutionNodeId}["${escapeLabel(`${solution.name}${solutionScoreText}`)}"]`);
7085
+ lines.push(`class ${solutionNodeId} ${scoreClass(solutionScore)}`);
7086
+ const projectByPath = new Map(projects.map((p) => [p.path, p]));
7087
+ for (const projectPath of solution.projectPaths) {
7088
+ const project = projectByPath.get(projectPath);
7089
+ if (!project) continue;
7090
+ const projectNodeId = sanitizeId(project.projectId || project.path || project.name);
7091
+ lines.push(`${projectNodeId}["${escapeLabel(nodeLabel(project))}"]`);
7092
+ lines.push(`class ${projectNodeId} ${scoreClass(project.drift?.score)}`);
7093
+ lines.push(`${solutionNodeId} --> ${projectNodeId}`);
7094
+ for (const ref of project.projectReferences ?? []) {
7095
+ const target = projectByPath.get(ref.path);
7096
+ if (!target) continue;
7097
+ const toId = sanitizeId(target.projectId || target.path || target.name);
7098
+ lines.push(`${projectNodeId} --> ${toId}`);
7099
+ }
7100
+ }
7101
+ lines.push(...buildDefs());
7102
+ return { mermaid: lines.join("\n") };
7103
+ }
7104
+
6623
7105
  // src/commands/scan.ts
7106
+ async function discoverSolutions(rootDir, fileCache) {
7107
+ const solutionFiles = await fileCache.findSolutionFiles(rootDir);
7108
+ const parsed = [];
7109
+ for (const solutionFile of solutionFiles) {
7110
+ try {
7111
+ const content = await fileCache.readTextFile(solutionFile);
7112
+ const dir = path21.dirname(solutionFile);
7113
+ const relSolutionPath = path21.relative(rootDir, solutionFile).replace(/\\/g, "/");
7114
+ const projectPaths = /* @__PURE__ */ new Set();
7115
+ const projectRegex = /Project\("[^"]*"\)\s*=\s*"([^"]*)",\s*"([^"]+\.csproj)"/g;
7116
+ let match;
7117
+ while ((match = projectRegex.exec(content)) !== null) {
7118
+ const projectRelative = match[2];
7119
+ const absProjectPath = path21.resolve(dir, projectRelative.replace(/\\/g, "/"));
7120
+ projectPaths.add(path21.relative(rootDir, absProjectPath).replace(/\\/g, "/"));
7121
+ }
7122
+ const solutionName = path21.basename(solutionFile, path21.extname(solutionFile));
7123
+ parsed.push({
7124
+ path: relSolutionPath,
7125
+ name: solutionName,
7126
+ type: "dotnet-sln",
7127
+ projectPaths: [...projectPaths]
7128
+ });
7129
+ } catch {
7130
+ }
7131
+ }
7132
+ return parsed;
7133
+ }
6624
7134
  async function runScan(rootDir, opts) {
6625
7135
  const scanStart = Date.now();
6626
7136
  const config = await loadConfig(rootDir);
6627
7137
  const sem = new Semaphore(opts.concurrency);
6628
- const npmCache = new NpmCache(rootDir, sem);
6629
- const nugetCache = new NuGetCache(sem);
6630
- const pypiCache = new PyPICache(sem);
6631
- const mavenCache = new MavenCache(sem);
7138
+ const packageManifest = opts.packageManifest ? await loadPackageVersionManifest(opts.packageManifest) : void 0;
7139
+ const offlineMode = opts.offline === true;
7140
+ const maxPrivacyMode = opts.maxPrivacy === true;
7141
+ const npmCache = new NpmCache(rootDir, sem, packageManifest, offlineMode);
7142
+ const nugetCache = new NuGetCache(sem, packageManifest, offlineMode);
7143
+ const pypiCache = new PyPICache(sem, packageManifest, offlineMode);
7144
+ const mavenCache = new MavenCache(sem, packageManifest, offlineMode);
6632
7145
  const fileCache = new FileCache();
6633
7146
  const excludePatterns = config.exclude ?? [];
6634
7147
  fileCache.setExcludePatterns(excludePatterns);
6635
7148
  fileCache.setMaxFileSize(config.maxFileSizeToScan ?? 5242880);
6636
7149
  const scanners = config.scanners;
7150
+ const scannerPolicy = {
7151
+ platformMatrix: !maxPrivacyMode,
7152
+ toolingInventory: true,
7153
+ serviceDependencies: !maxPrivacyMode,
7154
+ breakingChangeExposure: !maxPrivacyMode,
7155
+ securityPosture: true,
7156
+ securityScanners: !maxPrivacyMode,
7157
+ buildDeploy: !maxPrivacyMode,
7158
+ tsModernity: !maxPrivacyMode,
7159
+ fileHotspots: !maxPrivacyMode,
7160
+ dependencyGraph: true,
7161
+ dependencyRisk: true,
7162
+ architecture: !maxPrivacyMode,
7163
+ codeQuality: !maxPrivacyMode,
7164
+ owaspCategoryMapping: !maxPrivacyMode,
7165
+ uiPurpose: !maxPrivacyMode
7166
+ };
6637
7167
  let filesScanned = 0;
6638
7168
  const progress = new ScanProgress(rootDir);
6639
7169
  const steps = [
@@ -6646,27 +7176,28 @@ async function runScan(rootDir, opts) {
6646
7176
  { id: "python", label: "Scanning Python projects", weight: 3 },
6647
7177
  { id: "java", label: "Scanning Java projects", weight: 3 },
6648
7178
  ...scanners !== false ? [
6649
- ...scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
6650
- ...scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
6651
- ...scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
6652
- ...scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
6653
- ...scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
6654
- ...scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
6655
- ...scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
6656
- ...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
6657
- ...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
6658
- ...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
6659
- ...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
6660
- ...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
6661
- ...scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
6662
- ...scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : []
7179
+ ...scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
7180
+ ...scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
7181
+ ...scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
7182
+ ...scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
7183
+ ...scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
7184
+ ...scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
7185
+ ...scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
7186
+ ...scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
7187
+ ...scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
7188
+ ...scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
7189
+ ...scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
7190
+ ...scannerPolicy.architecture && scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
7191
+ ...scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
7192
+ ...scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : [],
7193
+ ...!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) ? [{ id: "uipurpose", label: "UI purpose evidence" }] : []
6663
7194
  ] : [],
6664
7195
  { id: "drift", label: "Computing drift score" },
6665
7196
  { id: "findings", label: "Generating findings" }
6666
7197
  ];
6667
7198
  progress.setSteps(steps);
6668
7199
  progress.completeStep("config", "loaded");
6669
- const registryOk = await checkRegistryAccess(rootDir);
7200
+ const registryOk = offlineMode ? true : await checkRegistryAccess(rootDir);
6670
7201
  if (!registryOk) {
6671
7202
  progress.finish();
6672
7203
  const msg = [
@@ -6748,10 +7279,45 @@ async function runScan(rootDir, opts) {
6748
7279
  project.drift = computeDriftScore([project]);
6749
7280
  project.projectId = computeProjectId(project.path, project.name, workspaceId);
6750
7281
  }
7282
+ const solutionsManifestPath = path21.join(rootDir, ".vibgrate", "solutions.json");
7283
+ const persistedSolutionIds = /* @__PURE__ */ new Map();
7284
+ if (await pathExists(solutionsManifestPath)) {
7285
+ try {
7286
+ const persisted = await readJsonFile(solutionsManifestPath);
7287
+ for (const solution of persisted.solutions ?? []) {
7288
+ if (solution.path && solution.solutionId) persistedSolutionIds.set(solution.path, solution.solutionId);
7289
+ }
7290
+ } catch {
7291
+ }
7292
+ }
7293
+ const discoveredSolutions = await discoverSolutions(rootDir, fileCache);
7294
+ const solutions = discoveredSolutions.map((solution) => ({
7295
+ solutionId: persistedSolutionIds.get(solution.path) ?? computeSolutionId(solution.path, solution.name, workspaceId),
7296
+ path: solution.path,
7297
+ name: solution.name,
7298
+ type: solution.type,
7299
+ projectPaths: solution.projectPaths
7300
+ }));
7301
+ const projectsByPath = new Map(allProjects.map((project) => [project.path, project]));
7302
+ for (const solution of solutions) {
7303
+ const includedProjects = solution.projectPaths.map((projectPath) => projectsByPath.get(projectPath)).filter((project) => Boolean(project));
7304
+ solution.drift = includedProjects.length > 0 ? computeDriftScore(includedProjects) : void 0;
7305
+ for (const project of includedProjects) {
7306
+ project.solutionId = solution.solutionId;
7307
+ project.solutionName = solution.name;
7308
+ }
7309
+ }
7310
+ for (const project of allProjects) {
7311
+ project.relationshipDiagram = generateProjectRelationshipMermaid(project, allProjects);
7312
+ }
7313
+ for (const solution of solutions) {
7314
+ solution.relationshipDiagram = generateSolutionRelationshipMermaid(solution, allProjects);
7315
+ }
7316
+ const relationshipDiagram = generateWorkspaceRelationshipMermaid(allProjects);
6751
7317
  const extended = {};
6752
7318
  if (scanners !== false) {
6753
7319
  const scannerTasks = [];
6754
- if (scanners?.platformMatrix?.enabled !== false) {
7320
+ if (scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false) {
6755
7321
  progress.startStep("platform");
6756
7322
  scannerTasks.push(
6757
7323
  scanPlatformMatrix(rootDir, fileCache).then((result) => {
@@ -6765,7 +7331,7 @@ async function runScan(rootDir, opts) {
6765
7331
  })
6766
7332
  );
6767
7333
  }
6768
- if (scanners?.toolingInventory?.enabled !== false) {
7334
+ if (scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false) {
6769
7335
  progress.startStep("tooling");
6770
7336
  scannerTasks.push(
6771
7337
  Promise.resolve().then(() => {
@@ -6775,7 +7341,7 @@ async function runScan(rootDir, opts) {
6775
7341
  })
6776
7342
  );
6777
7343
  }
6778
- if (scanners?.serviceDependencies?.enabled !== false) {
7344
+ if (scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false) {
6779
7345
  progress.startStep("services");
6780
7346
  scannerTasks.push(
6781
7347
  Promise.resolve().then(() => {
@@ -6785,7 +7351,7 @@ async function runScan(rootDir, opts) {
6785
7351
  })
6786
7352
  );
6787
7353
  }
6788
- if (scanners?.breakingChangeExposure?.enabled !== false) {
7354
+ if (scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false) {
6789
7355
  progress.startStep("breaking");
6790
7356
  scannerTasks.push(
6791
7357
  Promise.resolve().then(() => {
@@ -6800,7 +7366,7 @@ async function runScan(rootDir, opts) {
6800
7366
  })
6801
7367
  );
6802
7368
  }
6803
- if (scanners?.securityPosture?.enabled !== false) {
7369
+ if (scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false) {
6804
7370
  progress.startStep("security");
6805
7371
  scannerTasks.push(
6806
7372
  scanSecurityPosture(rootDir, fileCache).then((result) => {
@@ -6810,7 +7376,7 @@ async function runScan(rootDir, opts) {
6810
7376
  })
6811
7377
  );
6812
7378
  }
6813
- if (scanners?.securityScanners?.enabled !== false) {
7379
+ if (scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false) {
6814
7380
  if (opts.installTools) {
6815
7381
  const installResult = await installMissingTools();
6816
7382
  if (installResult.installed.length > 0) {
@@ -6833,7 +7399,7 @@ async function runScan(rootDir, opts) {
6833
7399
  })
6834
7400
  );
6835
7401
  }
6836
- if (scanners?.buildDeploy?.enabled !== false) {
7402
+ if (scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false) {
6837
7403
  progress.startStep("build");
6838
7404
  scannerTasks.push(
6839
7405
  scanBuildDeploy(rootDir, fileCache).then((result) => {
@@ -6845,7 +7411,7 @@ async function runScan(rootDir, opts) {
6845
7411
  })
6846
7412
  );
6847
7413
  }
6848
- if (scanners?.tsModernity?.enabled !== false) {
7414
+ if (scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false) {
6849
7415
  progress.startStep("ts");
6850
7416
  scannerTasks.push(
6851
7417
  scanTsModernity(rootDir, fileCache).then((result) => {
@@ -6858,7 +7424,7 @@ async function runScan(rootDir, opts) {
6858
7424
  })
6859
7425
  );
6860
7426
  }
6861
- if (scanners?.fileHotspots?.enabled !== false) {
7427
+ if (scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false) {
6862
7428
  progress.startStep("hotspots");
6863
7429
  scannerTasks.push(
6864
7430
  scanFileHotspots(rootDir, fileCache).then((result) => {
@@ -6867,7 +7433,7 @@ async function runScan(rootDir, opts) {
6867
7433
  })
6868
7434
  );
6869
7435
  }
6870
- if (scanners?.dependencyGraph?.enabled !== false) {
7436
+ if (scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false) {
6871
7437
  progress.startStep("depgraph");
6872
7438
  scannerTasks.push(
6873
7439
  scanDependencyGraph(rootDir, fileCache).then((result) => {
@@ -6877,7 +7443,7 @@ async function runScan(rootDir, opts) {
6877
7443
  })
6878
7444
  );
6879
7445
  }
6880
- if (scanners?.codeQuality?.enabled !== false) {
7446
+ if (scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false) {
6881
7447
  progress.startStep("codequality");
6882
7448
  scannerTasks.push(
6883
7449
  scanCodeQuality(rootDir, fileCache).then((result) => {
@@ -6890,7 +7456,7 @@ async function runScan(rootDir, opts) {
6890
7456
  })
6891
7457
  );
6892
7458
  }
6893
- if (scanners?.dependencyRisk?.enabled !== false) {
7459
+ if (scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false) {
6894
7460
  progress.startStep("deprisk");
6895
7461
  scannerTasks.push(
6896
7462
  Promise.resolve().then(() => {
@@ -6904,7 +7470,7 @@ async function runScan(rootDir, opts) {
6904
7470
  );
6905
7471
  }
6906
7472
  await Promise.all(scannerTasks);
6907
- if (scanners?.owaspCategoryMapping?.enabled !== false) {
7473
+ if (scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false) {
6908
7474
  progress.startStep("owasp");
6909
7475
  extended.owaspCategoryMapping = await scanOwaspCategoryMapping(
6910
7476
  rootDir,
@@ -6923,7 +7489,14 @@ async function runScan(rootDir, opts) {
6923
7489
  );
6924
7490
  }
6925
7491
  }
6926
- if (scanners?.architecture?.enabled !== false) {
7492
+ if (!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true)) {
7493
+ progress.startStep("uipurpose");
7494
+ extended.uiPurpose = await scanUiPurpose(rootDir, fileCache);
7495
+ const up = extended.uiPurpose;
7496
+ const summary = [`${up.topEvidence.length} evidence`, ...up.capped ? ["capped"] : []].join(" \xB7 ");
7497
+ progress.completeStep("uipurpose", summary, up.topEvidence.length);
7498
+ }
7499
+ if (scannerPolicy.architecture && scanners?.architecture?.enabled !== false) {
6927
7500
  progress.startStep("architecture");
6928
7501
  extended.architecture = await scanArchitecture(
6929
7502
  rootDir,
@@ -6934,6 +7507,14 @@ async function runScan(rootDir, opts) {
6934
7507
  );
6935
7508
  const arch = extended.architecture;
6936
7509
  const layerCount = arch.layers.filter((l) => l.fileCount > 0).length;
7510
+ await Promise.all(allProjects.map(async (project) => {
7511
+ project.architectureMermaid = await buildProjectArchitectureMermaid(
7512
+ rootDir,
7513
+ project,
7514
+ arch.archetype,
7515
+ fileCache
7516
+ );
7517
+ }));
6937
7518
  progress.completeStep(
6938
7519
  "architecture",
6939
7520
  `${arch.archetype} \xB7 ${layerCount} layer${layerCount !== 1 ? "s" : ""} \xB7 ${arch.totalClassified} files`,
@@ -7001,23 +7582,28 @@ async function runScan(rootDir, opts) {
7001
7582
  }
7002
7583
  if (extended.codeQuality) filesScanned += extended.codeQuality.filesAnalyzed;
7003
7584
  if (extended.owaspCategoryMapping) filesScanned += extended.owaspCategoryMapping.scannedFiles;
7585
+ if (extended.uiPurpose) filesScanned += extended.uiPurpose.topEvidence.length;
7004
7586
  const durationMs = Date.now() - scanStart;
7587
+ const repository = await buildRepositoryInfo(rootDir, vcs.remoteUrl, extended.buildDeploy?.ci);
7005
7588
  const artifact = {
7006
7589
  schemaVersion: "1.0",
7007
7590
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7008
7591
  vibgrateVersion: VERSION,
7009
- rootPath: path20.basename(rootDir),
7592
+ rootPath: path21.basename(rootDir),
7010
7593
  ...vcs.type !== "unknown" ? { vcs } : {},
7594
+ repository,
7011
7595
  projects: allProjects,
7596
+ ...solutions.length > 0 ? { solutions } : {},
7012
7597
  drift,
7013
7598
  findings,
7014
7599
  ...Object.keys(extended).length > 0 ? { extended } : {},
7015
7600
  durationMs,
7016
7601
  filesScanned,
7017
- treeSummary: treeCount
7602
+ treeSummary: treeCount,
7603
+ relationshipDiagram
7018
7604
  };
7019
7605
  if (opts.baseline) {
7020
- const baselinePath = path20.resolve(opts.baseline);
7606
+ const baselinePath = path21.resolve(opts.baseline);
7021
7607
  if (await pathExists(baselinePath)) {
7022
7608
  try {
7023
7609
  const baseline = await readJsonFile(baselinePath);
@@ -7028,9 +7614,21 @@ async function runScan(rootDir, opts) {
7028
7614
  }
7029
7615
  }
7030
7616
  }
7031
- const vibgrateDir = path20.join(rootDir, ".vibgrate");
7032
- await ensureDir(vibgrateDir);
7033
- await writeJsonFile(path20.join(vibgrateDir, "scan_result.json"), artifact);
7617
+ if (!opts.noLocalArtifacts && !maxPrivacyMode) {
7618
+ const vibgrateDir = path21.join(rootDir, ".vibgrate");
7619
+ await ensureDir(vibgrateDir);
7620
+ await writeJsonFile(path21.join(vibgrateDir, "scan_result.json"), artifact);
7621
+ await writeJsonFile(path21.join(vibgrateDir, "solutions.json"), {
7622
+ scannedAt: artifact.timestamp,
7623
+ solutions: solutions.map((solution) => ({
7624
+ solutionId: solution.solutionId,
7625
+ name: solution.name,
7626
+ path: solution.path,
7627
+ type: solution.type,
7628
+ projectPaths: solution.projectPaths
7629
+ }))
7630
+ });
7631
+ }
7034
7632
  await saveScanHistory(rootDir, {
7035
7633
  timestamp: artifact.timestamp,
7036
7634
  totalDurationMs: durationMs,
@@ -7038,29 +7636,33 @@ async function runScan(rootDir, opts) {
7038
7636
  totalDirs: treeCount.totalDirs,
7039
7637
  steps: progress.getStepTimings()
7040
7638
  });
7041
- for (const project of allProjects) {
7042
- if (project.drift && project.path) {
7043
- const projectDir = path20.resolve(rootDir, project.path);
7044
- const projectVibgrateDir = path20.join(projectDir, ".vibgrate");
7045
- await ensureDir(projectVibgrateDir);
7046
- await writeJsonFile(path20.join(projectVibgrateDir, "project_score.json"), {
7047
- projectId: project.projectId,
7048
- name: project.name,
7049
- type: project.type,
7050
- path: project.path,
7051
- score: project.drift.score,
7052
- riskLevel: project.drift.riskLevel,
7053
- components: project.drift.components,
7054
- measured: project.drift.measured,
7055
- scannedAt: artifact.timestamp,
7056
- vibgrateVersion: VERSION
7057
- });
7639
+ if (!opts.noLocalArtifacts && !maxPrivacyMode) {
7640
+ for (const project of allProjects) {
7641
+ if (project.drift && project.path) {
7642
+ const projectDir = path21.resolve(rootDir, project.path);
7643
+ const projectVibgrateDir = path21.join(projectDir, ".vibgrate");
7644
+ await ensureDir(projectVibgrateDir);
7645
+ await writeJsonFile(path21.join(projectVibgrateDir, "project_score.json"), {
7646
+ projectId: project.projectId,
7647
+ name: project.name,
7648
+ type: project.type,
7649
+ path: project.path,
7650
+ score: project.drift.score,
7651
+ riskLevel: project.drift.riskLevel,
7652
+ components: project.drift.components,
7653
+ measured: project.drift.measured,
7654
+ scannedAt: artifact.timestamp,
7655
+ vibgrateVersion: VERSION,
7656
+ solutionId: project.solutionId,
7657
+ solutionName: project.solutionName
7658
+ });
7659
+ }
7058
7660
  }
7059
7661
  }
7060
7662
  if (opts.format === "json") {
7061
7663
  const jsonStr = JSON.stringify(artifact, null, 2);
7062
7664
  if (opts.out) {
7063
- await writeTextFile(path20.resolve(opts.out), jsonStr);
7665
+ await writeTextFile(path21.resolve(opts.out), jsonStr);
7064
7666
  console.log(chalk6.green("\u2714") + ` JSON written to ${opts.out}`);
7065
7667
  } else {
7066
7668
  console.log(jsonStr);
@@ -7069,7 +7671,7 @@ async function runScan(rootDir, opts) {
7069
7671
  const sarif = formatSarif(artifact);
7070
7672
  const sarifStr = JSON.stringify(sarif, null, 2);
7071
7673
  if (opts.out) {
7072
- await writeTextFile(path20.resolve(opts.out), sarifStr);
7674
+ await writeTextFile(path21.resolve(opts.out), sarifStr);
7073
7675
  console.log(chalk6.green("\u2714") + ` SARIF written to ${opts.out}`);
7074
7676
  } else {
7075
7677
  console.log(sarifStr);
@@ -7078,11 +7680,34 @@ async function runScan(rootDir, opts) {
7078
7680
  const text = formatText(artifact);
7079
7681
  console.log(text);
7080
7682
  if (opts.out) {
7081
- await writeTextFile(path20.resolve(opts.out), text);
7683
+ await writeTextFile(path21.resolve(opts.out), text);
7082
7684
  }
7083
7685
  }
7084
7686
  return artifact;
7085
7687
  }
7688
+ async function buildRepositoryInfo(rootDir, remoteUrl, ciSystems) {
7689
+ const packageJsonPath = path21.join(rootDir, "package.json");
7690
+ let name = path21.basename(rootDir);
7691
+ let version;
7692
+ if (await pathExists(packageJsonPath)) {
7693
+ try {
7694
+ const packageJson = await readJsonFile(packageJsonPath);
7695
+ if (typeof packageJson.name === "string" && packageJson.name.trim()) {
7696
+ name = packageJson.name.trim();
7697
+ }
7698
+ if (typeof packageJson.version === "string" && packageJson.version.trim()) {
7699
+ version = packageJson.version.trim();
7700
+ }
7701
+ } catch {
7702
+ }
7703
+ }
7704
+ return {
7705
+ name,
7706
+ ...version ? { version } : {},
7707
+ ...ciSystems && ciSystems.length > 0 ? { pipeline: ciSystems.join(",") } : {},
7708
+ ...remoteUrl ? { remoteUrl } : {}
7709
+ };
7710
+ }
7086
7711
  async function autoPush(artifact, rootDir, opts) {
7087
7712
  const dsn = opts.dsn || process.env.VIBGRATE_DSN;
7088
7713
  if (!dsn) {
@@ -7145,8 +7770,16 @@ async function autoPush(artifact, rootDir, opts) {
7145
7770
  if (opts.strict) process.exit(1);
7146
7771
  }
7147
7772
  }
7148
- var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--install-tools", "Auto-install missing security scanners via Homebrew").action(async (targetPath, opts) => {
7149
- const rootDir = path20.resolve(targetPath);
7773
+ function parseNonNegativeNumber(value, label) {
7774
+ if (value === void 0) return void 0;
7775
+ const parsed = Number(value);
7776
+ if (!Number.isFinite(parsed) || parsed < 0) {
7777
+ throw new Error(`${label} must be a non-negative number.`);
7778
+ }
7779
+ return parsed;
7780
+ }
7781
+ var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--install-tools", "Auto-install missing security scanners via Homebrew").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").action(async (targetPath, opts) => {
7782
+ const rootDir = path21.resolve(targetPath);
7150
7783
  if (!await pathExists(rootDir)) {
7151
7784
  console.error(chalk6.red(`Path does not exist: ${rootDir}`));
7152
7785
  process.exit(1);
@@ -7162,7 +7795,14 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
7162
7795
  dsn: opts.dsn,
7163
7796
  region: opts.region,
7164
7797
  strict: opts.strict,
7165
- installTools: opts.installTools
7798
+ installTools: opts.installTools,
7799
+ uiPurpose: opts.uiPurpose,
7800
+ noLocalArtifacts: opts.noLocalArtifacts,
7801
+ maxPrivacy: opts.maxPrivacy,
7802
+ offline: opts.offline,
7803
+ packageManifest: opts.packageManifest,
7804
+ driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
7805
+ driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening")
7166
7806
  };
7167
7807
  const artifact = await runScan(rootDir, scanOpts);
7168
7808
  if (opts.failOn) {
@@ -7179,8 +7819,29 @@ Failing: findings detected at warn level or above.`));
7179
7819
  process.exit(2);
7180
7820
  }
7181
7821
  }
7822
+ if (scanOpts.driftBudget !== void 0 && artifact.drift.score > scanOpts.driftBudget) {
7823
+ console.error(chalk6.red(`
7824
+ Failing fitness function: drift score ${artifact.drift.score}/100 exceeds budget ${scanOpts.driftBudget}.`));
7825
+ process.exit(2);
7826
+ }
7827
+ if (scanOpts.driftWorseningPercent !== void 0) {
7828
+ if (artifact.delta === void 0) {
7829
+ console.error(chalk6.red("\nFailing fitness function: --drift-worsening requires --baseline to compare against previous drift."));
7830
+ process.exit(2);
7831
+ }
7832
+ if (artifact.delta > 0) {
7833
+ const baselineScore = artifact.drift.score - artifact.delta;
7834
+ const denominator = Math.max(Math.abs(baselineScore), 1e-4);
7835
+ const worseningPercent = artifact.delta / denominator * 100;
7836
+ if (worseningPercent > scanOpts.driftWorseningPercent) {
7837
+ console.error(chalk6.red(`
7838
+ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (threshold ${scanOpts.driftWorseningPercent}%).`));
7839
+ process.exit(2);
7840
+ }
7841
+ }
7842
+ }
7182
7843
  const hasDsn = !!(opts.dsn || process.env.VIBGRATE_DSN);
7183
- if (opts.push || hasDsn) {
7844
+ if (!scanOpts.offline && (opts.push || hasDsn)) {
7184
7845
  await autoPush(artifact, rootDir, scanOpts);
7185
7846
  }
7186
7847
  });