@vibgrate/cli 1.0.46 → 1.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -227,6 +227,10 @@ function computeProjectId(relativePath, projectName, workspaceId) {
227
227
  const input = workspaceId ? `${relativePath}:${projectName}:${workspaceId}` : `${relativePath}:${projectName}`;
228
228
  return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
229
229
  }
230
+ function computeSolutionId(relativePath, solutionName, workspaceId) {
231
+ const input = workspaceId ? `${relativePath}:${solutionName}:${workspaceId}` : `${relativePath}:${solutionName}`;
232
+ return crypto.createHash("sha256").update(input).digest("hex").slice(0, 16);
233
+ }
230
234
 
231
235
  // src/version.ts
232
236
  import { createRequire } from "module";
@@ -324,6 +328,18 @@ function formatText(artifact) {
324
328
  lines.push(chalk.dim(artifact.relationshipDiagram.mermaid));
325
329
  lines.push("");
326
330
  }
331
+ if (artifact.solutions && artifact.solutions.length > 0) {
332
+ lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
333
+ lines.push(chalk.bold.cyan("\u2551 Solution Drift Summary \u2551"));
334
+ lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
335
+ lines.push("");
336
+ for (const solution of artifact.solutions) {
337
+ const solScore = solution.drift?.score;
338
+ const color = typeof solScore === "number" ? solScore >= 70 ? chalk.green : solScore >= 40 ? chalk.yellow : chalk.red : chalk.dim;
339
+ lines.push(` \u2022 ${solution.name} (${solution.projectPaths.length} projects) \u2014 ${typeof solScore === "number" ? color(`${solScore}/100`) : chalk.dim("n/a")}`);
340
+ }
341
+ lines.push("");
342
+ }
327
343
  const scoreColor = artifact.drift.score >= 70 ? chalk.green : artifact.drift.score >= 40 ? chalk.yellow : chalk.red;
328
344
  lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
329
345
  lines.push(chalk.bold.cyan("\u2551 Drift Score Summary \u2551"));
@@ -1084,19 +1100,19 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
1084
1100
  });
1085
1101
 
1086
1102
  // src/commands/scan.ts
1087
- import * as path20 from "path";
1103
+ import * as path21 from "path";
1088
1104
  import { Command as Command3 } from "commander";
1089
1105
  import chalk6 from "chalk";
1090
1106
 
1091
1107
  // src/scanners/node-scanner.ts
1092
- import * as path3 from "path";
1108
+ import * as path4 from "path";
1093
1109
  import * as semver2 from "semver";
1094
1110
 
1095
1111
  // src/utils/timeout.ts
1096
1112
  async function withTimeout(promise, ms) {
1097
1113
  let timer;
1098
- const timeout = new Promise((resolve9) => {
1099
- timer = setTimeout(() => resolve9({ ok: false }), ms);
1114
+ const timeout = new Promise((resolve10) => {
1115
+ timer = setTimeout(() => resolve10({ ok: false }), ms);
1100
1116
  });
1101
1117
  try {
1102
1118
  const result = await Promise.race([
@@ -1110,8 +1126,78 @@ async function withTimeout(promise, ms) {
1110
1126
  }
1111
1127
 
1112
1128
  // src/scanners/npm-cache.ts
1113
- import { spawn } from "child_process";
1129
+ import { spawn as spawn2 } from "child_process";
1114
1130
  import * as semver from "semver";
1131
+
1132
+ // src/package-version-manifest.ts
1133
+ import { mkdtemp, readFile, rm } from "fs/promises";
1134
+ import * as path3 from "path";
1135
+ import * as os from "os";
1136
+ import { spawn } from "child_process";
1137
+ function runCommand(cmd, args) {
1138
+ return new Promise((resolve10, reject) => {
1139
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
1140
+ let out = "";
1141
+ let err = "";
1142
+ child.stdout.on("data", (d) => out += String(d));
1143
+ child.stderr.on("data", (d) => err += String(d));
1144
+ child.on("error", reject);
1145
+ child.on("close", (code) => {
1146
+ if (code !== 0) {
1147
+ reject(new Error(`${cmd} ${args.join(" ")} failed (code=${code}): ${err.trim()}`));
1148
+ return;
1149
+ }
1150
+ resolve10(out);
1151
+ });
1152
+ });
1153
+ }
1154
+ async function parseManifestText(text, source) {
1155
+ try {
1156
+ return JSON.parse(text);
1157
+ } catch {
1158
+ throw new Error(`Invalid JSON in package version manifest: ${source}`);
1159
+ }
1160
+ }
1161
+ async function loadManifestFromZip(zipPath) {
1162
+ const tmpDir = await mkdtemp(path3.join(os.tmpdir(), "vibgrate-manifest-"));
1163
+ try {
1164
+ await runCommand("unzip", ["-qq", zipPath, "-d", tmpDir]);
1165
+ const candidates = [
1166
+ path3.join(tmpDir, "package-versions.json"),
1167
+ path3.join(tmpDir, "manifest.json"),
1168
+ path3.join(tmpDir, "index.json")
1169
+ ];
1170
+ for (const candidate of candidates) {
1171
+ try {
1172
+ const text = await readFile(candidate, "utf8");
1173
+ return await parseManifestText(text, candidate);
1174
+ } catch {
1175
+ }
1176
+ }
1177
+ throw new Error("Zip must contain package-versions.json, manifest.json, or index.json");
1178
+ } finally {
1179
+ await rm(tmpDir, { recursive: true, force: true });
1180
+ }
1181
+ }
1182
+ async function loadPackageVersionManifest(filePath) {
1183
+ const resolved = path3.resolve(filePath);
1184
+ if (resolved.toLowerCase().endsWith(".zip")) {
1185
+ return loadManifestFromZip(resolved);
1186
+ }
1187
+ const text = await readFile(resolved, "utf8");
1188
+ return parseManifestText(text, resolved);
1189
+ }
1190
+ function getManifestEntry(manifest, ecosystem, packageName) {
1191
+ if (!manifest) return void 0;
1192
+ const table = manifest[ecosystem];
1193
+ if (!table) return void 0;
1194
+ if (ecosystem === "nuget") {
1195
+ return table[packageName.toLowerCase()] ?? table[packageName];
1196
+ }
1197
+ return table[packageName];
1198
+ }
1199
+
1200
+ // src/scanners/npm-cache.ts
1115
1201
  function stableOnly(versions) {
1116
1202
  return versions.filter((v) => semver.valid(v) && semver.prerelease(v) === null);
1117
1203
  }
@@ -1121,8 +1207,8 @@ function maxStable(versions) {
1121
1207
  return stable.sort(semver.rcompare)[0] ?? null;
1122
1208
  }
1123
1209
  async function npmViewJson(args, cwd) {
1124
- return new Promise((resolve9, reject) => {
1125
- const child = spawn("npm", ["view", ...args, "--json"], {
1210
+ return new Promise((resolve10, reject) => {
1211
+ const child = spawn2("npm", ["view", ...args, "--json"], {
1126
1212
  cwd,
1127
1213
  shell: true,
1128
1214
  stdio: ["ignore", "pipe", "pipe"]
@@ -1139,27 +1225,42 @@ async function npmViewJson(args, cwd) {
1139
1225
  }
1140
1226
  const trimmed = out.trim();
1141
1227
  if (!trimmed) {
1142
- resolve9(null);
1228
+ resolve10(null);
1143
1229
  return;
1144
1230
  }
1145
1231
  try {
1146
- resolve9(JSON.parse(trimmed));
1232
+ resolve10(JSON.parse(trimmed));
1147
1233
  } catch {
1148
- resolve9(trimmed.replace(/^"|"$/g, ""));
1234
+ resolve10(trimmed.replace(/^"|"$/g, ""));
1149
1235
  }
1150
1236
  });
1151
1237
  });
1152
1238
  }
1153
1239
  var NpmCache = class {
1154
- constructor(cwd, sem) {
1240
+ constructor(cwd, sem, manifest, offline = false) {
1155
1241
  this.cwd = cwd;
1156
1242
  this.sem = sem;
1243
+ this.manifest = manifest;
1244
+ this.offline = offline;
1157
1245
  }
1158
1246
  meta = /* @__PURE__ */ new Map();
1159
1247
  get(pkg2) {
1160
1248
  const existing = this.meta.get(pkg2);
1161
1249
  if (existing) return existing;
1162
1250
  const p = this.sem.run(async () => {
1251
+ const manifestEntry = getManifestEntry(this.manifest, "npm", pkg2);
1252
+ if (manifestEntry) {
1253
+ const stable2 = stableOnly(manifestEntry.versions ?? []);
1254
+ const latestStableOverall2 = maxStable(stable2);
1255
+ return {
1256
+ latest: manifestEntry.latest ?? latestStableOverall2,
1257
+ stableVersions: stable2,
1258
+ latestStableOverall: latestStableOverall2
1259
+ };
1260
+ }
1261
+ if (this.offline) {
1262
+ return { latest: null, stableVersions: [], latestStableOverall: null };
1263
+ }
1163
1264
  let latest = null;
1164
1265
  let versions = [];
1165
1266
  try {
@@ -1313,7 +1414,7 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
1313
1414
  results.push(result.value);
1314
1415
  packageNameToPath.set(result.value.name, result.value.path);
1315
1416
  } else {
1316
- const relPath = path3.relative(rootDir, path3.dirname(pjPath));
1417
+ const relPath = path4.relative(rootDir, path4.dirname(pjPath));
1317
1418
  if (cache) {
1318
1419
  cache.addStuckPath(relPath || ".");
1319
1420
  }
@@ -1344,8 +1445,8 @@ async function scanNodeProjects(rootDir, npmCache, cache) {
1344
1445
  }
1345
1446
  async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1346
1447
  const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
1347
- const absProjectPath = path3.dirname(packageJsonPath);
1348
- const projectPath = path3.relative(rootDir, absProjectPath) || ".";
1448
+ const absProjectPath = path4.dirname(packageJsonPath);
1449
+ const projectPath = path4.relative(rootDir, absProjectPath) || ".";
1349
1450
  const nodeEngine = pj.engines?.node ?? void 0;
1350
1451
  let runtimeLatest;
1351
1452
  let runtimeMajorsBehind;
@@ -1432,7 +1533,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1432
1533
  return {
1433
1534
  type: "node",
1434
1535
  path: projectPath,
1435
- name: pj.name ?? path3.basename(absProjectPath),
1536
+ name: pj.name ?? path4.basename(absProjectPath),
1436
1537
  runtime: nodeEngine,
1437
1538
  runtimeLatest,
1438
1539
  runtimeMajorsBehind,
@@ -1444,7 +1545,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
1444
1545
  }
1445
1546
 
1446
1547
  // src/scanners/dotnet-scanner.ts
1447
- import * as path4 from "path";
1548
+ import * as path5 from "path";
1448
1549
  import * as semver3 from "semver";
1449
1550
  import { XMLParser } from "fast-xml-parser";
1450
1551
  var parser = new XMLParser({
@@ -1646,7 +1747,7 @@ function parseCsproj(xml, filePath) {
1646
1747
  const parsed = parser.parse(xml);
1647
1748
  const project = parsed?.Project;
1648
1749
  if (!project) {
1649
- return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName: path4.basename(filePath, ".csproj") };
1750
+ return { targetFrameworks: [], packageReferences: [], projectReferences: [], projectName: path5.basename(filePath, ".csproj") };
1650
1751
  }
1651
1752
  const propertyGroups = Array.isArray(project.PropertyGroup) ? project.PropertyGroup : project.PropertyGroup ? [project.PropertyGroup] : [];
1652
1753
  const targetFrameworks = [];
@@ -1683,7 +1784,7 @@ function parseCsproj(xml, filePath) {
1683
1784
  targetFrameworks: [...new Set(targetFrameworks)],
1684
1785
  packageReferences,
1685
1786
  projectReferences,
1686
- projectName: path4.basename(filePath, ".csproj")
1787
+ projectName: path5.basename(filePath, ".csproj")
1687
1788
  };
1688
1789
  }
1689
1790
  async function scanDotnetProjects(rootDir, nugetCache, cache) {
@@ -1693,12 +1794,12 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
1693
1794
  for (const slnPath of slnFiles) {
1694
1795
  try {
1695
1796
  const slnContent = cache ? await cache.readTextFile(slnPath) : await readTextFile(slnPath);
1696
- const slnDir = path4.dirname(slnPath);
1797
+ const slnDir = path5.dirname(slnPath);
1697
1798
  const projectRegex = /Project\("[^"]*"\)\s*=\s*"[^"]*",\s*"([^"]+\.csproj)"/g;
1698
1799
  let match;
1699
1800
  while ((match = projectRegex.exec(slnContent)) !== null) {
1700
1801
  if (match[1]) {
1701
- const csprojPath = path4.resolve(slnDir, match[1].replace(/\\/g, "/"));
1802
+ const csprojPath = path5.resolve(slnDir, match[1].replace(/\\/g, "/"));
1702
1803
  slnCsprojPaths.add(csprojPath);
1703
1804
  }
1704
1805
  }
@@ -1715,7 +1816,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
1715
1816
  if (result.ok) {
1716
1817
  results.push(result.value);
1717
1818
  } else {
1718
- const relPath = path4.relative(rootDir, path4.dirname(csprojPath));
1819
+ const relPath = path5.relative(rootDir, path5.dirname(csprojPath));
1719
1820
  if (cache) {
1720
1821
  cache.addStuckPath(relPath || ".");
1721
1822
  }
@@ -1731,7 +1832,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache) {
1731
1832
  async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1732
1833
  const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
1733
1834
  const data = parseCsproj(xml, csprojPath);
1734
- const csprojDir = path4.dirname(csprojPath);
1835
+ const csprojDir = path5.dirname(csprojPath);
1735
1836
  const primaryTfm = data.targetFrameworks[0];
1736
1837
  let runtimeMajorsBehind;
1737
1838
  let targetFramework = primaryTfm;
@@ -1809,9 +1910,9 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1809
1910
  }
1810
1911
  }
1811
1912
  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");
1913
+ const absRefPath = path5.resolve(csprojDir, refPath);
1914
+ const relRefPath = path5.relative(rootDir, path5.dirname(absRefPath));
1915
+ const refName = path5.basename(absRefPath, ".csproj");
1815
1916
  return {
1816
1917
  path: relRefPath || ".",
1817
1918
  name: refName,
@@ -1832,7 +1933,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1832
1933
  const buckets = bucketsMut;
1833
1934
  return {
1834
1935
  type: "dotnet",
1835
- path: path4.relative(rootDir, csprojDir) || ".",
1936
+ path: path5.relative(rootDir, csprojDir) || ".",
1836
1937
  name: data.projectName,
1837
1938
  targetFramework,
1838
1939
  runtime: primaryTfm,
@@ -1847,7 +1948,7 @@ async function scanOneCsproj(csprojPath, rootDir, nugetCache, cache) {
1847
1948
  }
1848
1949
 
1849
1950
  // src/scanners/python-scanner.ts
1850
- import * as path5 from "path";
1951
+ import * as path6 from "path";
1851
1952
  import * as semver4 from "semver";
1852
1953
  var KNOWN_PYTHON_FRAMEWORKS = {
1853
1954
  // ── Web Frameworks ──
@@ -2088,7 +2189,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
2088
2189
  const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name)) : await findPythonManifests(rootDir);
2089
2190
  const projectDirs = /* @__PURE__ */ new Map();
2090
2191
  for (const f of manifestFiles) {
2091
- const dir = path5.dirname(f);
2192
+ const dir = path6.dirname(f);
2092
2193
  if (!projectDirs.has(dir)) projectDirs.set(dir, []);
2093
2194
  projectDirs.get(dir).push(f);
2094
2195
  }
@@ -2101,7 +2202,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache) {
2101
2202
  if (result.ok) {
2102
2203
  results.push(result.value);
2103
2204
  } else {
2104
- const relPath = path5.relative(rootDir, dir);
2205
+ const relPath = path6.relative(rootDir, dir);
2105
2206
  if (cache) cache.addStuckPath(relPath || ".");
2106
2207
  console.error(`Timeout scanning Python project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
2107
2208
  }
@@ -2117,12 +2218,12 @@ async function findPythonManifests(rootDir) {
2117
2218
  return findFiles2(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name));
2118
2219
  }
2119
2220
  async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cache) {
2120
- const relDir = path5.relative(rootDir, dir) || ".";
2121
- let projectName = path5.basename(dir === rootDir ? rootDir : dir);
2221
+ const relDir = path6.relative(rootDir, dir) || ".";
2222
+ let projectName = path6.basename(dir === rootDir ? rootDir : dir);
2122
2223
  let pythonVersion;
2123
2224
  const allDeps = /* @__PURE__ */ new Map();
2124
2225
  for (const f of manifestFiles) {
2125
- const fileName = path5.basename(f);
2226
+ const fileName = path6.basename(f);
2126
2227
  const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
2127
2228
  if (fileName === "pyproject.toml") {
2128
2229
  const parsed = parsePyprojectToml(content);
@@ -2239,7 +2340,7 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
2239
2340
  }
2240
2341
 
2241
2342
  // src/scanners/java-scanner.ts
2242
- import * as path6 from "path";
2343
+ import * as path7 from "path";
2243
2344
  import * as semver5 from "semver";
2244
2345
  import { XMLParser as XMLParser2 } from "fast-xml-parser";
2245
2346
  var parser2 = new XMLParser2({
@@ -2345,7 +2446,7 @@ function parsePom(xml, filePath) {
2345
2446
  const project = parsed?.project;
2346
2447
  if (!project) {
2347
2448
  return {
2348
- artifactId: path6.basename(path6.dirname(filePath)),
2449
+ artifactId: path7.basename(path7.dirname(filePath)),
2349
2450
  dependencies: [],
2350
2451
  modules: [],
2351
2452
  properties: {}
@@ -2405,7 +2506,7 @@ function parsePom(xml, filePath) {
2405
2506
  }
2406
2507
  return {
2407
2508
  groupId: project.groupId ? String(project.groupId) : parent?.groupId,
2408
- artifactId: String(project.artifactId ?? path6.basename(path6.dirname(filePath))),
2509
+ artifactId: String(project.artifactId ?? path7.basename(path7.dirname(filePath))),
2409
2510
  version: project.version ? String(project.version) : parent?.version,
2410
2511
  packaging: project.packaging ? String(project.packaging) : void 0,
2411
2512
  javaVersion,
@@ -2420,7 +2521,7 @@ function resolveProperty(value, properties) {
2420
2521
  }
2421
2522
  function parseGradleBuild(content, filePath) {
2422
2523
  const deps = [];
2423
- const projectName = path6.basename(path6.dirname(filePath));
2524
+ const projectName = path7.basename(path7.dirname(filePath));
2424
2525
  let javaVersion;
2425
2526
  const compatMatch = content.match(/(?:sourceCompatibility|targetCompatibility|javaVersion)\s*[=:]\s*['"]?(?:JavaVersion\.VERSION_)?(\d+)['"]?/);
2426
2527
  if (compatMatch) javaVersion = compatMatch[1];
@@ -2483,7 +2584,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
2483
2584
  const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => JAVA_MANIFEST_FILES.has(name)) : await findJavaManifests(rootDir);
2484
2585
  const projectDirs = /* @__PURE__ */ new Map();
2485
2586
  for (const f of manifestFiles) {
2486
- const dir = path6.dirname(f);
2587
+ const dir = path7.dirname(f);
2487
2588
  if (!projectDirs.has(dir)) projectDirs.set(dir, []);
2488
2589
  projectDirs.get(dir).push(f);
2489
2590
  }
@@ -2496,7 +2597,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache) {
2496
2597
  if (result.ok) {
2497
2598
  results.push(result.value);
2498
2599
  } else {
2499
- const relPath = path6.relative(rootDir, dir);
2600
+ const relPath = path7.relative(rootDir, dir);
2500
2601
  if (cache) cache.addStuckPath(relPath || ".");
2501
2602
  console.error(`Timeout scanning Java project ${dir} (>${STUCK_TIMEOUT_MS / 1e3}s) \u2014 skipped`);
2502
2603
  }
@@ -2516,13 +2617,13 @@ async function findJavaManifests(rootDir) {
2516
2617
  return findFiles2(rootDir, (name) => JAVA_MANIFEST_FILES.has(name));
2517
2618
  }
2518
2619
  async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache) {
2519
- const relDir = path6.relative(rootDir, dir) || ".";
2520
- let projectName = path6.basename(dir === rootDir ? rootDir : dir);
2620
+ const relDir = path7.relative(rootDir, dir) || ".";
2621
+ let projectName = path7.basename(dir === rootDir ? rootDir : dir);
2521
2622
  let javaVersion;
2522
2623
  const allDeps = /* @__PURE__ */ new Map();
2523
2624
  const projectReferences = [];
2524
2625
  for (const f of manifestFiles) {
2525
- const fileName = path6.basename(f);
2626
+ const fileName = path7.basename(f);
2526
2627
  const content = cache ? await cache.readTextFile(f) : await readTextFile(f);
2527
2628
  if (fileName === "pom.xml") {
2528
2629
  const pom = parsePom(content, f);
@@ -2536,7 +2637,7 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
2536
2637
  }
2537
2638
  for (const mod of pom.modules) {
2538
2639
  projectReferences.push({
2539
- path: path6.join(relDir, mod),
2640
+ path: path7.join(relDir, mod),
2540
2641
  name: mod,
2541
2642
  refType: "project"
2542
2643
  });
@@ -2642,8 +2743,10 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
2642
2743
  // src/scanners/nuget-cache.ts
2643
2744
  import * as semver6 from "semver";
2644
2745
  var NuGetCache = class {
2645
- constructor(sem) {
2746
+ constructor(sem, manifest, offline = false) {
2646
2747
  this.sem = sem;
2748
+ this.manifest = manifest;
2749
+ this.offline = offline;
2647
2750
  }
2648
2751
  meta = /* @__PURE__ */ new Map();
2649
2752
  baseUrl = "https://api.nuget.org/v3-flatcontainer";
@@ -2651,6 +2754,23 @@ var NuGetCache = class {
2651
2754
  const existing = this.meta.get(pkg2);
2652
2755
  if (existing) return existing;
2653
2756
  const p = this.sem.run(async () => {
2757
+ const manifestEntry = getManifestEntry(this.manifest, "nuget", pkg2);
2758
+ if (manifestEntry) {
2759
+ const stableVersions = (manifestEntry.versions ?? []).filter((v) => {
2760
+ const parsed = semver6.valid(v);
2761
+ return parsed && semver6.prerelease(v) === null;
2762
+ });
2763
+ const sorted = [...stableVersions].sort(semver6.rcompare);
2764
+ const latestStableOverall = sorted[0] ?? null;
2765
+ return {
2766
+ latest: manifestEntry.latest ?? latestStableOverall,
2767
+ stableVersions,
2768
+ latestStableOverall
2769
+ };
2770
+ }
2771
+ if (this.offline) {
2772
+ return { latest: null, stableVersions: [], latestStableOverall: null };
2773
+ }
2654
2774
  try {
2655
2775
  const url = `${this.baseUrl}/${pkg2.toLowerCase()}/index.json`;
2656
2776
  const response = await fetch(url, {
@@ -2693,14 +2813,34 @@ function pep440ToSemver2(ver) {
2693
2813
  return semver7.valid(v);
2694
2814
  }
2695
2815
  var PyPICache = class {
2696
- constructor(sem) {
2816
+ constructor(sem, manifest, offline = false) {
2697
2817
  this.sem = sem;
2818
+ this.manifest = manifest;
2819
+ this.offline = offline;
2698
2820
  }
2699
2821
  meta = /* @__PURE__ */ new Map();
2700
2822
  get(pkg2) {
2701
2823
  const existing = this.meta.get(pkg2);
2702
2824
  if (existing) return existing;
2703
2825
  const p = this.sem.run(async () => {
2826
+ const manifestEntry = getManifestEntry(this.manifest, "pypi", pkg2);
2827
+ if (manifestEntry) {
2828
+ const stableVersions = [];
2829
+ for (const ver of manifestEntry.versions ?? []) {
2830
+ const sv = pep440ToSemver2(ver);
2831
+ if (sv) stableVersions.push(sv);
2832
+ }
2833
+ const sorted = [...stableVersions].sort(semver7.rcompare);
2834
+ const latestStableOverall = sorted[0] ?? null;
2835
+ return {
2836
+ latest: manifestEntry.latest ? pep440ToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
2837
+ stableVersions,
2838
+ latestStableOverall
2839
+ };
2840
+ }
2841
+ if (this.offline) {
2842
+ return { latest: null, stableVersions: [], latestStableOverall: null };
2843
+ }
2704
2844
  try {
2705
2845
  const url = `https://pypi.org/pypi/${encodeURIComponent(pkg2)}/json`;
2706
2846
  const response = await fetch(url, {
@@ -2747,8 +2887,10 @@ function mavenToSemver2(ver) {
2747
2887
  return semver8.valid(v);
2748
2888
  }
2749
2889
  var MavenCache = class {
2750
- constructor(sem) {
2890
+ constructor(sem, manifest, offline = false) {
2751
2891
  this.sem = sem;
2892
+ this.manifest = manifest;
2893
+ this.offline = offline;
2752
2894
  }
2753
2895
  meta = /* @__PURE__ */ new Map();
2754
2896
  /**
@@ -2761,6 +2903,25 @@ var MavenCache = class {
2761
2903
  const existing = this.meta.get(key);
2762
2904
  if (existing) return existing;
2763
2905
  const p = this.sem.run(async () => {
2906
+ const key2 = `${groupId}:${artifactId}`;
2907
+ const manifestEntry = getManifestEntry(this.manifest, "maven", key2);
2908
+ if (manifestEntry) {
2909
+ const stableVersions = [];
2910
+ for (const ver of manifestEntry.versions ?? []) {
2911
+ const sv = mavenToSemver2(ver);
2912
+ if (sv) stableVersions.push(sv);
2913
+ }
2914
+ const sorted = [...stableVersions].sort(semver8.rcompare);
2915
+ const latestStableOverall = sorted[0] ?? null;
2916
+ return {
2917
+ latest: manifestEntry.latest ? mavenToSemver2(manifestEntry.latest) ?? latestStableOverall : latestStableOverall,
2918
+ stableVersions,
2919
+ latestStableOverall
2920
+ };
2921
+ }
2922
+ if (this.offline) {
2923
+ return { latest: null, stableVersions: [], latestStableOverall: null };
2924
+ }
2764
2925
  try {
2765
2926
  const url = `https://search.maven.org/solrsearch/select?q=g:%22${encodeURIComponent(groupId)}%22+AND+a:%22${encodeURIComponent(artifactId)}%22&core=gav&rows=100&wt=json`;
2766
2927
  const response = await fetch(url, {
@@ -2795,7 +2956,7 @@ var MavenCache = class {
2795
2956
  };
2796
2957
 
2797
2958
  // src/config.ts
2798
- import * as path7 from "path";
2959
+ import * as path8 from "path";
2799
2960
  import * as fs from "fs/promises";
2800
2961
  var CONFIG_FILES = [
2801
2962
  "vibgrate.config.ts",
@@ -2821,7 +2982,7 @@ var DEFAULT_CONFIG = {
2821
2982
  async function loadConfig(rootDir) {
2822
2983
  let config = DEFAULT_CONFIG;
2823
2984
  for (const file of CONFIG_FILES) {
2824
- const configPath = path7.join(rootDir, file);
2985
+ const configPath = path8.join(rootDir, file);
2825
2986
  if (await pathExists(configPath)) {
2826
2987
  if (file.endsWith(".json")) {
2827
2988
  const txt = await readTextFile(configPath);
@@ -2836,7 +2997,7 @@ async function loadConfig(rootDir) {
2836
2997
  }
2837
2998
  }
2838
2999
  }
2839
- const sidecarPath = path7.join(rootDir, ".vibgrate", "auto-excludes.json");
3000
+ const sidecarPath = path8.join(rootDir, ".vibgrate", "auto-excludes.json");
2840
3001
  if (await pathExists(sidecarPath)) {
2841
3002
  try {
2842
3003
  const txt = await readTextFile(sidecarPath);
@@ -2851,7 +3012,7 @@ async function loadConfig(rootDir) {
2851
3012
  return config;
2852
3013
  }
2853
3014
  async function writeDefaultConfig(rootDir) {
2854
- const configPath = path7.join(rootDir, "vibgrate.config.ts");
3015
+ const configPath = path8.join(rootDir, "vibgrate.config.ts");
2855
3016
  const content = `import type { VibgrateConfig } from '@vibgrate/cli';
2856
3017
 
2857
3018
  const config: VibgrateConfig = {
@@ -2877,7 +3038,7 @@ export default config;
2877
3038
  }
2878
3039
  async function appendExcludePatterns(rootDir, newPatterns) {
2879
3040
  if (newPatterns.length === 0) return false;
2880
- const jsonPath = path7.join(rootDir, "vibgrate.config.json");
3041
+ const jsonPath = path8.join(rootDir, "vibgrate.config.json");
2881
3042
  if (await pathExists(jsonPath)) {
2882
3043
  try {
2883
3044
  const txt = await readTextFile(jsonPath);
@@ -2890,8 +3051,8 @@ async function appendExcludePatterns(rootDir, newPatterns) {
2890
3051
  } catch {
2891
3052
  }
2892
3053
  }
2893
- const vibgrateDir = path7.join(rootDir, ".vibgrate");
2894
- const sidecarPath = path7.join(vibgrateDir, "auto-excludes.json");
3054
+ const vibgrateDir = path8.join(rootDir, ".vibgrate");
3055
+ const sidecarPath = path8.join(vibgrateDir, "auto-excludes.json");
2895
3056
  let existing = [];
2896
3057
  if (await pathExists(sidecarPath)) {
2897
3058
  try {
@@ -2912,7 +3073,7 @@ async function appendExcludePatterns(rootDir, newPatterns) {
2912
3073
  }
2913
3074
 
2914
3075
  // src/utils/vcs.ts
2915
- import * as path8 from "path";
3076
+ import * as path9 from "path";
2916
3077
  import * as fs2 from "fs/promises";
2917
3078
  async function detectVcs(rootDir) {
2918
3079
  try {
@@ -2926,7 +3087,7 @@ async function detectGit(rootDir) {
2926
3087
  if (!gitDir) {
2927
3088
  return { type: "unknown" };
2928
3089
  }
2929
- const headPath = path8.join(gitDir, "HEAD");
3090
+ const headPath = path9.join(gitDir, "HEAD");
2930
3091
  let headContent;
2931
3092
  try {
2932
3093
  headContent = (await fs2.readFile(headPath, "utf8")).trim();
@@ -2942,18 +3103,20 @@ async function detectGit(rootDir) {
2942
3103
  } else if (/^[0-9a-f]{40}$/i.test(headContent)) {
2943
3104
  sha = headContent;
2944
3105
  }
3106
+ const remoteUrl = await readGitRemoteUrl(gitDir);
2945
3107
  return {
2946
3108
  type: "git",
2947
3109
  sha: sha ?? void 0,
2948
3110
  shortSha: sha ? sha.slice(0, 7) : void 0,
2949
- branch: branch ?? void 0
3111
+ branch: branch ?? void 0,
3112
+ remoteUrl
2950
3113
  };
2951
3114
  }
2952
3115
  async function findGitDir(startDir) {
2953
- let dir = path8.resolve(startDir);
2954
- const root = path8.parse(dir).root;
3116
+ let dir = path9.resolve(startDir);
3117
+ const root = path9.parse(dir).root;
2955
3118
  while (dir !== root) {
2956
- const gitPath = path8.join(dir, ".git");
3119
+ const gitPath = path9.join(dir, ".git");
2957
3120
  try {
2958
3121
  const stat3 = await fs2.stat(gitPath);
2959
3122
  if (stat3.isDirectory()) {
@@ -2962,18 +3125,18 @@ async function findGitDir(startDir) {
2962
3125
  if (stat3.isFile()) {
2963
3126
  const content = (await fs2.readFile(gitPath, "utf8")).trim();
2964
3127
  if (content.startsWith("gitdir: ")) {
2965
- const resolved = path8.resolve(dir, content.slice(8));
3128
+ const resolved = path9.resolve(dir, content.slice(8));
2966
3129
  return resolved;
2967
3130
  }
2968
3131
  }
2969
3132
  } catch {
2970
3133
  }
2971
- dir = path8.dirname(dir);
3134
+ dir = path9.dirname(dir);
2972
3135
  }
2973
3136
  return null;
2974
3137
  }
2975
3138
  async function resolveRef(gitDir, refPath) {
2976
- const loosePath = path8.join(gitDir, refPath);
3139
+ const loosePath = path9.join(gitDir, refPath);
2977
3140
  try {
2978
3141
  const sha = (await fs2.readFile(loosePath, "utf8")).trim();
2979
3142
  if (/^[0-9a-f]{40}$/i.test(sha)) {
@@ -2981,7 +3144,7 @@ async function resolveRef(gitDir, refPath) {
2981
3144
  }
2982
3145
  } catch {
2983
3146
  }
2984
- const packedPath = path8.join(gitDir, "packed-refs");
3147
+ const packedPath = path9.join(gitDir, "packed-refs");
2985
3148
  try {
2986
3149
  const packed = await fs2.readFile(packedPath, "utf8");
2987
3150
  for (const line of packed.split("\n")) {
@@ -2995,6 +3158,39 @@ async function resolveRef(gitDir, refPath) {
2995
3158
  }
2996
3159
  return void 0;
2997
3160
  }
3161
+ async function readGitRemoteUrl(gitDir) {
3162
+ const configPath = await resolveGitConfigPath(gitDir);
3163
+ if (!configPath) return void 0;
3164
+ try {
3165
+ const config = await fs2.readFile(configPath, "utf8");
3166
+ const originBlock = config.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/);
3167
+ if (!originBlock) return void 0;
3168
+ const urlMatch = originBlock[1]?.match(/\n\s*url\s*=\s*(.+)\s*/);
3169
+ return urlMatch?.[1]?.trim();
3170
+ } catch {
3171
+ return void 0;
3172
+ }
3173
+ }
3174
+ async function resolveGitConfigPath(gitDir) {
3175
+ const directConfig = path9.join(gitDir, "config");
3176
+ try {
3177
+ const stat3 = await fs2.stat(directConfig);
3178
+ if (stat3.isFile()) return directConfig;
3179
+ } catch {
3180
+ }
3181
+ const commonDirFile = path9.join(gitDir, "commondir");
3182
+ try {
3183
+ const commonDir = (await fs2.readFile(commonDirFile, "utf8")).trim();
3184
+ if (!commonDir) return void 0;
3185
+ const resolvedCommonDir = path9.resolve(gitDir, commonDir);
3186
+ const commonConfig = path9.join(resolvedCommonDir, "config");
3187
+ const stat3 = await fs2.stat(commonConfig);
3188
+ if (stat3.isFile()) return commonConfig;
3189
+ } catch {
3190
+ return void 0;
3191
+ }
3192
+ return void 0;
3193
+ }
2998
3194
 
2999
3195
  // src/ui/progress.ts
3000
3196
  import chalk4 from "chalk";
@@ -3378,11 +3574,11 @@ var ScanProgress = class {
3378
3574
 
3379
3575
  // src/ui/scan-history.ts
3380
3576
  import * as fs3 from "fs/promises";
3381
- import * as path9 from "path";
3577
+ import * as path10 from "path";
3382
3578
  var HISTORY_FILENAME = "scan_history.json";
3383
3579
  var MAX_RECORDS = 10;
3384
3580
  async function loadScanHistory(rootDir) {
3385
- const filePath = path9.join(rootDir, ".vibgrate", HISTORY_FILENAME);
3581
+ const filePath = path10.join(rootDir, ".vibgrate", HISTORY_FILENAME);
3386
3582
  try {
3387
3583
  const txt = await fs3.readFile(filePath, "utf8");
3388
3584
  const data = JSON.parse(txt);
@@ -3395,8 +3591,8 @@ async function loadScanHistory(rootDir) {
3395
3591
  }
3396
3592
  }
3397
3593
  async function saveScanHistory(rootDir, record) {
3398
- const dir = path9.join(rootDir, ".vibgrate");
3399
- const filePath = path9.join(dir, HISTORY_FILENAME);
3594
+ const dir = path10.join(rootDir, ".vibgrate");
3595
+ const filePath = path10.join(dir, HISTORY_FILENAME);
3400
3596
  let history;
3401
3597
  const existing = await loadScanHistory(rootDir);
3402
3598
  if (existing) {
@@ -3460,7 +3656,7 @@ function estimateStepDurations(history, currentFileCount) {
3460
3656
  }
3461
3657
 
3462
3658
  // src/scanners/platform-matrix.ts
3463
- import * as path10 from "path";
3659
+ import * as path11 from "path";
3464
3660
  var NATIVE_MODULE_PACKAGES = /* @__PURE__ */ new Set([
3465
3661
  // Image / media processing
3466
3662
  "sharp",
@@ -3740,7 +3936,7 @@ async function scanPlatformMatrix(rootDir, cache) {
3740
3936
  }
3741
3937
  result.dockerBaseImages = [...baseImages].sort();
3742
3938
  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));
3939
+ const exists = cache ? await cache.pathExists(path11.join(rootDir, file)) : await pathExists(path11.join(rootDir, file));
3744
3940
  if (exists) {
3745
3941
  result.nodeVersionFiles.push(file);
3746
3942
  }
@@ -3817,7 +4013,7 @@ function scanDependencyRisk(projects) {
3817
4013
  }
3818
4014
 
3819
4015
  // src/scanners/dependency-graph.ts
3820
- import * as path11 from "path";
4016
+ import * as path12 from "path";
3821
4017
  function parsePnpmLock(content) {
3822
4018
  const entries = [];
3823
4019
  const regex = /^\s+\/?(@?[^@\s][^@\s]*?)@(\d+\.\d+\.\d+[^:\s]*)\s*:/gm;
@@ -3876,9 +4072,9 @@ async function scanDependencyGraph(rootDir, cache) {
3876
4072
  phantomDependencies: []
3877
4073
  };
3878
4074
  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");
4075
+ const pnpmLock = path12.join(rootDir, "pnpm-lock.yaml");
4076
+ const npmLock = path12.join(rootDir, "package-lock.json");
4077
+ const yarnLock = path12.join(rootDir, "yarn.lock");
3882
4078
  const _pathExists = cache ? (p) => cache.pathExists(p) : pathExists;
3883
4079
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
3884
4080
  if (await _pathExists(pnpmLock)) {
@@ -3925,7 +4121,7 @@ async function scanDependencyGraph(rootDir, cache) {
3925
4121
  for (const pjPath of pkgFiles) {
3926
4122
  try {
3927
4123
  const pj = cache ? await cache.readJsonFile(pjPath) : await readJsonFile(pjPath);
3928
- const relPath = path11.relative(rootDir, pjPath);
4124
+ const relPath = path12.relative(rootDir, pjPath);
3929
4125
  for (const section of ["dependencies", "devDependencies"]) {
3930
4126
  const deps = pj[section];
3931
4127
  if (!deps) continue;
@@ -4271,7 +4467,7 @@ function scanToolingInventory(projects) {
4271
4467
  }
4272
4468
 
4273
4469
  // src/scanners/build-deploy.ts
4274
- import * as path12 from "path";
4470
+ import * as path13 from "path";
4275
4471
  var CI_FILES = {
4276
4472
  ".github/workflows": "github-actions",
4277
4473
  ".gitlab-ci.yml": "gitlab-ci",
@@ -4324,17 +4520,17 @@ async function scanBuildDeploy(rootDir, cache) {
4324
4520
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
4325
4521
  const ciSystems = /* @__PURE__ */ new Set();
4326
4522
  for (const [file, system] of Object.entries(CI_FILES)) {
4327
- const fullPath = path12.join(rootDir, file);
4523
+ const fullPath = path13.join(rootDir, file);
4328
4524
  if (await _pathExists(fullPath)) {
4329
4525
  ciSystems.add(system);
4330
4526
  }
4331
4527
  }
4332
- const ghWorkflowDir = path12.join(rootDir, ".github", "workflows");
4528
+ const ghWorkflowDir = path13.join(rootDir, ".github", "workflows");
4333
4529
  if (await _pathExists(ghWorkflowDir)) {
4334
4530
  try {
4335
4531
  if (cache) {
4336
4532
  const entries = await cache.walkDir(rootDir);
4337
- const ghPrefix = path12.relative(rootDir, ghWorkflowDir) + path12.sep;
4533
+ const ghPrefix = path13.relative(rootDir, ghWorkflowDir) + path13.sep;
4338
4534
  result.ciWorkflowCount = entries.filter(
4339
4535
  (e) => e.isFile && e.relPath.startsWith(ghPrefix) && (e.name.endsWith(".yml") || e.name.endsWith(".yaml"))
4340
4536
  ).length;
@@ -4385,11 +4581,11 @@ async function scanBuildDeploy(rootDir, cache) {
4385
4581
  (name) => name.endsWith(".cfn.json") || name.endsWith(".cfn.yaml")
4386
4582
  );
4387
4583
  if (cfnFiles.length > 0) iacSystems.add("cloudformation");
4388
- if (await _pathExists(path12.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
4584
+ if (await _pathExists(path13.join(rootDir, "Pulumi.yaml"))) iacSystems.add("pulumi");
4389
4585
  result.iac = [...iacSystems].sort();
4390
4586
  const releaseTools = /* @__PURE__ */ new Set();
4391
4587
  for (const [file, tool] of Object.entries(RELEASE_FILES)) {
4392
- if (await _pathExists(path12.join(rootDir, file))) releaseTools.add(tool);
4588
+ if (await _pathExists(path13.join(rootDir, file))) releaseTools.add(tool);
4393
4589
  }
4394
4590
  const pkgFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
4395
4591
  for (const pjPath of pkgFiles) {
@@ -4414,19 +4610,19 @@ async function scanBuildDeploy(rootDir, cache) {
4414
4610
  };
4415
4611
  const managers = /* @__PURE__ */ new Set();
4416
4612
  for (const [file, manager] of Object.entries(lockfileMap)) {
4417
- if (await _pathExists(path12.join(rootDir, file))) managers.add(manager);
4613
+ if (await _pathExists(path13.join(rootDir, file))) managers.add(manager);
4418
4614
  }
4419
4615
  result.packageManagers = [...managers].sort();
4420
4616
  const monoTools = /* @__PURE__ */ new Set();
4421
4617
  for (const [file, tool] of Object.entries(MONOREPO_FILES)) {
4422
- if (await _pathExists(path12.join(rootDir, file))) monoTools.add(tool);
4618
+ if (await _pathExists(path13.join(rootDir, file))) monoTools.add(tool);
4423
4619
  }
4424
4620
  result.monorepoTools = [...monoTools].sort();
4425
4621
  return result;
4426
4622
  }
4427
4623
 
4428
4624
  // src/scanners/ts-modernity.ts
4429
- import * as path13 from "path";
4625
+ import * as path14 from "path";
4430
4626
  async function scanTsModernity(rootDir, cache) {
4431
4627
  const result = {
4432
4628
  typescriptVersion: null,
@@ -4464,7 +4660,7 @@ async function scanTsModernity(rootDir, cache) {
4464
4660
  if (hasEsm && hasCjs) result.moduleType = "mixed";
4465
4661
  else if (hasEsm) result.moduleType = "esm";
4466
4662
  else if (hasCjs) result.moduleType = "cjs";
4467
- let tsConfigPath = path13.join(rootDir, "tsconfig.json");
4663
+ let tsConfigPath = path14.join(rootDir, "tsconfig.json");
4468
4664
  const tsConfigExists = cache ? await cache.pathExists(tsConfigPath) : await pathExists(tsConfigPath);
4469
4665
  if (!tsConfigExists) {
4470
4666
  const tsConfigs = cache ? await cache.findFiles(rootDir, (name) => name === "tsconfig.json") : await findFiles(rootDir, (name) => name === "tsconfig.json");
@@ -4811,7 +5007,7 @@ function scanBreakingChangeExposure(projects) {
4811
5007
 
4812
5008
  // src/scanners/file-hotspots.ts
4813
5009
  import * as fs4 from "fs/promises";
4814
- import * as path14 from "path";
5010
+ import * as path15 from "path";
4815
5011
  var SKIP_DIRS = /* @__PURE__ */ new Set([
4816
5012
  "node_modules",
4817
5013
  ".git",
@@ -4856,9 +5052,9 @@ async function scanFileHotspots(rootDir, cache) {
4856
5052
  const entries = await cache.walkDir(rootDir);
4857
5053
  for (const entry of entries) {
4858
5054
  if (!entry.isFile) continue;
4859
- const ext = path14.extname(entry.name).toLowerCase();
5055
+ const ext = path15.extname(entry.name).toLowerCase();
4860
5056
  if (SKIP_EXTENSIONS.has(ext)) continue;
4861
- const depth = entry.relPath.split(path14.sep).length - 1;
5057
+ const depth = entry.relPath.split(path15.sep).length - 1;
4862
5058
  if (depth > maxDepth) maxDepth = depth;
4863
5059
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
4864
5060
  try {
@@ -4887,15 +5083,15 @@ async function scanFileHotspots(rootDir, cache) {
4887
5083
  for (const e of entries) {
4888
5084
  if (e.isDirectory) {
4889
5085
  if (SKIP_DIRS.has(e.name)) continue;
4890
- await walk(path14.join(dir, e.name), depth + 1);
5086
+ await walk(path15.join(dir, e.name), depth + 1);
4891
5087
  } else if (e.isFile) {
4892
- const ext = path14.extname(e.name).toLowerCase();
5088
+ const ext = path15.extname(e.name).toLowerCase();
4893
5089
  if (SKIP_EXTENSIONS.has(ext)) continue;
4894
5090
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
4895
5091
  try {
4896
- const stat3 = await fs4.stat(path14.join(dir, e.name));
5092
+ const stat3 = await fs4.stat(path15.join(dir, e.name));
4897
5093
  allFiles.push({
4898
- path: path14.relative(rootDir, path14.join(dir, e.name)),
5094
+ path: path15.relative(rootDir, path15.join(dir, e.name)),
4899
5095
  bytes: stat3.size
4900
5096
  });
4901
5097
  } catch {
@@ -4918,7 +5114,7 @@ async function scanFileHotspots(rootDir, cache) {
4918
5114
  }
4919
5115
 
4920
5116
  // src/scanners/security-posture.ts
4921
- import * as path15 from "path";
5117
+ import * as path16 from "path";
4922
5118
  var LOCKFILES = {
4923
5119
  "pnpm-lock.yaml": "pnpm",
4924
5120
  "package-lock.json": "npm",
@@ -4939,14 +5135,14 @@ async function scanSecurityPosture(rootDir, cache) {
4939
5135
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
4940
5136
  const foundLockfiles = [];
4941
5137
  for (const [file, type] of Object.entries(LOCKFILES)) {
4942
- if (await _pathExists(path15.join(rootDir, file))) {
5138
+ if (await _pathExists(path16.join(rootDir, file))) {
4943
5139
  foundLockfiles.push(type);
4944
5140
  }
4945
5141
  }
4946
5142
  result.lockfilePresent = foundLockfiles.length > 0;
4947
5143
  result.multipleLockfileTypes = foundLockfiles.length > 1;
4948
5144
  result.lockfileTypes = foundLockfiles.sort();
4949
- const gitignorePath = path15.join(rootDir, ".gitignore");
5145
+ const gitignorePath = path16.join(rootDir, ".gitignore");
4950
5146
  if (await _pathExists(gitignorePath)) {
4951
5147
  try {
4952
5148
  const content = await _readTextFile(gitignorePath);
@@ -4961,7 +5157,7 @@ async function scanSecurityPosture(rootDir, cache) {
4961
5157
  }
4962
5158
  }
4963
5159
  for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
4964
- if (await _pathExists(path15.join(rootDir, envFile))) {
5160
+ if (await _pathExists(path16.join(rootDir, envFile))) {
4965
5161
  if (!result.gitignoreCoversEnv) {
4966
5162
  result.envFilesTracked = true;
4967
5163
  break;
@@ -4972,8 +5168,8 @@ async function scanSecurityPosture(rootDir, cache) {
4972
5168
  }
4973
5169
 
4974
5170
  // src/scanners/security-scanners.ts
4975
- import { spawn as spawn2 } from "child_process";
4976
- import * as path16 from "path";
5171
+ import { spawn as spawn3 } from "child_process";
5172
+ import * as path17 from "path";
4977
5173
  var TOOL_MATRIX = [
4978
5174
  { key: "semgrep", category: "sast", command: "semgrep", versionArgs: ["--version"], minRecommendedVersion: "1.75.0" },
4979
5175
  { key: "gitleaks", category: "secrets", command: "gitleaks", versionArgs: ["version"], minRecommendedVersion: "8.20.0" },
@@ -4986,8 +5182,8 @@ var SECRET_HEURISTICS = [
4986
5182
  { detector: "private-key", pattern: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/g },
4987
5183
  { detector: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g }
4988
5184
  ];
4989
- var defaultRunner = (command, args) => new Promise((resolve9, reject) => {
4990
- const child = spawn2(command, args, { stdio: ["ignore", "pipe", "pipe"] });
5185
+ var defaultRunner = (command, args) => new Promise((resolve10, reject) => {
5186
+ const child = spawn3(command, args, { stdio: ["ignore", "pipe", "pipe"] });
4991
5187
  let stdout = "";
4992
5188
  let stderr = "";
4993
5189
  child.stdout.on("data", (d) => {
@@ -4998,7 +5194,7 @@ var defaultRunner = (command, args) => new Promise((resolve9, reject) => {
4998
5194
  });
4999
5195
  child.on("error", reject);
5000
5196
  child.on("close", (code) => {
5001
- resolve9({ stdout, stderr, exitCode: code ?? 1 });
5197
+ resolve10({ stdout, stderr, exitCode: code ?? 1 });
5002
5198
  });
5003
5199
  });
5004
5200
  function compareSemver(a, b) {
@@ -5068,7 +5264,7 @@ async function detectSecretHeuristics(rootDir, cache) {
5068
5264
  const findings = [];
5069
5265
  for (const entry of entries) {
5070
5266
  if (!entry.isFile) continue;
5071
- const ext = path16.extname(entry.name).toLowerCase();
5267
+ const ext = path17.extname(entry.name).toLowerCase();
5072
5268
  if (ext && [".png", ".jpg", ".jpeg", ".gif", ".zip", ".pdf"].includes(ext)) continue;
5073
5269
  const content = await cache.readTextFile(entry.absPath);
5074
5270
  if (!content || content.length > 3e5) continue;
@@ -5091,9 +5287,9 @@ async function scanSecurityScanners(rootDir, cache, runner = defaultRunner) {
5091
5287
  const [semgrep, gitleaks, trufflehog] = await Promise.all(TOOL_MATRIX.map((tool) => assessTool(tool, runner)));
5092
5288
  const heuristicFindings = await detectSecretHeuristics(rootDir, cache);
5093
5289
  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"))
5290
+ semgrep: await cache.pathExists(path17.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path17.join(rootDir, ".semgrep.yaml")),
5291
+ gitleaks: await cache.pathExists(path17.join(rootDir, ".gitleaks.toml")),
5292
+ trufflehog: await cache.pathExists(path17.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path17.join(rootDir, ".trufflehog.yaml"))
5097
5293
  };
5098
5294
  return {
5099
5295
  semgrep,
@@ -5518,7 +5714,7 @@ function scanServiceDependencies(projects) {
5518
5714
  }
5519
5715
 
5520
5716
  // src/scanners/architecture.ts
5521
- import * as path17 from "path";
5717
+ import * as path18 from "path";
5522
5718
  import * as fs5 from "fs/promises";
5523
5719
  var ARCHETYPE_SIGNALS = [
5524
5720
  // Meta-frameworks (highest priority — they imply routing patterns)
@@ -5817,9 +6013,9 @@ async function walkSourceFiles(rootDir, cache) {
5817
6013
  const entries = await cache.walkDir(rootDir);
5818
6014
  return entries.filter((e) => {
5819
6015
  if (!e.isFile) return false;
5820
- const name = path17.basename(e.absPath);
6016
+ const name = path18.basename(e.absPath);
5821
6017
  if (name.startsWith(".") && name !== ".") return false;
5822
- const ext = path17.extname(name);
6018
+ const ext = path18.extname(name);
5823
6019
  return SOURCE_EXTENSIONS.has(ext);
5824
6020
  }).map((e) => e.relPath);
5825
6021
  }
@@ -5833,15 +6029,15 @@ async function walkSourceFiles(rootDir, cache) {
5833
6029
  }
5834
6030
  for (const entry of entries) {
5835
6031
  if (entry.name.startsWith(".") && entry.name !== ".") continue;
5836
- const fullPath = path17.join(dir, entry.name);
6032
+ const fullPath = path18.join(dir, entry.name);
5837
6033
  if (entry.isDirectory()) {
5838
6034
  if (!IGNORE_DIRS.has(entry.name)) {
5839
6035
  await walk(fullPath);
5840
6036
  }
5841
6037
  } else if (entry.isFile()) {
5842
- const ext = path17.extname(entry.name);
6038
+ const ext = path18.extname(entry.name);
5843
6039
  if (SOURCE_EXTENSIONS.has(ext)) {
5844
- files.push(path17.relative(rootDir, fullPath));
6040
+ files.push(path18.relative(rootDir, fullPath));
5845
6041
  }
5846
6042
  }
5847
6043
  }
@@ -5865,7 +6061,7 @@ function classifyFile(filePath, archetype) {
5865
6061
  }
5866
6062
  }
5867
6063
  if (!bestMatch || bestMatch.confidence < 0.7) {
5868
- const baseName = path17.basename(filePath, path17.extname(filePath));
6064
+ const baseName = path18.basename(filePath, path18.extname(filePath));
5869
6065
  const cleanBase = baseName.replace(/\.(test|spec)$/, "");
5870
6066
  for (const rule of SUFFIX_RULES) {
5871
6067
  if (cleanBase.endsWith(rule.suffix)) {
@@ -5974,7 +6170,7 @@ function generateLayerFlowMermaid(layers) {
5974
6170
  return lines.join("\n");
5975
6171
  }
5976
6172
  async function buildProjectArchitectureMermaid(rootDir, project, archetype, cache) {
5977
- const projectRoot = path17.resolve(rootDir, project.path || ".");
6173
+ const projectRoot = path18.resolve(rootDir, project.path || ".");
5978
6174
  const allFiles = await walkSourceFiles(projectRoot, cache);
5979
6175
  const layerSet = /* @__PURE__ */ new Set();
5980
6176
  for (const rel of allFiles) {
@@ -6100,7 +6296,7 @@ async function scanArchitecture(rootDir, projects, tooling, services, cache) {
6100
6296
  }
6101
6297
 
6102
6298
  // src/scanners/code-quality.ts
6103
- import * as path18 from "path";
6299
+ import * as path19 from "path";
6104
6300
  import * as ts from "typescript";
6105
6301
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
6106
6302
  var DEFAULT_RESULT = {
@@ -6131,9 +6327,9 @@ async function scanCodeQuality(rootDir, cache) {
6131
6327
  continue;
6132
6328
  }
6133
6329
  if (!raw.trim()) continue;
6134
- const rel = normalizeModuleId(path18.relative(rootDir, filePath));
6330
+ const rel = normalizeModuleId(path19.relative(rootDir, filePath));
6135
6331
  const source = ts.createSourceFile(filePath, raw, ts.ScriptTarget.Latest, true);
6136
- const imports = collectLocalImports(source, path18.dirname(filePath), rootDir);
6332
+ const imports = collectLocalImports(source, path19.dirname(filePath), rootDir);
6137
6333
  depGraph.set(rel, imports);
6138
6334
  const fileMetrics = computeFileMetrics(source, raw);
6139
6335
  totalFunctions += fileMetrics.functionsAnalyzed;
@@ -6167,9 +6363,9 @@ async function scanCodeQuality(rootDir, cache) {
6167
6363
  async function findSourceFiles(rootDir, cache) {
6168
6364
  if (cache) {
6169
6365
  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);
6366
+ return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path19.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
6171
6367
  }
6172
- const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path18.extname(name).toLowerCase()));
6368
+ const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path19.extname(name).toLowerCase()));
6173
6369
  return files;
6174
6370
  }
6175
6371
  function collectLocalImports(source, fileDir, rootDir) {
@@ -6190,8 +6386,8 @@ function collectLocalImports(source, fileDir, rootDir) {
6190
6386
  }
6191
6387
  function resolveLocalImport(specifier, fileDir, rootDir) {
6192
6388
  if (!specifier.startsWith(".")) return null;
6193
- const rawTarget = path18.resolve(fileDir, specifier);
6194
- const normalized = path18.relative(rootDir, rawTarget).replace(/\\/g, "/");
6389
+ const rawTarget = path19.resolve(fileDir, specifier);
6390
+ const normalized = path19.relative(rootDir, rawTarget).replace(/\\/g, "/");
6195
6391
  if (!normalized || normalized.startsWith("..")) return null;
6196
6392
  return normalizeModuleId(normalized);
6197
6393
  }
@@ -6332,8 +6528,8 @@ function visitEach(node, cb) {
6332
6528
  }
6333
6529
 
6334
6530
  // src/scanners/owasp-category-mapping.ts
6335
- import { spawn as spawn3 } from "child_process";
6336
- import * as path19 from "path";
6531
+ import { spawn as spawn4 } from "child_process";
6532
+ import * as path20 from "path";
6337
6533
  var OWASP_CONFIG = "p/owasp-top-ten";
6338
6534
  var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
6339
6535
  ".js",
@@ -6358,8 +6554,8 @@ var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
6358
6554
  ".env"
6359
6555
  ]);
6360
6556
  async function runSemgrep(args, cwd, stdin) {
6361
- return new Promise((resolve9, reject) => {
6362
- const child = spawn3("semgrep", args, {
6557
+ return new Promise((resolve10, reject) => {
6558
+ const child = spawn4("semgrep", args, {
6363
6559
  cwd,
6364
6560
  shell: true,
6365
6561
  stdio: ["pipe", "pipe", "pipe"]
@@ -6374,7 +6570,7 @@ async function runSemgrep(args, cwd, stdin) {
6374
6570
  });
6375
6571
  child.on("error", reject);
6376
6572
  child.on("close", (code) => {
6377
- resolve9({ code: code ?? 1, stdout, stderr });
6573
+ resolve10({ code: code ?? 1, stdout, stderr });
6378
6574
  });
6379
6575
  if (stdin !== void 0) child.stdin.write(stdin);
6380
6576
  child.stdin.end();
@@ -6412,7 +6608,7 @@ function parseFindings(results, rootDir) {
6412
6608
  const metadata = r.extra?.metadata;
6413
6609
  return {
6414
6610
  ruleId: r.check_id ?? "unknown",
6415
- path: r.path ? path19.relative(rootDir, path19.resolve(rootDir, r.path)) : "",
6611
+ path: r.path ? path20.relative(rootDir, path20.resolve(rootDir, r.path)) : "",
6416
6612
  line: r.start?.line ?? 1,
6417
6613
  endLine: r.end?.line,
6418
6614
  message: r.extra?.message ?? "Potential security issue",
@@ -6472,7 +6668,7 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
6472
6668
  }
6473
6669
  }
6474
6670
  const entries = cache ? await cache.walkDir(rootDir) : [];
6475
- const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path19.extname(e.name).toLowerCase()));
6671
+ const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path20.extname(e.name).toLowerCase()));
6476
6672
  const findings = [];
6477
6673
  const errors = [];
6478
6674
  let scannedFiles = 0;
@@ -6720,7 +6916,7 @@ function isUsefulString(s) {
6720
6916
  }
6721
6917
 
6722
6918
  // src/utils/tool-installer.ts
6723
- import { spawn as spawn4 } from "child_process";
6919
+ import { spawn as spawn5 } from "child_process";
6724
6920
  import chalk5 from "chalk";
6725
6921
  var SECURITY_TOOLS = [
6726
6922
  { name: "semgrep", command: "semgrep", brew: "semgrep", winget: null, scoop: null, pip: "semgrep" },
@@ -6728,9 +6924,9 @@ var SECURITY_TOOLS = [
6728
6924
  { name: "trufflehog", command: "trufflehog", brew: "trufflehog", winget: null, scoop: "trufflehog", pip: null }
6729
6925
  ];
6730
6926
  var IS_WIN = process.platform === "win32";
6731
- function runCommand(cmd, args) {
6732
- return new Promise((resolve9) => {
6733
- const child = spawn4(cmd, args, {
6927
+ function runCommand2(cmd, args) {
6928
+ return new Promise((resolve10) => {
6929
+ const child = spawn5(cmd, args, {
6734
6930
  stdio: ["ignore", "pipe", "pipe"],
6735
6931
  shell: IS_WIN
6736
6932
  // required for .cmd/.ps1 wrappers on Windows
@@ -6743,19 +6939,19 @@ function runCommand(cmd, args) {
6743
6939
  child.stderr.on("data", (d) => {
6744
6940
  stderr += d.toString();
6745
6941
  });
6746
- child.on("error", () => resolve9({ exitCode: 127, stdout, stderr }));
6747
- child.on("close", (code) => resolve9({ exitCode: code ?? 1, stdout, stderr }));
6942
+ child.on("error", () => resolve10({ exitCode: 127, stdout, stderr }));
6943
+ child.on("close", (code) => resolve10({ exitCode: code ?? 1, stdout, stderr }));
6748
6944
  });
6749
6945
  }
6750
6946
  async function commandExists(command) {
6751
6947
  const checker = IS_WIN ? "where" : "which";
6752
- const { exitCode } = await runCommand(checker, [command]);
6948
+ const { exitCode } = await runCommand2(checker, [command]);
6753
6949
  return exitCode === 0;
6754
6950
  }
6755
6951
  async function tryInstall(tool, strategies, log) {
6756
6952
  for (const { pm, args } of strategies) {
6757
6953
  log(chalk5.dim(` ${pm} ${args.join(" ")}\u2026`));
6758
- const { exitCode, stderr } = await runCommand(pm, args);
6954
+ const { exitCode, stderr } = await runCommand2(pm, args);
6759
6955
  if (exitCode === 0) {
6760
6956
  log(` ${chalk5.green("\u2714")} ${tool.name} installed via ${pm}`);
6761
6957
  return { ok: true, pm };
@@ -6880,21 +7076,94 @@ function generateProjectRelationshipMermaid(project, projects) {
6880
7076
  lines.push(...buildDefs());
6881
7077
  return { mermaid: lines.join("\n") };
6882
7078
  }
7079
+ function generateSolutionRelationshipMermaid(solution, projects) {
7080
+ const lines = ["flowchart TB"];
7081
+ const solutionNodeId = sanitizeId(solution.solutionId || solution.path || solution.name);
7082
+ const solutionScore = solution.drift?.score;
7083
+ const solutionScoreText = typeof solutionScore === "number" ? ` (${solutionScore})` : " (n/a)";
7084
+ lines.push(`${solutionNodeId}["${escapeLabel(`${solution.name}${solutionScoreText}`)}"]`);
7085
+ lines.push(`class ${solutionNodeId} ${scoreClass(solutionScore)}`);
7086
+ const projectByPath = new Map(projects.map((p) => [p.path, p]));
7087
+ for (const projectPath of solution.projectPaths) {
7088
+ const project = projectByPath.get(projectPath);
7089
+ if (!project) continue;
7090
+ const projectNodeId = sanitizeId(project.projectId || project.path || project.name);
7091
+ lines.push(`${projectNodeId}["${escapeLabel(nodeLabel(project))}"]`);
7092
+ lines.push(`class ${projectNodeId} ${scoreClass(project.drift?.score)}`);
7093
+ lines.push(`${solutionNodeId} --> ${projectNodeId}`);
7094
+ for (const ref of project.projectReferences ?? []) {
7095
+ const target = projectByPath.get(ref.path);
7096
+ if (!target) continue;
7097
+ const toId = sanitizeId(target.projectId || target.path || target.name);
7098
+ lines.push(`${projectNodeId} --> ${toId}`);
7099
+ }
7100
+ }
7101
+ lines.push(...buildDefs());
7102
+ return { mermaid: lines.join("\n") };
7103
+ }
6883
7104
 
6884
7105
  // src/commands/scan.ts
7106
+ async function discoverSolutions(rootDir, fileCache) {
7107
+ const solutionFiles = await fileCache.findSolutionFiles(rootDir);
7108
+ const parsed = [];
7109
+ for (const solutionFile of solutionFiles) {
7110
+ try {
7111
+ const content = await fileCache.readTextFile(solutionFile);
7112
+ const dir = path21.dirname(solutionFile);
7113
+ const relSolutionPath = path21.relative(rootDir, solutionFile).replace(/\\/g, "/");
7114
+ const projectPaths = /* @__PURE__ */ new Set();
7115
+ const projectRegex = /Project\("[^"]*"\)\s*=\s*"([^"]*)",\s*"([^"]+\.csproj)"/g;
7116
+ let match;
7117
+ while ((match = projectRegex.exec(content)) !== null) {
7118
+ const projectRelative = match[2];
7119
+ const absProjectPath = path21.resolve(dir, projectRelative.replace(/\\/g, "/"));
7120
+ projectPaths.add(path21.relative(rootDir, absProjectPath).replace(/\\/g, "/"));
7121
+ }
7122
+ const solutionName = path21.basename(solutionFile, path21.extname(solutionFile));
7123
+ parsed.push({
7124
+ path: relSolutionPath,
7125
+ name: solutionName,
7126
+ type: "dotnet-sln",
7127
+ projectPaths: [...projectPaths]
7128
+ });
7129
+ } catch {
7130
+ }
7131
+ }
7132
+ return parsed;
7133
+ }
6885
7134
  async function runScan(rootDir, opts) {
6886
7135
  const scanStart = Date.now();
6887
7136
  const config = await loadConfig(rootDir);
6888
7137
  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);
7138
+ const packageManifest = opts.packageManifest ? await loadPackageVersionManifest(opts.packageManifest) : void 0;
7139
+ const offlineMode = opts.offline === true;
7140
+ const maxPrivacyMode = opts.maxPrivacy === true;
7141
+ const npmCache = new NpmCache(rootDir, sem, packageManifest, offlineMode);
7142
+ const nugetCache = new NuGetCache(sem, packageManifest, offlineMode);
7143
+ const pypiCache = new PyPICache(sem, packageManifest, offlineMode);
7144
+ const mavenCache = new MavenCache(sem, packageManifest, offlineMode);
6893
7145
  const fileCache = new FileCache();
6894
7146
  const excludePatterns = config.exclude ?? [];
6895
7147
  fileCache.setExcludePatterns(excludePatterns);
6896
7148
  fileCache.setMaxFileSize(config.maxFileSizeToScan ?? 5242880);
6897
7149
  const scanners = config.scanners;
7150
+ const scannerPolicy = {
7151
+ platformMatrix: !maxPrivacyMode,
7152
+ toolingInventory: true,
7153
+ serviceDependencies: !maxPrivacyMode,
7154
+ breakingChangeExposure: !maxPrivacyMode,
7155
+ securityPosture: true,
7156
+ securityScanners: !maxPrivacyMode,
7157
+ buildDeploy: !maxPrivacyMode,
7158
+ tsModernity: !maxPrivacyMode,
7159
+ fileHotspots: !maxPrivacyMode,
7160
+ dependencyGraph: true,
7161
+ dependencyRisk: true,
7162
+ architecture: !maxPrivacyMode,
7163
+ codeQuality: !maxPrivacyMode,
7164
+ owaspCategoryMapping: !maxPrivacyMode,
7165
+ uiPurpose: !maxPrivacyMode
7166
+ };
6898
7167
  let filesScanned = 0;
6899
7168
  const progress = new ScanProgress(rootDir);
6900
7169
  const steps = [
@@ -6907,28 +7176,28 @@ async function runScan(rootDir, opts) {
6907
7176
  { id: "python", label: "Scanning Python projects", weight: 3 },
6908
7177
  { id: "java", label: "Scanning Java projects", weight: 3 },
6909
7178
  ...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" }] : []
7179
+ ...scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false ? [{ id: "platform", label: "Platform matrix" }] : [],
7180
+ ...scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false ? [{ id: "tooling", label: "Tooling inventory" }] : [],
7181
+ ...scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false ? [{ id: "services", label: "Service dependencies" }] : [],
7182
+ ...scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false ? [{ id: "breaking", label: "Breaking change exposure" }] : [],
7183
+ ...scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false ? [{ id: "security", label: "Security posture" }] : [],
7184
+ ...scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false ? [{ id: "secscan", label: "Security scanners" }] : [],
7185
+ ...scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false ? [{ id: "build", label: "Build & deploy analysis" }] : [],
7186
+ ...scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false ? [{ id: "ts", label: "TypeScript modernity" }] : [],
7187
+ ...scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false ? [{ id: "hotspots", label: "File hotspots" }] : [],
7188
+ ...scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false ? [{ id: "depgraph", label: "Dependency graph" }] : [],
7189
+ ...scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false ? [{ id: "deprisk", label: "Dependency risk" }] : [],
7190
+ ...scannerPolicy.architecture && scanners?.architecture?.enabled !== false ? [{ id: "architecture", label: "Architecture layers" }] : [],
7191
+ ...scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false ? [{ id: "codequality", label: "Code quality metrics" }] : [],
7192
+ ...scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false ? [{ id: "owasp", label: "OWASP category mapping" }] : [],
7193
+ ...!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) ? [{ id: "uipurpose", label: "UI purpose evidence" }] : []
6925
7194
  ] : [],
6926
7195
  { id: "drift", label: "Computing drift score" },
6927
7196
  { id: "findings", label: "Generating findings" }
6928
7197
  ];
6929
7198
  progress.setSteps(steps);
6930
7199
  progress.completeStep("config", "loaded");
6931
- const registryOk = await checkRegistryAccess(rootDir);
7200
+ const registryOk = offlineMode ? true : await checkRegistryAccess(rootDir);
6932
7201
  if (!registryOk) {
6933
7202
  progress.finish();
6934
7203
  const msg = [
@@ -7010,14 +7279,45 @@ async function runScan(rootDir, opts) {
7010
7279
  project.drift = computeDriftScore([project]);
7011
7280
  project.projectId = computeProjectId(project.path, project.name, workspaceId);
7012
7281
  }
7282
+ const solutionsManifestPath = path21.join(rootDir, ".vibgrate", "solutions.json");
7283
+ const persistedSolutionIds = /* @__PURE__ */ new Map();
7284
+ if (await pathExists(solutionsManifestPath)) {
7285
+ try {
7286
+ const persisted = await readJsonFile(solutionsManifestPath);
7287
+ for (const solution of persisted.solutions ?? []) {
7288
+ if (solution.path && solution.solutionId) persistedSolutionIds.set(solution.path, solution.solutionId);
7289
+ }
7290
+ } catch {
7291
+ }
7292
+ }
7293
+ const discoveredSolutions = await discoverSolutions(rootDir, fileCache);
7294
+ const solutions = discoveredSolutions.map((solution) => ({
7295
+ solutionId: persistedSolutionIds.get(solution.path) ?? computeSolutionId(solution.path, solution.name, workspaceId),
7296
+ path: solution.path,
7297
+ name: solution.name,
7298
+ type: solution.type,
7299
+ projectPaths: solution.projectPaths
7300
+ }));
7301
+ const projectsByPath = new Map(allProjects.map((project) => [project.path, project]));
7302
+ for (const solution of solutions) {
7303
+ const includedProjects = solution.projectPaths.map((projectPath) => projectsByPath.get(projectPath)).filter((project) => Boolean(project));
7304
+ solution.drift = includedProjects.length > 0 ? computeDriftScore(includedProjects) : void 0;
7305
+ for (const project of includedProjects) {
7306
+ project.solutionId = solution.solutionId;
7307
+ project.solutionName = solution.name;
7308
+ }
7309
+ }
7013
7310
  for (const project of allProjects) {
7014
7311
  project.relationshipDiagram = generateProjectRelationshipMermaid(project, allProjects);
7015
7312
  }
7313
+ for (const solution of solutions) {
7314
+ solution.relationshipDiagram = generateSolutionRelationshipMermaid(solution, allProjects);
7315
+ }
7016
7316
  const relationshipDiagram = generateWorkspaceRelationshipMermaid(allProjects);
7017
7317
  const extended = {};
7018
7318
  if (scanners !== false) {
7019
7319
  const scannerTasks = [];
7020
- if (scanners?.platformMatrix?.enabled !== false) {
7320
+ if (scannerPolicy.platformMatrix && scanners?.platformMatrix?.enabled !== false) {
7021
7321
  progress.startStep("platform");
7022
7322
  scannerTasks.push(
7023
7323
  scanPlatformMatrix(rootDir, fileCache).then((result) => {
@@ -7031,7 +7331,7 @@ async function runScan(rootDir, opts) {
7031
7331
  })
7032
7332
  );
7033
7333
  }
7034
- if (scanners?.toolingInventory?.enabled !== false) {
7334
+ if (scannerPolicy.toolingInventory && scanners?.toolingInventory?.enabled !== false) {
7035
7335
  progress.startStep("tooling");
7036
7336
  scannerTasks.push(
7037
7337
  Promise.resolve().then(() => {
@@ -7041,7 +7341,7 @@ async function runScan(rootDir, opts) {
7041
7341
  })
7042
7342
  );
7043
7343
  }
7044
- if (scanners?.serviceDependencies?.enabled !== false) {
7344
+ if (scannerPolicy.serviceDependencies && scanners?.serviceDependencies?.enabled !== false) {
7045
7345
  progress.startStep("services");
7046
7346
  scannerTasks.push(
7047
7347
  Promise.resolve().then(() => {
@@ -7051,7 +7351,7 @@ async function runScan(rootDir, opts) {
7051
7351
  })
7052
7352
  );
7053
7353
  }
7054
- if (scanners?.breakingChangeExposure?.enabled !== false) {
7354
+ if (scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false) {
7055
7355
  progress.startStep("breaking");
7056
7356
  scannerTasks.push(
7057
7357
  Promise.resolve().then(() => {
@@ -7066,7 +7366,7 @@ async function runScan(rootDir, opts) {
7066
7366
  })
7067
7367
  );
7068
7368
  }
7069
- if (scanners?.securityPosture?.enabled !== false) {
7369
+ if (scannerPolicy.securityPosture && scanners?.securityPosture?.enabled !== false) {
7070
7370
  progress.startStep("security");
7071
7371
  scannerTasks.push(
7072
7372
  scanSecurityPosture(rootDir, fileCache).then((result) => {
@@ -7076,7 +7376,7 @@ async function runScan(rootDir, opts) {
7076
7376
  })
7077
7377
  );
7078
7378
  }
7079
- if (scanners?.securityScanners?.enabled !== false) {
7379
+ if (scannerPolicy.securityScanners && scanners?.securityScanners?.enabled !== false) {
7080
7380
  if (opts.installTools) {
7081
7381
  const installResult = await installMissingTools();
7082
7382
  if (installResult.installed.length > 0) {
@@ -7099,7 +7399,7 @@ async function runScan(rootDir, opts) {
7099
7399
  })
7100
7400
  );
7101
7401
  }
7102
- if (scanners?.buildDeploy?.enabled !== false) {
7402
+ if (scannerPolicy.buildDeploy && scanners?.buildDeploy?.enabled !== false) {
7103
7403
  progress.startStep("build");
7104
7404
  scannerTasks.push(
7105
7405
  scanBuildDeploy(rootDir, fileCache).then((result) => {
@@ -7111,7 +7411,7 @@ async function runScan(rootDir, opts) {
7111
7411
  })
7112
7412
  );
7113
7413
  }
7114
- if (scanners?.tsModernity?.enabled !== false) {
7414
+ if (scannerPolicy.tsModernity && scanners?.tsModernity?.enabled !== false) {
7115
7415
  progress.startStep("ts");
7116
7416
  scannerTasks.push(
7117
7417
  scanTsModernity(rootDir, fileCache).then((result) => {
@@ -7124,7 +7424,7 @@ async function runScan(rootDir, opts) {
7124
7424
  })
7125
7425
  );
7126
7426
  }
7127
- if (scanners?.fileHotspots?.enabled !== false) {
7427
+ if (scannerPolicy.fileHotspots && scanners?.fileHotspots?.enabled !== false) {
7128
7428
  progress.startStep("hotspots");
7129
7429
  scannerTasks.push(
7130
7430
  scanFileHotspots(rootDir, fileCache).then((result) => {
@@ -7133,7 +7433,7 @@ async function runScan(rootDir, opts) {
7133
7433
  })
7134
7434
  );
7135
7435
  }
7136
- if (scanners?.dependencyGraph?.enabled !== false) {
7436
+ if (scannerPolicy.dependencyGraph && scanners?.dependencyGraph?.enabled !== false) {
7137
7437
  progress.startStep("depgraph");
7138
7438
  scannerTasks.push(
7139
7439
  scanDependencyGraph(rootDir, fileCache).then((result) => {
@@ -7143,7 +7443,7 @@ async function runScan(rootDir, opts) {
7143
7443
  })
7144
7444
  );
7145
7445
  }
7146
- if (scanners?.codeQuality?.enabled !== false) {
7446
+ if (scannerPolicy.codeQuality && scanners?.codeQuality?.enabled !== false) {
7147
7447
  progress.startStep("codequality");
7148
7448
  scannerTasks.push(
7149
7449
  scanCodeQuality(rootDir, fileCache).then((result) => {
@@ -7156,7 +7456,7 @@ async function runScan(rootDir, opts) {
7156
7456
  })
7157
7457
  );
7158
7458
  }
7159
- if (scanners?.dependencyRisk?.enabled !== false) {
7459
+ if (scannerPolicy.dependencyRisk && scanners?.dependencyRisk?.enabled !== false) {
7160
7460
  progress.startStep("deprisk");
7161
7461
  scannerTasks.push(
7162
7462
  Promise.resolve().then(() => {
@@ -7170,7 +7470,7 @@ async function runScan(rootDir, opts) {
7170
7470
  );
7171
7471
  }
7172
7472
  await Promise.all(scannerTasks);
7173
- if (scanners?.owaspCategoryMapping?.enabled !== false) {
7473
+ if (scannerPolicy.owaspCategoryMapping && scanners?.owaspCategoryMapping?.enabled !== false) {
7174
7474
  progress.startStep("owasp");
7175
7475
  extended.owaspCategoryMapping = await scanOwaspCategoryMapping(
7176
7476
  rootDir,
@@ -7189,14 +7489,14 @@ async function runScan(rootDir, opts) {
7189
7489
  );
7190
7490
  }
7191
7491
  }
7192
- if (opts.uiPurpose || scanners?.uiPurpose?.enabled === true) {
7492
+ if (!maxPrivacyMode && (opts.uiPurpose || scanners?.uiPurpose?.enabled === true)) {
7193
7493
  progress.startStep("uipurpose");
7194
7494
  extended.uiPurpose = await scanUiPurpose(rootDir, fileCache);
7195
7495
  const up = extended.uiPurpose;
7196
7496
  const summary = [`${up.topEvidence.length} evidence`, ...up.capped ? ["capped"] : []].join(" \xB7 ");
7197
7497
  progress.completeStep("uipurpose", summary, up.topEvidence.length);
7198
7498
  }
7199
- if (scanners?.architecture?.enabled !== false) {
7499
+ if (scannerPolicy.architecture && scanners?.architecture?.enabled !== false) {
7200
7500
  progress.startStep("architecture");
7201
7501
  extended.architecture = await scanArchitecture(
7202
7502
  rootDir,
@@ -7284,13 +7584,16 @@ async function runScan(rootDir, opts) {
7284
7584
  if (extended.owaspCategoryMapping) filesScanned += extended.owaspCategoryMapping.scannedFiles;
7285
7585
  if (extended.uiPurpose) filesScanned += extended.uiPurpose.topEvidence.length;
7286
7586
  const durationMs = Date.now() - scanStart;
7587
+ const repository = await buildRepositoryInfo(rootDir, vcs.remoteUrl, extended.buildDeploy?.ci);
7287
7588
  const artifact = {
7288
7589
  schemaVersion: "1.0",
7289
7590
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7290
7591
  vibgrateVersion: VERSION,
7291
- rootPath: path20.basename(rootDir),
7592
+ rootPath: path21.basename(rootDir),
7292
7593
  ...vcs.type !== "unknown" ? { vcs } : {},
7594
+ repository,
7293
7595
  projects: allProjects,
7596
+ ...solutions.length > 0 ? { solutions } : {},
7294
7597
  drift,
7295
7598
  findings,
7296
7599
  ...Object.keys(extended).length > 0 ? { extended } : {},
@@ -7300,7 +7603,7 @@ async function runScan(rootDir, opts) {
7300
7603
  relationshipDiagram
7301
7604
  };
7302
7605
  if (opts.baseline) {
7303
- const baselinePath = path20.resolve(opts.baseline);
7606
+ const baselinePath = path21.resolve(opts.baseline);
7304
7607
  if (await pathExists(baselinePath)) {
7305
7608
  try {
7306
7609
  const baseline = await readJsonFile(baselinePath);
@@ -7311,9 +7614,21 @@ async function runScan(rootDir, opts) {
7311
7614
  }
7312
7615
  }
7313
7616
  }
7314
- const vibgrateDir = path20.join(rootDir, ".vibgrate");
7315
- await ensureDir(vibgrateDir);
7316
- await writeJsonFile(path20.join(vibgrateDir, "scan_result.json"), artifact);
7617
+ if (!opts.noLocalArtifacts && !maxPrivacyMode) {
7618
+ const vibgrateDir = path21.join(rootDir, ".vibgrate");
7619
+ await ensureDir(vibgrateDir);
7620
+ await writeJsonFile(path21.join(vibgrateDir, "scan_result.json"), artifact);
7621
+ await writeJsonFile(path21.join(vibgrateDir, "solutions.json"), {
7622
+ scannedAt: artifact.timestamp,
7623
+ solutions: solutions.map((solution) => ({
7624
+ solutionId: solution.solutionId,
7625
+ name: solution.name,
7626
+ path: solution.path,
7627
+ type: solution.type,
7628
+ projectPaths: solution.projectPaths
7629
+ }))
7630
+ });
7631
+ }
7317
7632
  await saveScanHistory(rootDir, {
7318
7633
  timestamp: artifact.timestamp,
7319
7634
  totalDurationMs: durationMs,
@@ -7321,29 +7636,33 @@ async function runScan(rootDir, opts) {
7321
7636
  totalDirs: treeCount.totalDirs,
7322
7637
  steps: progress.getStepTimings()
7323
7638
  });
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
- });
7639
+ if (!opts.noLocalArtifacts && !maxPrivacyMode) {
7640
+ for (const project of allProjects) {
7641
+ if (project.drift && project.path) {
7642
+ const projectDir = path21.resolve(rootDir, project.path);
7643
+ const projectVibgrateDir = path21.join(projectDir, ".vibgrate");
7644
+ await ensureDir(projectVibgrateDir);
7645
+ await writeJsonFile(path21.join(projectVibgrateDir, "project_score.json"), {
7646
+ projectId: project.projectId,
7647
+ name: project.name,
7648
+ type: project.type,
7649
+ path: project.path,
7650
+ score: project.drift.score,
7651
+ riskLevel: project.drift.riskLevel,
7652
+ components: project.drift.components,
7653
+ measured: project.drift.measured,
7654
+ scannedAt: artifact.timestamp,
7655
+ vibgrateVersion: VERSION,
7656
+ solutionId: project.solutionId,
7657
+ solutionName: project.solutionName
7658
+ });
7659
+ }
7341
7660
  }
7342
7661
  }
7343
7662
  if (opts.format === "json") {
7344
7663
  const jsonStr = JSON.stringify(artifact, null, 2);
7345
7664
  if (opts.out) {
7346
- await writeTextFile(path20.resolve(opts.out), jsonStr);
7665
+ await writeTextFile(path21.resolve(opts.out), jsonStr);
7347
7666
  console.log(chalk6.green("\u2714") + ` JSON written to ${opts.out}`);
7348
7667
  } else {
7349
7668
  console.log(jsonStr);
@@ -7352,7 +7671,7 @@ async function runScan(rootDir, opts) {
7352
7671
  const sarif = formatSarif(artifact);
7353
7672
  const sarifStr = JSON.stringify(sarif, null, 2);
7354
7673
  if (opts.out) {
7355
- await writeTextFile(path20.resolve(opts.out), sarifStr);
7674
+ await writeTextFile(path21.resolve(opts.out), sarifStr);
7356
7675
  console.log(chalk6.green("\u2714") + ` SARIF written to ${opts.out}`);
7357
7676
  } else {
7358
7677
  console.log(sarifStr);
@@ -7361,11 +7680,34 @@ async function runScan(rootDir, opts) {
7361
7680
  const text = formatText(artifact);
7362
7681
  console.log(text);
7363
7682
  if (opts.out) {
7364
- await writeTextFile(path20.resolve(opts.out), text);
7683
+ await writeTextFile(path21.resolve(opts.out), text);
7365
7684
  }
7366
7685
  }
7367
7686
  return artifact;
7368
7687
  }
7688
+ async function buildRepositoryInfo(rootDir, remoteUrl, ciSystems) {
7689
+ const packageJsonPath = path21.join(rootDir, "package.json");
7690
+ let name = path21.basename(rootDir);
7691
+ let version;
7692
+ if (await pathExists(packageJsonPath)) {
7693
+ try {
7694
+ const packageJson = await readJsonFile(packageJsonPath);
7695
+ if (typeof packageJson.name === "string" && packageJson.name.trim()) {
7696
+ name = packageJson.name.trim();
7697
+ }
7698
+ if (typeof packageJson.version === "string" && packageJson.version.trim()) {
7699
+ version = packageJson.version.trim();
7700
+ }
7701
+ } catch {
7702
+ }
7703
+ }
7704
+ return {
7705
+ name,
7706
+ ...version ? { version } : {},
7707
+ ...ciSystems && ciSystems.length > 0 ? { pipeline: ciSystems.join(",") } : {},
7708
+ ...remoteUrl ? { remoteUrl } : {}
7709
+ };
7710
+ }
7369
7711
  async function autoPush(artifact, rootDir, opts) {
7370
7712
  const dsn = opts.dsn || process.env.VIBGRATE_DSN;
7371
7713
  if (!dsn) {
@@ -7436,8 +7778,8 @@ function parseNonNegativeNumber(value, label) {
7436
7778
  }
7437
7779
  return parsed;
7438
7780
  }
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);
7781
+ var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--install-tools", "Auto-install missing security scanners via Homebrew").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").action(async (targetPath, opts) => {
7782
+ const rootDir = path21.resolve(targetPath);
7441
7783
  if (!await pathExists(rootDir)) {
7442
7784
  console.error(chalk6.red(`Path does not exist: ${rootDir}`));
7443
7785
  process.exit(1);
@@ -7455,6 +7797,10 @@ var scanCommand = new Command3("scan").description("Scan a project for upgrade d
7455
7797
  strict: opts.strict,
7456
7798
  installTools: opts.installTools,
7457
7799
  uiPurpose: opts.uiPurpose,
7800
+ noLocalArtifacts: opts.noLocalArtifacts,
7801
+ maxPrivacy: opts.maxPrivacy,
7802
+ offline: opts.offline,
7803
+ packageManifest: opts.packageManifest,
7458
7804
  driftBudget: parseNonNegativeNumber(opts.driftBudget, "--drift-budget"),
7459
7805
  driftWorseningPercent: parseNonNegativeNumber(opts.driftWorsening, "--drift-worsening")
7460
7806
  };
@@ -7495,7 +7841,7 @@ Failing fitness function: drift worsened by ${worseningPercent.toFixed(2)}% (thr
7495
7841
  }
7496
7842
  }
7497
7843
  const hasDsn = !!(opts.dsn || process.env.VIBGRATE_DSN);
7498
- if (opts.push || hasDsn) {
7844
+ if (!scanOpts.offline && (opts.push || hasDsn)) {
7499
7845
  await autoPush(artifact, rootDir, scanOpts);
7500
7846
  }
7501
7847
  });