@vibgrate/cli 2026.611.3 → 2026.615.1

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.
@@ -1717,6 +1717,195 @@ var require_fxp = __commonJS({
1717
1717
  }
1718
1718
  });
1719
1719
 
1720
+ // ../vibgrate-core/dist/chunk-KML6OTXK.js
1721
+ function productForType(type) {
1722
+ switch (type) {
1723
+ case "node":
1724
+ return "nodejs";
1725
+ case "python":
1726
+ return "python";
1727
+ case "dotnet":
1728
+ return "dotnet";
1729
+ case "java":
1730
+ return "java";
1731
+ case "php":
1732
+ return "php";
1733
+ case "ruby":
1734
+ return "ruby";
1735
+ case "go":
1736
+ return "go";
1737
+ default:
1738
+ return null;
1739
+ }
1740
+ }
1741
+ function extractCycle(type, runtime) {
1742
+ if (!runtime) return null;
1743
+ switch (type) {
1744
+ case "python":
1745
+ case "php":
1746
+ case "ruby":
1747
+ case "go": {
1748
+ const m = runtime.match(/(\d+)\.(\d+)/);
1749
+ return m ? `${m[1]}.${m[2]}` : null;
1750
+ }
1751
+ case "dotnet": {
1752
+ const m = runtime.match(/(\d+)\.(\d+)/) ?? runtime.match(/(\d+)/);
1753
+ if (!m) return null;
1754
+ return m[2] !== void 0 ? `${m[1]}.${m[2]}` : `${m[1]}.0`;
1755
+ }
1756
+ case "node": {
1757
+ const m = runtime.match(/(\d+)/);
1758
+ return m ? m[1] : null;
1759
+ }
1760
+ case "java": {
1761
+ const legacy = runtime.match(/1\.(\d+)/);
1762
+ if (legacy) return legacy[1];
1763
+ const m = runtime.match(/(\d+)/);
1764
+ return m ? m[1] : null;
1765
+ }
1766
+ default:
1767
+ return null;
1768
+ }
1769
+ }
1770
+ function cycleEolStatus(cycle, cycles, now = Date.now()) {
1771
+ const entry = cycles.find((c) => String(c.cycle) === cycle);
1772
+ if (!entry) return null;
1773
+ if (typeof entry.eol === "boolean") return entry.eol;
1774
+ const eolMs = Date.parse(entry.eol);
1775
+ if (Number.isNaN(eolMs)) return null;
1776
+ return eolMs <= now;
1777
+ }
1778
+ function parseCycle(cycle) {
1779
+ const m = String(cycle).match(/^(\d+)(?:\.(\d+))?/);
1780
+ if (!m) return null;
1781
+ return { major: parseInt(m[1], 10), minor: m[2] ? parseInt(m[2], 10) : 0 };
1782
+ }
1783
+ function rank(v) {
1784
+ return v.major * 1e3 + v.minor;
1785
+ }
1786
+ function isReleased(cycle, now) {
1787
+ if (typeof cycle.releaseDate !== "string") return true;
1788
+ const ts3 = Date.parse(cycle.releaseDate);
1789
+ return Number.isNaN(ts3) ? true : ts3 <= now;
1790
+ }
1791
+ function isLts(cycle, now) {
1792
+ if (cycle.lts === true) return true;
1793
+ if (typeof cycle.lts === "string") {
1794
+ const ts3 = Date.parse(cycle.lts);
1795
+ return !Number.isNaN(ts3) && ts3 <= now;
1796
+ }
1797
+ return false;
1798
+ }
1799
+ function cyclesFor(catalog, product) {
1800
+ return catalog.products[product]?.cycles ?? [];
1801
+ }
1802
+ function latestStable(catalog, product, now = Date.now()) {
1803
+ let best = null;
1804
+ for (const cycle of cyclesFor(catalog, product)) {
1805
+ if (!isReleased(cycle, now)) continue;
1806
+ const v = parseCycle(cycle.cycle);
1807
+ if (v && (!best || rank(v) > rank(best))) best = v;
1808
+ }
1809
+ return best;
1810
+ }
1811
+ function latestLts(catalog, product, now = Date.now()) {
1812
+ let best = null;
1813
+ for (const cycle of cyclesFor(catalog, product)) {
1814
+ if (!isReleased(cycle, now) || !isLts(cycle, now)) continue;
1815
+ const v = parseCycle(cycle.cycle);
1816
+ if (v && (!best || rank(v) > rank(best))) best = v;
1817
+ }
1818
+ return best;
1819
+ }
1820
+ function eolDate(catalog, product, cycle) {
1821
+ const entry = cyclesFor(catalog, product).find((c) => String(c.cycle) === cycle);
1822
+ return entry && typeof entry.eol === "string" ? entry.eol : void 0;
1823
+ }
1824
+ function runtimeEolStatus(catalog, type, runtime, now = Date.now()) {
1825
+ const product = productForType(type);
1826
+ if (!product) return null;
1827
+ const cycle = extractCycle(type, runtime);
1828
+ if (!cycle) return null;
1829
+ return cycleEolStatus(cycle, cyclesFor(catalog, product), now);
1830
+ }
1831
+ var BUNDLED_RUNTIME_CATALOG = {
1832
+ generatedAt: "2026-06-01",
1833
+ source: "endoflife.date",
1834
+ products: {
1835
+ nodejs: {
1836
+ product: "nodejs",
1837
+ cycles: [
1838
+ { cycle: "24", releaseDate: "2025-05-06", lts: "2025-10-28", eol: "2028-04-30" },
1839
+ { cycle: "23", releaseDate: "2024-10-16", lts: false, eol: "2025-06-01" },
1840
+ { cycle: "22", releaseDate: "2024-04-24", lts: "2024-10-29", eol: "2027-04-30" },
1841
+ { cycle: "20", releaseDate: "2023-04-18", lts: "2023-10-24", eol: "2026-04-30" },
1842
+ { cycle: "18", releaseDate: "2022-04-19", lts: "2022-10-25", eol: "2025-04-30" }
1843
+ ]
1844
+ },
1845
+ python: {
1846
+ product: "python",
1847
+ cycles: [
1848
+ { cycle: "3.14", releaseDate: "2025-10-07", eol: "2030-10-01" },
1849
+ { cycle: "3.13", releaseDate: "2024-10-07", eol: "2029-10-01" },
1850
+ { cycle: "3.12", releaseDate: "2023-10-02", eol: "2028-10-01" },
1851
+ { cycle: "3.11", releaseDate: "2022-10-24", eol: "2027-10-01" },
1852
+ { cycle: "3.10", releaseDate: "2021-10-04", eol: "2026-10-01" },
1853
+ { cycle: "3.9", releaseDate: "2020-10-05", eol: "2025-10-01" },
1854
+ { cycle: "3.8", releaseDate: "2019-10-14", eol: "2024-10-07" }
1855
+ ]
1856
+ },
1857
+ dotnet: {
1858
+ product: "dotnet",
1859
+ cycles: [
1860
+ { cycle: "10.0", releaseDate: "2025-11-11", lts: true, eol: "2028-11-10" },
1861
+ { cycle: "9.0", releaseDate: "2024-11-12", lts: false, eol: "2026-05-12" },
1862
+ { cycle: "8.0", releaseDate: "2023-11-14", lts: true, eol: "2026-11-10" },
1863
+ { cycle: "7.0", releaseDate: "2022-11-08", lts: false, eol: "2024-05-14" },
1864
+ { cycle: "6.0", releaseDate: "2021-11-08", lts: true, eol: "2024-11-12" }
1865
+ ]
1866
+ },
1867
+ java: {
1868
+ product: "java",
1869
+ cycles: [
1870
+ { cycle: "25", releaseDate: "2025-09-16", lts: true, eol: "2033-09-01" },
1871
+ { cycle: "21", releaseDate: "2023-09-19", lts: true, eol: "2031-09-01" },
1872
+ { cycle: "17", releaseDate: "2021-09-14", lts: true, eol: "2029-09-01" },
1873
+ { cycle: "11", releaseDate: "2018-09-25", lts: true, eol: "2026-09-01" },
1874
+ { cycle: "8", releaseDate: "2014-03-18", lts: true, eol: "2030-12-01" }
1875
+ ]
1876
+ },
1877
+ php: {
1878
+ product: "php",
1879
+ cycles: [
1880
+ { cycle: "8.4", releaseDate: "2024-11-21", eol: "2028-12-31" },
1881
+ { cycle: "8.3", releaseDate: "2023-11-23", eol: "2027-12-31" },
1882
+ { cycle: "8.2", releaseDate: "2022-12-08", eol: "2026-12-31" },
1883
+ { cycle: "8.1", releaseDate: "2021-11-25", eol: "2025-12-31" },
1884
+ { cycle: "8.0", releaseDate: "2020-11-26", eol: "2023-11-26" }
1885
+ ]
1886
+ },
1887
+ ruby: {
1888
+ product: "ruby",
1889
+ cycles: [
1890
+ { cycle: "3.5", releaseDate: "2025-12-25", eol: "2029-03-31" },
1891
+ { cycle: "3.4", releaseDate: "2024-12-25", eol: "2028-03-31" },
1892
+ { cycle: "3.3", releaseDate: "2023-12-25", eol: "2027-03-31" },
1893
+ { cycle: "3.2", releaseDate: "2022-12-25", eol: "2026-03-31" },
1894
+ { cycle: "3.1", releaseDate: "2021-12-25", eol: "2025-03-31" }
1895
+ ]
1896
+ },
1897
+ go: {
1898
+ product: "go",
1899
+ cycles: [
1900
+ { cycle: "1.25", releaseDate: "2025-08-13", eol: false },
1901
+ { cycle: "1.24", releaseDate: "2025-02-11", eol: false },
1902
+ { cycle: "1.23", releaseDate: "2024-08-13", eol: true },
1903
+ { cycle: "1.22", releaseDate: "2024-02-06", eol: true }
1904
+ ]
1905
+ }
1906
+ }
1907
+ };
1908
+
1720
1909
  // ../vibgrate-core/dist/index.js
1721
1910
  var semver2 = __toESM(require_semver(), 1);
1722
1911
  var semver = __toESM(require_semver(), 1);
@@ -1799,13 +1988,16 @@ import * as path30 from "path";
1799
1988
  import * as crypto2 from "crypto";
1800
1989
  import * as fs6 from "fs/promises";
1801
1990
  import * as path31 from "path";
1802
- import * as path33 from "path";
1991
+ import * as path34 from "path";
1803
1992
  import chalk3 from "chalk";
1993
+ import * as os4 from "os";
1994
+ import * as path32 from "path";
1995
+ import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
1804
1996
  import chalk2 from "chalk";
1805
1997
  import { writeFileSync, mkdirSync } from "fs";
1806
1998
  import { dirname as dirname182, basename as basename192 } from "path";
1807
1999
  import * as fs7 from "fs/promises";
1808
- import * as path32 from "path";
2000
+ import * as path33 from "path";
1809
2001
  var CONFIG_FILES = [
1810
2002
  "vibgrate.config.ts",
1811
2003
  "vibgrate.config.js",
@@ -1999,10 +2191,10 @@ async function appendExcludePatterns(rootDir, newPatterns) {
1999
2191
  }
2000
2192
  var MS_PER_DAY = 864e5;
2001
2193
  var DAYS_PER_YEAR = 365.25;
2002
- function ageDaysBetween(resolvedVersion, latestStable, releaseDates) {
2003
- if (!resolvedVersion || !latestStable || !releaseDates) return null;
2194
+ function ageDaysBetween(resolvedVersion, latestStable2, releaseDates) {
2195
+ if (!resolvedVersion || !latestStable2 || !releaseDates) return null;
2004
2196
  const resolvedIso = releaseDates[resolvedVersion];
2005
- const latestIso = releaseDates[latestStable];
2197
+ const latestIso = releaseDates[latestStable2];
2006
2198
  if (!resolvedIso || !latestIso) return null;
2007
2199
  const resolvedMs = Date.parse(resolvedIso);
2008
2200
  const latestMs = Date.parse(latestIso);
@@ -2089,11 +2281,29 @@ function dependencyScore(projects) {
2089
2281
  const twoPct = totalTwo / total;
2090
2282
  return clamp(Math.round(currentPct * 100 - onePct * 10 - twoPct * 40), 0, 100);
2091
2283
  }
2284
+ var MONTH_MS = 1e3 * 60 * 60 * 24 * 30;
2092
2285
  function eolScore(projects) {
2093
- const hasRuntimeData = projects.some((p) => p.runtimeMajorsBehind !== void 0);
2286
+ const hasRuntimeData = projects.some(
2287
+ (p) => p.runtimeMajorsBehind !== void 0 || p.runtimeEol !== void 0 && p.runtimeEol !== null
2288
+ );
2094
2289
  if (!hasRuntimeData) return null;
2095
2290
  let score = 100;
2291
+ const now = Date.now();
2096
2292
  for (const p of projects) {
2293
+ if (p.runtimeEol === true) {
2294
+ score = Math.min(score, 0);
2295
+ continue;
2296
+ }
2297
+ if (p.runtimeEol === false) {
2298
+ if (p.runtimeEolDate) {
2299
+ const remaining = Date.parse(p.runtimeEolDate) - now;
2300
+ if (!Number.isNaN(remaining)) {
2301
+ if (remaining <= 6 * MONTH_MS) score = Math.min(score, 40);
2302
+ else if (remaining <= 12 * MONTH_MS) score = Math.min(score, 75);
2303
+ }
2304
+ }
2305
+ continue;
2306
+ }
2097
2307
  if (p.type === "node" && p.runtimeMajorsBehind !== void 0) {
2098
2308
  if (p.runtimeMajorsBehind >= 3) score = Math.min(score, 0);
2099
2309
  else if (p.runtimeMajorsBehind >= 2) score = Math.min(score, 30);
@@ -2201,8 +2411,16 @@ function generateFindings(projects, config) {
2201
2411
  };
2202
2412
  const findings = [];
2203
2413
  for (const project of projects) {
2204
- if (project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind >= 3) {
2205
- const runtimeLabel = project.type === "node" ? "Node.js" : project.type === "dotnet" ? ".NET" : project.type === "python" ? "Python" : project.type === "java" ? "Java" : project.type;
2414
+ const runtimeLabel = project.type === "node" ? "Node.js" : project.type === "dotnet" ? ".NET" : project.type === "python" ? "Python" : project.type === "java" ? "Java" : project.type;
2415
+ if (project.runtimeEol === true) {
2416
+ const when = project.runtimeEolDate ? ` on ${project.runtimeEolDate}` : "";
2417
+ findings.push({
2418
+ ruleId: "vibgrate/runtime-eol",
2419
+ level: "error",
2420
+ message: `${runtimeLabel} runtime "${project.runtime}" reached end-of-life${when} (latest: ${project.runtimeLatest}).`,
2421
+ location: project.path
2422
+ });
2423
+ } else if (project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind >= 3) {
2206
2424
  findings.push({
2207
2425
  ruleId: "vibgrate/runtime-eol",
2208
2426
  level: "error",
@@ -2210,7 +2428,6 @@ function generateFindings(projects, config) {
2210
2428
  location: project.path
2211
2429
  });
2212
2430
  } else if (project.runtimeMajorsBehind !== void 0 && project.runtimeMajorsBehind >= 2) {
2213
- const runtimeLabel = project.type === "node" ? "Node.js" : project.type === "dotnet" ? ".NET" : project.type === "python" ? "Python" : project.type === "java" ? "Java" : project.type;
2214
2431
  findings.push({
2215
2432
  ruleId: "vibgrate/runtime-lag",
2216
2433
  level: "warning",
@@ -3991,7 +4208,7 @@ var KNOWN_FRAMEWORKS = {
3991
4208
  "storybook": "Storybook",
3992
4209
  "@storybook/react": "Storybook"
3993
4210
  };
3994
- async function scanNodeProjects(rootDir, npmCache, cache, projectScanTimeout) {
4211
+ async function scanNodeProjects(rootDir, npmCache, cache, projectScanTimeout, catalog = BUNDLED_RUNTIME_CATALOG) {
3995
4212
  const packageJsonFiles = cache ? await cache.findPackageJsonFiles(rootDir) : await findPackageJsonFiles(rootDir);
3996
4213
  const results = [];
3997
4214
  const packageNameToPath = /* @__PURE__ */ new Map();
@@ -4055,7 +4272,7 @@ async function scanNodeProjects(rootDir, npmCache, cache, projectScanTimeout) {
4055
4272
  }
4056
4273
  const scannedProjects = await Promise.all(packageJsonFiles.map(async (pjPath) => projectSem.run(async () => {
4057
4274
  try {
4058
- const scanPromise = scanOnePackageJson(pjPath, rootDir, npmCache, cache);
4275
+ const scanPromise = scanOnePackageJson(pjPath, rootDir, npmCache, cache, catalog);
4059
4276
  const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
4060
4277
  if (result.ok) {
4061
4278
  return result.value;
@@ -4101,21 +4318,26 @@ async function scanNodeProjects(rootDir, npmCache, cache, projectScanTimeout) {
4101
4318
  }
4102
4319
  return results;
4103
4320
  }
4104
- async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
4321
+ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache, catalog) {
4105
4322
  const pj = cache ? await cache.readJsonFile(packageJsonPath) : await readJsonFile(packageJsonPath);
4106
4323
  const absProjectPath = path3.dirname(packageJsonPath);
4107
4324
  const projectPath = path3.relative(rootDir, absProjectPath) || ".";
4108
4325
  const nodeEngine = pj.engines?.node ?? void 0;
4109
4326
  let runtimeLatest;
4110
4327
  let runtimeMajorsBehind;
4328
+ let runtimeEol;
4329
+ let runtimeEolDate;
4111
4330
  if (nodeEngine) {
4112
- const latestLtsMajor = 22;
4331
+ const latest = latestLts(catalog, "nodejs");
4113
4332
  const parsed = semver2.minVersion(nodeEngine);
4114
- if (parsed) {
4333
+ if (latest && parsed) {
4115
4334
  const currentMajor = semver2.major(parsed);
4116
- runtimeLatest = `${latestLtsMajor}.0.0`;
4117
- runtimeMajorsBehind = Math.max(0, latestLtsMajor - currentMajor);
4335
+ runtimeLatest = `${latest.major}.0.0`;
4336
+ runtimeMajorsBehind = Math.max(0, latest.major - currentMajor);
4118
4337
  }
4338
+ runtimeEol = runtimeEolStatus(catalog, "node", nodeEngine);
4339
+ const cycle = extractCycle("node", nodeEngine);
4340
+ if (cycle) runtimeEolDate = eolDate(catalog, "nodejs", cycle);
4119
4341
  }
4120
4342
  const sections = [
4121
4343
  { name: "dependencies", deps: pj.dependencies },
@@ -4142,15 +4364,15 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
4142
4364
  const resolved = await Promise.all(metaPromises);
4143
4365
  for (const { pkg: pkg2, section, spec, meta } of resolved) {
4144
4366
  const resolvedVersion = meta.stableVersions.length > 0 ? semver2.maxSatisfying(meta.stableVersions, spec) ?? null : null;
4145
- const latestStable = meta.latestStableOverall;
4367
+ const latestStable2 = meta.latestStableOverall;
4146
4368
  let majorsBehind = null;
4147
4369
  let drift = "unknown";
4148
- if (resolvedVersion && latestStable) {
4370
+ if (resolvedVersion && latestStable2) {
4149
4371
  const currentMajor = semver2.major(resolvedVersion);
4150
- const latestMajor = semver2.major(latestStable);
4372
+ const latestMajor = semver2.major(latestStable2);
4151
4373
  majorsBehind = latestMajor - currentMajor;
4152
4374
  if (majorsBehind === 0) {
4153
- drift = semver2.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
4375
+ drift = semver2.eq(resolvedVersion, latestStable2) ? "current" : "minor-behind";
4154
4376
  } else {
4155
4377
  drift = "major-behind";
4156
4378
  }
@@ -4160,14 +4382,14 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
4160
4382
  } else {
4161
4383
  buckets.unknown++;
4162
4384
  }
4163
- const ageDays = ageDaysBetween(resolvedVersion, latestStable, meta.releaseDates);
4385
+ const ageDays = ageDaysBetween(resolvedVersion, latestStable2, meta.releaseDates);
4164
4386
  const libyears = daysToLibyears(ageDays);
4165
4387
  dependencies.push({
4166
4388
  package: pkg2,
4167
4389
  section,
4168
4390
  currentSpec: spec,
4169
4391
  resolvedVersion,
4170
- latestStable,
4392
+ latestStable: latestStable2,
4171
4393
  majorsBehind,
4172
4394
  drift,
4173
4395
  license: buildDependencyLicense(meta.license, "registry"),
@@ -4178,7 +4400,7 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
4178
4400
  frameworks.push({
4179
4401
  name: KNOWN_FRAMEWORKS[pkg2],
4180
4402
  currentVersion: resolvedVersion,
4181
- latestVersion: latestStable,
4403
+ latestVersion: latestStable2,
4182
4404
  majorsBehind
4183
4405
  });
4184
4406
  }
@@ -4218,6 +4440,8 @@ async function scanOnePackageJson(packageJsonPath, rootDir, npmCache, cache) {
4218
4440
  runtime: nodeEngine,
4219
4441
  runtimeLatest,
4220
4442
  runtimeMajorsBehind,
4443
+ runtimeEol,
4444
+ runtimeEolDate,
4221
4445
  frameworks,
4222
4446
  dependencies,
4223
4447
  dependencyAgeBuckets: buckets,
@@ -4442,7 +4666,6 @@ var KNOWN_DOTNET_FRAMEWORKS = {
4442
4666
  // ── Real-time ──
4443
4667
  "Microsoft.AspNetCore.SignalR.Client": "SignalR Client"
4444
4668
  };
4445
- var LATEST_DOTNET_MAJOR = 9;
4446
4669
  function normalizePath(p) {
4447
4670
  return p.replace(/\\/g, "/");
4448
4671
  }
@@ -4506,7 +4729,7 @@ function parseDotnetProjectFile(xml, filePath) {
4506
4729
  projectName: stripDotnetProjectExtension(filePath)
4507
4730
  };
4508
4731
  }
4509
- async function scanDotnetProjects(rootDir, nugetCache, cache, projectScanTimeout) {
4732
+ async function scanDotnetProjects(rootDir, nugetCache, cache, projectScanTimeout, catalog = BUNDLED_RUNTIME_CATALOG) {
4510
4733
  const projectFiles = cache ? await cache.findFiles(rootDir, isDotnetProjectFile) : await findFiles(rootDir, isDotnetProjectFile);
4511
4734
  const slnFiles = cache ? await cache.findSolutionFiles(rootDir) : await findSolutionFiles(rootDir);
4512
4735
  const slnProjectPaths = /* @__PURE__ */ new Set();
@@ -4530,7 +4753,7 @@ async function scanDotnetProjects(rootDir, nugetCache, cache, projectScanTimeout
4530
4753
  const STUCK_TIMEOUT_MS = projectScanTimeout ?? cache?.projectScanTimeout ?? 18e4;
4531
4754
  for (const csprojPath of allCsprojFiles) {
4532
4755
  try {
4533
- const scanPromise = scanOneDotnetProjectFile(csprojPath, rootDir, nugetCache, cache);
4756
+ const scanPromise = scanOneDotnetProjectFile(csprojPath, rootDir, nugetCache, cache, catalog);
4534
4757
  const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
4535
4758
  if (result.ok) {
4536
4759
  results.push(result.value);
@@ -4551,18 +4774,24 @@ async function scanDotnetProjects(rootDir, nugetCache, cache, projectScanTimeout
4551
4774
  }
4552
4775
  return results;
4553
4776
  }
4554
- async function scanOneDotnetProjectFile(csprojPath, rootDir, nugetCache, cache) {
4777
+ async function scanOneDotnetProjectFile(csprojPath, rootDir, nugetCache, cache, catalog) {
4555
4778
  const xml = cache ? await cache.readTextFile(csprojPath) : await readTextFile(csprojPath);
4556
4779
  const data = parseDotnetProjectFile(xml, csprojPath);
4557
4780
  const csprojDir = path4.dirname(csprojPath);
4558
4781
  const primaryTfm = data.targetFrameworks[0];
4782
+ const dotnetLatest = latestStable(catalog, "dotnet")?.major;
4559
4783
  let runtimeMajorsBehind;
4784
+ let runtimeEol;
4785
+ let runtimeEolDate;
4560
4786
  let targetFramework = primaryTfm;
4561
4787
  if (primaryTfm) {
4562
4788
  const major11 = parseTfmMajor(primaryTfm);
4563
- if (major11 !== null) {
4564
- runtimeMajorsBehind = Math.max(0, LATEST_DOTNET_MAJOR - major11);
4789
+ if (major11 !== null && dotnetLatest !== void 0) {
4790
+ runtimeMajorsBehind = Math.max(0, dotnetLatest - major11);
4565
4791
  }
4792
+ runtimeEol = runtimeEolStatus(catalog, "dotnet", primaryTfm);
4793
+ const cycle = extractCycle("dotnet", primaryTfm);
4794
+ if (cycle) runtimeEolDate = eolDate(catalog, "dotnet", cycle);
4566
4795
  }
4567
4796
  const dependencies = [];
4568
4797
  const bucketsMut = { current: 0, oneBehind: 0, twoPlusBehind: 0, unknown: 0 };
@@ -4574,15 +4803,15 @@ async function scanOneDotnetProjectFile(csprojPath, rootDir, nugetCache, cache)
4574
4803
  const resolved = await Promise.all(metaPromises);
4575
4804
  for (const { ref, meta } of resolved) {
4576
4805
  const resolvedVersion = semver3.valid(ref.version) ? ref.version : null;
4577
- const latestStable = meta.latestStableOverall;
4806
+ const latestStable2 = meta.latestStableOverall;
4578
4807
  let majorsBehind = null;
4579
4808
  let drift = "unknown";
4580
- if (resolvedVersion && latestStable) {
4809
+ if (resolvedVersion && latestStable2) {
4581
4810
  const currentMajor = semver3.major(resolvedVersion);
4582
- const latestMajor = semver3.major(latestStable);
4811
+ const latestMajor = semver3.major(latestStable2);
4583
4812
  majorsBehind = latestMajor - currentMajor;
4584
4813
  if (majorsBehind === 0) {
4585
- drift = semver3.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
4814
+ drift = semver3.eq(resolvedVersion, latestStable2) ? "current" : "minor-behind";
4586
4815
  } else if (majorsBehind > 0) {
4587
4816
  drift = "major-behind";
4588
4817
  } else {
@@ -4599,7 +4828,7 @@ async function scanOneDotnetProjectFile(csprojPath, rootDir, nugetCache, cache)
4599
4828
  section: "dependencies",
4600
4829
  currentSpec: ref.version,
4601
4830
  resolvedVersion,
4602
- latestStable,
4831
+ latestStable: latestStable2,
4603
4832
  majorsBehind,
4604
4833
  drift
4605
4834
  });
@@ -4659,8 +4888,10 @@ async function scanOneDotnetProjectFile(csprojPath, rootDir, nugetCache, cache)
4659
4888
  name: data.projectName,
4660
4889
  targetFramework,
4661
4890
  runtime: primaryTfm,
4662
- runtimeLatest: `net${LATEST_DOTNET_MAJOR}.0`,
4891
+ runtimeLatest: dotnetLatest !== void 0 ? `net${dotnetLatest}.0` : void 0,
4663
4892
  runtimeMajorsBehind,
4893
+ runtimeEol,
4894
+ runtimeEolDate,
4664
4895
  frameworks,
4665
4896
  dependencies,
4666
4897
  dependencyAgeBuckets: buckets,
@@ -4773,7 +5004,6 @@ var KNOWN_PYTHON_FRAMEWORKS = {
4773
5004
  "opentelemetry-api": "OpenTelemetry",
4774
5005
  "prometheus-client": "prometheus-client"
4775
5006
  };
4776
- var LATEST_PYTHON_MINOR = { major: 3, minor: 13 };
4777
5007
  function parseRequirementLine(line) {
4778
5008
  const trimmed = line.trim();
4779
5009
  if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) return null;
@@ -4903,7 +5133,7 @@ var PYTHON_MANIFEST_FILES = /* @__PURE__ */ new Set([
4903
5133
  "setup.cfg",
4904
5134
  "Pipfile"
4905
5135
  ]);
4906
- async function scanPythonProjects(rootDir, pypiCache, cache, projectScanTimeout) {
5136
+ async function scanPythonProjects(rootDir, pypiCache, cache, projectScanTimeout, catalog = BUNDLED_RUNTIME_CATALOG) {
4907
5137
  const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name)) : await findPythonManifests(rootDir);
4908
5138
  const projectDirs = /* @__PURE__ */ new Map();
4909
5139
  for (const f of manifestFiles) {
@@ -4915,7 +5145,7 @@ async function scanPythonProjects(rootDir, pypiCache, cache, projectScanTimeout)
4915
5145
  const STUCK_TIMEOUT_MS = projectScanTimeout ?? cache?.projectScanTimeout ?? 18e4;
4916
5146
  for (const [dir, files] of projectDirs) {
4917
5147
  try {
4918
- const scanPromise = scanOnePythonProject(dir, files, rootDir, pypiCache, cache);
5148
+ const scanPromise = scanOnePythonProject(dir, files, rootDir, pypiCache, cache, catalog);
4919
5149
  const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
4920
5150
  if (result.ok) {
4921
5151
  results.push(result.value);
@@ -4938,7 +5168,7 @@ async function findPythonManifests(rootDir) {
4938
5168
  const { findFiles: findFiles2 } = await import("./fs-PXXYZATK-EW5LCUA7.js");
4939
5169
  return findFiles2(rootDir, (name) => PYTHON_MANIFEST_FILES.has(name) || /^requirements.*\.txt$/.test(name));
4940
5170
  }
4941
- async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cache) {
5171
+ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cache, catalog) {
4942
5172
  const relDir = path5.relative(rootDir, dir) || ".";
4943
5173
  let projectName = path5.basename(dir === rootDir ? rootDir : dir);
4944
5174
  let pythonVersion;
@@ -4972,18 +5202,26 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
4972
5202
  }
4973
5203
  let runtimeMajorsBehind;
4974
5204
  let runtimeLatest;
5205
+ let runtimeEol;
5206
+ let runtimeEolDate;
4975
5207
  if (pythonVersion) {
4976
5208
  const verMatch = pythonVersion.match(/(\d+)\.(\d+)/);
4977
5209
  if (verMatch) {
4978
5210
  const reqMajor = parseInt(verMatch[1], 10);
4979
5211
  const reqMinor = parseInt(verMatch[2], 10);
4980
- if (reqMajor === LATEST_PYTHON_MINOR.major) {
4981
- runtimeMajorsBehind = Math.max(0, LATEST_PYTHON_MINOR.minor - reqMinor);
4982
- } else if (reqMajor < LATEST_PYTHON_MINOR.major) {
4983
- runtimeMajorsBehind = LATEST_PYTHON_MINOR.minor + (LATEST_PYTHON_MINOR.major - reqMajor) * 10;
5212
+ const LATEST_PYTHON_MINOR = latestStable(catalog, "python");
5213
+ if (LATEST_PYTHON_MINOR) {
5214
+ if (reqMajor === LATEST_PYTHON_MINOR.major) {
5215
+ runtimeMajorsBehind = Math.max(0, LATEST_PYTHON_MINOR.minor - reqMinor);
5216
+ } else if (reqMajor < LATEST_PYTHON_MINOR.major) {
5217
+ runtimeMajorsBehind = LATEST_PYTHON_MINOR.minor + (LATEST_PYTHON_MINOR.major - reqMajor) * 10;
5218
+ }
5219
+ runtimeLatest = `${LATEST_PYTHON_MINOR.major}.${LATEST_PYTHON_MINOR.minor}`;
4984
5220
  }
4985
- runtimeLatest = `${LATEST_PYTHON_MINOR.major}.${LATEST_PYTHON_MINOR.minor}`;
4986
5221
  }
5222
+ runtimeEol = runtimeEolStatus(catalog, "python", pythonVersion);
5223
+ const cycle = extractCycle("python", pythonVersion);
5224
+ if (cycle) runtimeEolDate = eolDate(catalog, "python", cycle);
4987
5225
  }
4988
5226
  const dependencies = [];
4989
5227
  const frameworks = [];
@@ -4997,15 +5235,15 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
4997
5235
  for (const { dep, meta } of resolved) {
4998
5236
  const pinnedVersion = extractPinnedVersion(dep.spec);
4999
5237
  const resolvedVersion = pinnedVersion ? pep440ToSemver(pinnedVersion) : null;
5000
- const latestStable = meta.latestStableOverall;
5238
+ const latestStable2 = meta.latestStableOverall;
5001
5239
  let majorsBehind = null;
5002
5240
  let drift = "unknown";
5003
- if (resolvedVersion && latestStable) {
5241
+ if (resolvedVersion && latestStable2) {
5004
5242
  const currentMajor = semver4.major(resolvedVersion);
5005
- const latestMajor = semver4.major(latestStable);
5243
+ const latestMajor = semver4.major(latestStable2);
5006
5244
  majorsBehind = latestMajor - currentMajor;
5007
5245
  if (majorsBehind === 0) {
5008
- drift = semver4.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
5246
+ drift = semver4.eq(resolvedVersion, latestStable2) ? "current" : "minor-behind";
5009
5247
  } else if (majorsBehind > 0) {
5010
5248
  drift = "major-behind";
5011
5249
  } else {
@@ -5022,7 +5260,7 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
5022
5260
  section: "dependencies",
5023
5261
  currentSpec: dep.spec || "*",
5024
5262
  resolvedVersion,
5025
- latestStable,
5263
+ latestStable: latestStable2,
5026
5264
  majorsBehind,
5027
5265
  drift
5028
5266
  });
@@ -5030,7 +5268,7 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
5030
5268
  frameworks.push({
5031
5269
  name: KNOWN_PYTHON_FRAMEWORKS[dep.normalisedName],
5032
5270
  currentVersion: resolvedVersion,
5033
- latestVersion: latestStable,
5271
+ latestVersion: latestStable2,
5034
5272
  majorsBehind
5035
5273
  });
5036
5274
  }
@@ -5053,6 +5291,8 @@ async function scanOnePythonProject(dir, manifestFiles, rootDir, pypiCache, cach
5053
5291
  runtime: pythonVersion,
5054
5292
  runtimeLatest,
5055
5293
  runtimeMajorsBehind,
5294
+ runtimeEol,
5295
+ runtimeEolDate,
5056
5296
  frameworks,
5057
5297
  dependencies,
5058
5298
  dependencyAgeBuckets: buckets,
@@ -5156,7 +5396,6 @@ var KNOWN_JAVA_FRAMEWORKS = {
5156
5396
  "io.projectreactor:reactor-core": "Project Reactor",
5157
5397
  "io.reactivex.rxjava3:rxjava": "RxJava 3"
5158
5398
  };
5159
- var LATEST_JAVA_LTS = 21;
5160
5399
  function parsePom(xml, filePath) {
5161
5400
  const parsed = parser2.parse(xml);
5162
5401
  const project = parsed?.project;
@@ -5296,7 +5535,7 @@ function mavenToSemver(ver) {
5296
5535
  return semver5.valid(v);
5297
5536
  }
5298
5537
  var JAVA_MANIFEST_FILES = /* @__PURE__ */ new Set(["pom.xml", "build.gradle", "build.gradle.kts"]);
5299
- async function scanJavaProjects(rootDir, mavenCache, cache, projectScanTimeout) {
5538
+ async function scanJavaProjects(rootDir, mavenCache, cache, projectScanTimeout, catalog = BUNDLED_RUNTIME_CATALOG) {
5300
5539
  const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => JAVA_MANIFEST_FILES.has(name)) : await findJavaManifests(rootDir);
5301
5540
  const projectDirs = /* @__PURE__ */ new Map();
5302
5541
  for (const f of manifestFiles) {
@@ -5308,7 +5547,7 @@ async function scanJavaProjects(rootDir, mavenCache, cache, projectScanTimeout)
5308
5547
  const STUCK_TIMEOUT_MS = projectScanTimeout ?? cache?.projectScanTimeout ?? 18e4;
5309
5548
  for (const [dir, files] of projectDirs) {
5310
5549
  try {
5311
- const scanPromise = scanOneJavaProject(dir, files, rootDir, mavenCache, cache);
5550
+ const scanPromise = scanOneJavaProject(dir, files, rootDir, mavenCache, cache, catalog);
5312
5551
  const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
5313
5552
  if (result.ok) {
5314
5553
  results.push(result.value);
@@ -5335,7 +5574,7 @@ async function findJavaManifests(rootDir) {
5335
5574
  const { findFiles: findFiles2 } = await import("./fs-PXXYZATK-EW5LCUA7.js");
5336
5575
  return findFiles2(rootDir, (name) => JAVA_MANIFEST_FILES.has(name));
5337
5576
  }
5338
- async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache) {
5577
+ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache, catalog) {
5339
5578
  const relDir = path6.relative(rootDir, dir) || ".";
5340
5579
  let projectName = path6.basename(dir === rootDir ? rootDir : dir);
5341
5580
  let javaVersion;
@@ -5374,14 +5613,19 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
5374
5613
  }
5375
5614
  }
5376
5615
  let runtimeMajorsBehind;
5377
- let runtimeLatest;
5616
+ let runtimeEol;
5617
+ let runtimeEolDate;
5618
+ const javaLts = latestLts(catalog, "java")?.major;
5619
+ const runtimeLatest = javaLts !== void 0 ? String(javaLts) : void 0;
5378
5620
  if (javaVersion) {
5379
5621
  const jvMatch = javaVersion.match(/^(1\.)?(\d+)/);
5380
5622
  if (jvMatch) {
5381
5623
  const major11 = jvMatch[1] ? parseInt(jvMatch[2], 10) : parseInt(jvMatch[2], 10);
5382
- runtimeMajorsBehind = Math.max(0, LATEST_JAVA_LTS - major11);
5383
- runtimeLatest = String(LATEST_JAVA_LTS);
5624
+ if (javaLts !== void 0) runtimeMajorsBehind = Math.max(0, javaLts - major11);
5384
5625
  }
5626
+ runtimeEol = runtimeEolStatus(catalog, "java", javaVersion);
5627
+ const cycle = extractCycle("java", javaVersion);
5628
+ if (cycle) runtimeEolDate = eolDate(catalog, "java", cycle);
5385
5629
  }
5386
5630
  const dependencies = [];
5387
5631
  const frameworks = [];
@@ -5394,15 +5638,15 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
5394
5638
  const resolved = await Promise.all(metaPromises);
5395
5639
  for (const { key, dep, meta } of resolved) {
5396
5640
  const resolvedVersion = mavenToSemver(dep.version);
5397
- const latestStable = meta.latestStableOverall;
5641
+ const latestStable2 = meta.latestStableOverall;
5398
5642
  let majorsBehind = null;
5399
5643
  let drift = "unknown";
5400
- if (resolvedVersion && latestStable) {
5644
+ if (resolvedVersion && latestStable2) {
5401
5645
  const currentMajor = semver5.major(resolvedVersion);
5402
- const latestMajor = semver5.major(latestStable);
5646
+ const latestMajor = semver5.major(latestStable2);
5403
5647
  majorsBehind = latestMajor - currentMajor;
5404
5648
  if (majorsBehind === 0) {
5405
- drift = semver5.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
5649
+ drift = semver5.eq(resolvedVersion, latestStable2) ? "current" : "minor-behind";
5406
5650
  } else if (majorsBehind > 0) {
5407
5651
  drift = "major-behind";
5408
5652
  } else {
@@ -5419,7 +5663,7 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
5419
5663
  section: "dependencies",
5420
5664
  currentSpec: dep.version,
5421
5665
  resolvedVersion,
5422
- latestStable,
5666
+ latestStable: latestStable2,
5423
5667
  majorsBehind,
5424
5668
  drift
5425
5669
  });
@@ -5427,7 +5671,7 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
5427
5671
  frameworks.push({
5428
5672
  name: KNOWN_JAVA_FRAMEWORKS[key],
5429
5673
  currentVersion: resolvedVersion,
5430
- latestVersion: latestStable,
5674
+ latestVersion: latestStable2,
5431
5675
  majorsBehind
5432
5676
  });
5433
5677
  }
@@ -5448,8 +5692,10 @@ async function scanOneJavaProject(dir, manifestFiles, rootDir, mavenCache, cache
5448
5692
  path: relDir,
5449
5693
  name: projectName,
5450
5694
  runtime: javaVersion ? `Java ${javaVersion}` : void 0,
5451
- runtimeLatest: String(LATEST_JAVA_LTS),
5695
+ runtimeLatest,
5452
5696
  runtimeMajorsBehind,
5697
+ runtimeEol,
5698
+ runtimeEolDate,
5453
5699
  targetFramework: javaVersion ? `Java ${javaVersion}` : void 0,
5454
5700
  frameworks,
5455
5701
  dependencies,
@@ -5575,7 +5821,6 @@ var KNOWN_RUBY_FRAMEWORKS = {
5575
5821
  "dotenv": "dotenv",
5576
5822
  "figaro": "Figaro"
5577
5823
  };
5578
- var LATEST_RUBY_MINOR = { major: 3, minor: 4 };
5579
5824
  function parseGemfileLine(line) {
5580
5825
  const trimmed = line.trim();
5581
5826
  if (!trimmed || trimmed.startsWith("#")) return null;
@@ -5665,7 +5910,7 @@ var RUBY_MANIFEST_FILES = /* @__PURE__ */ new Set([
5665
5910
  function isGemspec(name) {
5666
5911
  return name.endsWith(".gemspec");
5667
5912
  }
5668
- async function scanRubyProjects(rootDir, rubygemsCache, cache, projectScanTimeout) {
5913
+ async function scanRubyProjects(rootDir, rubygemsCache, cache, projectScanTimeout, catalog = BUNDLED_RUNTIME_CATALOG) {
5669
5914
  const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => RUBY_MANIFEST_FILES.has(name) || isGemspec(name)) : await findRubyManifests(rootDir);
5670
5915
  const projectDirs = /* @__PURE__ */ new Map();
5671
5916
  for (const f of manifestFiles) {
@@ -5677,7 +5922,7 @@ async function scanRubyProjects(rootDir, rubygemsCache, cache, projectScanTimeou
5677
5922
  const STUCK_TIMEOUT_MS = projectScanTimeout ?? cache?.projectScanTimeout ?? 18e4;
5678
5923
  for (const [dir, files] of projectDirs) {
5679
5924
  try {
5680
- const scanPromise = scanOneRubyProject(dir, files, rootDir, rubygemsCache, cache);
5925
+ const scanPromise = scanOneRubyProject(dir, files, rootDir, rubygemsCache, cache, catalog);
5681
5926
  const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
5682
5927
  if (result.ok) {
5683
5928
  results.push(result.value);
@@ -5700,7 +5945,7 @@ async function findRubyManifests(rootDir) {
5700
5945
  const { findFiles: findFiles2 } = await import("./fs-PXXYZATK-EW5LCUA7.js");
5701
5946
  return findFiles2(rootDir, (name) => RUBY_MANIFEST_FILES.has(name) || isGemspec(name));
5702
5947
  }
5703
- async function scanOneRubyProject(dir, manifestFiles, rootDir, rubygemsCache, cache) {
5948
+ async function scanOneRubyProject(dir, manifestFiles, rootDir, rubygemsCache, cache, catalog) {
5704
5949
  const relDir = path7.relative(rootDir, dir) || ".";
5705
5950
  let projectName = path7.basename(dir === rootDir ? rootDir : dir);
5706
5951
  let rubyVersion;
@@ -5724,18 +5969,26 @@ async function scanOneRubyProject(dir, manifestFiles, rootDir, rubygemsCache, ca
5724
5969
  }
5725
5970
  let runtimeMajorsBehind;
5726
5971
  let runtimeLatest;
5972
+ let runtimeEol;
5973
+ let runtimeEolDate;
5727
5974
  if (rubyVersion) {
5728
5975
  const verMatch = rubyVersion.match(/(\d+)\.(\d+)/);
5729
5976
  if (verMatch) {
5730
5977
  const reqMajor = parseInt(verMatch[1], 10);
5731
5978
  const reqMinor = parseInt(verMatch[2], 10);
5732
- if (reqMajor === LATEST_RUBY_MINOR.major) {
5733
- runtimeMajorsBehind = Math.max(0, LATEST_RUBY_MINOR.minor - reqMinor);
5734
- } else if (reqMajor < LATEST_RUBY_MINOR.major) {
5735
- runtimeMajorsBehind = LATEST_RUBY_MINOR.minor + (LATEST_RUBY_MINOR.major - reqMajor) * 10;
5979
+ const LATEST_RUBY_MINOR = latestStable(catalog, "ruby");
5980
+ if (LATEST_RUBY_MINOR) {
5981
+ if (reqMajor === LATEST_RUBY_MINOR.major) {
5982
+ runtimeMajorsBehind = Math.max(0, LATEST_RUBY_MINOR.minor - reqMinor);
5983
+ } else if (reqMajor < LATEST_RUBY_MINOR.major) {
5984
+ runtimeMajorsBehind = LATEST_RUBY_MINOR.minor + (LATEST_RUBY_MINOR.major - reqMajor) * 10;
5985
+ }
5986
+ runtimeLatest = `${LATEST_RUBY_MINOR.major}.${LATEST_RUBY_MINOR.minor}`;
5736
5987
  }
5737
- runtimeLatest = `${LATEST_RUBY_MINOR.major}.${LATEST_RUBY_MINOR.minor}`;
5738
5988
  }
5989
+ runtimeEol = runtimeEolStatus(catalog, "ruby", rubyVersion);
5990
+ const cycle = extractCycle("ruby", rubyVersion);
5991
+ if (cycle) runtimeEolDate = eolDate(catalog, "ruby", cycle);
5739
5992
  }
5740
5993
  const dependencies = [];
5741
5994
  const frameworks = [];
@@ -5749,15 +6002,15 @@ async function scanOneRubyProject(dir, manifestFiles, rootDir, rubygemsCache, ca
5749
6002
  for (const { dep, meta } of resolved) {
5750
6003
  const rawVersion = extractGemVersion(dep.spec);
5751
6004
  const resolvedVersion = rawVersion ? rubyVersionToSemver(rawVersion) : null;
5752
- const latestStable = meta.latestStableOverall;
6005
+ const latestStable2 = meta.latestStableOverall;
5753
6006
  let majorsBehind = null;
5754
6007
  let drift = "unknown";
5755
- if (resolvedVersion && latestStable) {
6008
+ if (resolvedVersion && latestStable2) {
5756
6009
  const currentMajor = semver6.major(resolvedVersion);
5757
- const latestMajor = semver6.major(latestStable);
6010
+ const latestMajor = semver6.major(latestStable2);
5758
6011
  majorsBehind = latestMajor - currentMajor;
5759
6012
  if (majorsBehind === 0) {
5760
- drift = semver6.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
6013
+ drift = semver6.eq(resolvedVersion, latestStable2) ? "current" : "minor-behind";
5761
6014
  } else if (majorsBehind > 0) {
5762
6015
  drift = "major-behind";
5763
6016
  } else {
@@ -5775,7 +6028,7 @@ async function scanOneRubyProject(dir, manifestFiles, rootDir, rubygemsCache, ca
5775
6028
  section,
5776
6029
  currentSpec: dep.spec,
5777
6030
  resolvedVersion,
5778
- latestStable,
6031
+ latestStable: latestStable2,
5779
6032
  majorsBehind,
5780
6033
  drift
5781
6034
  });
@@ -5783,7 +6036,7 @@ async function scanOneRubyProject(dir, manifestFiles, rootDir, rubygemsCache, ca
5783
6036
  frameworks.push({
5784
6037
  name: KNOWN_RUBY_FRAMEWORKS[dep.name],
5785
6038
  currentVersion: resolvedVersion,
5786
- latestVersion: latestStable,
6039
+ latestVersion: latestStable2,
5787
6040
  majorsBehind
5788
6041
  });
5789
6042
  }
@@ -5806,6 +6059,8 @@ async function scanOneRubyProject(dir, manifestFiles, rootDir, rubygemsCache, ca
5806
6059
  runtime: rubyVersion,
5807
6060
  runtimeLatest,
5808
6061
  runtimeMajorsBehind,
6062
+ runtimeEol,
6063
+ runtimeEolDate,
5809
6064
  frameworks,
5810
6065
  dependencies,
5811
6066
  dependencyAgeBuckets: buckets,
@@ -5963,15 +6218,15 @@ async function scanOneSwiftProject(dir, manifestFile, rootDir, swiftCache, cache
5963
6218
  for (const { dep, meta } of resolved) {
5964
6219
  const resolvedVersionStr = resolvedVersions.get(dep.name.toLowerCase()) ?? (dep.type === "exact" ? dep.version : null);
5965
6220
  const resolvedVersion = resolvedVersionStr ? semver7.valid(semver7.clean(resolvedVersionStr)) : null;
5966
- const latestStable = meta.latestStableOverall;
6221
+ const latestStable2 = meta.latestStableOverall;
5967
6222
  let majorsBehind = null;
5968
6223
  let drift = "unknown";
5969
- if (resolvedVersion && latestStable) {
6224
+ if (resolvedVersion && latestStable2) {
5970
6225
  const currentMajor = semver7.major(resolvedVersion);
5971
- const latestMajor = semver7.major(latestStable);
6226
+ const latestMajor = semver7.major(latestStable2);
5972
6227
  majorsBehind = latestMajor - currentMajor;
5973
6228
  if (majorsBehind === 0) {
5974
- drift = semver7.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
6229
+ drift = semver7.eq(resolvedVersion, latestStable2) ? "current" : "minor-behind";
5975
6230
  } else if (majorsBehind > 0) {
5976
6231
  drift = "major-behind";
5977
6232
  } else {
@@ -5988,7 +6243,7 @@ async function scanOneSwiftProject(dir, manifestFile, rootDir, swiftCache, cache
5988
6243
  section: "dependencies",
5989
6244
  currentSpec: dep.version,
5990
6245
  resolvedVersion,
5991
- latestStable,
6246
+ latestStable: latestStable2,
5992
6247
  majorsBehind,
5993
6248
  drift
5994
6249
  });
@@ -5997,7 +6252,7 @@ async function scanOneSwiftProject(dir, manifestFile, rootDir, swiftCache, cache
5997
6252
  frameworks.push({
5998
6253
  name: KNOWN_SWIFT_FRAMEWORKS[lowerName],
5999
6254
  currentVersion: resolvedVersion,
6000
- latestVersion: latestStable,
6255
+ latestVersion: latestStable2,
6001
6256
  majorsBehind
6002
6257
  });
6003
6258
  }
@@ -6084,7 +6339,6 @@ var KNOWN_GO_FRAMEWORKS = {
6084
6339
  "github.com/spf13/cast": "Cast",
6085
6340
  "github.com/pkg/errors": "pkg/errors"
6086
6341
  };
6087
- var LATEST_GO_MINOR = { major: 1, minor: 23 };
6088
6342
  function parseGoMod(content) {
6089
6343
  const deps = [];
6090
6344
  let goVersion;
@@ -6131,14 +6385,14 @@ function parseGoMod(content) {
6131
6385
  var GO_MANIFEST_FILES = /* @__PURE__ */ new Set([
6132
6386
  "go.mod"
6133
6387
  ]);
6134
- async function scanGoProjects(rootDir, goCache, cache, projectScanTimeout) {
6388
+ async function scanGoProjects(rootDir, goCache, cache, projectScanTimeout, catalog = BUNDLED_RUNTIME_CATALOG) {
6135
6389
  const manifestFiles = cache ? await cache.findFiles(rootDir, (name) => GO_MANIFEST_FILES.has(name)) : await findGoManifests(rootDir);
6136
6390
  const results = [];
6137
6391
  const STUCK_TIMEOUT_MS = projectScanTimeout ?? cache?.projectScanTimeout ?? 18e4;
6138
6392
  for (const manifestFile of manifestFiles) {
6139
6393
  const dir = path9.dirname(manifestFile);
6140
6394
  try {
6141
- const scanPromise = scanOneGoProject(dir, manifestFile, rootDir, goCache, cache);
6395
+ const scanPromise = scanOneGoProject(dir, manifestFile, rootDir, goCache, cache, catalog);
6142
6396
  const result = await withTimeout(scanPromise, STUCK_TIMEOUT_MS);
6143
6397
  if (result.ok) {
6144
6398
  results.push(result.value);
@@ -6161,7 +6415,7 @@ async function findGoManifests(rootDir) {
6161
6415
  const { findFiles: findFiles2 } = await import("./fs-PXXYZATK-EW5LCUA7.js");
6162
6416
  return findFiles2(rootDir, (name) => GO_MANIFEST_FILES.has(name));
6163
6417
  }
6164
- async function scanOneGoProject(dir, manifestFile, rootDir, goCache, cache) {
6418
+ async function scanOneGoProject(dir, manifestFile, rootDir, goCache, cache, catalog) {
6165
6419
  const relDir = path9.relative(rootDir, dir) || ".";
6166
6420
  const projectName = path9.basename(dir === rootDir ? rootDir : dir);
6167
6421
  const content = cache ? await cache.readTextFile(manifestFile) : await readTextFile(manifestFile);
@@ -6169,18 +6423,26 @@ async function scanOneGoProject(dir, manifestFile, rootDir, goCache, cache) {
6169
6423
  const directDeps = allDeps.filter((d) => !d.indirect);
6170
6424
  let runtimeMajorsBehind;
6171
6425
  let runtimeLatest;
6426
+ let runtimeEol;
6427
+ let runtimeEolDate;
6172
6428
  if (goVersion) {
6173
6429
  const verMatch = goVersion.match(/(\d+)\.(\d+)/);
6174
6430
  if (verMatch) {
6175
6431
  const reqMajor = parseInt(verMatch[1], 10);
6176
6432
  const reqMinor = parseInt(verMatch[2], 10);
6177
- if (reqMajor === LATEST_GO_MINOR.major) {
6178
- runtimeMajorsBehind = Math.max(0, LATEST_GO_MINOR.minor - reqMinor);
6179
- } else if (reqMajor < LATEST_GO_MINOR.major) {
6180
- runtimeMajorsBehind = LATEST_GO_MINOR.minor + (LATEST_GO_MINOR.major - reqMajor) * 100;
6433
+ const LATEST_GO_MINOR = latestStable(catalog, "go");
6434
+ if (LATEST_GO_MINOR) {
6435
+ if (reqMajor === LATEST_GO_MINOR.major) {
6436
+ runtimeMajorsBehind = Math.max(0, LATEST_GO_MINOR.minor - reqMinor);
6437
+ } else if (reqMajor < LATEST_GO_MINOR.major) {
6438
+ runtimeMajorsBehind = LATEST_GO_MINOR.minor + (LATEST_GO_MINOR.major - reqMajor) * 100;
6439
+ }
6440
+ runtimeLatest = `${LATEST_GO_MINOR.major}.${LATEST_GO_MINOR.minor}`;
6181
6441
  }
6182
- runtimeLatest = `${LATEST_GO_MINOR.major}.${LATEST_GO_MINOR.minor}`;
6183
6442
  }
6443
+ runtimeEol = runtimeEolStatus(catalog, "go", goVersion);
6444
+ const cycle = extractCycle("go", goVersion);
6445
+ if (cycle) runtimeEolDate = eolDate(catalog, "go", cycle);
6184
6446
  }
6185
6447
  const dependencies = [];
6186
6448
  const frameworks = [];
@@ -6192,15 +6454,15 @@ async function scanOneGoProject(dir, manifestFile, rootDir, goCache, cache) {
6192
6454
  const resolved = await Promise.all(metaPromises);
6193
6455
  for (const { dep, meta } of resolved) {
6194
6456
  const resolvedVersion = semver8.valid(semver8.clean(dep.version));
6195
- const latestStable = meta.latestStableOverall;
6457
+ const latestStable2 = meta.latestStableOverall;
6196
6458
  let majorsBehind = null;
6197
6459
  let drift = "unknown";
6198
- if (resolvedVersion && latestStable) {
6460
+ if (resolvedVersion && latestStable2) {
6199
6461
  const currentMajor = semver8.major(resolvedVersion);
6200
- const latestMajor = semver8.major(latestStable);
6462
+ const latestMajor = semver8.major(latestStable2);
6201
6463
  majorsBehind = latestMajor - currentMajor;
6202
6464
  if (majorsBehind === 0) {
6203
- drift = semver8.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
6465
+ drift = semver8.eq(resolvedVersion, latestStable2) ? "current" : "minor-behind";
6204
6466
  } else if (majorsBehind > 0) {
6205
6467
  drift = "major-behind";
6206
6468
  } else {
@@ -6217,7 +6479,7 @@ async function scanOneGoProject(dir, manifestFile, rootDir, goCache, cache) {
6217
6479
  section: "dependencies",
6218
6480
  currentSpec: dep.version,
6219
6481
  resolvedVersion,
6220
- latestStable,
6482
+ latestStable: latestStable2,
6221
6483
  majorsBehind,
6222
6484
  drift
6223
6485
  });
@@ -6225,7 +6487,7 @@ async function scanOneGoProject(dir, manifestFile, rootDir, goCache, cache) {
6225
6487
  frameworks.push({
6226
6488
  name: KNOWN_GO_FRAMEWORKS[dep.path],
6227
6489
  currentVersion: resolvedVersion,
6228
- latestVersion: latestStable,
6490
+ latestVersion: latestStable2,
6229
6491
  majorsBehind
6230
6492
  });
6231
6493
  }
@@ -6248,6 +6510,8 @@ async function scanOneGoProject(dir, manifestFile, rootDir, goCache, cache) {
6248
6510
  runtime: goVersion,
6249
6511
  runtimeLatest,
6250
6512
  runtimeMajorsBehind,
6513
+ runtimeEol,
6514
+ runtimeEolDate,
6251
6515
  frameworks,
6252
6516
  dependencies,
6253
6517
  dependencyAgeBuckets: buckets,
@@ -6420,15 +6684,15 @@ async function scanOneRustProject(dir, manifestFile, rootDir, cargoCache, cache)
6420
6684
  const resolved = await Promise.all(metaPromises);
6421
6685
  for (const { dep, meta } of resolved) {
6422
6686
  const resolvedVersion = semver9.valid(semver9.coerce(dep.version));
6423
- const latestStable = meta.latestStableOverall;
6687
+ const latestStable2 = meta.latestStableOverall;
6424
6688
  let majorsBehind = null;
6425
6689
  let drift = "unknown";
6426
- if (resolvedVersion && latestStable) {
6690
+ if (resolvedVersion && latestStable2) {
6427
6691
  const currentMajor = semver9.major(resolvedVersion);
6428
- const latestMajor = semver9.major(latestStable);
6692
+ const latestMajor = semver9.major(latestStable2);
6429
6693
  majorsBehind = latestMajor - currentMajor;
6430
6694
  if (majorsBehind === 0) {
6431
- drift = semver9.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
6695
+ drift = semver9.eq(resolvedVersion, latestStable2) ? "current" : "minor-behind";
6432
6696
  } else if (majorsBehind > 0) {
6433
6697
  drift = "major-behind";
6434
6698
  } else {
@@ -6446,7 +6710,7 @@ async function scanOneRustProject(dir, manifestFile, rootDir, cargoCache, cache)
6446
6710
  section,
6447
6711
  currentSpec: dep.version,
6448
6712
  resolvedVersion,
6449
- latestStable,
6713
+ latestStable: latestStable2,
6450
6714
  majorsBehind,
6451
6715
  drift
6452
6716
  });
@@ -6454,7 +6718,7 @@ async function scanOneRustProject(dir, manifestFile, rootDir, cargoCache, cache)
6454
6718
  frameworks.push({
6455
6719
  name: KNOWN_RUST_FRAMEWORKS[dep.name],
6456
6720
  currentVersion: resolvedVersion,
6457
- latestVersion: latestStable,
6721
+ latestVersion: latestStable2,
6458
6722
  majorsBehind
6459
6723
  });
6460
6724
  }
@@ -6660,15 +6924,15 @@ async function scanOnePhpProject(dir, manifestFile, rootDir, composerCache, cach
6660
6924
  for (const { dep, meta } of resolved) {
6661
6925
  const resolvedVersionStr = resolvedVersions.get(dep.name);
6662
6926
  const resolvedVersion = resolvedVersionStr ? semver10.valid(semver10.clean(resolvedVersionStr)) : null;
6663
- const latestStable = meta.latestStableOverall;
6927
+ const latestStable2 = meta.latestStableOverall;
6664
6928
  let majorsBehind = null;
6665
6929
  let drift = "unknown";
6666
- if (resolvedVersion && latestStable) {
6930
+ if (resolvedVersion && latestStable2) {
6667
6931
  const currentMajor = semver10.major(resolvedVersion);
6668
- const latestMajor = semver10.major(latestStable);
6932
+ const latestMajor = semver10.major(latestStable2);
6669
6933
  majorsBehind = latestMajor - currentMajor;
6670
6934
  if (majorsBehind === 0) {
6671
- drift = semver10.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
6935
+ drift = semver10.eq(resolvedVersion, latestStable2) ? "current" : "minor-behind";
6672
6936
  } else if (majorsBehind > 0) {
6673
6937
  drift = "major-behind";
6674
6938
  } else {
@@ -6686,7 +6950,7 @@ async function scanOnePhpProject(dir, manifestFile, rootDir, composerCache, cach
6686
6950
  section,
6687
6951
  currentSpec: dep.version,
6688
6952
  resolvedVersion,
6689
- latestStable,
6953
+ latestStable: latestStable2,
6690
6954
  majorsBehind,
6691
6955
  drift
6692
6956
  });
@@ -6694,7 +6958,7 @@ async function scanOnePhpProject(dir, manifestFile, rootDir, composerCache, cach
6694
6958
  frameworks.push({
6695
6959
  name: KNOWN_PHP_FRAMEWORKS[dep.name],
6696
6960
  currentVersion: resolvedVersion,
6697
- latestVersion: latestStable,
6961
+ latestVersion: latestStable2,
6698
6962
  majorsBehind
6699
6963
  });
6700
6964
  }
@@ -6926,15 +7190,15 @@ async function scanOneDartProject(dir, manifestFile, rootDir, pubCache, cache) {
6926
7190
  for (const { dep, meta } of resolved) {
6927
7191
  const resolvedVersionStr = resolvedVersions.get(dep.name);
6928
7192
  const resolvedVersion = resolvedVersionStr ? semver11.valid(semver11.clean(resolvedVersionStr)) : null;
6929
- const latestStable = meta.latestStableOverall;
7193
+ const latestStable2 = meta.latestStableOverall;
6930
7194
  let majorsBehind = null;
6931
7195
  let drift = "unknown";
6932
- if (resolvedVersion && latestStable) {
7196
+ if (resolvedVersion && latestStable2) {
6933
7197
  const currentMajor = semver11.major(resolvedVersion);
6934
- const latestMajor = semver11.major(latestStable);
7198
+ const latestMajor = semver11.major(latestStable2);
6935
7199
  majorsBehind = latestMajor - currentMajor;
6936
7200
  if (majorsBehind === 0) {
6937
- drift = semver11.eq(resolvedVersion, latestStable) ? "current" : "minor-behind";
7201
+ drift = semver11.eq(resolvedVersion, latestStable2) ? "current" : "minor-behind";
6938
7202
  } else if (majorsBehind > 0) {
6939
7203
  drift = "major-behind";
6940
7204
  } else {
@@ -6952,7 +7216,7 @@ async function scanOneDartProject(dir, manifestFile, rootDir, pubCache, cache) {
6952
7216
  section,
6953
7217
  currentSpec: dep.version,
6954
7218
  resolvedVersion,
6955
- latestStable,
7219
+ latestStable: latestStable2,
6956
7220
  majorsBehind,
6957
7221
  drift
6958
7222
  });
@@ -6960,7 +7224,7 @@ async function scanOneDartProject(dir, manifestFile, rootDir, pubCache, cache) {
6960
7224
  frameworks.push({
6961
7225
  name: KNOWN_DART_FRAMEWORKS[dep.name],
6962
7226
  currentVersion: resolvedVersion,
6963
- latestVersion: latestStable,
7227
+ latestVersion: latestStable2,
6964
7228
  majorsBehind
6965
7229
  });
6966
7230
  }
@@ -13414,6 +13678,96 @@ async function computeTreeMetadataHash(rootDir, options) {
13414
13678
  }
13415
13679
  return digest.digest("hex");
13416
13680
  }
13681
+ var DEFAULT_API_BASE = "https://api.vibgrate.com";
13682
+ var CACHE_TTL_MS = 1e3 * 60 * 60 * 24;
13683
+ var FETCH_TIMEOUT_MS = 5e3;
13684
+ function isRuntimeCatalog(value) {
13685
+ return !!value && typeof value === "object" && typeof value.generatedAt === "string" && typeof value.products === "object" && value.products !== null;
13686
+ }
13687
+ var RuntimeCatalogClient = class {
13688
+ apiBase;
13689
+ offline;
13690
+ manifest;
13691
+ cacheDir;
13692
+ fetchImpl;
13693
+ resolution = null;
13694
+ constructor(opts = {}) {
13695
+ this.apiBase = (opts.apiBase ?? process.env.VIBGRATE_API_BASE ?? DEFAULT_API_BASE).replace(/\/+$/, "");
13696
+ this.offline = opts.offline ?? false;
13697
+ this.manifest = opts.manifest;
13698
+ this.cacheDir = opts.cacheDir ?? path32.join(os4.homedir(), ".vibgrate", "cache");
13699
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch;
13700
+ }
13701
+ /** Resolve the catalog once; concurrent/repeat calls share the result. */
13702
+ resolve() {
13703
+ if (!this.resolution) this.resolution = this.doResolve();
13704
+ return this.resolution;
13705
+ }
13706
+ get cacheFile() {
13707
+ return path32.join(this.cacheDir, "runtimes.json");
13708
+ }
13709
+ async doResolve() {
13710
+ let staleCache;
13711
+ if (!this.offline) {
13712
+ const envelope = await this.readCache();
13713
+ if (envelope) {
13714
+ const fresh = Date.now() - Date.parse(envelope.fetchedAt) < CACHE_TTL_MS;
13715
+ if (fresh) return { catalog: envelope.catalog, source: "cache" };
13716
+ staleCache = envelope.catalog;
13717
+ }
13718
+ const fetched = await this.fetchFromApi();
13719
+ if (fetched) {
13720
+ await this.writeCache(fetched);
13721
+ return { catalog: fetched, source: "api" };
13722
+ }
13723
+ }
13724
+ const manifestCatalog = this.manifest?.runtimes;
13725
+ if (isRuntimeCatalog(manifestCatalog)) {
13726
+ return { catalog: manifestCatalog, source: "manifest" };
13727
+ }
13728
+ if (staleCache && Date.parse(staleCache.generatedAt) > Date.parse(BUNDLED_RUNTIME_CATALOG.generatedAt)) {
13729
+ return { catalog: staleCache, source: "cache" };
13730
+ }
13731
+ return { catalog: BUNDLED_RUNTIME_CATALOG, source: "bundled" };
13732
+ }
13733
+ async fetchFromApi() {
13734
+ if (!this.fetchImpl) return null;
13735
+ const controller = new AbortController();
13736
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
13737
+ try {
13738
+ const res = await this.fetchImpl(`${this.apiBase}/v1/reference/runtimes`, {
13739
+ signal: controller.signal,
13740
+ headers: { Accept: "application/json" }
13741
+ });
13742
+ if (!res.ok) return null;
13743
+ const data = await res.json();
13744
+ return isRuntimeCatalog(data) ? data : null;
13745
+ } catch {
13746
+ return null;
13747
+ } finally {
13748
+ clearTimeout(timer);
13749
+ }
13750
+ }
13751
+ async readCache() {
13752
+ try {
13753
+ const text = await readFile4(this.cacheFile, "utf8");
13754
+ const parsed = JSON.parse(text);
13755
+ if (parsed && typeof parsed.fetchedAt === "string" && isRuntimeCatalog(parsed.catalog)) {
13756
+ return parsed;
13757
+ }
13758
+ } catch {
13759
+ }
13760
+ return null;
13761
+ }
13762
+ async writeCache(catalog) {
13763
+ try {
13764
+ await mkdir3(this.cacheDir, { recursive: true });
13765
+ const envelope = { fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), catalog };
13766
+ await writeFile3(this.cacheFile, JSON.stringify(envelope), "utf8");
13767
+ } catch {
13768
+ }
13769
+ }
13770
+ };
13417
13771
  var ProgressTrace = class _ProgressTrace {
13418
13772
  constructor(outPath) {
13419
13773
  this.outPath = outPath;
@@ -13424,8 +13778,8 @@ var ProgressTrace = class _ProgressTrace {
13424
13778
  flushed = false;
13425
13779
  /** Returns a trace when VIBGRATE_TRACE_EVENTS is set, else null. */
13426
13780
  static fromEnv() {
13427
- const path34 = process.env.VIBGRATE_TRACE_EVENTS;
13428
- return path34 ? new _ProgressTrace(path34) : null;
13781
+ const path35 = process.env.VIBGRATE_TRACE_EVENTS;
13782
+ return path35 ? new _ProgressTrace(path35) : null;
13429
13783
  }
13430
13784
  record(op, data = {}) {
13431
13785
  if (this.flushed) return;
@@ -13942,7 +14296,7 @@ var ScanProgress = class {
13942
14296
  var HISTORY_FILENAME = "scan_history.json";
13943
14297
  var MAX_RECORDS = 10;
13944
14298
  async function loadScanHistory(rootDir) {
13945
- const filePath = path32.join(rootDir, ".vibgrate", HISTORY_FILENAME);
14299
+ const filePath = path33.join(rootDir, ".vibgrate", HISTORY_FILENAME);
13946
14300
  try {
13947
14301
  const txt = await fs7.readFile(filePath, "utf8");
13948
14302
  const data = JSON.parse(txt);
@@ -13955,8 +14309,8 @@ async function loadScanHistory(rootDir) {
13955
14309
  }
13956
14310
  }
13957
14311
  async function saveScanHistory(rootDir, record2) {
13958
- const dir = path32.join(rootDir, ".vibgrate");
13959
- const filePath = path32.join(dir, HISTORY_FILENAME);
14312
+ const dir = path33.join(rootDir, ".vibgrate");
14313
+ const filePath = path33.join(dir, HISTORY_FILENAME);
13960
14314
  let history;
13961
14315
  const existing = await loadScanHistory(rootDir);
13962
14316
  if (existing) {
@@ -14024,18 +14378,18 @@ async function discoverSolutions(rootDir, fileCache) {
14024
14378
  for (const solutionFile of solutionFiles) {
14025
14379
  try {
14026
14380
  const content = await fileCache.readTextFile(solutionFile);
14027
- const dir = path33.dirname(solutionFile);
14028
- const rootBasename = path33.basename(rootDir);
14029
- const relSolutionPath = [rootBasename, path33.relative(rootDir, solutionFile).replace(/\\/g, "/")].join("/");
14381
+ const dir = path34.dirname(solutionFile);
14382
+ const rootBasename = path34.basename(rootDir);
14383
+ const relSolutionPath = [rootBasename, path34.relative(rootDir, solutionFile).replace(/\\/g, "/")].join("/");
14030
14384
  const projectPaths = /* @__PURE__ */ new Set();
14031
14385
  const projectRegex = /Project\("[^"]*"\)\s*=\s*"([^"]*)",\s*"([^"]+\.(?:cs|vb)proj)"/g;
14032
14386
  let match;
14033
14387
  while ((match = projectRegex.exec(content)) !== null) {
14034
14388
  const projectRelative = match[2];
14035
- const absProjectPath = path33.resolve(dir, projectRelative.replace(/\\/g, "/"));
14036
- projectPaths.add(path33.relative(rootDir, absProjectPath).replace(/\\/g, "/"));
14389
+ const absProjectPath = path34.resolve(dir, projectRelative.replace(/\\/g, "/"));
14390
+ projectPaths.add(path34.relative(rootDir, absProjectPath).replace(/\\/g, "/"));
14037
14391
  }
14038
- const solutionName = path33.basename(solutionFile, path33.extname(solutionFile));
14392
+ const solutionName = path34.basename(solutionFile, path34.extname(solutionFile));
14039
14393
  parsed.push({
14040
14394
  path: relSolutionPath,
14041
14395
  name: solutionName,
@@ -14143,6 +14497,9 @@ async function runScan(rootDir, opts) {
14143
14497
  ].join("\n");
14144
14498
  throw new Error(msg);
14145
14499
  }
14500
+ const runtimeClient = new RuntimeCatalogClient({ offline: offlineMode, manifest: packageManifest });
14501
+ const resolvedRuntimeCatalog = await runtimeClient.resolve();
14502
+ const runtimeCatalog = resolvedRuntimeCatalog.catalog;
14146
14503
  const treeCountPromise = quickTreeCount(rootDir, excludePatterns);
14147
14504
  progress.startStep("discovery");
14148
14505
  const treeCount = await treeCountPromise;
@@ -14168,7 +14525,7 @@ async function runScan(rootDir, opts) {
14168
14525
  progress.updateStats({ treeSummary: indexedTreeCount });
14169
14526
  }
14170
14527
  progress.completeStep("walk", `${treeCount.totalFiles.toLocaleString()} files indexed`);
14171
- const nodeProjects = await scanNodeProjects(rootDir, npmCache, fileCache, projectScanTimeoutMs);
14528
+ const nodeProjects = await scanNodeProjects(rootDir, npmCache, fileCache, projectScanTimeoutMs, runtimeCatalog);
14172
14529
  if (nodeProjects.length > 0) {
14173
14530
  progress.insertStepBefore("drift", { id: "node", label: "Found Node projects", weight: 4 });
14174
14531
  progress.startStep("node");
@@ -14180,7 +14537,7 @@ async function runScan(rootDir, opts) {
14180
14537
  progress.addProjects(nodeProjects.length);
14181
14538
  progress.completeStep("node", `${nodeProjects.length} project${nodeProjects.length !== 1 ? "s" : ""}`, nodeProjects.length);
14182
14539
  }
14183
- const dotnetProjects = await scanDotnetProjects(rootDir, nugetCache, fileCache, projectScanTimeoutMs);
14540
+ const dotnetProjects = await scanDotnetProjects(rootDir, nugetCache, fileCache, projectScanTimeoutMs, runtimeCatalog);
14184
14541
  if (dotnetProjects.length > 0) {
14185
14542
  progress.insertStepBefore("drift", { id: "dotnet", label: "Found .NET projects", weight: 2 });
14186
14543
  progress.startStep("dotnet");
@@ -14192,7 +14549,7 @@ async function runScan(rootDir, opts) {
14192
14549
  progress.addProjects(dotnetProjects.length);
14193
14550
  progress.completeStep("dotnet", `${dotnetProjects.length} project${dotnetProjects.length !== 1 ? "s" : ""}`, dotnetProjects.length);
14194
14551
  }
14195
- const pythonProjects = await scanPythonProjects(rootDir, pypiCache, fileCache, projectScanTimeoutMs);
14552
+ const pythonProjects = await scanPythonProjects(rootDir, pypiCache, fileCache, projectScanTimeoutMs, runtimeCatalog);
14196
14553
  if (pythonProjects.length > 0) {
14197
14554
  progress.insertStepBefore("drift", { id: "python", label: "Found Python projects", weight: 3 });
14198
14555
  progress.startStep("python");
@@ -14204,7 +14561,7 @@ async function runScan(rootDir, opts) {
14204
14561
  progress.addProjects(pythonProjects.length);
14205
14562
  progress.completeStep("python", `${pythonProjects.length} project${pythonProjects.length !== 1 ? "s" : ""}`, pythonProjects.length);
14206
14563
  }
14207
- const javaProjects = await scanJavaProjects(rootDir, mavenCache, fileCache, projectScanTimeoutMs);
14564
+ const javaProjects = await scanJavaProjects(rootDir, mavenCache, fileCache, projectScanTimeoutMs, runtimeCatalog);
14208
14565
  if (javaProjects.length > 0) {
14209
14566
  progress.insertStepBefore("drift", { id: "java", label: "Found Java projects", weight: 3 });
14210
14567
  progress.startStep("java");
@@ -14216,7 +14573,7 @@ async function runScan(rootDir, opts) {
14216
14573
  progress.addProjects(javaProjects.length);
14217
14574
  progress.completeStep("java", `${javaProjects.length} project${javaProjects.length !== 1 ? "s" : ""}`, javaProjects.length);
14218
14575
  }
14219
- const rubyProjects = await scanRubyProjects(rootDir, rubygemsCache, fileCache, projectScanTimeoutMs);
14576
+ const rubyProjects = await scanRubyProjects(rootDir, rubygemsCache, fileCache, projectScanTimeoutMs, runtimeCatalog);
14220
14577
  if (rubyProjects.length > 0) {
14221
14578
  progress.insertStepBefore("drift", { id: "ruby", label: "Found Ruby projects", weight: 2 });
14222
14579
  progress.startStep("ruby");
@@ -14240,7 +14597,7 @@ async function runScan(rootDir, opts) {
14240
14597
  progress.addProjects(swiftProjects.length);
14241
14598
  progress.completeStep("swift", `${swiftProjects.length} project${swiftProjects.length !== 1 ? "s" : ""}`, swiftProjects.length);
14242
14599
  }
14243
- const goProjects = await scanGoProjects(rootDir, goCache, fileCache, projectScanTimeoutMs);
14600
+ const goProjects = await scanGoProjects(rootDir, goCache, fileCache, projectScanTimeoutMs, runtimeCatalog);
14244
14601
  if (goProjects.length > 0) {
14245
14602
  progress.insertStepBefore("drift", { id: "go", label: "Found Go projects", weight: 2 });
14246
14603
  progress.startStep("go");
@@ -14379,7 +14736,7 @@ async function runScan(rootDir, opts) {
14379
14736
  for (const project of allProjects) {
14380
14737
  project.drift = computeDriftScore([project]);
14381
14738
  project.projectId = computeProjectId(project.path, project.name, workspaceId);
14382
- const absProjectDir = path33.resolve(rootDir, project.path);
14739
+ const absProjectDir = path34.resolve(rootDir, project.path);
14383
14740
  try {
14384
14741
  const source = await fileCache.sourceMetricsUnder(rootDir, absProjectDir);
14385
14742
  if (project.fileCount === void 0) project.fileCount = source.fileCount;
@@ -14393,7 +14750,7 @@ async function runScan(rootDir, opts) {
14393
14750
  dependencyCount: project.dependencyCount
14394
14751
  });
14395
14752
  }
14396
- const solutionsManifestPath = path33.join(rootDir, ".vibgrate", "solutions.json");
14753
+ const solutionsManifestPath = path34.join(rootDir, ".vibgrate", "solutions.json");
14397
14754
  const persistedSolutionIds = /* @__PURE__ */ new Map();
14398
14755
  if (await pathExists(solutionsManifestPath)) {
14399
14756
  try {
@@ -14415,7 +14772,7 @@ async function runScan(rootDir, opts) {
14415
14772
  const projectsByPath = new Map(allProjects.map((project) => [project.path, project]));
14416
14773
  for (const solution of solutions) {
14417
14774
  const includedProjects = solution.projectPaths.map((projectPath) => {
14418
- return projectsByPath.get(projectPath) ?? projectsByPath.get(path33.dirname(projectPath).replace(/\\/g, "/"));
14775
+ return projectsByPath.get(projectPath) ?? projectsByPath.get(path34.dirname(projectPath).replace(/\\/g, "/"));
14419
14776
  }).filter((project) => Boolean(project));
14420
14777
  solution.drift = includedProjects.length > 0 ? computeDriftScore(includedProjects) : void 0;
14421
14778
  for (const project of includedProjects) {
@@ -14630,7 +14987,7 @@ async function runScan(rootDir, opts) {
14630
14987
  const summary = [`${up.topEvidence.length} evidence`, ...up.capped ? ["capped"] : []].join(" \xB7 ");
14631
14988
  progress.completeStep("uipurpose", summary, up.topEvidence.length);
14632
14989
  await Promise.all(allProjects.map(async (project) => {
14633
- const projectDir = path33.join(rootDir, project.path);
14990
+ const projectDir = path34.join(rootDir, project.path);
14634
14991
  const projectResult = await scanUiPurpose(projectDir, fileCache, 150);
14635
14992
  if (projectResult.topEvidence.length > 0) {
14636
14993
  project.uiPurpose = compactUiPurpose(projectResult);
@@ -14638,7 +14995,7 @@ async function runScan(rootDir, opts) {
14638
14995
  }));
14639
14996
  }
14640
14997
  await Promise.all(allProjects.map(async (project) => {
14641
- const projectAbsPath = path33.join(rootDir, project.path);
14998
+ const projectAbsPath = path34.join(rootDir, project.path);
14642
14999
  const favicon = await findProjectFavicon(projectAbsPath);
14643
15000
  if (favicon) project.faviconBase64 = favicon;
14644
15001
  }));
@@ -14663,7 +15020,7 @@ async function runScan(rootDir, opts) {
14663
15020
  project.architecture = await scanProjectArchitecture(rootDir, project, fileCache);
14664
15021
  }));
14665
15022
  for (const solution of solutions) {
14666
- const memberProjects = solution.projectPaths.map((pp) => projectsByPath.get(pp) ?? projectsByPath.get(path33.dirname(pp).replace(/\\/g, "/"))).filter((p) => Boolean(p));
15023
+ const memberProjects = solution.projectPaths.map((pp) => projectsByPath.get(pp) ?? projectsByPath.get(path34.dirname(pp).replace(/\\/g, "/"))).filter((p) => Boolean(p));
14667
15024
  const memberArchResults = memberProjects.map((p) => p.architecture).filter((a) => Boolean(a));
14668
15025
  if (memberArchResults.length > 0) {
14669
15026
  solution.architecture = aggregateSolutionArchitecture(memberArchResults);
@@ -14676,6 +15033,13 @@ async function runScan(rootDir, opts) {
14676
15033
  );
14677
15034
  }
14678
15035
  }
15036
+ const catalogAgeDays = (Date.now() - Date.parse(runtimeCatalog.generatedAt)) / (1e3 * 60 * 60 * 24);
15037
+ const runtimeCatalogStale = Number.isFinite(catalogAgeDays) && catalogAgeDays > 30;
15038
+ extended.runtimeCatalogInfo = {
15039
+ generatedAt: runtimeCatalog.generatedAt,
15040
+ source: resolvedRuntimeCatalog.source,
15041
+ stale: runtimeCatalogStale
15042
+ };
14679
15043
  progress.startStep("drift");
14680
15044
  const drift = computeDriftScore(allProjects);
14681
15045
  progress.completeStep("drift", `${drift.score}/100 \u2014 ${drift.riskLevel} risk`);
@@ -14747,7 +15111,7 @@ async function runScan(rootDir, opts) {
14747
15111
  schemaVersion: "1.0",
14748
15112
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14749
15113
  vibgrateVersion,
14750
- rootPath: path33.basename(rootDir),
15114
+ rootPath: path34.basename(rootDir),
14751
15115
  ...vcs.type !== "unknown" ? { vcs } : {},
14752
15116
  repository,
14753
15117
  projects: allProjects,
@@ -14762,7 +15126,7 @@ async function runScan(rootDir, opts) {
14762
15126
  billing: summarizeBilling(allProjects)
14763
15127
  };
14764
15128
  if (opts.baseline) {
14765
- const baselinePath = path33.resolve(opts.baseline);
15129
+ const baselinePath = path34.resolve(opts.baseline);
14766
15130
  if (await pathExists(baselinePath)) {
14767
15131
  try {
14768
15132
  const baseline = await readJsonFile(baselinePath);
@@ -14774,10 +15138,10 @@ async function runScan(rootDir, opts) {
14774
15138
  }
14775
15139
  }
14776
15140
  if (!opts.noLocalArtifacts && !maxPrivacyMode) {
14777
- const vibgrateDir = path33.join(rootDir, ".vibgrate");
15141
+ const vibgrateDir = path34.join(rootDir, ".vibgrate");
14778
15142
  await ensureDir(vibgrateDir);
14779
- await writeJsonFile(path33.join(vibgrateDir, "scan_result.json"), artifact);
14780
- await writeJsonFile(path33.join(vibgrateDir, "solutions.json"), {
15143
+ await writeJsonFile(path34.join(vibgrateDir, "scan_result.json"), artifact);
15144
+ await writeJsonFile(path34.join(vibgrateDir, "solutions.json"), {
14781
15145
  scannedAt: artifact.timestamp,
14782
15146
  solutions: solutions.map((solution) => ({
14783
15147
  solutionId: solution.solutionId,
@@ -14816,15 +15180,15 @@ async function runScan(rootDir, opts) {
14816
15180
  }
14817
15181
  }
14818
15182
  if (Object.keys(projectScores).length > 0) {
14819
- const vibgrateDir = path33.join(rootDir, ".vibgrate");
15183
+ const vibgrateDir = path34.join(rootDir, ".vibgrate");
14820
15184
  await ensureDir(vibgrateDir);
14821
- await writeJsonFile(path33.join(vibgrateDir, "project_scores.json"), projectScores);
15185
+ await writeJsonFile(path34.join(vibgrateDir, "project_scores.json"), projectScores);
14822
15186
  }
14823
15187
  }
14824
15188
  if (opts.format === "json") {
14825
15189
  const jsonStr = JSON.stringify(artifact, null, 2);
14826
15190
  if (opts.out) {
14827
- await writeTextFile(path33.resolve(opts.out), jsonStr);
15191
+ await writeTextFile(path34.resolve(opts.out), jsonStr);
14828
15192
  console.log(chalk3.green("\u2714") + ` JSON written to ${opts.out}`);
14829
15193
  } else {
14830
15194
  console.log(jsonStr);
@@ -14833,7 +15197,7 @@ async function runScan(rootDir, opts) {
14833
15197
  const sarif = formatSarif(artifact);
14834
15198
  const sarifStr = JSON.stringify(sarif, null, 2);
14835
15199
  if (opts.out) {
14836
- await writeTextFile(path33.resolve(opts.out), sarifStr);
15200
+ await writeTextFile(path34.resolve(opts.out), sarifStr);
14837
15201
  console.log(chalk3.green("\u2714") + ` SARIF written to ${opts.out}`);
14838
15202
  } else {
14839
15203
  console.log(sarifStr);
@@ -14842,13 +15206,13 @@ async function runScan(rootDir, opts) {
14842
15206
  const markdown = formatMarkdown(artifact);
14843
15207
  console.log(markdown);
14844
15208
  if (opts.out) {
14845
- await writeTextFile(path33.resolve(opts.out), markdown);
15209
+ await writeTextFile(path34.resolve(opts.out), markdown);
14846
15210
  }
14847
15211
  } else {
14848
15212
  const text = formatText(artifact);
14849
15213
  console.log(text);
14850
15214
  if (opts.out) {
14851
- await writeTextFile(path33.resolve(opts.out), text);
15215
+ await writeTextFile(path34.resolve(opts.out), text);
14852
15216
  }
14853
15217
  }
14854
15218
  return artifact;
@@ -14856,7 +15220,7 @@ async function runScan(rootDir, opts) {
14856
15220
  async function buildRepositoryInfo(rootDir, remoteUrl, ciSystems) {
14857
15221
  const name = await resolveRepositoryName(rootDir);
14858
15222
  let version;
14859
- const packageJsonPath = path33.join(rootDir, "package.json");
15223
+ const packageJsonPath = path34.join(rootDir, "package.json");
14860
15224
  if (await pathExists(packageJsonPath)) {
14861
15225
  try {
14862
15226
  const packageJson = await readJsonFile(packageJsonPath);