@vibgrate/cli 1.0.46 → 1.0.48

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";
@@ -320,8 +324,25 @@ function formatText(artifact) {
320
324
  lines.push(chalk.bold.cyan("\u2551 Project Relationship Diagram \u2551"));
321
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"));
322
326
  lines.push("");
323
- lines.push(chalk.bold(" Mermaid"));
324
- lines.push(chalk.dim(artifact.relationshipDiagram.mermaid));
327
+ lines.push(chalk.bold(" Mermaid") + chalk.dim(" (copy into https://mermaid.live or a ```mermaid code block)"));
328
+ lines.push("");
329
+ lines.push(chalk.dim(" ```mermaid"));
330
+ for (const mLine of artifact.relationshipDiagram.mermaid.split("\n")) {
331
+ lines.push(chalk.dim(` ${mLine}`));
332
+ }
333
+ lines.push(chalk.dim(" ```"));
334
+ lines.push("");
335
+ }
336
+ if (artifact.solutions && artifact.solutions.length > 0) {
337
+ 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"));
338
+ lines.push(chalk.bold.cyan("\u2551 Solution Drift Summary \u2551"));
339
+ 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"));
340
+ lines.push("");
341
+ for (const solution of artifact.solutions) {
342
+ const solScore = solution.drift?.score;
343
+ const color = typeof solScore === "number" ? solScore >= 70 ? chalk.green : solScore >= 40 ? chalk.yellow : chalk.red : chalk.dim;
344
+ lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk.dim("n/a")}`);
345
+ }
325
346
  lines.push("");
326
347
  }
327
348
  const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
@@ -1084,19 +1105,19 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
1084
1105
  });
1085
1106
 
1086
1107
  // src/commands/scan.ts
1087
- import * as path20 from "path";
1108
+ import * as path22 from "path";
1088
1109
  import { Command as Command3 } from "commander";
1089
1110
  import chalk6 from "chalk";
1090
1111
 
1091
1112
  // src/scanners/node-scanner.ts
1092
- import * as path3 from "path";
1113
+ import * as path4 from "path";
1093
1114
  import * as semver2 from "semver";
1094
1115
 
1095
1116
  // src/utils/timeout.ts
1096
1117
  async function withTimeout(promise, ms) {
1097
1118
  let timer;
1098
- const timeout = new Promise((resolve9) => {
1099
- timer = setTimeout(() => resolve9({ ok: false }), ms);
1119
+ const timeout = new Promise((resolve10) => {
1120
+ timer = setTimeout(() => resolve10({ ok: false }), ms);
1100
1121
  });
1101
1122
  try {
1102
1123
  const result = await Promise.race([
@@ -1110,8 +1131,78 @@ async function withTimeout(promise, ms) {
1110
1131
  }
1111
1132
 
1112
1133
  // src/scanners/npm-cache.ts
1113
- import { spawn } from "child_process";
1134
+ import { spawn as spawn2 } from "child_process";
1114
1135
  import * as semver from "semver";
1136
+
1137
+ // src/package-version-manifest.ts
1138
+ import { mkdtemp, readFile, rm } from "fs/promises";
1139
+ import * as path3 from "path";
1140
+ import * as os from "os";
1141
+ import { spawn } from "child_process";
1142
+ function runCommand(cmd, args) {
1143
+ return new Promise((resolve10, reject) => {
1144
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1145
+ let out = "";
1146
+ let err = "";
1147
+ child.stdout.on("data", (d) => out += String(d));
1148
+ child.stderr.on("data", (d) => err += String(d));
1149
+ child.on("error", reject);
1150
+ child.on("close", (code) => {
1151
+ if (code !== 0) {
1152
+ reject(new Error(`${cmd} ${args.join(" ")} failed (code=${code}): ${err.trim()}`));
1153
+ return;
1154
+ }
1155
+ resolve10(out);
1156
+ });
1157
+ });
1158
+ }
1159
+ async function parseManifestText(text, source) {
1160
+ try {
1161
+ return JSON.parse(text);
1162
+ } catch {
1163
+ throw new Error(`Invalid JSON in package version manifest: ${source}`);
1164
+ }
1165
+ }
1166
+ async function loadManifestFromZip(zipPath) {
1167
+ const tmpDir = await mkdtemp(path3.join(os.tmpdir(), "vibgrate-manifest-"));
1168
+ try {
1169
+ await runCommand("unzip", ["-qq", zipPath, "-d", tmpDir]);
1170
+ const candidates = [
1171
+ path3.join(tmpDir, "package-versions.json"),
1172
+ path3.join(tmpDir, "manifest.json"),
1173
+ path3.join(tmpDir, "index.json")
1174
+ ];
1175
+ for (const candidate of candidates) {
1176
+ try {
1177
+ const text = await readFile(candidate, "utf8");
1178
+ return await parseManifestText(text, candidate);
1179
+ } catch {
1180
+ }
1181
+ }
1182
+ throw new Error("Zip must contain package-versions.json, manifest.json, or index.json");
1183
+ } finally {
1184
+ await rm(tmpDir, { recursive: true, force: true });
1185
+ }
1186
+ }
1187
+ async function loadPackageVersionManifest(filePath) {
1188
+ const resolved = path3.resolve(filePath);
1189
+ if (resolved.toLowerCase().endsWith(".zip")) {
1190
+ return loadManifestFromZip(resolved);
1191
+ }
1192
+ const text = await readFile(resolved, "utf8");
1193
+ return parseManifestText(text, resolved);
1194
+ }
1195
+ function getManifestEntry(manifest, ecosystem, packageName) {
1196
+ if (!manifest) return void 0;
1197
+ const table = manifest[ecosystem];
1198
+ if (!table) return void 0;
1199
+ if (ecosystem === "nuget") {
1200
+ return table[packageName.toLowerCase()] ?? table[packageName];
1201
+ }
1202
+ return table[packageName];
1203
+ }
1204
+
1205
+ // src/scanners/npm-cache.ts
1115
1206
  function stableOnly(versions) {
1116
1207
  return versions.filter((v) => semver.valid(v) && semver.prerelease(v) === null);
1117
1208
  }
@@ -1121,8 +1212,8 @@ function maxStable(versions) {
1121
1212
  return stable.sort(semver.rcompare)[0] ?? null;
1122
1213
  }
1123
1214
  async function npmViewJson(args, cwd) {
1124
- return new Promise((resolve9, reject) => {
1125
- const child = spawn("npm", ["view", ...args, "--json"], {
1215
+ return new Promise((resolve10, reject) => {
1216
+ const child = spawn2("npm", ["view", ...args, "--json"], {
1126
1217
  cwd,
1127
1218
  shell: true,
1128
1219
  stdio: ["ignore", "pipe", "pipe"]
@@ -1139,27 +1230,42 @@ async function npmViewJson(args, cwd) {
1139
1230
  }
1140
1231
  const trimmed = out.trim();
1141
1232
  if (!trimmed) {
1142
- resolve9(null);
1233
+ resolve10(null);
1143
1234
  return;
1144
1235
  }
1145
1236
  try {
1146
- resolve9(JSON.parse(trimmed));
1237
+ resolve10(JSON.parse(trimmed));
1147
1238
  } catch {
1148
- resolve9(trimmed.replace(/^"|"$/g, ""));
1239
+ resolve10(trimmed.replace(/^"|"$/g, ""));
1149
1240
  }
1150
1241
  });
1151
1242
  });
1152
1243
  }
1153
1244
  var NpmCache = class {
1154
- constructor(cwd, sem) {
1245
+ constructor(cwd, sem, manifest, offline = false) {
1155
1246
  this.cwd = cwd;
1156
1247
  this.sem = sem;
1248
+ this.manifest = manifest;
1249
+ this.offline = offline;
1157
1250
  }
1158
1251
  meta = /* @__PURE__ */ new Map();
1159
1252
  get(pkg2) {
1160
1253
  const existing = this.meta.get(pkg2);
1161
1254
  if (existing) return existing;
1162
1255
  const p = this.sem.run(async () => {
1256
+ const manifestEntry = getManifestEntry(this.manifest, "npm", pkg2);
1257
+ if (manifestEntry) {
1258
+ const stable2 = stableOnly(manifestEntry.versions ?? []);
1259
+ const latestStableOverall2 = maxStable(stable2);
1260
+ return {
1261
+ latest: manifestEntry.latest ?? latestStableOverall2,
1262
+ stableVersions: stable2,
1263
+ latestStableOverall: latestStableOverall2
1264
+ };
1265
+ }
1266
+ if (this.offline) {
1267
+ return { latest: null, stableVersions: [], latestStableOverall: null };
1268
+ }
1163
1269
  let latest = null;
1164
1270
  let versions = [];
1165
1271
  try {
@@ -1313,7 +1419,7 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
1313
1419
  results.push(result.value);
1314
1420
  packageNameToPath.set(result.value.name, result.value.path);
1315
1421
  } else {
1316
- const relPath = path3.relative(rootDir, path3.dirname(pjPath));
1422
+ const relPath = path4.relative(rootDir, path4.dirname(pjPath));
1317
1423
  if (cache) {
1318
1424
  cache.addStuckPath(relPath || ".");
1319
1425
  }
@@ -1344,8 +1450,8 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
1344
1450
  }
1345
1451
  async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1346
1452
  const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
1347
- const absProjectPath = path3.dirname(packageJsonPath);
1348
- const projectPath = path3.relative(rootDir, absProjectPath) || ".";
1453
+ const absProjectPath = path4.dirname(packageJsonPath);
1454
+ const projectPath = path4.relative(rootDir, absProjectPath) || ".";
1349
1455
  const nodeEngine = pj.engines?.node ?? void 0;
1350
1456
  let runtimeLatest;
1351
1457
  let runtimeMajorsBehind;
@@ -1432,7 +1538,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1432
1538
  return {
1433
1539
  type: "node",
1434
1540
  path: projectPath,
1435
- name: pj.name ?? path3.basename(absProjectPath),
1541
+ name: pj.name ?? path4.basename(absProjectPath),
1436
1542
  runtime: nodeEngine,
1437
1543
  runtimeLatest,
1438
1544
  runtimeMajorsBehind,
@@ -1444,7 +1550,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1444
1550
  }
1445
1551
 
1446
1552
  // src/scanners/dotnet-scanner.ts
1447
- import * as path4 from "path";
1553
+ import * as path5 from "path";
1448
1554
  import * as semver3 from "semver";
1449
1555
  import { XMLParser } from "fast-xml-parser";
1450
1556
  var parser = new XMLParser({
@@ -1646,7 +1752,7 @@ function parseCsproj(xml, filePath) {
1646
1752
  const parsed = parser.parse(xml);
1647
1753
  const project = parsed?.Project;
1648
1754
  if (!project) {
1649
- return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName: path4.basename(filePath, ".csproj") };
1755
+ return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName: path5.basename(filePath, ".csproj") };
1650
1756
  }
1651
1757
  const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
1652
1758
  const targetFrameworks = [];
@@ -1683,7 +1789,7 @@ function parseCsproj(xml, filePath) {
1683
1789
  targetFrameworks: [...new Set(targetFrameworks)],
1684
1790
  packageReferences,
1685
1791
  projectReferences,
1686
- projectName: path4.basename(filePath, ".csproj")
1792
+ projectName: path5.basename(filePath, ".csproj")
1687
1793
  };
1688
1794
  }
1689
1795
  async function scanDotnetProjects(rootDir, nugetCache, cache) {
@@ -1693,12 +1799,12 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
1693
1799
  for (const slnPath of slnFiles) {
1694
1800
  try {
1695
1801
  const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
1696
- const slnDir = path4.dirname(slnPath);
1802
+ const slnDir = path5.dirname(slnPath);
1697
1803
  const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
1698
1804
  let match;
1699
1805
  while ((match = projectRegex.exec(slnContent)) !== null) {
1700
1806
  if (match[1]) {
1701
- const csprojPath = path4.resolve(slnDir, match[1].replace(/\\/g, "/"));
1807
+ const csprojPath = path5.resolve(slnDir, match[1].replace(/\\/g, "/"));
1702
1808
  slnCsprojPaths.add(csprojPath);
1703
1809
  }
1704
1810
  }
@@ -1715,7 +1821,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
1715
1821
  if (result.ok) {
1716
1822
  results.push(result.value);
1717
1823
  } else {
1718
- const relPath = path4.relative(rootDir, path4.dirname(csprojPath));
1824
+ const relPath = path5.relative(rootDir, path5.dirname(csprojPath));
1719
1825
  if (cache) {
1720
1826
  cache.addStuckPath(relPath || ".");
1721
1827
  }
@@ -1731,7 +1837,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
1731
1837
  async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1732
1838
  const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
1733
1839
  const data = parseCsproj(xml, csprojPath);
1734
- const csprojDir = path4.dirname(csprojPath);
1840
+ const csprojDir = path5.dirname(csprojPath);
1735
1841
  const primaryTfm = data.targetFrameworks[0];
1736
1842
  let runtimeMajorsBehind;
1737
1843
  let targetFramework = primaryTfm;
@@ -1809,9 +1915,9 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1809
1915
  }
1810
1916
  }
1811
1917
  const projectReferences = data.projectReferences.map((refPath) => {
1812
- const absRefPath = path4.resolve(csprojDir, refPath);
1813
- const relRefPath = path4.relative(rootDir, path4.dirname(absRefPath));
1814
- const refName = path4.basename(absRefPath, ".csproj");
1918
+ const absRefPath = path5.resolve(csprojDir, refPath);
1919
+ const relRefPath = path5.relative(rootDir, path5.dirname(absRefPath));
1920
+ const refName = path5.basename(absRefPath, ".csproj");
1815
1921
  return {
1816
1922
  path: relRefPath || ".",
1817
1923
  name: refName,
@@ -1832,7 +1938,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1832
1938
  const buckets = bucketsMut;
1833
1939
  return {
1834
1940
  type: "dotnet",
1835
- path: path4.relative(rootDir, csprojDir) || ".",
1941
+ path: path5.relative(rootDir, csprojDir) || ".",
1836
1942
  name: data.projectName,
1837
1943
  targetFramework,
1838
1944
  runtime: primaryTfm,
@@ -1847,7 +1953,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1847
1953
  }
1848
1954
 
1849
1955
  // src/scanners/python-scanner.ts
1850
- import * as path5 from "path";
1956
+ import * as path6 from "path";
1851
1957
  import * as semver4 from "semver";
1852
1958
  var KNOWN_PYTHON_FRAMEWORKS = {
1853
1959
  // ── Web Frameworks ──
@@ -2088,7 +2194,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
2088
2194
  const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name)) : await findPythonManifests(rootDir);
2089
2195
  const projectDirs = /* @__PURE__ */ new Map();
2090
2196
  for (const f of manifestFiles) {
2091
- const dir = path5.dirname(f);
2197
+ const dir = path6.dirname(f);
2092
2198
  if (!projectDirs.has(dir)) projectDirs.set(dir, []);
2093
2199
  projectDirs.get(dir).push(f);
2094
2200
  }
@@ -2101,7 +2207,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
2101
2207
  if (result.ok) {
2102
2208
  results.push(result.value);
2103
2209
  } else {
2104
- const relPath = path5.relative(rootDir, dir);
2210
+ const relPath = path6.relative(rootDir, dir);
2105
2211
  if (cache) cache.addStuckPath(relPath || ".");
2106
2212
  console.error(`Timeout scanning Python project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
2107
2213
  }
@@ -2117,12 +2223,12 @@ async function findPythonManifests(rootDir) {
2117
2223
  return findFiles2(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name));
2118
2224
  }
2119
2225
  async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cache) {
2120
- const relDir = path5.relative(rootDir, dir) || ".";
2121
- let projectName = path5.basename(dir === rootDir ? rootDir : dir);
2226
+ const relDir = path6.relative(rootDir, dir) || ".";
2227
+ let projectName = path6.basename(dir === rootDir ? rootDir : dir);
2122
2228
  let pythonVersion;
2123
2229
  const allDeps = /* @__PURE__ */ new Map();
2124
2230
  for (const f of manifestFiles) {
2125
- const fileName = path5.basename(f);
2231
+ const fileName = path6.basename(f);
2126
2232
  const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
2127
2233
  if (fileName === "pyproject.toml") {
2128
2234
  const parsed = parsePyprojectToml(content);
@@ -2239,7 +2345,7 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
2239
2345
  }
2240
2346
 
2241
2347
  // src/scanners/java-scanner.ts
2242
- import * as path6 from "path";
2348
+ import * as path7 from "path";
2243
2349
  import * as semver5 from "semver";
2244
2350
  import { XMLParser as XMLParser2 } from "fast-xml-parser";
2245
2351
  var parser2 = new XMLParser2({
@@ -2345,7 +2451,7 @@ function parsePom(xml, filePath) {
2345
2451
  const project = parsed?.project;
2346
2452
  if (!project) {
2347
2453
  return {
2348
- artifactId: path6.basename(path6.dirname(filePath)),
2454
+ artifactId: path7.basename(path7.dirname(filePath)),
2349
2455
  dependencies: [],
2350
2456
  modules: [],
2351
2457
  properties: {}
@@ -2405,7 +2511,7 @@ function parsePom(xml, filePath) {
2405
2511
  }
2406
2512
  return {
2407
2513
  groupId: project.groupId ? String(project.groupId) : parent?.groupId,
2408
- artifactId: String(project.artifactId ?? path6.basename(path6.dirname(filePath))),
2514
+ artifactId: String(project.artifactId ?? path7.basename(path7.dirname(filePath))),
2409
2515
  version: project.version ? String(project.version) : parent?.version,
2410
2516
  packaging: project.packaging ? String(project.packaging) : void 0,
2411
2517
  javaVersion,
@@ -2420,7 +2526,7 @@ function resolveProperty(value, properties) {
2420
2526
  }
2421
2527
  function parseGradleBuild(content, filePath) {
2422
2528
  const deps = [];
2423
- const projectName = path6.basename(path6.dirname(filePath));
2529
+ const projectName = path7.basename(path7.dirname(filePath));
2424
2530
  let javaVersion;
2425
2531
  const compatMatch = content.match(/(?:sourceCompatibility|targetCompatibility|javaVersion)\s*[=:]\s*['"]?(?:JavaVersion\.VERSION_)?(\d+)['"]?/);
2426
2532
  if (compatMatch) javaVersion = compatMatch[1];
@@ -2483,7 +2589,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
2483
2589
  const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => JAVA_MANIFEST_FILES.has(name)) : await findJavaManifests(rootDir);
2484
2590
  const projectDirs = /* @__PURE__ */ new Map();
2485
2591
  for (const f of manifestFiles) {
2486
- const dir = path6.dirname(f);
2592
+ const dir = path7.dirname(f);
2487
2593
  if (!projectDirs.has(dir)) projectDirs.set(dir, []);
2488
2594
  projectDirs.get(dir).push(f);
2489
2595
  }
@@ -2496,7 +2602,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
2496
2602
  if (result.ok) {
2497
2603
  results.push(result.value);
2498
2604
  } else {
2499
- const relPath = path6.relative(rootDir, dir);
2605
+ const relPath = path7.relative(rootDir, dir);
2500
2606
  if (cache) cache.addStuckPath(relPath || ".");
2501
2607
  console.error(`Timeout scanning Java project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
2502
2608
  }
@@ -2516,13 +2622,13 @@ async function findJavaManifests(rootDir) {
2516
2622
  return findFiles2(rootDir, (name) => JAVA_MANIFEST_FILES.has(name));
2517
2623
  }
2518
2624
  async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache) {
2519
- const relDir = path6.relative(rootDir, dir) || ".";
2520
- let projectName = path6.basename(dir === rootDir ? rootDir : dir);
2625
+ const relDir = path7.relative(rootDir, dir) || ".";
2626
+ let projectName = path7.basename(dir === rootDir ? rootDir : dir);
2521
2627
  let javaVersion;
2522
2628
  const allDeps = /* @__PURE__ */ new Map();
2523
2629
  const projectReferences = [];
2524
2630
  for (const f of manifestFiles) {
2525
- const fileName = path6.basename(f);
2631
+ const fileName = path7.basename(f);
2526
2632
  const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
2527
2633
  if (fileName === "pom.xml") {
2528
2634
  const pom = parsePom(content, f);
@@ -2536,7 +2642,7 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
2536
2642
  }
2537
2643
  for (const mod of pom.modules) {
2538
2644
  projectReferences.push({
2539
- path: path6.join(relDir, mod),
2645
+ path: path7.join(relDir, mod),
2540
2646
  name: mod,
2541
2647
  refType: "project"
2542
2648
  });
@@ -2639,11 +2745,156 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
2639
2745
  };
2640
2746
  }
2641
2747
 
2748
+ // src/scanners/polyglot-scanner.ts
2749
+ import * as path8 from "path";
2750
+ var MANIFEST_TO_LANGUAGE = [
2751
+ { name: "go.mod", type: "go" },
2752
+ { name: "Cargo.toml", type: "rust" },
2753
+ { name: "composer.json", type: "php" },
2754
+ { name: "Gemfile", type: "ruby" },
2755
+ { name: "Package.swift", type: "swift" },
2756
+ { name: "pubspec.yaml", type: "dart" },
2757
+ { name: "build.gradle.kts", type: "kotlin" },
2758
+ { name: "build.sbt", type: "scala" },
2759
+ { name: "DESCRIPTION", type: "r" },
2760
+ { name: "Podfile", type: "objective-c" },
2761
+ { name: "mix.exs", type: "elixir" },
2762
+ { name: "cpanfile", type: "perl" },
2763
+ { name: "Project.toml", type: "julia" },
2764
+ { name: "deps.edn", type: "clojure" },
2765
+ { name: "build.gradle", type: "groovy" },
2766
+ { name: "tsconfig.json", type: "typescript" },
2767
+ { name: "Makefile", type: "c" },
2768
+ { name: "CMakeLists.txt", type: "cpp" },
2769
+ { name: "*.vbp", type: "visual-basic" }
2770
+ ];
2771
+ var EXTENSION_TO_LANGUAGE = [
2772
+ { extensions: [".m", ".mm"], type: "objective-c" },
2773
+ { extensions: [".c", ".h"], type: "c" },
2774
+ { extensions: [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"], type: "cpp" },
2775
+ { extensions: [".cob", ".cbl", ".cpy"], type: "cobol" },
2776
+ { extensions: [".f", ".for", ".f90", ".f95", ".f03", ".f08"], type: "fortran" },
2777
+ { extensions: [".pas", ".pp", ".lpr"], type: "pascal" },
2778
+ { extensions: [".adb", ".ads", ".ada"], type: "ada" },
2779
+ { extensions: [".asm", ".s", ".s43", ".s65"], type: "assembly" },
2780
+ { extensions: [".rpg", ".rpgle", ".sqlrpgle"], type: "rpg" }
2781
+ ];
2782
+ function makeDep(pkg2, spec = "unknown") {
2783
+ return {
2784
+ package: pkg2,
2785
+ section: "dependencies",
2786
+ currentSpec: spec,
2787
+ resolvedVersion: null,
2788
+ latestStable: null,
2789
+ majorsBehind: null,
2790
+ drift: "unknown"
2791
+ };
2792
+ }
2793
+ function parseLineDependencies(content, regex, capture = 1) {
2794
+ const deps = /* @__PURE__ */ new Set();
2795
+ for (const line of content.split(/\r?\n/)) {
2796
+ const match = line.match(regex);
2797
+ if (match?.[capture]) deps.add(match[capture]);
2798
+ }
2799
+ return [...deps];
2800
+ }
2801
+ function getProjectName(projectPath, rootDir) {
2802
+ return path8.basename(projectPath) || path8.basename(rootDir);
2803
+ }
2804
+ function addProject(projects, seen, type, projectPath, rootDir, dependencies = []) {
2805
+ const normalizedPath = projectPath || ".";
2806
+ const key = `${type}:${normalizedPath}`;
2807
+ if (seen.has(key)) return;
2808
+ seen.add(key);
2809
+ projects.push({
2810
+ type,
2811
+ path: normalizedPath,
2812
+ name: getProjectName(normalizedPath, rootDir),
2813
+ frameworks: [],
2814
+ dependencies,
2815
+ dependencyAgeBuckets: {
2816
+ current: 0,
2817
+ oneBehind: 0,
2818
+ twoPlusBehind: 0,
2819
+ unknown: dependencies.length
2820
+ }
2821
+ });
2822
+ }
2823
+ async function parseDepsByManifest(manifestPath, cache) {
2824
+ const filename = path8.basename(manifestPath);
2825
+ const readText = async () => cache ? cache.readTextFile(manifestPath) : readTextFile(manifestPath);
2826
+ if (filename === "composer.json") {
2827
+ const content = cache ? await cache.readJsonFile(manifestPath) : await readJsonFile(manifestPath);
2828
+ if (!content || typeof content !== "object") return [];
2829
+ const deps = /* @__PURE__ */ new Set();
2830
+ for (const key of ["require", "require-dev"]) {
2831
+ const section = content[key];
2832
+ if (section && typeof section === "object") {
2833
+ for (const dep of Object.keys(section)) deps.add(dep);
2834
+ }
2835
+ }
2836
+ return [...deps].map((dep) => makeDep(dep));
2837
+ }
2838
+ const text = await readText();
2839
+ if (!text) return [];
2840
+ if (filename === "go.mod") return parseLineDependencies(text, /^\s*require\s+([^\s]+)\s+(.+)$/).map((dep) => makeDep(dep));
2841
+ if (filename === "Gemfile") return parseLineDependencies(text, /^\s*gem\s+['"]([^'"]+)['"]/).map((dep) => makeDep(dep));
2842
+ if (filename === "Cargo.toml") return parseLineDependencies(text, /^\s*([A-Za-z0-9_\-]+)\s*=\s*['"{]/).map((dep) => makeDep(dep));
2843
+ if (filename === "pubspec.yaml") return parseLineDependencies(text, /^\s{2,}([A-Za-z0-9_\-]+):\s*.+$/).map((dep) => makeDep(dep));
2844
+ if (filename === "mix.exs") return parseLineDependencies(text, /\{\s*:([a-zA-Z0-9_]+),/).map((dep) => makeDep(dep));
2845
+ if (filename === "cpanfile") return parseLineDependencies(text, /^\s*requires\s+['"]([^'"]+)['"]/).map((dep) => makeDep(dep));
2846
+ if (filename === "build.sbt") return parseLineDependencies(text, /"([A-Za-z0-9_.\-]+)"\s*%{1,2}\s*"([A-Za-z0-9_.\-]+)"/, 2).map((dep) => makeDep(dep));
2847
+ return [];
2848
+ }
2849
+ function detectExtensionBackedProjects(entries) {
2850
+ const byDir = /* @__PURE__ */ new Map();
2851
+ for (const entry of entries) {
2852
+ if (!entry.isFile) continue;
2853
+ const ext = path8.extname(entry.name).toLowerCase();
2854
+ if (!ext) continue;
2855
+ for (const mapping of EXTENSION_TO_LANGUAGE) {
2856
+ if (!mapping.extensions.includes(ext)) continue;
2857
+ const dir = path8.dirname(entry.relPath) || ".";
2858
+ if (!byDir.has(dir)) byDir.set(dir, /* @__PURE__ */ new Set());
2859
+ byDir.get(dir)?.add(mapping.type);
2860
+ }
2861
+ }
2862
+ return byDir;
2863
+ }
2864
+ async function scanPolyglotProjects(rootDir, cache) {
2865
+ const entries = cache ? await cache.walkDir(rootDir) : [];
2866
+ const candidateFiles = entries.filter((entry) => entry.isFile && MANIFEST_TO_LANGUAGE.some((m) => m.name === entry.name || m.name.startsWith("*.") && entry.name.endsWith(m.name.slice(1))));
2867
+ const shellDirs = new Set(
2868
+ entries.filter((entry) => entry.isFile && entry.name.endsWith(".sh")).map((entry) => path8.dirname(entry.relPath) || ".")
2869
+ );
2870
+ const projects = [];
2871
+ const seen = /* @__PURE__ */ new Set();
2872
+ for (const file of candidateFiles) {
2873
+ const mapping = MANIFEST_TO_LANGUAGE.find((m) => m.name === file.name || m.name.startsWith("*.") && file.name.endsWith(m.name.slice(1)));
2874
+ if (!mapping) continue;
2875
+ const projectPath = path8.dirname(file.relPath) || ".";
2876
+ const dependencies = await parseDepsByManifest(file.absPath, cache);
2877
+ addProject(projects, seen, mapping.type, projectPath, rootDir, dependencies);
2878
+ }
2879
+ for (const dir of shellDirs) {
2880
+ addProject(projects, seen, "shell", dir, rootDir);
2881
+ }
2882
+ const extensionProjects = detectExtensionBackedProjects(entries);
2883
+ for (const [dir, types] of extensionProjects) {
2884
+ for (const type of types) {
2885
+ addProject(projects, seen, type, dir, rootDir);
2886
+ }
2887
+ }
2888
+ return projects;
2889
+ }
2890
+
2642
2891
  // src/scanners/nuget-cache.ts
2643
2892
  import * as semver6 from "semver";
2644
2893
  var NuGetCache = class {
2645
- constructor(sem) {
2894
+ constructor(sem, manifest, offline = false) {
2646
2895
  this.sem = sem;
2896
+ this.manifest = manifest;
2897
+ this.offline = offline;
2647
2898
  }
2648
2899
  meta = /* @__PURE__ */ new Map();
2649
2900
  baseUrl = "https://api.nuget.org/v3-flatcontainer";
@@ -2651,6 +2902,23 @@ var NuGetCache = class {
2651
2902
  const existing = this.meta.get(pkg2);
2652
2903
  if (existing) return existing;
2653
2904
  const p = this.sem.run(async () => {
2905
+ const manifestEntry = getManifestEntry(this.manifest, "nuget", pkg2);
2906
+ if (manifestEntry) {
2907
+ const stableVersions = (manifestEntry.versions ?? []).filter((v) => {
2908
+ const parsed = semver6.valid(v);
2909
+ return parsed && semver6.prerelease(v) === null;
2910
+ });
2911
+ const sorted = [...stableVersions].sort(semver6.rcompare);
2912
+ const latestStableOverall = sorted[0] ?? null;
2913
+ return {
2914
+ latest: manifestEntry.latest ?? latestStableOverall,
2915
+ stableVersions,
2916
+ latestStableOverall
2917
+ };
2918
+ }
2919
+ if (this.offline) {
2920
+ return { latest: null, stableVersions: [], latestStableOverall: null };
2921
+ }
2654
2922
  try {
2655
2923
  const url = `${this.baseUrl}/${pkg2.toLowerCase()}/index.json`;
2656
2924
  const response = await fetch(url, {
@@ -2693,14 +2961,34 @@ function pep440ToSemver2(ver) {
2693
2961
  return semver7.valid(v);
2694
2962
  }
2695
2963
  var PyPICache = class {
2696
- constructor(sem) {
2964
+ constructor(sem, manifest, offline = false) {
2697
2965
  this.sem = sem;
2966
+ this.manifest = manifest;
2967
+ this.offline = offline;
2698
2968
  }
2699
2969
  meta = /* @__PURE__ */ new Map();
2700
2970
  get(pkg2) {
2701
2971
  const existing = this.meta.get(pkg2);
2702
2972
  if (existing) return existing;
2703
2973
  const p = this.sem.run(async () => {
2974
+ const manifestEntry = getManifestEntry(this.manifest, "pypi", pkg2);
2975
+ if (manifestEntry) {
2976
+ const stableVersions = [];
2977
+ for (const ver of manifestEntry.versions ?? []) {
2978
+ const sv = pep440ToSemver2(ver);
2979
+ if (sv) stableVersions.push(sv);
2980
+ }
2981
+ const sorted = [...stableVersions].sort(semver7.rcompare);
2982
+ const latestStableOverall = sorted[0] ?? null;
2983
+ return {
2984
+ latest: manifestEntry.latest ? pep440ToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
2985
+ stableVersions,
2986
+ latestStableOverall
2987
+ };
2988
+ }
2989
+ if (this.offline) {
2990
+ return { latest: null, stableVersions: [], latestStableOverall: null };
2991
+ }
2704
2992
  try {
2705
2993
  const url = `https://pypi.org/pypi/${encodeURIComponent(pkg2)}/json`;
2706
2994
  const response = await fetch(url, {
@@ -2747,8 +3035,10 @@ function mavenToSemver2(ver) {
2747
3035
  return semver8.valid(v);
2748
3036
  }
2749
3037
  var MavenCache = class {
2750
- constructor(sem) {
3038
+ constructor(sem, manifest, offline = false) {
2751
3039
  this.sem = sem;
3040
+ this.manifest = manifest;
3041
+ this.offline = offline;
2752
3042
  }
2753
3043
  meta = /* @__PURE__ */ new Map();
2754
3044
  /**
@@ -2761,6 +3051,25 @@ var MavenCache = class {
2761
3051
  const existing = this.meta.get(key);
2762
3052
  if (existing) return existing;
2763
3053
  const p = this.sem.run(async () => {
3054
+ const key2 = `${groupId}:${artifactId}`;
3055
+ const manifestEntry = getManifestEntry(this.manifest, "maven", key2);
3056
+ if (manifestEntry) {
3057
+ const stableVersions = [];
3058
+ for (const ver of manifestEntry.versions ?? []) {
3059
+ const sv = mavenToSemver2(ver);
3060
+ if (sv) stableVersions.push(sv);
3061
+ }
3062
+ const sorted = [...stableVersions].sort(semver8.rcompare);
3063
+ const latestStableOverall = sorted[0] ?? null;
3064
+ return {
3065
+ latest: manifestEntry.latest ? mavenToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
3066
+ stableVersions,
3067
+ latestStableOverall
3068
+ };
3069
+ }
3070
+ if (this.offline) {
3071
+ return { latest: null, stableVersions: [], latestStableOverall: null };
3072
+ }
2764
3073
  try {
2765
3074
  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`;
2766
3075
  const response = await fetch(url, {
@@ -2795,7 +3104,7 @@ var MavenCache = class {
2795
3104
  };
2796
3105
 
2797
3106
  // src/config.ts
2798
- import * as path7 from "path";
3107
+ import * as path9 from "path";
2799
3108
  import * as fs from "fs/promises";
2800
3109
  var CONFIG_FILES = [
2801
3110
  "vibgrate.config.ts",
@@ -2821,7 +3130,7 @@ var DEFAULT_CONFIG = {
2821
3130
  async function loadConfig(rootDir) {
2822
3131
  let config = DEFAULT_CONFIG;
2823
3132
  for (const file of CONFIG_FILES) {
2824
- const configPath = path7.join(rootDir, file);
3133
+ const configPath = path9.join(rootDir, file);
2825
3134
  if (await pathExists(configPath)) {
2826
3135
  if (file.endsWith(".json")) {
2827
3136
  const txt = await readTextFile(configPath);
@@ -2836,7 +3145,7 @@ async function loadConfig(rootDir) {
2836
3145
  }
2837
3146
  }
2838
3147
  }
2839
- const sidecarPath = path7.join(rootDir, ".vibgrate", "auto-excludes.json");
3148
+ const sidecarPath = path9.join(rootDir, ".vibgrate", "auto-excludes.json");
2840
3149
  if (await pathExists(sidecarPath)) {
2841
3150
  try {
2842
3151
  const txt = await readTextFile(sidecarPath);
@@ -2851,7 +3160,7 @@ async function loadConfig(rootDir) {
2851
3160
  return config;
2852
3161
  }
2853
3162
  async function writeDefaultConfig(rootDir) {
2854
- const configPath = path7.join(rootDir, "vibgrate.config.ts");
3163
+ const configPath = path9.join(rootDir, "vibgrate.config.ts");
2855
3164
  const content = `import type { VibgrateConfig } from '@vibgrate/cli';
2856
3165
 
2857
3166
  const config: VibgrateConfig = {
@@ -2877,7 +3186,7 @@ export default config;
2877
3186
  }
2878
3187
  async function appendExcludePatterns(rootDir, newPatterns) {
2879
3188
  if (newPatterns.length === 0) return false;
2880
- const jsonPath = path7.join(rootDir, "vibgrate.config.json");
3189
+ const jsonPath = path9.join(rootDir, "vibgrate.config.json");
2881
3190
  if (await pathExists(jsonPath)) {
2882
3191
  try {
2883
3192
  const txt = await readTextFile(jsonPath);
@@ -2890,8 +3199,8 @@ async function appendExcludePatterns(rootDir, newPatterns) {
2890
3199
  } catch {
2891
3200
  }
2892
3201
  }
2893
- const vibgrateDir = path7.join(rootDir, ".vibgrate");
2894
- const sidecarPath = path7.join(vibgrateDir, "auto-excludes.json");
3202
+ const vibgrateDir = path9.join(rootDir, ".vibgrate");
3203
+ const sidecarPath = path9.join(vibgrateDir, "auto-excludes.json");
2895
3204
  let existing = [];
2896
3205
  if (await pathExists(sidecarPath)) {
2897
3206
  try {
@@ -2912,7 +3221,7 @@ async function appendExcludePatterns(rootDir, newPatterns) {
2912
3221
  }
2913
3222
 
2914
3223
  // src/utils/vcs.ts
2915
- import * as path8 from "path";
3224
+ import * as path10 from "path";
2916
3225
  import * as fs2 from "fs/promises";
2917
3226
  async function detectVcs(rootDir) {
2918
3227
  try {
@@ -2926,7 +3235,7 @@ async function detectGit(rootDir) {
2926
3235
  if (!gitDir) {
2927
3236
  return { type: "unknown" };
2928
3237
  }
2929
- const headPath = path8.join(gitDir, "HEAD");
3238
+ const headPath = path10.join(gitDir, "HEAD");
2930
3239
  let headContent;
2931
3240
  try {
2932
3241
  headContent = (await fs2.readFile(headPath, "utf8")).trim();
@@ -2942,18 +3251,20 @@ async function detectGit(rootDir) {
2942
3251
  } else if (/^[0-9a-f]{40}$/i.test(headContent)) {
2943
3252
  sha = headContent;
2944
3253
  }
3254
+ const remoteUrl = await readGitRemoteUrl(gitDir);
2945
3255
  return {
2946
3256
  type: "git",
2947
3257
  sha: sha ?? void 0,
2948
3258
  shortSha: sha ? sha.slice(0, 7) : void 0,
2949
- branch: branch ?? void 0
3259
+ branch: branch ?? void 0,
3260
+ remoteUrl
2950
3261
  };
2951
3262
  }
2952
3263
  async function findGitDir(startDir) {
2953
- let dir = path8.resolve(startDir);
2954
- const root = path8.parse(dir).root;
3264
+ let dir = path10.resolve(startDir);
3265
+ const root = path10.parse(dir).root;
2955
3266
  while (dir !== root) {
2956
- const gitPath = path8.join(dir, ".git");
3267
+ const gitPath = path10.join(dir, ".git");
2957
3268
  try {
2958
3269
  const stat3 = await fs2.stat(gitPath);
2959
3270
  if (stat3.isDirectory()) {
@@ -2962,18 +3273,18 @@ async function findGitDir(startDir) {
2962
3273
  if (stat3.isFile()) {
2963
3274
  const content = (await fs2.readFile(gitPath, "utf8")).trim();
2964
3275
  if (content.startsWith("gitdir: ")) {
2965
- const resolved = path8.resolve(dir, content.slice(8));
3276
+ const resolved = path10.resolve(dir, content.slice(8));
2966
3277
  return resolved;
2967
3278
  }
2968
3279
  }
2969
3280
  } catch {
2970
3281
  }
2971
- dir = path8.dirname(dir);
3282
+ dir = path10.dirname(dir);
2972
3283
  }
2973
3284
  return null;
2974
3285
  }
2975
3286
  async function resolveRef(gitDir, refPath) {
2976
- const loosePath = path8.join(gitDir, refPath);
3287
+ const loosePath = path10.join(gitDir, refPath);
2977
3288
  try {
2978
3289
  const sha = (await fs2.readFile(loosePath, "utf8")).trim();
2979
3290
  if (/^[0-9a-f]{40}$/i.test(sha)) {
@@ -2981,7 +3292,7 @@ async function resolveRef(gitDir, refPath) {
2981
3292
  }
2982
3293
  } catch {
2983
3294
  }
2984
- const packedPath = path8.join(gitDir, "packed-refs");
3295
+ const packedPath = path10.join(gitDir, "packed-refs");
2985
3296
  try {
2986
3297
  const packed = await fs2.readFile(packedPath, "utf8");
2987
3298
  for (const line of packed.split("\n")) {
@@ -2995,6 +3306,39 @@ async function resolveRef(gitDir, refPath) {
2995
3306
  }
2996
3307
  return void 0;
2997
3308
  }
3309
+ async function readGitRemoteUrl(gitDir) {
3310
+ const configPath = await resolveGitConfigPath(gitDir);
3311
+ if (!configPath) return void 0;
3312
+ try {
3313
+ const config = await fs2.readFile(configPath, "utf8");
3314
+ const originBlock = config.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
3315
+ if (!originBlock) return void 0;
3316
+ const urlMatch = originBlock[1]?.match(/\n\s*url\s*=\s*(.+)\s*/);
3317
+ return urlMatch?.[1]?.trim();
3318
+ } catch {
3319
+ return void 0;
3320
+ }
3321
+ }
3322
+ async function resolveGitConfigPath(gitDir) {
3323
+ const directConfig = path10.join(gitDir, "config");
3324
+ try {
3325
+ const stat3 = await fs2.stat(directConfig);
3326
+ if (stat3.isFile()) return directConfig;
3327
+ } catch {
3328
+ }
3329
+ const commonDirFile = path10.join(gitDir, "commondir");
3330
+ try {
3331
+ const commonDir = (await fs2.readFile(commonDirFile, "utf8")).trim();
3332
+ if (!commonDir) return void 0;
3333
+ const resolvedCommonDir = path10.resolve(gitDir, commonDir);
3334
+ const commonConfig = path10.join(resolvedCommonDir, "config");
3335
+ const stat3 = await fs2.stat(commonConfig);
3336
+ if (stat3.isFile()) return commonConfig;
3337
+ } catch {
3338
+ return void 0;
3339
+ }
3340
+ return void 0;
3341
+ }
2998
3342
 
2999
3343
  // src/ui/progress.ts
3000
3344
  import chalk4 from "chalk";
@@ -3378,11 +3722,11 @@ var ScanProgress = class {
3378
3722
 
3379
3723
  // src/ui/scan-history.ts
3380
3724
  import * as fs3 from "fs/promises";
3381
- import * as path9 from "path";
3725
+ import * as path11 from "path";
3382
3726
  var HISTORY_FILENAME = "scan_history.json";
3383
3727
  var MAX_RECORDS = 10;
3384
3728
  async function loadScanHistory(rootDir) {
3385
- const filePath = path9.join(rootDir, ".vibgrate", HISTORY_FILENAME);
3729
+ const filePath = path11.join(rootDir, ".vibgrate", HISTORY_FILENAME);
3386
3730
  try {
3387
3731
  const txt = await fs3.readFile(filePath, "utf8");
3388
3732
  const data = JSON.parse(txt);
@@ -3395,8 +3739,8 @@ async function loadScanHistory(rootDir) {
3395
3739
  }
3396
3740
  }
3397
3741
  async function saveScanHistory(rootDir, record) {
3398
- const dir = path9.join(rootDir, ".vibgrate");
3399
- const filePath = path9.join(dir, HISTORY_FILENAME);
3742
+ const dir = path11.join(rootDir, ".vibgrate");
3743
+ const filePath = path11.join(dir, HISTORY_FILENAME);
3400
3744
  let history;
3401
3745
  const existing = await loadScanHistory(rootDir);
3402
3746
  if (existing) {
@@ -3460,7 +3804,7 @@ function estimateStepDurations(history, currentFileCount) {
3460
3804
  }
3461
3805
 
3462
3806
  // src/scanners/platform-matrix.ts
3463
- import * as path10 from "path";
3807
+ import * as path12 from "path";
3464
3808
  var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
3465
3809
  // Image / media processing
3466
3810
  "sharp",
@@ -3740,7 +4084,7 @@ async function scanPlatformMatrix(rootDir, cache) {
3740
4084
  }
3741
4085
  result.dockerBaseImages = [...baseImages].sort();
3742
4086
  for (const file of [".nvmrc", ".node-version", ".tool-versions"]) {
3743
- const exists = cache ? await cache.pathExists(path10.join(rootDir, file)) : await pathExists(path10.join(rootDir, file));
4087
+ const exists = cache ? await cache.pathExists(path12.join(rootDir, file)) : await pathExists(path12.join(rootDir, file));
3744
4088
  if (exists) {
3745
4089
  result.nodeVersionFiles.push(file);
3746
4090
  }
@@ -3817,7 +4161,7 @@ function scanDependencyRisk(projects) {
3817
4161
  }
3818
4162
 
3819
4163
  // src/scanners/dependency-graph.ts
3820
- import * as path11 from "path";
4164
+ import * as path13 from "path";
3821
4165
  function parsePnpmLock(content) {
3822
4166
  const entries = [];
3823
4167
  const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
@@ -3876,9 +4220,9 @@ async function scanDependencyGraph(rootDir, cache) {
3876
4220
  phantomDependencies: []
3877
4221
  };
3878
4222
  let entries = [];
3879
- const pnpmLock = path11.join(rootDir, "pnpm-lock.yaml");
3880
- const npmLock = path11.join(rootDir, "package-lock.json");
3881
- const yarnLock = path11.join(rootDir, "yarn.lock");
4223
+ const pnpmLock = path13.join(rootDir, "pnpm-lock.yaml");
4224
+ const npmLock = path13.join(rootDir, "package-lock.json");
4225
+ const yarnLock = path13.join(rootDir, "yarn.lock");
3882
4226
  const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
3883
4227
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3884
4228
  if (await _pathExists(pnpmLock)) {
@@ -3925,7 +4269,7 @@ async function scanDependencyGraph(rootDir, cache) {
3925
4269
  for (const pjPath of pkgFiles) {
3926
4270
  try {
3927
4271
  const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
3928
- const relPath = path11.relative(rootDir, pjPath);
4272
+ const relPath = path13.relative(rootDir, pjPath);
3929
4273
  for (const section of ["dependencies", "devDependencies"]) {
3930
4274
  const deps = pj[section];
3931
4275
  if (!deps) continue;
@@ -4271,7 +4615,7 @@ function scanToolingInventory(projects) {
4271
4615
  }
4272
4616
 
4273
4617
  // src/scanners/build-deploy.ts
4274
- import * as path12 from "path";
4618
+ import * as path14 from "path";
4275
4619
  var CI_FILES = {
4276
4620
  ".github/workflows": "github-actions",
4277
4621
  ".gitlab-ci.yml": "gitlab-ci",
@@ -4324,17 +4668,17 @@ async function scanBuildDeploy(rootDir, cache) {
4324
4668
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
4325
4669
  const ciSystems = /* @__PURE__ */ new Set();
4326
4670
  for (const [file, system] of Object.entries(CI_FILES)) {
4327
- const fullPath = path12.join(rootDir, file);
4671
+ const fullPath = path14.join(rootDir, file);
4328
4672
  if (await _pathExists(fullPath)) {
4329
4673
  ciSystems.add(system);
4330
4674
  }
4331
4675
  }
4332
- const ghWorkflowDir = path12.join(rootDir, ".github", "workflows");
4676
+ const ghWorkflowDir = path14.join(rootDir, ".github", "workflows");
4333
4677
  if (await _pathExists(ghWorkflowDir)) {
4334
4678
  try {
4335
4679
  if (cache) {
4336
4680
  const entries = await cache.walkDir(rootDir);
4337
- const ghPrefix = path12.relative(rootDir, ghWorkflowDir) + path12.sep;
4681
+ const ghPrefix = path14.relative(rootDir, ghWorkflowDir) + path14.sep;
4338
4682
  result.ciWorkflowCount = entries.filter(
4339
4683
  (e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
4340
4684
  ).length;
@@ -4385,11 +4729,11 @@ async function scanBuildDeploy(rootDir, cache) {
4385
4729
  (name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
4386
4730
  );
4387
4731
  if (cfnFiles.length > 0) iacSystems.add("cloudformation");
4388
- if (await _pathExists(path12.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
4732
+ if (await _pathExists(path14.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
4389
4733
  result.iac = [...iacSystems].sort();
4390
4734
  const releaseTools = /* @__PURE__ */ new Set();
4391
4735
  for (const [file, tool] of Object.entries(RELEASE_FILES)) {
4392
- if (await _pathExists(path12.join(rootDir, file))) releaseTools.add(tool);
4736
+ if (await _pathExists(path14.join(rootDir, file))) releaseTools.add(tool);
4393
4737
  }
4394
4738
  const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
4395
4739
  for (const pjPath of pkgFiles) {
@@ -4414,19 +4758,19 @@ async function scanBuildDeploy(rootDir, cache) {
4414
4758
  };
4415
4759
  const managers = /* @__PURE__ */ new Set();
4416
4760
  for (const [file, manager] of Object.entries(lockfileMap)) {
4417
- if (await _pathExists(path12.join(rootDir, file))) managers.add(manager);
4761
+ if (await _pathExists(path14.join(rootDir, file))) managers.add(manager);
4418
4762
  }
4419
4763
  result.packageManagers = [...managers].sort();
4420
4764
  const monoTools = /* @__PURE__ */ new Set();
4421
4765
  for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
4422
- if (await _pathExists(path12.join(rootDir, file))) monoTools.add(tool);
4766
+ if (await _pathExists(path14.join(rootDir, file))) monoTools.add(tool);
4423
4767
  }
4424
4768
  result.monorepoTools = [...monoTools].sort();
4425
4769
  return result;
4426
4770
  }
4427
4771
 
4428
4772
  // src/scanners/ts-modernity.ts
4429
- import * as path13 from "path";
4773
+ import * as path15 from "path";
4430
4774
  async function scanTsModernity(rootDir, cache) {
4431
4775
  const result = {
4432
4776
  typescriptVersion: null,
@@ -4464,7 +4808,7 @@ async function scanTsModernity(rootDir, cache) {
4464
4808
  if (hasEsm && hasCjs) result.moduleType = "mixed";
4465
4809
  else if (hasEsm) result.moduleType = "esm";
4466
4810
  else if (hasCjs) result.moduleType = "cjs";
4467
- let tsConfigPath = path13.join(rootDir, "tsconfig.json");
4811
+ let tsConfigPath = path15.join(rootDir, "tsconfig.json");
4468
4812
  const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
4469
4813
  if (!tsConfigExists) {
4470
4814
  const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
@@ -4811,7 +5155,7 @@ function scanBreakingChangeExposure(projects) {
4811
5155
 
4812
5156
  // src/scanners/file-hotspots.ts
4813
5157
  import * as fs4 from "fs/promises";
4814
- import * as path14 from "path";
5158
+ import * as path16 from "path";
4815
5159
  var SKIP_DIRS = /* @__PURE__ */ new Set([
4816
5160
  "node_modules",
4817
5161
  ".git",
@@ -4856,9 +5200,9 @@ async function scanFileHotspots(rootDir, cache) {
4856
5200
  const entries = await cache.walkDir(rootDir);
4857
5201
  for (const entry of entries) {
4858
5202
  if (!entry.isFile) continue;
4859
- const ext = path14.extname(entry.name).toLowerCase();
5203
+ const ext = path16.extname(entry.name).toLowerCase();
4860
5204
  if (SKIP_EXTENSIONS.has(ext)) continue;
4861
- const depth = entry.relPath.split(path14.sep).length - 1;
5205
+ const depth = entry.relPath.split(path16.sep).length - 1;
4862
5206
  if (depth > maxDepth) maxDepth = depth;
4863
5207
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
4864
5208
  try {
@@ -4887,15 +5231,15 @@ async function scanFileHotspots(rootDir, cache) {
4887
5231
  for (const e of entries) {
4888
5232
  if (e.isDirectory) {
4889
5233
  if (SKIP_DIRS.has(e.name)) continue;
4890
- await walk(path14.join(dir, e.name), depth + 1);
5234
+ await walk(path16.join(dir, e.name), depth + 1);
4891
5235
  } else if (e.isFile) {
4892
- const ext = path14.extname(e.name).toLowerCase();
5236
+ const ext = path16.extname(e.name).toLowerCase();
4893
5237
  if (SKIP_EXTENSIONS.has(ext)) continue;
4894
5238
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
4895
5239
  try {
4896
- const stat3 = await fs4.stat(path14.join(dir, e.name));
5240
+ const stat3 = await fs4.stat(path16.join(dir, e.name));
4897
5241
  allFiles.push({
4898
- path: path14.relative(rootDir, path14.join(dir, e.name)),
5242
+ path: path16.relative(rootDir, path16.join(dir, e.name)),
4899
5243
  bytes: stat3.size
4900
5244
  });
4901
5245
  } catch {
@@ -4918,7 +5262,7 @@ async function scanFileHotspots(rootDir, cache) {
4918
5262
  }
4919
5263
 
4920
5264
  // src/scanners/security-posture.ts
4921
- import * as path15 from "path";
5265
+ import * as path17 from "path";
4922
5266
  var LOCKFILES = {
4923
5267
  "pnpm-lock.yaml": "pnpm",
4924
5268
  "package-lock.json": "npm",
@@ -4939,14 +5283,14 @@ async function scanSecurityPosture(rootDir, cache) {
4939
5283
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
4940
5284
  const foundLockfiles = [];
4941
5285
  for (const [file, type] of Object.entries(LOCKFILES)) {
4942
- if (await _pathExists(path15.join(rootDir, file))) {
5286
+ if (await _pathExists(path17.join(rootDir, file))) {
4943
5287
  foundLockfiles.push(type);
4944
5288
  }
4945
5289
  }
4946
5290
  result.lockfilePresent = foundLockfiles.length > 0;
4947
5291
  result.multipleLockfileTypes = foundLockfiles.length > 1;
4948
5292
  result.lockfileTypes = foundLockfiles.sort();
4949
- const gitignorePath = path15.join(rootDir, ".gitignore");
5293
+ const gitignorePath = path17.join(rootDir, ".gitignore");
4950
5294
  if (await _pathExists(gitignorePath)) {
4951
5295
  try {
4952
5296
  const content = await _readTextFile(gitignorePath);
@@ -4961,7 +5305,7 @@ async function scanSecurityPosture(rootDir, cache) {
4961
5305
  }
4962
5306
  }
4963
5307
  for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
4964
- if (await _pathExists(path15.join(rootDir, envFile))) {
5308
+ if (await _pathExists(path17.join(rootDir, envFile))) {
4965
5309
  if (!result.gitignoreCoversEnv) {
4966
5310
  result.envFilesTracked = true;
4967
5311
  break;
@@ -4972,8 +5316,8 @@ async function scanSecurityPosture(rootDir, cache) {
4972
5316
  }
4973
5317
 
4974
5318
  // src/scanners/security-scanners.ts
4975
- import { spawn as spawn2 } from "child_process";
4976
- import * as path16 from "path";
5319
+ import { spawn as spawn3 } from "child_process";
5320
+ import * as path18 from "path";
4977
5321
  var TOOL_MATRIX = [
4978
5322
  { key: "semgrep", category: "sast", command: "semgrep", versionArgs: ["--version"], minRecommendedVersion: "1.75.0" },
4979
5323
  { key: "gitleaks", category: "secrets", command: "gitleaks", versionArgs: ["version"], minRecommendedVersion: "8.20.0" },
@@ -4986,8 +5330,8 @@ var SECRET_HEURISTICS = [
4986
5330
  { detector: "private-key", pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
4987
5331
  { detector: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
4988
5332
  ];
4989
- var defaultRunner = (command, args) => new Promise((resolve9, reject) => {
4990
- const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"] });
5333
+ var defaultRunner = (command, args) => new Promise((resolve10, reject) => {
5334
+ const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
4991
5335
  let stdout = "";
4992
5336
  let stderr = "";
4993
5337
  child.stdout.on("data", (d) => {
@@ -4998,7 +5342,7 @@ var defaultRunner = (command, args) => new Promise((resolve9, reject) => {
4998
5342
  });
4999
5343
  child.on("error", reject);
5000
5344
  child.on("close", (code) => {
5001
- resolve9({ stdout, stderr, exitCode: code ?? 1 });
5345
+ resolve10({ stdout, stderr, exitCode: code ?? 1 });
5002
5346
  });
5003
5347
  });
5004
5348
  function compareSemver(a, b) {
@@ -5068,7 +5412,7 @@ async function detectSecretHeuristics(rootDir, cache) {
5068
5412
  const findings = [];
5069
5413
  for (const entry of entries) {
5070
5414
  if (!entry.isFile) continue;
5071
- const ext = path16.extname(entry.name).toLowerCase();
5415
+ const ext = path18.extname(entry.name).toLowerCase();
5072
5416
  if (ext && [".png", ".jpg", ".jpeg", ".gif", ".zip", ".pdf"].includes(ext)) continue;
5073
5417
  const content = await cache.readTextFile(entry.absPath);
5074
5418
  if (!content || content.length > 3e5) continue;
@@ -5091,9 +5435,9 @@ async function scanSecurityScanners(rootDir, cache, runner = defaultRunner) {
5091
5435
  const [semgrep, gitleaks, trufflehog] = await Promise.all(TOOL_MATRIX.map((tool) => assessTool(tool, runner)));
5092
5436
  const heuristicFindings = await detectSecretHeuristics(rootDir, cache);
5093
5437
  const configFiles = {
5094
- semgrep: await cache.pathExists(path16.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path16.join(rootDir, ".semgrep.yaml")),
5095
- gitleaks: await cache.pathExists(path16.join(rootDir, ".gitleaks.toml")),
5096
- trufflehog: await cache.pathExists(path16.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path16.join(rootDir, ".trufflehog.yaml"))
5438
+ semgrep: await cache.pathExists(path18.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path18.join(rootDir, ".semgrep.yaml")),
5439
+ gitleaks: await cache.pathExists(path18.join(rootDir, ".gitleaks.toml")),
5440
+ trufflehog: await cache.pathExists(path18.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path18.join(rootDir, ".trufflehog.yaml"))
5097
5441
  };
5098
5442
  return {
5099
5443
  semgrep,
@@ -5518,7 +5862,7 @@ function scanServiceDependencies(projects) {
5518
5862
  }
5519
5863
 
5520
5864
  // src/scanners/architecture.ts
5521
- import * as path17 from "path";
5865
+ import * as path19 from "path";
5522
5866
  import * as fs5 from "fs/promises";
5523
5867
  var ARCHETYPE_SIGNALS = [
5524
5868
  // Meta-frameworks (highest priority — they imply routing patterns)
@@ -5817,9 +6161,9 @@ async function walkSourceFiles(rootDir, cache) {
5817
6161
  const entries = await cache.walkDir(rootDir);
5818
6162
  return entries.filter((e) => {
5819
6163
  if (!e.isFile) return false;
5820
- const name = path17.basename(e.absPath);
6164
+ const name = path19.basename(e.absPath);
5821
6165
  if (name.startsWith(".") && name !== ".") return false;
5822
- const ext = path17.extname(name);
6166
+ const ext = path19.extname(name);
5823
6167
  return SOURCE_EXTENSIONS.has(ext);
5824
6168
  }).map((e) => e.relPath);
5825
6169
  }
@@ -5833,15 +6177,15 @@ async function walkSourceFiles(rootDir, cache) {
5833
6177
  }
5834
6178
  for (const entry of entries) {
5835
6179
  if (entry.name.startsWith(".") && entry.name !== ".") continue;
5836
- const fullPath = path17.join(dir, entry.name);
6180
+ const fullPath = path19.join(dir, entry.name);
5837
6181
  if (entry.isDirectory()) {
5838
6182
  if (!IGNORE_DIRS.has(entry.name)) {
5839
6183
  await walk(fullPath);
5840
6184
  }
5841
6185
  } else if (entry.isFile()) {
5842
- const ext = path17.extname(entry.name);
6186
+ const ext = path19.extname(entry.name);
5843
6187
  if (SOURCE_EXTENSIONS.has(ext)) {
5844
- files.push(path17.relative(rootDir, fullPath));
6188
+ files.push(path19.relative(rootDir, fullPath));
5845
6189
  }
5846
6190
  }
5847
6191
  }
@@ -5865,7 +6209,7 @@ function classifyFile(filePath, archetype) {
5865
6209
  }
5866
6210
  }
5867
6211
  if (!bestMatch || bestMatch.confidence < 0.7) {
5868
- const baseName = path17.basename(filePath, path17.extname(filePath));
6212
+ const baseName = path19.basename(filePath, path19.extname(filePath));
5869
6213
  const cleanBase = baseName.replace(/\.(test|spec)$/, "");
5870
6214
  for (const rule of SUFFIX_RULES) {
5871
6215
  if (cleanBase.endsWith(rule.suffix)) {
@@ -5974,7 +6318,7 @@ function generateLayerFlowMermaid(layers) {
5974
6318
  return lines.join("\n");
5975
6319
  }
5976
6320
  async function buildProjectArchitectureMermaid(rootDir, project, archetype, cache) {
5977
- const projectRoot = path17.resolve(rootDir, project.path || ".");
6321
+ const projectRoot = path19.resolve(rootDir, project.path || ".");
5978
6322
  const allFiles = await walkSourceFiles(projectRoot, cache);
5979
6323
  const layerSet = /* @__PURE__ */ new Set();
5980
6324
  for (const rel of allFiles) {
@@ -6100,7 +6444,7 @@ async function scanArchitecture(rootDir, projects, tooling, services, cache) {
6100
6444
  }
6101
6445
 
6102
6446
  // src/scanners/code-quality.ts
6103
- import * as path18 from "path";
6447
+ import * as path20 from "path";
6104
6448
  import * as ts from "typescript";
6105
6449
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
6106
6450
  var DEFAULT_RESULT = {
@@ -6131,9 +6475,9 @@ async function scanCodeQuality(rootDir, cache) {
6131
6475
  continue;
6132
6476
  }
6133
6477
  if (!raw.trim()) continue;
6134
- const rel = normalizeModuleId(path18.relative(rootDir, filePath));
6478
+ const rel = normalizeModuleId(path20.relative(rootDir, filePath));
6135
6479
  const source = ts.createSourceFile(filePath, raw, ts.ScriptTarget.Latest, true);
6136
- const imports = collectLocalImports(source, path18.dirname(filePath), rootDir);
6480
+ const imports = collectLocalImports(source, path20.dirname(filePath), rootDir);
6137
6481
  depGraph.set(rel, imports);
6138
6482
  const fileMetrics = computeFileMetrics(source, raw);
6139
6483
  totalFunctions += fileMetrics.functionsAnalyzed;
@@ -6167,9 +6511,9 @@ async function scanCodeQuality(rootDir, cache) {
6167
6511
  async function findSourceFiles(rootDir, cache) {
6168
6512
  if (cache) {
6169
6513
  const entries = await cache.walkDir(rootDir);
6170
- return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path18.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
6514
+ return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path20.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
6171
6515
  }
6172
- const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path18.extname(name).toLowerCase()));
6516
+ const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path20.extname(name).toLowerCase()));
6173
6517
  return files;
6174
6518
  }
6175
6519
  function collectLocalImports(source, fileDir, rootDir) {
@@ -6190,8 +6534,8 @@ function collectLocalImports(source, fileDir, rootDir) {
6190
6534
  }
6191
6535
  function resolveLocalImport(specifier, fileDir, rootDir) {
6192
6536
  if (!specifier.startsWith(".")) return null;
6193
- const rawTarget = path18.resolve(fileDir, specifier);
6194
- const normalized = path18.relative(rootDir, rawTarget).replace(/\\/g, "/");
6537
+ const rawTarget = path20.resolve(fileDir, specifier);
6538
+ const normalized = path20.relative(rootDir, rawTarget).replace(/\\/g, "/");
6195
6539
  if (!normalized || normalized.startsWith("..")) return null;
6196
6540
  return normalizeModuleId(normalized);
6197
6541
  }
@@ -6332,8 +6676,8 @@ function visitEach(node, cb) {
6332
6676
  }
6333
6677
 
6334
6678
  // src/scanners/owasp-category-mapping.ts
6335
- import { spawn as spawn3 } from "child_process";
6336
- import * as path19 from "path";
6679
+ import { spawn as spawn4 } from "child_process";
6680
+ import * as path21 from "path";
6337
6681
  var OWASP_CONFIG = "p/owasp-top-ten";
6338
6682
  var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
6339
6683
  ".js",
@@ -6358,8 +6702,8 @@ var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
6358
6702
  ".env"
6359
6703
  ]);
6360
6704
  async function runSemgrep(args, cwd, stdin) {
6361
- return new Promise((resolve9, reject) => {
6362
- const child = spawn3("semgrep", args, {
6705
+ return new Promise((resolve10, reject) => {
6706
+ const child = spawn4("semgrep", args, {
6363
6707
  cwd,
6364
6708
  shell: true,
6365
6709
  stdio: ["pipe", "pipe", "pipe"]
@@ -6374,7 +6718,7 @@ async function runSemgrep(args, cwd, stdin) {
6374
6718
  });
6375
6719
  child.on("error", reject);
6376
6720
  child.on("close", (code) => {
6377
- resolve9({ code: code ?? 1, stdout, stderr });
6721
+ resolve10({ code: code ?? 1, stdout, stderr });
6378
6722
  });
6379
6723
  if (stdin !== void 0) child.stdin.write(stdin);
6380
6724
  child.stdin.end();
@@ -6412,7 +6756,7 @@ function parseFindings(results, rootDir) {
6412
6756
  const metadata = r.extra?.metadata;
6413
6757
  return {
6414
6758
  ruleId: r.check_id ?? "unknown",
6415
- path: r.path ? path19.relative(rootDir, path19.resolve(rootDir, r.path)) : "",
6759
+ path: r.path ? path21.relative(rootDir, path21.resolve(rootDir, r.path)) : "",
6416
6760
  line: r.start?.line ?? 1,
6417
6761
  endLine: r.end?.line,
6418
6762
  message: r.extra?.message ?? "Potential security issue",
@@ -6472,7 +6816,7 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
6472
6816
  }
6473
6817
  }
6474
6818
  const entries = cache ? await cache.walkDir(rootDir) : [];
6475
- const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path19.extname(e.name).toLowerCase()));
6819
+ const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path21.extname(e.name).toLowerCase()));
6476
6820
  const findings = [];
6477
6821
  const errors = [];
6478
6822
  let scannedFiles = 0;
@@ -6720,7 +7064,7 @@ function isUsefulString(s) {
6720
7064
  }
6721
7065
 
6722
7066
  // src/utils/tool-installer.ts
6723
- import { spawn as spawn4 } from "child_process";
7067
+ import { spawn as spawn5 } from "child_process";
6724
7068
  import chalk5 from "chalk";
6725
7069
  var SECURITY_TOOLS = [
6726
7070
  { name: "semgrep", command: "semgrep", brew: "semgrep", winget: null, scoop: null, pip: "semgrep" },
@@ -6728,9 +7072,9 @@ var SECURITY_TOOLS = [
6728
7072
  { name: "trufflehog", command: "trufflehog", brew: "trufflehog", winget: null, scoop: "trufflehog", pip: null }
6729
7073
  ];
6730
7074
  var IS_WIN = process.platform === "win32";
6731
- function runCommand(cmd, args) {
6732
- return new Promise((resolve9) => {
6733
- const child = spawn4(cmd, args, {
7075
+ function runCommand2(cmd, args) {
7076
+ return new Promise((resolve10) => {
7077
+ const child = spawn5(cmd, args, {
6734
7078
  stdio: ["ignore", "pipe", "pipe"],
6735
7079
  shell: IS_WIN
6736
7080
  // required for .cmd/.ps1 wrappers on Windows
@@ -6743,19 +7087,19 @@ function runCommand(cmd, args) {
6743
7087
  child.stderr.on("data", (d) => {
6744
7088
  stderr += d.toString();
6745
7089
  });
6746
- child.on("error", () => resolve9({ exitCode: 127, stdout, stderr }));
6747
- child.on("close", (code) => resolve9({ exitCode: code ?? 1, stdout, stderr }));
7090
+ child.on("error", () => resolve10({ exitCode: 127, stdout, stderr }));
7091
+ child.on("close", (code) => resolve10({ exitCode: code ?? 1, stdout, stderr }));
6748
7092
  });
6749
7093
  }
6750
7094
  async function commandExists(command) {
6751
7095
  const checker = IS_WIN ? "where" : "which";
6752
- const { exitCode } = await runCommand(checker, [command]);
7096
+ const { exitCode } = await runCommand2(checker, [command]);
6753
7097
  return exitCode === 0;
6754
7098
  }
6755
7099
  async function tryInstall(tool, strategies, log) {
6756
7100
  for (const { pm, args } of strategies) {
6757
7101
  log(chalk5.dim(` ${pm} ${args.join(" ")}\u2026`));
6758
- const { exitCode, stderr } = await runCommand(pm, args);
7102
+ const { exitCode, stderr } = await runCommand2(pm, args);
6759
7103
  if (exitCode === 0) {
6760
7104
  log(` ${chalk5.green("\u2714")} ${tool.name} installed via ${pm}`);
6761
7105
  return { ok: true, pm };
@@ -6840,20 +7184,26 @@ function buildDefs() {
6840
7184
  function generateWorkspaceRelationshipMermaid(projects) {
6841
7185
  const lines = ["flowchart LR"];
6842
7186
  const byPath = new Map(projects.map((p) => [p.path, p]));
6843
- for (const project of projects) {
6844
- const id = sanitizeId(project.projectId || project.path || project.name);
6845
- lines.push(`${id}["${escapeLabel(nodeLabel(project))}"]`);
6846
- lines.push(`class ${id} ${scoreClass(project.drift?.score)}`);
6847
- }
7187
+ const edges = [];
7188
+ const connectedIds = /* @__PURE__ */ new Set();
6848
7189
  for (const project of projects) {
6849
7190
  const fromId = sanitizeId(project.projectId || project.path || project.name);
6850
7191
  for (const ref of project.projectReferences ?? []) {
6851
7192
  const target = byPath.get(ref.path);
6852
7193
  if (!target) continue;
6853
7194
  const toId = sanitizeId(target.projectId || target.path || target.name);
6854
- lines.push(`${fromId} --> ${toId}`);
7195
+ edges.push(`${fromId} --> ${toId}`);
7196
+ connectedIds.add(fromId);
7197
+ connectedIds.add(toId);
6855
7198
  }
6856
7199
  }
7200
+ for (const project of projects) {
7201
+ const id = sanitizeId(project.projectId || project.path || project.name);
7202
+ if (!connectedIds.has(id)) continue;
7203
+ lines.push(`${id}["${escapeLabel(nodeLabel(project))}"]`);
7204
+ lines.push(`class ${id} ${scoreClass(project.drift?.score)}`);
7205
+ }
7206
+ lines.push(...edges);
6857
7207
  lines.push(...buildDefs());
6858
7208
  return { mermaid: lines.join("\n") };
6859
7209
  }
@@ -6880,21 +7230,94 @@ function generateProjectRelationshipMermaid(project, projects) {
6880
7230
  lines.push(...buildDefs());
6881
7231
  return { mermaid: lines.join("\n") };
6882
7232
  }
7233
+ function generateSolutionRelationshipMermaid(solution, projects) {
7234
+ const lines = ["flowchart TB"];
7235
+ const solutionNodeId = sanitizeId(solution.solutionId || solution.path || solution.name);
7236
+ const solutionScore = solution.drift?.score;
7237
+ const solutionScoreText = typeof solutionScore === "number" ? ` (${solutionScore})` : " (n/a)";
7238
+ lines.push(`${solutionNodeId}["${escapeLabel(`${solution.name}${solutionScoreText}`)}"]`);
7239
+ lines.push(`class ${solutionNodeId} ${scoreClass(solutionScore)}`);
7240
+ const projectByPath = new Map(projects.map((p) => [p.path, p]));
7241
+ for (const projectPath of solution.projectPaths) {
7242
+ const project = projectByPath.get(projectPath);
7243
+ if (!project) continue;
7244
+ const projectNodeId = sanitizeId(project.projectId || project.path || project.name);
7245
+ lines.push(`${projectNodeId}["${escapeLabel(nodeLabel(project))}"]`);
7246
+ lines.push(`class ${projectNodeId} ${scoreClass(project.drift?.score)}`);
7247
+ lines.push(`${solutionNodeId} --> ${projectNodeId}`);
7248
+ for (const ref of project.projectReferences ?? []) {
7249
+ const target = projectByPath.get(ref.path);
7250
+ if (!target) continue;
7251
+ const toId = sanitizeId(target.projectId || target.path || target.name);
7252
+ lines.push(`${projectNodeId} --> ${toId}`);
7253
+ }
7254
+ }
7255
+ lines.push(...buildDefs());
7256
+ return { mermaid: lines.join("\n") };
7257
+ }
6883
7258
 
6884
7259
  // src/commands/scan.ts
7260
+ async function discoverSolutions(rootDir, fileCache) {
7261
+ const solutionFiles = await fileCache.findSolutionFiles(rootDir);
7262
+ const parsed = [];
7263
+ for (const solutionFile of solutionFiles) {
7264
+ try {
7265
+ const content = await fileCache.readTextFile(solutionFile);
7266
+ const dir = path22.dirname(solutionFile);
7267
+ const relSolutionPath = path22.relative(rootDir, solutionFile).replace(/\\/g, "/");
7268
+ const projectPaths = /* @__PURE__ */ new Set();
7269
+ const projectRegex = /Project\("[^"]*"\)\s*=\s*"([^"]*)",\s*"([^"]+\.csproj)"/g;
7270
+ let match;
7271
+ while ((match = projectRegex.exec(content)) !== null) {
7272
+ const projectRelative = match[2];
7273
+ const absProjectPath = path22.resolve(dir, projectRelative.replace(/\\/g, "/"));
7274
+ projectPaths.add(path22.relative(rootDir, absProjectPath).replace(/\\/g, "/"));
7275
+ }
7276
+ const solutionName = path22.basename(solutionFile, path22.extname(solutionFile));
7277
+ parsed.push({
7278
+ path: relSolutionPath,
7279
+ name: solutionName,
7280
+ type: "dotnet-sln",
7281
+ projectPaths: [...projectPaths]
7282
+ });
7283
+ } catch {
7284
+ }
7285
+ }
7286
+ return parsed;
7287
+ }
6885
7288
  async function runScan(rootDir, opts) {
6886
7289
  const scanStart = Date.now();
6887
7290
  const config = await loadConfig(rootDir);
6888
7291
  const sem = new Semaphore(opts.concurrency);
6889
- const npmCache = new NpmCache(rootDir, sem);
6890
- const nugetCache = new NuGetCache(sem);
6891
- const pypiCache = new PyPICache(sem);
6892
- const mavenCache = new MavenCache(sem);
7292
+ const packageManifest = opts.packageManifest ? await loadPackageVersionManifest(opts.packageManifest) : void 0;
7293
+ const offlineMode = opts.offline === true;
7294
+ const maxPrivacyMode = opts.maxPrivacy === true;
7295
+ const npmCache = new NpmCache(rootDir, sem, packageManifest, offlineMode);
7296
+ const nugetCache = new NuGetCache(sem, packageManifest, offlineMode);
7297
+ const pypiCache = new PyPICache(sem, packageManifest, offlineMode);
7298
+ const mavenCache = new MavenCache(sem, packageManifest, offlineMode);
6893
7299
  const fileCache = new FileCache();
6894
7300
  const excludePatterns = config.exclude ?? [];
6895
7301
  fileCache.setExcludePatterns(excludePatterns);
6896
7302
  fileCache.setMaxFileSize(config.maxFileSizeToScan ?? 5242880);
6897
7303
  const scanners = config.scanners;
7304
+ const scannerPolicy = {
7305
+ platformMatrix: !maxPrivacyMode,
7306
+ toolingInventory: true,
7307
+ serviceDependencies: !maxPrivacyMode,
7308
+ breakingChangeExposure: !maxPrivacyMode,
7309
+ securityPosture: true,
7310
+ securityScanners: !maxPrivacyMode,
7311
+ buildDeploy: !maxPrivacyMode,
7312
+ tsModernity: !maxPrivacyMode,
7313
+ fileHotspots: !maxPrivacyMode,
7314
+ dependencyGraph: true,
7315
+ dependencyRisk: true,
7316
+ architecture: !maxPrivacyMode,
7317
+ codeQuality: !maxPrivacyMode,
7318
+ owaspCategoryMapping: !maxPrivacyMode,
7319
+ uiPurpose: !maxPrivacyMode
7320
+ };
6898
7321
  let filesScanned = 0;
6899
7322
  const progress = new ScanProgress(rootDir);
6900
7323
  const steps = [
@@ -6906,29 +7329,30 @@ async function runScan(rootDir, opts) {
6906
7329
  { id: "dotnet", label: "Scanning .NET projects", weight: 2 },
6907
7330
  { id: "python", label: "Scanning Python projects", weight: 3 },
6908
7331
  { id: "java", label: "Scanning Java projects", weight: 3 },
7332
+ { id: "polyglot", label: "Scanning additional language projects", weight: 2 },
6909
7333
  ...scanners !== false ? [
6910
- ...scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
6911
- ...scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
6912
- ...scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
6913
- ...scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
6914
- ...scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
6915
- ...scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
6916
- ...scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
6917
- ...scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
6918
- ...scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
6919
- ...scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
6920
- ...scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
6921
- ...scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
6922
- ...scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
6923
- ...scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : [],
6924
- ...opts.uiPurpose || scanners?.uiPurpose?.enabled === true ? [{ id: "uipurpose", label: "UI purpose evidence" }] : []
7334
+ ...scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
7335
+ ...scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
7336
+ ...scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
7337
+ ...scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
7338
+ ...scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
7339
+ ...scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
7340
+ ...scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
7341
+ ...scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
7342
+ ...scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
7343
+ ...scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
7344
+ ...scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
7345
+ ...scannerPolicy.architecture && scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
7346
+ ...scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
7347
+ ...scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : [],
7348
+ ...!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) ? [{ id: "uipurpose", label: "UI purpose evidence" }] : []
6925
7349
  ] : [],
6926
7350
  { id: "drift", label: "Computing drift score" },
6927
7351
  { id: "findings", label: "Generating findings" }
6928
7352
  ];
6929
7353
  progress.setSteps(steps);
6930
7354
  progress.completeStep("config", "loaded");
6931
- const registryOk = await checkRegistryAccess(rootDir);
7355
+ const registryOk = offlineMode ? true : await checkRegistryAccess(rootDir);
6932
7356
  if (!registryOk) {
6933
7357
  progress.finish();
6934
7358
  const msg = [
@@ -7002,7 +7426,16 @@ async function runScan(rootDir, opts) {
7002
7426
  filesScanned += javaProjects.length;
7003
7427
  progress.addProjects(javaProjects.length);
7004
7428
  progress.completeStep("java", `${javaProjects.length} project${javaProjects.length !== 1 ? "s" : ""}`, javaProjects.length);
7005
- const allProjects = [...nodeProjects, ...dotnetProjects, ...pythonProjects, ...javaProjects];
7429
+ progress.startStep("polyglot");
7430
+ const polyglotProjects = await scanPolyglotProjects(rootDir, fileCache);
7431
+ for (const p of polyglotProjects) {
7432
+ progress.addDependencies(p.dependencies.length);
7433
+ progress.addFrameworks(p.frameworks.length);
7434
+ }
7435
+ filesScanned += polyglotProjects.length;
7436
+ progress.addProjects(polyglotProjects.length);
7437
+ progress.completeStep("polyglot", `${polyglotProjects.length} project${polyglotProjects.length !== 1 ? "s" : ""}`, polyglotProjects.length);
7438
+ const allProjects = [...nodeProjects, ...dotnetProjects, ...pythonProjects, ...javaProjects, ...polyglotProjects];
7006
7439
  const dsn = opts.dsn || process.env.VIBGRATE_DSN;
7007
7440
  const parsedDsn = dsn ? parseDsn(dsn) : null;
7008
7441
  const workspaceId = parsedDsn?.workspaceId;
@@ -7010,14 +7443,45 @@ async function runScan(rootDir, opts) {
7010
7443
  project.drift = computeDriftScore([project]);
7011
7444
  project.projectId = computeProjectId(project.path, project.name, workspaceId);
7012
7445
  }
7446
+ const solutionsManifestPath = path22.join(rootDir, ".vibgrate", "solutions.json");
7447
+ const persistedSolutionIds = /* @__PURE__ */ new Map();
7448
+ if (await pathExists(solutionsManifestPath)) {
7449
+ try {
7450
+ const persisted = await readJsonFile(solutionsManifestPath);
7451
+ for (const solution of persisted.solutions ?? []) {
7452
+ if (solution.path && solution.solutionId) persistedSolutionIds.set(solution.path, solution.solutionId);
7453
+ }
7454
+ } catch {
7455
+ }
7456
+ }
7457
+ const discoveredSolutions = await discoverSolutions(rootDir, fileCache);
7458
+ const solutions = discoveredSolutions.map((solution) => ({
7459
+ solutionId: persistedSolutionIds.get(solution.path) ?? computeSolutionId(solution.path, solution.name, workspaceId),
7460
+ path: solution.path,
7461
+ name: solution.name,
7462
+ type: solution.type,
7463
+ projectPaths: solution.projectPaths
7464
+ }));
7465
+ const projectsByPath = new Map(allProjects.map((project) => [project.path, project]));
7466
+ for (const solution of solutions) {
7467
+ const includedProjects = solution.projectPaths.map((projectPath) => projectsByPath.get(projectPath)).filter((project) => Boolean(project));
7468
+ solution.drift = includedProjects.length > 0 ? computeDriftScore(includedProjects) : void 0;
7469
+ for (const project of includedProjects) {
7470
+ project.solutionId = solution.solutionId;
7471
+ project.solutionName = solution.name;
7472
+ }
7473
+ }
7013
7474
  for (const project of allProjects) {
7014
7475
  project.relationshipDiagram = generateProjectRelationshipMermaid(project, allProjects);
7015
7476
  }
7477
+ for (const solution of solutions) {
7478
+ solution.relationshipDiagram = generateSolutionRelationshipMermaid(solution, allProjects);
7479
+ }
7016
7480
  const relationshipDiagram = generateWorkspaceRelationshipMermaid(allProjects);
7017
7481
  const extended = {};
7018
7482
  if (scanners !== false) {
7019
7483
  const scannerTasks = [];
7020
- if (scanners?.platformMatrix?.enabled !== false) {
7484
+ if (scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false) {
7021
7485
  progress.startStep("platform");
7022
7486
  scannerTasks.push(
7023
7487
  scanPlatformMatrix(rootDir, fileCache).then((result) => {
@@ -7031,7 +7495,7 @@ async function runScan(rootDir, opts) {
7031
7495
  })
7032
7496
  );
7033
7497
  }
7034
- if (scanners?.toolingInventory?.enabled !== false) {
7498
+ if (scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false) {
7035
7499
  progress.startStep("tooling");
7036
7500
  scannerTasks.push(
7037
7501
  Promise.resolve().then(() => {
@@ -7041,7 +7505,7 @@ async function runScan(rootDir, opts) {
7041
7505
  })
7042
7506
  );
7043
7507
  }
7044
- if (scanners?.serviceDependencies?.enabled !== false) {
7508
+ if (scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false) {
7045
7509
  progress.startStep("services");
7046
7510
  scannerTasks.push(
7047
7511
  Promise.resolve().then(() => {
@@ -7051,7 +7515,7 @@ async function runScan(rootDir, opts) {
7051
7515
  })
7052
7516
  );
7053
7517
  }
7054
- if (scanners?.breakingChangeExposure?.enabled !== false) {
7518
+ if (scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false) {
7055
7519
  progress.startStep("breaking");
7056
7520
  scannerTasks.push(
7057
7521
  Promise.resolve().then(() => {
@@ -7066,7 +7530,7 @@ async function runScan(rootDir, opts) {
7066
7530
  })
7067
7531
  );
7068
7532
  }
7069
- if (scanners?.securityPosture?.enabled !== false) {
7533
+ if (scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false) {
7070
7534
  progress.startStep("security");
7071
7535
  scannerTasks.push(
7072
7536
  scanSecurityPosture(rootDir, fileCache).then((result) => {
@@ -7076,7 +7540,7 @@ async function runScan(rootDir, opts) {
7076
7540
  })
7077
7541
  );
7078
7542
  }
7079
- if (scanners?.securityScanners?.enabled !== false) {
7543
+ if (scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false) {
7080
7544
  if (opts.installTools) {
7081
7545
  const installResult = await installMissingTools();
7082
7546
  if (installResult.installed.length > 0) {
@@ -7099,7 +7563,7 @@ async function runScan(rootDir, opts) {
7099
7563
  })
7100
7564
  );
7101
7565
  }
7102
- if (scanners?.buildDeploy?.enabled !== false) {
7566
+ if (scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false) {
7103
7567
  progress.startStep("build");
7104
7568
  scannerTasks.push(
7105
7569
  scanBuildDeploy(rootDir, fileCache).then((result) => {
@@ -7111,7 +7575,7 @@ async function runScan(rootDir, opts) {
7111
7575
  })
7112
7576
  );
7113
7577
  }
7114
- if (scanners?.tsModernity?.enabled !== false) {
7578
+ if (scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false) {
7115
7579
  progress.startStep("ts");
7116
7580
  scannerTasks.push(
7117
7581
  scanTsModernity(rootDir, fileCache).then((result) => {
@@ -7124,7 +7588,7 @@ async function runScan(rootDir, opts) {
7124
7588
  })
7125
7589
  );
7126
7590
  }
7127
- if (scanners?.fileHotspots?.enabled !== false) {
7591
+ if (scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false) {
7128
7592
  progress.startStep("hotspots");
7129
7593
  scannerTasks.push(
7130
7594
  scanFileHotspots(rootDir, fileCache).then((result) => {
@@ -7133,7 +7597,7 @@ async function runScan(rootDir, opts) {
7133
7597
  })
7134
7598
  );
7135
7599
  }
7136
- if (scanners?.dependencyGraph?.enabled !== false) {
7600
+ if (scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false) {
7137
7601
  progress.startStep("depgraph");
7138
7602
  scannerTasks.push(
7139
7603
  scanDependencyGraph(rootDir, fileCache).then((result) => {
@@ -7143,7 +7607,7 @@ async function runScan(rootDir, opts) {
7143
7607
  })
7144
7608
  );
7145
7609
  }
7146
- if (scanners?.codeQuality?.enabled !== false) {
7610
+ if (scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false) {
7147
7611
  progress.startStep("codequality");
7148
7612
  scannerTasks.push(
7149
7613
  scanCodeQuality(rootDir, fileCache).then((result) => {
@@ -7156,7 +7620,7 @@ async function runScan(rootDir, opts) {
7156
7620
  })
7157
7621
  );
7158
7622
  }
7159
- if (scanners?.dependencyRisk?.enabled !== false) {
7623
+ if (scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false) {
7160
7624
  progress.startStep("deprisk");
7161
7625
  scannerTasks.push(
7162
7626
  Promise.resolve().then(() => {
@@ -7170,7 +7634,7 @@ async function runScan(rootDir, opts) {
7170
7634
  );
7171
7635
  }
7172
7636
  await Promise.all(scannerTasks);
7173
- if (scanners?.owaspCategoryMapping?.enabled !== false) {
7637
+ if (scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false) {
7174
7638
  progress.startStep("owasp");
7175
7639
  extended.owaspCategoryMapping = await scanOwaspCategoryMapping(
7176
7640
  rootDir,
@@ -7189,14 +7653,14 @@ async function runScan(rootDir, opts) {
7189
7653
  );
7190
7654
  }
7191
7655
  }
7192
- if (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) {
7656
+ if (!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true)) {
7193
7657
  progress.startStep("uipurpose");
7194
7658
  extended.uiPurpose = await scanUiPurpose(rootDir, fileCache);
7195
7659
  const up = extended.uiPurpose;
7196
7660
  const summary = [`${up.topEvidence.length} evidence`, ...up.capped ? ["capped"] : []].join(" \xB7 ");
7197
7661
  progress.completeStep("uipurpose", summary, up.topEvidence.length);
7198
7662
  }
7199
- if (scanners?.architecture?.enabled !== false) {
7663
+ if (scannerPolicy.architecture && scanners?.architecture?.enabled !== false) {
7200
7664
  progress.startStep("architecture");
7201
7665
  extended.architecture = await scanArchitecture(
7202
7666
  rootDir,
@@ -7284,13 +7748,16 @@ async function runScan(rootDir, opts) {
7284
7748
  if (extended.owaspCategoryMapping) filesScanned += extended.owaspCategoryMapping.scannedFiles;
7285
7749
  if (extended.uiPurpose) filesScanned += extended.uiPurpose.topEvidence.length;
7286
7750
  const durationMs = Date.now() - scanStart;
7751
+ const repository = await buildRepositoryInfo(rootDir, vcs.remoteUrl, extended.buildDeploy?.ci);
7287
7752
  const artifact = {
7288
7753
  schemaVersion: "1.0",
7289
7754
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7290
7755
  vibgrateVersion: VERSION,
7291
- rootPath: path20.basename(rootDir),
7756
+ rootPath: path22.basename(rootDir),
7292
7757
  ...vcs.type !== "unknown" ? { vcs } : {},
7758
+ repository,
7293
7759
  projects: allProjects,
7760
+ ...solutions.length > 0 ? { solutions } : {},
7294
7761
  drift,
7295
7762
  findings,
7296
7763
  ...Object.keys(extended).length > 0 ? { extended } : {},
@@ -7300,7 +7767,7 @@ async function runScan(rootDir, opts) {
7300
7767
  relationshipDiagram
7301
7768
  };
7302
7769
  if (opts.baseline) {
7303
- const baselinePath = path20.resolve(opts.baseline);
7770
+ const baselinePath = path22.resolve(opts.baseline);
7304
7771
  if (await pathExists(baselinePath)) {
7305
7772
  try {
7306
7773
  const baseline = await readJsonFile(baselinePath);
@@ -7311,9 +7778,21 @@ async function runScan(rootDir, opts) {
7311
7778
  }
7312
7779
  }
7313
7780
  }
7314
- const vibgrateDir = path20.join(rootDir, ".vibgrate");
7315
- await ensureDir(vibgrateDir);
7316
- await writeJsonFile(path20.join(vibgrateDir, "scan_result.json"), artifact);
7781
+ if (!opts.noLocalArtifacts && !maxPrivacyMode) {
7782
+ const vibgrateDir = path22.join(rootDir, ".vibgrate");
7783
+ await ensureDir(vibgrateDir);
7784
+ await writeJsonFile(path22.join(vibgrateDir, "scan_result.json"), artifact);
7785
+ await writeJsonFile(path22.join(vibgrateDir, "solutions.json"), {
7786
+ scannedAt: artifact.timestamp,
7787
+ solutions: solutions.map((solution) => ({
7788
+ solutionId: solution.solutionId,
7789
+ name: solution.name,
7790
+ path: solution.path,
7791
+ type: solution.type,
7792
+ projectPaths: solution.projectPaths
7793
+ }))
7794
+ });
7795
+ }
7317
7796
  await saveScanHistory(rootDir, {
7318
7797
  timestamp: artifact.timestamp,
7319
7798
  totalDurationMs: durationMs,
@@ -7321,29 +7800,33 @@ async function runScan(rootDir, opts) {
7321
7800
  totalDirs: treeCount.totalDirs,
7322
7801
  steps: progress.getStepTimings()
7323
7802
  });
7324
- for (const project of allProjects) {
7325
- if (project.drift && project.path) {
7326
- const projectDir = path20.resolve(rootDir, project.path);
7327
- const projectVibgrateDir = path20.join(projectDir, ".vibgrate");
7328
- await ensureDir(projectVibgrateDir);
7329
- await writeJsonFile(path20.join(projectVibgrateDir, "project_score.json"), {
7330
- projectId: project.projectId,
7331
- name: project.name,
7332
- type: project.type,
7333
- path: project.path,
7334
- score: project.drift.score,
7335
- riskLevel: project.drift.riskLevel,
7336
- components: project.drift.components,
7337
- measured: project.drift.measured,
7338
- scannedAt: artifact.timestamp,
7339
- vibgrateVersion: VERSION
7340
- });
7803
+ if (!opts.noLocalArtifacts && !maxPrivacyMode) {
7804
+ for (const project of allProjects) {
7805
+ if (project.drift && project.path) {
7806
+ const projectDir = path22.resolve(rootDir, project.path);
7807
+ const projectVibgrateDir = path22.join(projectDir, ".vibgrate");
7808
+ await ensureDir(projectVibgrateDir);
7809
+ await writeJsonFile(path22.join(projectVibgrateDir, "project_score.json"), {
7810
+ projectId: project.projectId,
7811
+ name: project.name,
7812
+ type: project.type,
7813
+ path: project.path,
7814
+ score: project.drift.score,
7815
+ riskLevel: project.drift.riskLevel,
7816
+ components: project.drift.components,
7817
+ measured: project.drift.measured,
7818
+ scannedAt: artifact.timestamp,
7819
+ vibgrateVersion: VERSION,
7820
+ solutionId: project.solutionId,
7821
+ solutionName: project.solutionName
7822
+ });
7823
+ }
7341
7824
  }
7342
7825
  }
7343
7826
  if (opts.format === "json") {
7344
7827
  const jsonStr = JSON.stringify(artifact, null, 2);
7345
7828
  if (opts.out) {
7346
- await writeTextFile(path20.resolve(opts.out), jsonStr);
7829
+ await writeTextFile(path22.resolve(opts.out), jsonStr);
7347
7830
  console.log(chalk6.green("\u2714") + ` JSON written to ${opts.out}`);
7348
7831
  } else {
7349
7832
  console.log(jsonStr);
@@ -7352,7 +7835,7 @@ async function runScan(rootDir, opts) {
7352
7835
  const sarif = formatSarif(artifact);
7353
7836
  const sarifStr = JSON.stringify(sarif, null, 2);
7354
7837
  if (opts.out) {
7355
- await writeTextFile(path20.resolve(opts.out), sarifStr);
7838
+ await writeTextFile(path22.resolve(opts.out), sarifStr);
7356
7839
  console.log(chalk6.green("\u2714") + ` SARIF written to ${opts.out}`);
7357
7840
  } else {
7358
7841
  console.log(sarifStr);
@@ -7361,11 +7844,34 @@ async function runScan(rootDir, opts) {
7361
7844
  const text = formatText(artifact);
7362
7845
  console.log(text);
7363
7846
  if (opts.out) {
7364
- await writeTextFile(path20.resolve(opts.out), text);
7847
+ await writeTextFile(path22.resolve(opts.out), text);
7365
7848
  }
7366
7849
  }
7367
7850
  return artifact;
7368
7851
  }
7852
+ async function buildRepositoryInfo(rootDir, remoteUrl, ciSystems) {
7853
+ const packageJsonPath = path22.join(rootDir, "package.json");
7854
+ let name = path22.basename(rootDir);
7855
+ let version;
7856
+ if (await pathExists(packageJsonPath)) {
7857
+ try {
7858
+ const packageJson = await readJsonFile(packageJsonPath);
7859
+ if (typeof packageJson.name === "string" && packageJson.name.trim()) {
7860
+ name = packageJson.name.trim();
7861
+ }
7862
+ if (typeof packageJson.version === "string" && packageJson.version.trim()) {
7863
+ version = packageJson.version.trim();
7864
+ }
7865
+ } catch {
7866
+ }
7867
+ }
7868
+ return {
7869
+ name,
7870
+ ...version ? { version } : {},
7871
+ ...ciSystems && ciSystems.length > 0 ? { pipeline: ciSystems.join(",") } : {},
7872
+ ...remoteUrl ? { remoteUrl } : {}
7873
+ };
7874
+ }
7369
7875
  async function autoPush(artifact, rootDir, opts) {
7370
7876
  const dsn = opts.dsn || process.env.VIBGRATE_DSN;
7371
7877
  if (!dsn) {
@@ -7436,8 +7942,8 @@ function parseNonNegativeNumber(value, label) {
7436
7942
  }
7437
7943
  return parsed;
7438
7944
  }
7439
- 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("--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) => {
7440
- const rootDir = path20.resolve(targetPath);
7945
+ 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) => {
7946
+ const rootDir = path22.resolve(targetPath);
7441
7947
  if (!await pathExists(rootDir)) {
7442
7948
  console.error(chalk6.red(`Path does not exist: ${rootDir}`));
7443
7949
  process.exit(1);
@@ -7455,6 +7961,10 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
7455
7961
  strict: opts.strict,
7456
7962
  installTools: opts.installTools,
7457
7963
  uiPurpose: opts.uiPurpose,
7964
+ noLocalArtifacts: opts.noLocalArtifacts,
7965
+ maxPrivacy: opts.maxPrivacy,
7966
+ offline: opts.offline,
7967
+ packageManifest: opts.packageManifest,
7458
7968
  driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
7459
7969
  driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening")
7460
7970
  };
@@ -7495,7 +8005,7 @@ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (thr
7495
8005
  }
7496
8006
  }
7497
8007
  const hasDsn = !!(opts.dsn || process.env.VIBGRATE_DSN);
7498
- if (opts.push || hasDsn) {
8008
+ if (!scanOpts.offline && (opts.push || hasDsn)) {
7499
8009
  await autoPush(artifact, rootDir, scanOpts);
7500
8010
  }
7501
8011
  });