@creeperhost/modlens-mcp 1.6.0

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.
Files changed (382) hide show
  1. package/README.md +938 -0
  2. package/dist/access-flags.d.ts +58 -0
  3. package/dist/access-flags.d.ts.map +1 -0
  4. package/dist/access-flags.js +79 -0
  5. package/dist/access-flags.js.map +1 -0
  6. package/dist/access-flags.test.d.ts +2 -0
  7. package/dist/access-flags.test.d.ts.map +1 -0
  8. package/dist/access-flags.test.js +189 -0
  9. package/dist/access-flags.test.js.map +1 -0
  10. package/dist/batch-ingest.d.ts +2 -0
  11. package/dist/batch-ingest.d.ts.map +1 -0
  12. package/dist/batch-ingest.js +54 -0
  13. package/dist/batch-ingest.js.map +1 -0
  14. package/dist/batch-resolve-mixins.d.ts +2 -0
  15. package/dist/batch-resolve-mixins.d.ts.map +1 -0
  16. package/dist/batch-resolve-mixins.js +36 -0
  17. package/dist/batch-resolve-mixins.js.map +1 -0
  18. package/dist/cache.d.ts +16 -0
  19. package/dist/cache.d.ts.map +1 -0
  20. package/dist/cache.js +36 -0
  21. package/dist/cache.js.map +1 -0
  22. package/dist/cli.d.ts +2 -0
  23. package/dist/cli.d.ts.map +1 -0
  24. package/dist/cli.js +1318 -0
  25. package/dist/cli.js.map +1 -0
  26. package/dist/curseforge.d.ts +53 -0
  27. package/dist/curseforge.d.ts.map +1 -0
  28. package/dist/curseforge.js +104 -0
  29. package/dist/curseforge.js.map +1 -0
  30. package/dist/db-backend.d.ts +17 -0
  31. package/dist/db-backend.d.ts.map +1 -0
  32. package/dist/db-backend.js +40 -0
  33. package/dist/db-backend.js.map +1 -0
  34. package/dist/db.d.ts +11 -0
  35. package/dist/db.d.ts.map +1 -0
  36. package/dist/db.js +55 -0
  37. package/dist/db.js.map +1 -0
  38. package/dist/embed-queue.d.ts +12 -0
  39. package/dist/embed-queue.d.ts.map +1 -0
  40. package/dist/embed-queue.js +103 -0
  41. package/dist/embed-queue.js.map +1 -0
  42. package/dist/embeddings.d.ts +21 -0
  43. package/dist/embeddings.d.ts.map +1 -0
  44. package/dist/embeddings.js +55 -0
  45. package/dist/embeddings.js.map +1 -0
  46. package/dist/env-path.d.ts +19 -0
  47. package/dist/env-path.d.ts.map +1 -0
  48. package/dist/env-path.js +40 -0
  49. package/dist/env-path.js.map +1 -0
  50. package/dist/fetch-utils.d.ts +24 -0
  51. package/dist/fetch-utils.d.ts.map +1 -0
  52. package/dist/fetch-utils.js +73 -0
  53. package/dist/fetch-utils.js.map +1 -0
  54. package/dist/fetch-utils.test.d.ts +2 -0
  55. package/dist/fetch-utils.test.d.ts.map +1 -0
  56. package/dist/fetch-utils.test.js +133 -0
  57. package/dist/fetch-utils.test.js.map +1 -0
  58. package/dist/generated/sqlite/browser.d.ts +55 -0
  59. package/dist/generated/sqlite/browser.d.ts.map +1 -0
  60. package/dist/generated/sqlite/browser.js +18 -0
  61. package/dist/generated/sqlite/browser.js.map +1 -0
  62. package/dist/generated/sqlite/client.d.ts +72 -0
  63. package/dist/generated/sqlite/client.d.ts.map +1 -0
  64. package/dist/generated/sqlite/client.js +33 -0
  65. package/dist/generated/sqlite/client.js.map +1 -0
  66. package/dist/generated/sqlite/commonInputTypes.d.ts +387 -0
  67. package/dist/generated/sqlite/commonInputTypes.d.ts.map +1 -0
  68. package/dist/generated/sqlite/commonInputTypes.js +11 -0
  69. package/dist/generated/sqlite/commonInputTypes.js.map +1 -0
  70. package/dist/generated/sqlite/enums.d.ts +2 -0
  71. package/dist/generated/sqlite/enums.d.ts.map +1 -0
  72. package/dist/generated/sqlite/enums.js +11 -0
  73. package/dist/generated/sqlite/enums.js.map +1 -0
  74. package/dist/generated/sqlite/internal/class.d.ts +226 -0
  75. package/dist/generated/sqlite/internal/class.d.ts.map +1 -0
  76. package/dist/generated/sqlite/internal/class.js +84 -0
  77. package/dist/generated/sqlite/internal/class.js.map +1 -0
  78. package/dist/generated/sqlite/internal/prismaNamespace.d.ts +1344 -0
  79. package/dist/generated/sqlite/internal/prismaNamespace.d.ts.map +1 -0
  80. package/dist/generated/sqlite/internal/prismaNamespace.js +220 -0
  81. package/dist/generated/sqlite/internal/prismaNamespace.js.map +1 -0
  82. package/dist/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +204 -0
  83. package/dist/generated/sqlite/internal/prismaNamespaceBrowser.d.ts.map +1 -0
  84. package/dist/generated/sqlite/internal/prismaNamespaceBrowser.js +191 -0
  85. package/dist/generated/sqlite/internal/prismaNamespaceBrowser.js.map +1 -0
  86. package/dist/generated/sqlite/models/DocEntry.d.ts +1223 -0
  87. package/dist/generated/sqlite/models/DocEntry.d.ts.map +1 -0
  88. package/dist/generated/sqlite/models/DocEntry.js +2 -0
  89. package/dist/generated/sqlite/models/DocEntry.js.map +1 -0
  90. package/dist/generated/sqlite/models/McSourceFile.d.ts +1289 -0
  91. package/dist/generated/sqlite/models/McSourceFile.d.ts.map +1 -0
  92. package/dist/generated/sqlite/models/McSourceFile.js +2 -0
  93. package/dist/generated/sqlite/models/McSourceFile.js.map +1 -0
  94. package/dist/generated/sqlite/models/McVersion.d.ts +1315 -0
  95. package/dist/generated/sqlite/models/McVersion.d.ts.map +1 -0
  96. package/dist/generated/sqlite/models/McVersion.js +2 -0
  97. package/dist/generated/sqlite/models/McVersion.js.map +1 -0
  98. package/dist/generated/sqlite/models/McVersionDiff.d.ts +1061 -0
  99. package/dist/generated/sqlite/models/McVersionDiff.d.ts.map +1 -0
  100. package/dist/generated/sqlite/models/McVersionDiff.js +2 -0
  101. package/dist/generated/sqlite/models/McVersionDiff.js.map +1 -0
  102. package/dist/generated/sqlite/models/Mod.d.ts +2396 -0
  103. package/dist/generated/sqlite/models/Mod.d.ts.map +1 -0
  104. package/dist/generated/sqlite/models/Mod.js +2 -0
  105. package/dist/generated/sqlite/models/Mod.js.map +1 -0
  106. package/dist/generated/sqlite/models/ModClass.d.ts +1254 -0
  107. package/dist/generated/sqlite/models/ModClass.d.ts.map +1 -0
  108. package/dist/generated/sqlite/models/ModClass.js +2 -0
  109. package/dist/generated/sqlite/models/ModClass.js.map +1 -0
  110. package/dist/generated/sqlite/models/ModSourceFile.d.ts +1283 -0
  111. package/dist/generated/sqlite/models/ModSourceFile.d.ts.map +1 -0
  112. package/dist/generated/sqlite/models/ModSourceFile.js +2 -0
  113. package/dist/generated/sqlite/models/ModSourceFile.js.map +1 -0
  114. package/dist/generated/sqlite/models/ModTag.d.ts +1278 -0
  115. package/dist/generated/sqlite/models/ModTag.d.ts.map +1 -0
  116. package/dist/generated/sqlite/models/ModTag.js +2 -0
  117. package/dist/generated/sqlite/models/ModTag.js.map +1 -0
  118. package/dist/generated/sqlite/models/ModVersionDiff.d.ts +1073 -0
  119. package/dist/generated/sqlite/models/ModVersionDiff.d.ts.map +1 -0
  120. package/dist/generated/sqlite/models/ModVersionDiff.js +2 -0
  121. package/dist/generated/sqlite/models/ModVersionDiff.js.map +1 -0
  122. package/dist/generated/sqlite/models/Primer.d.ts +1319 -0
  123. package/dist/generated/sqlite/models/Primer.d.ts.map +1 -0
  124. package/dist/generated/sqlite/models/Primer.js +2 -0
  125. package/dist/generated/sqlite/models/Primer.js.map +1 -0
  126. package/dist/generated/sqlite/models.d.ts +12 -0
  127. package/dist/generated/sqlite/models.d.ts.map +1 -0
  128. package/dist/generated/sqlite/models.js +2 -0
  129. package/dist/generated/sqlite/models.js.map +1 -0
  130. package/dist/jar.d.ts +7 -0
  131. package/dist/jar.d.ts.map +1 -0
  132. package/dist/jar.js +52 -0
  133. package/dist/jar.js.map +1 -0
  134. package/dist/java-tools.d.ts +46 -0
  135. package/dist/java-tools.d.ts.map +1 -0
  136. package/dist/java-tools.js +263 -0
  137. package/dist/java-tools.js.map +1 -0
  138. package/dist/launcher.d.ts +3 -0
  139. package/dist/launcher.d.ts.map +1 -0
  140. package/dist/launcher.js +105 -0
  141. package/dist/launcher.js.map +1 -0
  142. package/dist/mappings.d.ts +95 -0
  143. package/dist/mappings.d.ts.map +1 -0
  144. package/dist/mappings.js +1094 -0
  145. package/dist/mappings.js.map +1 -0
  146. package/dist/mappings.test.d.ts +2 -0
  147. package/dist/mappings.test.d.ts.map +1 -0
  148. package/dist/mappings.test.js +161 -0
  149. package/dist/mappings.test.js.map +1 -0
  150. package/dist/minecraft.d.ts +21 -0
  151. package/dist/minecraft.d.ts.map +1 -0
  152. package/dist/minecraft.js +70 -0
  153. package/dist/minecraft.js.map +1 -0
  154. package/dist/modpacks-ch.d.ts +224 -0
  155. package/dist/modpacks-ch.d.ts.map +1 -0
  156. package/dist/modpacks-ch.js +146 -0
  157. package/dist/modpacks-ch.js.map +1 -0
  158. package/dist/modrinth.d.ts +67 -0
  159. package/dist/modrinth.d.ts.map +1 -0
  160. package/dist/modrinth.js +88 -0
  161. package/dist/modrinth.js.map +1 -0
  162. package/dist/platform-adapter.d.ts +23 -0
  163. package/dist/platform-adapter.d.ts.map +1 -0
  164. package/dist/platform-adapter.js +2 -0
  165. package/dist/platform-adapter.js.map +1 -0
  166. package/dist/platform.d.ts +47 -0
  167. package/dist/platform.d.ts.map +1 -0
  168. package/dist/platform.js +175 -0
  169. package/dist/platform.js.map +1 -0
  170. package/dist/processor.d.ts +51 -0
  171. package/dist/processor.d.ts.map +1 -0
  172. package/dist/processor.js +655 -0
  173. package/dist/processor.js.map +1 -0
  174. package/dist/processor.test.d.ts +2 -0
  175. package/dist/processor.test.d.ts.map +1 -0
  176. package/dist/processor.test.js +629 -0
  177. package/dist/processor.test.js.map +1 -0
  178. package/dist/repositories/embeddings-sqlite.d.ts +38 -0
  179. package/dist/repositories/embeddings-sqlite.d.ts.map +1 -0
  180. package/dist/repositories/embeddings-sqlite.js +208 -0
  181. package/dist/repositories/embeddings-sqlite.js.map +1 -0
  182. package/dist/repositories/embeddings.d.ts +40 -0
  183. package/dist/repositories/embeddings.d.ts.map +1 -0
  184. package/dist/repositories/embeddings.js +136 -0
  185. package/dist/repositories/embeddings.js.map +1 -0
  186. package/dist/repositories/index.d.ts +2 -0
  187. package/dist/repositories/index.d.ts.map +1 -0
  188. package/dist/repositories/index.js +15 -0
  189. package/dist/repositories/index.js.map +1 -0
  190. package/dist/repositories/mcVersion.d.ts +16 -0
  191. package/dist/repositories/mcVersion.d.ts.map +1 -0
  192. package/dist/repositories/mcVersion.js +52 -0
  193. package/dist/repositories/mcVersion.js.map +1 -0
  194. package/dist/repositories/mod.d.ts +241 -0
  195. package/dist/repositories/mod.d.ts.map +1 -0
  196. package/dist/repositories/mod.js +342 -0
  197. package/dist/repositories/mod.js.map +1 -0
  198. package/dist/repositories/packs.d.ts +44 -0
  199. package/dist/repositories/packs.d.ts.map +1 -0
  200. package/dist/repositories/packs.js +138 -0
  201. package/dist/repositories/packs.js.map +1 -0
  202. package/dist/search-adapter.d.ts +34 -0
  203. package/dist/search-adapter.d.ts.map +1 -0
  204. package/dist/search-adapter.js +195 -0
  205. package/dist/search-adapter.js.map +1 -0
  206. package/dist/search.d.ts +6 -0
  207. package/dist/search.d.ts.map +1 -0
  208. package/dist/search.js +54 -0
  209. package/dist/search.js.map +1 -0
  210. package/dist/search.test.d.ts +2 -0
  211. package/dist/search.test.d.ts.map +1 -0
  212. package/dist/search.test.js +71 -0
  213. package/dist/search.test.js.map +1 -0
  214. package/dist/security.d.ts +98 -0
  215. package/dist/security.d.ts.map +1 -0
  216. package/dist/security.js +446 -0
  217. package/dist/security.js.map +1 -0
  218. package/dist/security.test.d.ts +2 -0
  219. package/dist/security.test.d.ts.map +1 -0
  220. package/dist/security.test.js +687 -0
  221. package/dist/security.test.js.map +1 -0
  222. package/dist/server.d.ts +2 -0
  223. package/dist/server.d.ts.map +1 -0
  224. package/dist/server.js +1465 -0
  225. package/dist/server.js.map +1 -0
  226. package/dist/setup.d.ts +2 -0
  227. package/dist/setup.d.ts.map +1 -0
  228. package/dist/setup.js +1113 -0
  229. package/dist/setup.js.map +1 -0
  230. package/dist/tools/bytecode.d.ts +129 -0
  231. package/dist/tools/bytecode.d.ts.map +1 -0
  232. package/dist/tools/bytecode.js +679 -0
  233. package/dist/tools/bytecode.js.map +1 -0
  234. package/dist/tools/catalog.d.ts +54 -0
  235. package/dist/tools/catalog.d.ts.map +1 -0
  236. package/dist/tools/catalog.js +249 -0
  237. package/dist/tools/catalog.js.map +1 -0
  238. package/dist/tools/compat-check.d.ts +23 -0
  239. package/dist/tools/compat-check.d.ts.map +1 -0
  240. package/dist/tools/compat-check.js +250 -0
  241. package/dist/tools/compat-check.js.map +1 -0
  242. package/dist/tools/compat-check.test.d.ts +2 -0
  243. package/dist/tools/compat-check.test.d.ts.map +1 -0
  244. package/dist/tools/compat-check.test.js +179 -0
  245. package/dist/tools/compat-check.test.js.map +1 -0
  246. package/dist/tools/diagnostics.d.ts +10 -0
  247. package/dist/tools/diagnostics.d.ts.map +1 -0
  248. package/dist/tools/diagnostics.js +133 -0
  249. package/dist/tools/diagnostics.js.map +1 -0
  250. package/dist/tools/diagnostics.test.d.ts +2 -0
  251. package/dist/tools/diagnostics.test.d.ts.map +1 -0
  252. package/dist/tools/diagnostics.test.js +168 -0
  253. package/dist/tools/diagnostics.test.js.map +1 -0
  254. package/dist/tools/docs.d.ts +19 -0
  255. package/dist/tools/docs.d.ts.map +1 -0
  256. package/dist/tools/docs.js +230 -0
  257. package/dist/tools/docs.js.map +1 -0
  258. package/dist/tools/embed-registry.d.ts +125 -0
  259. package/dist/tools/embed-registry.d.ts.map +1 -0
  260. package/dist/tools/embed-registry.js +560 -0
  261. package/dist/tools/embed-registry.js.map +1 -0
  262. package/dist/tools/gradle.d.ts +16 -0
  263. package/dist/tools/gradle.d.ts.map +1 -0
  264. package/dist/tools/gradle.js +182 -0
  265. package/dist/tools/gradle.js.map +1 -0
  266. package/dist/tools/graphify.d.ts +144 -0
  267. package/dist/tools/graphify.d.ts.map +1 -0
  268. package/dist/tools/graphify.js +798 -0
  269. package/dist/tools/graphify.js.map +1 -0
  270. package/dist/tools/graphify.test.d.ts +2 -0
  271. package/dist/tools/graphify.test.d.ts.map +1 -0
  272. package/dist/tools/graphify.test.js +39 -0
  273. package/dist/tools/graphify.test.js.map +1 -0
  274. package/dist/tools/ingest.d.ts +85 -0
  275. package/dist/tools/ingest.d.ts.map +1 -0
  276. package/dist/tools/ingest.js +477 -0
  277. package/dist/tools/ingest.js.map +1 -0
  278. package/dist/tools/ingest.test.d.ts +2 -0
  279. package/dist/tools/ingest.test.d.ts.map +1 -0
  280. package/dist/tools/ingest.test.js +340 -0
  281. package/dist/tools/ingest.test.js.map +1 -0
  282. package/dist/tools/kubejs.d.ts +37 -0
  283. package/dist/tools/kubejs.d.ts.map +1 -0
  284. package/dist/tools/kubejs.js +245 -0
  285. package/dist/tools/kubejs.js.map +1 -0
  286. package/dist/tools/mappings.d.ts +17 -0
  287. package/dist/tools/mappings.d.ts.map +1 -0
  288. package/dist/tools/mappings.js +85 -0
  289. package/dist/tools/mappings.js.map +1 -0
  290. package/dist/tools/mc-fts.d.ts +101 -0
  291. package/dist/tools/mc-fts.d.ts.map +1 -0
  292. package/dist/tools/mc-fts.js +279 -0
  293. package/dist/tools/mc-fts.js.map +1 -0
  294. package/dist/tools/mcmeta.d.ts +41 -0
  295. package/dist/tools/mcmeta.d.ts.map +1 -0
  296. package/dist/tools/mcmeta.js +365 -0
  297. package/dist/tools/mcmeta.js.map +1 -0
  298. package/dist/tools/mixin-scan.d.ts +113 -0
  299. package/dist/tools/mixin-scan.d.ts.map +1 -0
  300. package/dist/tools/mixin-scan.js +433 -0
  301. package/dist/tools/mixin-scan.js.map +1 -0
  302. package/dist/tools/mod-data.d.ts +83 -0
  303. package/dist/tools/mod-data.d.ts.map +1 -0
  304. package/dist/tools/mod-data.js +630 -0
  305. package/dist/tools/mod-data.js.map +1 -0
  306. package/dist/tools/mod-tags.d.ts +59 -0
  307. package/dist/tools/mod-tags.d.ts.map +1 -0
  308. package/dist/tools/mod-tags.js +279 -0
  309. package/dist/tools/mod-tags.js.map +1 -0
  310. package/dist/tools/mod-tags.test.d.ts +2 -0
  311. package/dist/tools/mod-tags.test.d.ts.map +1 -0
  312. package/dist/tools/mod-tags.test.js +75 -0
  313. package/dist/tools/mod-tags.test.js.map +1 -0
  314. package/dist/tools/modpacks-ch.d.ts +257 -0
  315. package/dist/tools/modpacks-ch.d.ts.map +1 -0
  316. package/dist/tools/modpacks-ch.js +516 -0
  317. package/dist/tools/modpacks-ch.js.map +1 -0
  318. package/dist/tools/packtools.d.ts +113 -0
  319. package/dist/tools/packtools.d.ts.map +1 -0
  320. package/dist/tools/packtools.js +809 -0
  321. package/dist/tools/packtools.js.map +1 -0
  322. package/dist/tools/packtools.test.d.ts +2 -0
  323. package/dist/tools/packtools.test.d.ts.map +1 -0
  324. package/dist/tools/packtools.test.js +99 -0
  325. package/dist/tools/packtools.test.js.map +1 -0
  326. package/dist/tools/platform.d.ts +68 -0
  327. package/dist/tools/platform.d.ts.map +1 -0
  328. package/dist/tools/platform.js +452 -0
  329. package/dist/tools/platform.js.map +1 -0
  330. package/dist/tools/primers.d.ts +36 -0
  331. package/dist/tools/primers.d.ts.map +1 -0
  332. package/dist/tools/primers.js +451 -0
  333. package/dist/tools/primers.js.map +1 -0
  334. package/dist/tools/reports.d.ts +19 -0
  335. package/dist/tools/reports.d.ts.map +1 -0
  336. package/dist/tools/reports.js +573 -0
  337. package/dist/tools/reports.js.map +1 -0
  338. package/dist/tools/source.d.ts +10 -0
  339. package/dist/tools/source.d.ts.map +1 -0
  340. package/dist/tools/source.js +101 -0
  341. package/dist/tools/source.js.map +1 -0
  342. package/dist/tools/vanilla-data.d.ts +127 -0
  343. package/dist/tools/vanilla-data.d.ts.map +1 -0
  344. package/dist/tools/vanilla-data.js +824 -0
  345. package/dist/tools/vanilla-data.js.map +1 -0
  346. package/dist/tools/vanilla.d.ts +171 -0
  347. package/dist/tools/vanilla.d.ts.map +1 -0
  348. package/dist/tools/vanilla.js +612 -0
  349. package/dist/tools/vanilla.js.map +1 -0
  350. package/dist/tools/version-diff.d.ts +113 -0
  351. package/dist/tools/version-diff.d.ts.map +1 -0
  352. package/dist/tools/version-diff.js +519 -0
  353. package/dist/tools/version-diff.js.map +1 -0
  354. package/dist/tools/version-diff.test.d.ts +2 -0
  355. package/dist/tools/version-diff.test.d.ts.map +1 -0
  356. package/dist/tools/version-diff.test.js +102 -0
  357. package/dist/tools/version-diff.test.js.map +1 -0
  358. package/dist/validate.d.ts +19 -0
  359. package/dist/validate.d.ts.map +1 -0
  360. package/dist/validate.js +54 -0
  361. package/dist/validate.js.map +1 -0
  362. package/dist/validate.test.d.ts +2 -0
  363. package/dist/validate.test.d.ts.map +1 -0
  364. package/dist/validate.test.js +73 -0
  365. package/dist/validate.test.js.map +1 -0
  366. package/package.json +62 -0
  367. package/prisma/backends/schema.sqlite.prisma +215 -0
  368. package/prisma/backends/template.db +0 -0
  369. package/prisma/schema.prisma +303 -0
  370. package/scripts/backup.mjs +151 -0
  371. package/scripts/build-template-db.mjs +38 -0
  372. package/scripts/check-atm10.mjs +27 -0
  373. package/scripts/check-sync.mjs +13 -0
  374. package/scripts/create-fts-index.mjs +50 -0
  375. package/scripts/enable-pgvector.mjs +90 -0
  376. package/scripts/enable-sqlite-vec.mjs +158 -0
  377. package/scripts/migrate-backend.mjs +188 -0
  378. package/scripts/migrate-embed-provenance.mjs +80 -0
  379. package/scripts/migrate-metadata-source.mjs +62 -0
  380. package/scripts/migrate-mod-source-files.mjs +67 -0
  381. package/scripts/post-update.mjs +77 -0
  382. package/scripts/verify-atm10.mjs +63 -0
@@ -0,0 +1,809 @@
1
+ /**
2
+ * Modpack-specific analysis tools.
3
+ *
4
+ * Tools for modpack developers that go beyond single-mod analysis:
5
+ * - Asset conflict detection across all mod JARs
6
+ * - Vanilla data/asset override tracking
7
+ * - Mod sidedness classification (client-only / server-only / common / client-optional)
8
+ * - Mod complexity scoring for performance/compat triage
9
+ * - Pack-level changelog between two sets of mods
10
+ * - Pack knowledge graph — composite graph of all mods, deps, tags, mixins, and optionally KubeJS scripts
11
+ */
12
+ import AdmZip from "adm-zip";
13
+ import { readFileSync, readdirSync, statSync, existsSync } from "fs";
14
+ import { join, relative, normalize, isAbsolute } from "path";
15
+ import { resolveModRef, listModsSlim, listMods, findModsByIds } from "../repositories/mod.js";
16
+ import { listEntries, extractEntry } from "../jar.js";
17
+ import { indexJar } from "../java-tools.js";
18
+ import { assertJarPath } from "../security.js";
19
+ import { getDb } from "../db.js";
20
+ import { indexKubeJsScripts } from "./kubejs.js";
21
+ // ── Asset conflict detection ───────────────────────────────────────────────────
22
+ /**
23
+ * Scan all mod JARs in the DB for duplicate asset paths.
24
+ * When two mods ship the same assets/ path, the last-loaded mod silently wins.
25
+ *
26
+ * assetType: filter to a specific sub-folder (textures | models | sounds | blockstates | shaders | all)
27
+ * mcVersion / loader: optional DB filters
28
+ * limit: max conflicts to return (default 300)
29
+ */
30
+ export async function findAssetConflicts(assetType, mcVersion, loader, limit = 300) {
31
+ const mods = await listModsSlim({ mcVersion, loader });
32
+ const typeFilter = assetType && assetType !== "all" ? assetType : null;
33
+ const pathMap = new Map();
34
+ for (const mod of mods) {
35
+ try {
36
+ assertJarPath(mod.jarPath);
37
+ const entries = listEntries(mod.jarPath, "assets/");
38
+ for (const entry of entries) {
39
+ if (entry.endsWith("/"))
40
+ continue;
41
+ if (typeFilter && !entry.includes(`/${typeFilter}/`))
42
+ continue;
43
+ const list = pathMap.get(entry) ?? [];
44
+ list.push({ mod: mod.modId, display: mod.displayName });
45
+ pathMap.set(entry, list);
46
+ }
47
+ }
48
+ catch { /* skip unreadable JARs */ }
49
+ }
50
+ const conflicts = [...pathMap.entries()]
51
+ .filter(([, owners]) => owners.length >= 2)
52
+ .map(([path, owners]) => ({ path, modCount: owners.length, mods: owners }))
53
+ .sort((a, b) => b.modCount - a.modCount)
54
+ .slice(0, limit);
55
+ // Group by asset folder type
56
+ const byType = {};
57
+ for (const c of conflicts) {
58
+ const m = c.path.match(/^assets\/[^/]+\/([^/]+)\//);
59
+ const t = m?.[1] ?? "other";
60
+ byType[t] = (byType[t] ?? 0) + 1;
61
+ }
62
+ return {
63
+ modsScanned: mods.length,
64
+ totalConflicts: conflicts.length,
65
+ capped: conflicts.length >= limit,
66
+ note: "When two mods ship the same assets/ path, the last-loaded mod wins and silently replaces the other's visuals/sounds.",
67
+ byType,
68
+ conflicts,
69
+ };
70
+ }
71
+ // ── Vanilla override tracking ──────────────────────────────────────────────────
72
+ /**
73
+ * Find all mods that override vanilla (minecraft namespace) data or assets.
74
+ * data/minecraft/ overrides affect recipes, loot tables, advancements, etc.
75
+ * assets/minecraft/ overrides change vanilla textures, sounds, models.
76
+ *
77
+ * overrideType: "data" | "assets" | "all" (default all)
78
+ * dataSubtype: optional subfolder filter, e.g. "recipes", "loot_tables", "advancements"
79
+ */
80
+ export async function findVanillaOverrides(overrideType, dataSubtype, mcVersion, loader) {
81
+ const mods = await listModsSlim({ mcVersion, loader });
82
+ const checkData = !overrideType || overrideType === "all" || overrideType === "data";
83
+ const checkAssets = !overrideType || overrideType === "all" || overrideType === "assets";
84
+ const results = [];
85
+ for (const mod of mods) {
86
+ try {
87
+ const dataOverrides = [];
88
+ const assetOverrides = [];
89
+ if (checkData) {
90
+ const entries = listEntries(mod.jarPath, "data/minecraft/");
91
+ const filtered = dataSubtype
92
+ ? entries.filter(e => e.includes(`/minecraft/${dataSubtype}/`))
93
+ : entries;
94
+ dataOverrides.push(...filtered.filter(e => !e.endsWith("/")));
95
+ }
96
+ if (checkAssets) {
97
+ assetOverrides.push(...listEntries(mod.jarPath, "assets/minecraft/").filter(e => !e.endsWith("/")));
98
+ }
99
+ if (dataOverrides.length > 0 || assetOverrides.length > 0) {
100
+ results.push({ mod: mod.modId, display: mod.displayName, version: mod.version, dataOverrides, assetOverrides });
101
+ }
102
+ }
103
+ catch { /* skip */ }
104
+ }
105
+ const totalData = results.reduce((s, r) => s + r.dataOverrides.length, 0);
106
+ const totalAssets = results.reduce((s, r) => s + r.assetOverrides.length, 0);
107
+ return {
108
+ modsScanned: mods.length,
109
+ modsWithOverrides: results.length,
110
+ totalDataOverrides: totalData,
111
+ totalAssetOverrides: totalAssets,
112
+ note: "data/minecraft/ overrides can silently change vanilla recipes, loot tables, and advancements. assets/minecraft/ overrides replace vanilla textures and sounds.",
113
+ results,
114
+ };
115
+ }
116
+ // ── Mod sidedness analysis ─────────────────────────────────────────────────────
117
+ const DISPLAY_TEST_MAP = {
118
+ "IGNORE_SERVER_VERSION": "client_optional", // client recommends it; dedicated server doesn't require it
119
+ "IGNORE_ALL_VERSION": "client_only", // not needed on dedicated server at all
120
+ "MATCH_VERSION": "common", // required on both sides (default)
121
+ };
122
+ const CLIENT_MARKERS = [
123
+ "net/minecraft/client/",
124
+ "net/neoforged/neoforge/client/",
125
+ "net/minecraftforge/client/",
126
+ "net/fabricmc/fabric/api/client/",
127
+ "FMLClientSetupEvent",
128
+ ];
129
+ const SERVER_MARKERS = [
130
+ "FMLDedicatedServerSetupEvent",
131
+ "net/neoforged/neoforge/event/server/",
132
+ "ServerLifecycleEvents",
133
+ ];
134
+ /**
135
+ * Determine the sidedness of a single mod:
136
+ * client_only: not needed on dedicated server
137
+ * server_only: not needed on client
138
+ * client_optional: works without it on server (cosmetic/HUD/minimap mods)
139
+ * common: required on both sides
140
+ *
141
+ * Detection order:
142
+ * 1. fabric.mod.json "environment" field (authoritative for Fabric/Quilt)
143
+ * 2. neoforge.mods.toml / mods.toml "displayTest" (authoritative for NeoForge/Forge)
144
+ * 3. Bytecode reference heuristic (fallback)
145
+ */
146
+ export async function analyzeModSidedness(modIdOrDbId) {
147
+ const mod = await resolveModRef(String(modIdOrDbId));
148
+ if (!mod)
149
+ return { error: `Mod not found: ${modIdOrDbId}` };
150
+ let sidedness = "unknown";
151
+ let source = "unknown";
152
+ let evidence = "";
153
+ const zip = new AdmZip(mod.jarPath);
154
+ // 1. Fabric / Quilt: fabric.mod.json "environment"
155
+ for (const manifestFile of ["fabric.mod.json", "quilt.mod.json"]) {
156
+ const entry = zip.getEntry(manifestFile);
157
+ if (!entry)
158
+ continue;
159
+ try {
160
+ const json = JSON.parse(zip.readFile(entry).toString("utf8"));
161
+ if (json.environment === "client") {
162
+ sidedness = "client_only";
163
+ source = manifestFile;
164
+ evidence = `"environment": "client"`;
165
+ }
166
+ else if (json.environment === "server") {
167
+ sidedness = "server_only";
168
+ source = manifestFile;
169
+ evidence = `"environment": "server"`;
170
+ }
171
+ else if (json.environment === "*") {
172
+ sidedness = "common";
173
+ source = manifestFile;
174
+ evidence = `"environment": "*"`;
175
+ }
176
+ }
177
+ catch { }
178
+ if (sidedness !== "unknown")
179
+ break;
180
+ }
181
+ // 2. NeoForge / Forge: mods.toml displayTest
182
+ if (sidedness === "unknown") {
183
+ for (const tomlPath of ["META-INF/neoforge.mods.toml", "META-INF/mods.toml"]) {
184
+ const entry = zip.getEntry(tomlPath);
185
+ if (!entry)
186
+ continue;
187
+ const raw = zip.readFile(entry).toString("utf8");
188
+ const m = raw.match(/displayTest\s*=\s*["']?([A-Z_]+)["']?/i);
189
+ if (m) {
190
+ sidedness = DISPLAY_TEST_MAP[m[1]] ?? "common";
191
+ source = tomlPath;
192
+ evidence = `displayTest = "${m[1]}"`;
193
+ }
194
+ else if (raw.includes("[[mods]]")) {
195
+ // Present but no displayTest → defaults to MATCH_VERSION (common)
196
+ sidedness = "common";
197
+ source = tomlPath;
198
+ evidence = "no displayTest field → defaults to MATCH_VERSION (common)";
199
+ }
200
+ if (sidedness !== "unknown")
201
+ break;
202
+ }
203
+ }
204
+ // 3. Bytecode heuristic
205
+ if (sidedness === "unknown") {
206
+ try {
207
+ const index = await indexJar(mod.jarPath);
208
+ const refKeys = Object.keys(index.references);
209
+ const hasClient = CLIENT_MARKERS.some(m => refKeys.some(k => k.includes(m)));
210
+ const hasServer = SERVER_MARKERS.some(m => refKeys.some(k => k.includes(m)));
211
+ if (hasClient && !hasServer) {
212
+ sidedness = "client_only";
213
+ source = "bytecode";
214
+ evidence = "references client APIs, no server-only APIs";
215
+ }
216
+ else if (!hasClient && hasServer) {
217
+ sidedness = "server_only";
218
+ source = "bytecode";
219
+ evidence = "references server APIs, no client-only APIs";
220
+ }
221
+ else if (hasClient && hasServer) {
222
+ sidedness = "common";
223
+ source = "bytecode";
224
+ evidence = "references both client and server APIs";
225
+ }
226
+ else {
227
+ sidedness = "common";
228
+ source = "bytecode";
229
+ evidence = "no clear client/server markers — assumed common";
230
+ }
231
+ }
232
+ catch { }
233
+ }
234
+ return {
235
+ mod: mod.modId,
236
+ display: mod.displayName,
237
+ version: mod.version,
238
+ loader: mod.loader,
239
+ sidedness,
240
+ source,
241
+ evidence,
242
+ };
243
+ }
244
+ /**
245
+ * Classify all mods in the DB by sidedness.
246
+ * Groups results into: client_only / server_only / client_optional / common / unknown.
247
+ */
248
+ export async function analyzePackSidedness(mcVersion, loader) {
249
+ const mods = await listModsSlim({ mcVersion, loader });
250
+ // Analyse concurrently in batches of 10 to avoid overwhelming the JAR reader
251
+ const BATCH = 10;
252
+ const all = [];
253
+ for (let i = 0; i < mods.length; i += BATCH) {
254
+ const batch = mods.slice(i, i + BATCH);
255
+ const results = await Promise.all(batch.map(m => analyzeModSidedness(m.id)));
256
+ all.push(...results);
257
+ }
258
+ const grouped = {
259
+ client_only: [], server_only: [], client_optional: [], common: [], unknown: [],
260
+ };
261
+ for (const r of all) {
262
+ if (r.error)
263
+ continue;
264
+ grouped[r.sidedness].push({ mod: r.mod, display: r.display, version: r.version, source: r.source, evidence: r.evidence });
265
+ }
266
+ return {
267
+ mcVersion: mcVersion ?? "(all)",
268
+ loader: loader ?? "(all)",
269
+ summary: {
270
+ client_only: grouped.client_only.length,
271
+ server_only: grouped.server_only.length,
272
+ client_optional: grouped.client_optional.length,
273
+ common: grouped.common.length,
274
+ unknown: grouped.unknown.length,
275
+ total: mods.length,
276
+ },
277
+ note: "client_only mods can be removed from dedicated servers. client_optional mods are not required on the server side. server_only mods can be removed from client-only installations.",
278
+ grouped,
279
+ };
280
+ }
281
+ // ── Mod complexity scoring ────────────────────────────────────────────────────
282
+ /**
283
+ * Compute a complexity / heaviness score per mod.
284
+ * Score = classCount + atCount×10 + awCount×10 + mixinCount×20
285
+ *
286
+ * Useful for identifying which mods are the biggest sources of class
287
+ * transformer / mixin overhead — helpful when diagnosing crashes or lag.
288
+ */
289
+ export async function computeModComplexity(mcVersion, loader) {
290
+ const mods = await listMods({ mcVersion, loader, limit: 9999 });
291
+ const results = [];
292
+ for (const mod of mods) {
293
+ let classCount = 0;
294
+ try {
295
+ classCount = listEntries(mod.jarPath).filter(e => e.endsWith(".class")).length;
296
+ }
297
+ catch { }
298
+ const atCount = mod.atEntries.length;
299
+ const awCount = mod.awEntries.length;
300
+ const mixinCount = mod.mixinTargets.length;
301
+ // Weighted: raw class count, mixin targets are heavier (each is a transformation hook)
302
+ const score = classCount + atCount * 10 + awCount * 10 + mixinCount * 20;
303
+ results.push({ mod: mod.modId, display: mod.displayName, version: mod.version, loader: mod.loader, classCount, mixinCount, atCount, awCount, score });
304
+ }
305
+ results.sort((a, b) => b.score - a.score);
306
+ return {
307
+ mcVersion: mcVersion ?? "(all)",
308
+ loader: loader ?? "(all)",
309
+ note: "Score = classCount + (atEntries + awEntries)×10 + mixinTargets×20. Higher score = heavier class transformer/mixin footprint.",
310
+ mods: results,
311
+ };
312
+ }
313
+ // ── Pack changelog ─────────────────────────────────────────────────────────────
314
+ /**
315
+ * Compare two sets of mod DB ids (old pack vs new pack).
316
+ * Returns: added mods, removed mods, updated mods (same modId, different version).
317
+ *
318
+ * oldIds: DB ids representing the old pack state
319
+ * newIds: DB ids representing the new pack state
320
+ */
321
+ export async function computePackChangelog(oldIds, newIds) {
322
+ const [oldMods, newMods] = await Promise.all([
323
+ findModsByIds(oldIds),
324
+ findModsByIds(newIds),
325
+ ]);
326
+ const oldMap = new Map(oldMods.map(m => [m.modId, m]));
327
+ const newMap = new Map(newMods.map(m => [m.modId, m]));
328
+ const added = newMods.filter(m => !oldMap.has(m.modId)).map(m => ({ mod: m.modId, display: m.displayName, version: m.version }));
329
+ const removed = oldMods.filter(m => !newMap.has(m.modId)).map(m => ({ mod: m.modId, display: m.displayName, version: m.version }));
330
+ const updated = [];
331
+ for (const [modId, oldMod] of oldMap) {
332
+ const newMod = newMap.get(modId);
333
+ if (newMod && newMod.version !== oldMod.version) {
334
+ updated.push({ mod: modId, display: newMod.displayName, oldVersion: oldMod.version, newVersion: newMod.version });
335
+ }
336
+ }
337
+ return {
338
+ summary: { added: added.length, removed: removed.length, updated: updated.length },
339
+ oldPackSize: oldMods.length,
340
+ newPackSize: newMods.length,
341
+ added,
342
+ removed,
343
+ updated,
344
+ };
345
+ }
346
+ // ── Data conflict detection ────────────────────────────────────────────────────
347
+ /**
348
+ * Scan all mod JARs in the DB for duplicate data resource paths.
349
+ * When two mods ship the same data/ path, the last-loaded mod silently wins.
350
+ *
351
+ * dataType: filter to specific data sub-folder
352
+ * (recipe | loot_tables | advancements | tags | structures | all)
353
+ * mcVersion / loader: optional DB filters
354
+ * limit: max conflicts to return (default 300)
355
+ */
356
+ export async function findDataConflicts(dataType, mcVersion, loader, limit = 300) {
357
+ const mods = await listModsSlim({ mcVersion, loader });
358
+ const typeFilter = dataType && dataType !== "all" ? dataType : null;
359
+ const pathMap = new Map();
360
+ for (const mod of mods) {
361
+ try {
362
+ const entries = listEntries(mod.jarPath, "data/");
363
+ for (const entry of entries) {
364
+ if (entry.endsWith("/"))
365
+ continue;
366
+ if (typeFilter && !entry.includes(`/${typeFilter}/`))
367
+ continue;
368
+ const list = pathMap.get(entry) ?? [];
369
+ list.push({ mod: mod.modId, display: mod.displayName });
370
+ pathMap.set(entry, list);
371
+ }
372
+ }
373
+ catch { /* skip unreadable JARs */ }
374
+ }
375
+ const conflicts = [];
376
+ for (const [path, owners] of pathMap) {
377
+ if (owners.length < 2)
378
+ continue;
379
+ conflicts.push({
380
+ path,
381
+ isVanillaOverride: path.startsWith("data/minecraft/"),
382
+ modCount: owners.length,
383
+ mods: owners,
384
+ });
385
+ }
386
+ conflicts.sort((a, b) => b.modCount - a.modCount);
387
+ const capped = conflicts.length > limit;
388
+ const limited = conflicts.slice(0, limit);
389
+ // byType breakdown: data/<namespace>/<type>/... → parts[2]
390
+ const byType = {};
391
+ for (const c of limited) {
392
+ const parts = c.path.split("/");
393
+ const t = parts[2] ?? "unknown";
394
+ byType[t] = (byType[t] ?? 0) + 1;
395
+ }
396
+ const vanillaOverrideConflicts = limited.filter((c) => c.isVanillaOverride).length;
397
+ return {
398
+ modsScanned: mods.length,
399
+ totalConflicts: limited.length,
400
+ capped,
401
+ byType,
402
+ vanillaOverrideConflicts,
403
+ note: capped ? `Results capped at ${limit}. Use dataType or loader/mcVersion to narrow.` : "",
404
+ conflicts: limited,
405
+ };
406
+ }
407
+ /**
408
+ * Build a composite knowledge graph of the entire modpack.
409
+ *
410
+ * Nodes: mods, mixin target classes, tags, KubeJS scripts, script categories
411
+ * Edges: depends_on, mixes_into, contributes_tag, tag_conflict, script_modifies, has_category
412
+ *
413
+ * @param mcVersion optional MC version filter
414
+ * @param loader optional loader filter
415
+ * @param scriptsDir optional path to kubejs/ directory to include script analysis
416
+ */
417
+ export async function buildPackGraph(mcVersion, loader, scriptsDir) {
418
+ const db = await getDb();
419
+ // 1. Fetch all mods with deps and mixin data
420
+ const mods = await db.mod.findMany({
421
+ where: {
422
+ ...(mcVersion ? { mcVersion: { contains: mcVersion } } : {}),
423
+ ...(loader ? { loader } : {}),
424
+ },
425
+ select: {
426
+ id: true, modId: true, displayName: true, version: true,
427
+ loader: true, mcVersion: true, hasMixins: true,
428
+ dependencies: true, mixinTargets: true,
429
+ },
430
+ });
431
+ const nodes = [];
432
+ const edges = [];
433
+ const nodeIds = new Set();
434
+ const addNode = (n) => {
435
+ if (nodeIds.has(n.id))
436
+ return;
437
+ nodeIds.add(n.id);
438
+ nodes.push(n);
439
+ };
440
+ // Loader pseudo-deps to skip
441
+ const PSEUDO_DEPS = new Set(["minecraft", "neoforge", "forge", "fabricloader", "fabric-api", "java", "quilt_loader"]);
442
+ // 2. Mod nodes + dependency edges
443
+ for (const mod of mods) {
444
+ addNode({
445
+ id: `mod:${mod.modId}`,
446
+ type: "mod",
447
+ label: mod.displayName,
448
+ meta: { version: mod.version, loader: mod.loader, mcVersion: mod.mcVersion },
449
+ });
450
+ const deps = (mod.dependencies ?? []);
451
+ for (const dep of deps) {
452
+ if (!dep.id || PSEUDO_DEPS.has(dep.id))
453
+ continue;
454
+ const targetId = `mod:${dep.id}`;
455
+ addNode({ id: targetId, type: "mod", label: dep.id });
456
+ edges.push({
457
+ source: `mod:${mod.modId}`,
458
+ target: targetId,
459
+ type: "depends_on",
460
+ meta: { required: dep.required ?? true, versionRange: dep.version },
461
+ });
462
+ }
463
+ // 3. Mixin target edges
464
+ const targets = (mod.mixinTargets ?? []);
465
+ for (const cls of targets) {
466
+ const classId = `class:${cls}`;
467
+ addNode({ id: classId, type: "mc_class", label: cls.split("/").pop() ?? cls, meta: { fqn: cls } });
468
+ edges.push({ source: `mod:${mod.modId}`, target: classId, type: "mixes_into" });
469
+ }
470
+ }
471
+ // 4. Tag contribution edges
472
+ const tags = await db.modTag.findMany({
473
+ where: {
474
+ mod: {
475
+ ...(mcVersion ? { mcVersion: { contains: mcVersion } } : {}),
476
+ ...(loader ? { loader } : {}),
477
+ },
478
+ },
479
+ select: { modId: true, tagPath: true, registry: true, replace: true, mod: { select: { modId: true } } },
480
+ });
481
+ const tagContributors = new Map();
482
+ for (const t of tags) {
483
+ const tagId = `tag:${t.registry}/${t.tagPath}`;
484
+ addNode({ id: tagId, type: "tag", label: `#${t.tagPath}`, meta: { registry: t.registry } });
485
+ edges.push({
486
+ source: `mod:${t.mod.modId}`,
487
+ target: tagId,
488
+ type: "contributes_tag",
489
+ meta: { replace: t.replace },
490
+ });
491
+ const list = tagContributors.get(tagId) ?? [];
492
+ list.push({ modId: t.mod.modId, replace: t.replace });
493
+ tagContributors.set(tagId, list);
494
+ }
495
+ // 5. Tag conflict edges (multiple mods with replace:true on same tag)
496
+ for (const [tagId, contributors] of tagContributors) {
497
+ const replacers = contributors.filter(c => c.replace);
498
+ if (replacers.length >= 2) {
499
+ for (let i = 0; i < replacers.length; i++) {
500
+ for (let j = i + 1; j < replacers.length; j++) {
501
+ edges.push({
502
+ source: `mod:${replacers[i].modId}`,
503
+ target: `mod:${replacers[j].modId}`,
504
+ type: "tag_conflict",
505
+ meta: { tag: tagId },
506
+ });
507
+ }
508
+ }
509
+ }
510
+ }
511
+ // 6. KubeJS scripts (optional)
512
+ let kubeJsStats;
513
+ if (scriptsDir) {
514
+ try {
515
+ const kjResult = await indexKubeJsScripts(scriptsDir);
516
+ kubeJsStats = { fileCount: kjResult.fileCount, categories: kjResult.categorySummary };
517
+ for (const cat of Object.keys(kjResult.categorySummary)) {
518
+ addNode({ id: `kjs_cat:${cat}`, type: "script_category", label: cat });
519
+ }
520
+ for (const script of kjResult.scripts) {
521
+ const scriptId = `script:${script.path}`;
522
+ addNode({
523
+ id: scriptId,
524
+ type: "script",
525
+ label: script.path.split("/").pop() ?? script.path,
526
+ meta: { path: script.path, lineCount: script.lineCount },
527
+ });
528
+ for (const cat of script.categories) {
529
+ edges.push({ source: scriptId, target: `kjs_cat:${cat}`, type: "has_category" });
530
+ if (cat.startsWith("recipe_") || cat === "tag_modify" || cat === "loot_modify") {
531
+ edges.push({ source: scriptId, target: `kjs_cat:${cat}`, type: "script_modifies" });
532
+ }
533
+ }
534
+ }
535
+ }
536
+ catch { /* scriptsDir unreadable — skip */ }
537
+ }
538
+ // 7. Summary stats
539
+ const byNodeType = {};
540
+ for (const n of nodes)
541
+ byNodeType[n.type] = (byNodeType[n.type] ?? 0) + 1;
542
+ const byEdgeType = {};
543
+ for (const e of edges)
544
+ byEdgeType[e.type] = (byEdgeType[e.type] ?? 0) + 1;
545
+ const edgeCounts = new Map();
546
+ for (const e of edges) {
547
+ edgeCounts.set(e.source, (edgeCounts.get(e.source) ?? 0) + 1);
548
+ edgeCounts.set(e.target, (edgeCounts.get(e.target) ?? 0) + 1);
549
+ }
550
+ const hubs = [...edgeCounts.entries()]
551
+ .filter(([id]) => id.startsWith("mod:"))
552
+ .sort((a, b) => b[1] - a[1])
553
+ .slice(0, 10)
554
+ .map(([id, count]) => ({ id, connections: count }));
555
+ const mixinHotspots = [...edgeCounts.entries()]
556
+ .filter(([id]) => id.startsWith("class:"))
557
+ .sort((a, b) => b[1] - a[1])
558
+ .slice(0, 10)
559
+ .map(([id, count]) => ({ id, modCount: count }));
560
+ return {
561
+ summary: {
562
+ totalNodes: nodes.length,
563
+ totalEdges: edges.length,
564
+ byNodeType,
565
+ byEdgeType,
566
+ hubs,
567
+ mixinHotspots,
568
+ ...(kubeJsStats ? { kubeJs: kubeJsStats } : {}),
569
+ },
570
+ nodes,
571
+ edges,
572
+ };
573
+ }
574
+ // ── Config diff analysis ───────────────────────────────────────────────────────
575
+ /**
576
+ * Walk a directory tree recursively and return relative paths of all files.
577
+ */
578
+ function walkDir(dir, base = dir) {
579
+ const files = [];
580
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
581
+ const full = join(dir, entry.name);
582
+ if (entry.isDirectory()) {
583
+ files.push(...walkDir(full, base));
584
+ }
585
+ else if (entry.isFile()) {
586
+ files.push(relative(base, full).replace(/\\/g, "/"));
587
+ }
588
+ }
589
+ return files;
590
+ }
591
+ /**
592
+ * Compare a modpack's config directory against default configs bundled in mod JARs.
593
+ *
594
+ * For each ingested mod, extracts default config files from the JAR
595
+ * (`defaultconfigs/` and `config/` prefixes) and compares against the
596
+ * on-disk pack config directory.
597
+ *
598
+ * Returns:
599
+ * - modified: files that exist in both the pack and a JAR default, but content differs
600
+ * - custom: files on disk that don't match any mod's JAR defaults
601
+ * - unchanged: files identical to the JAR default
602
+ *
603
+ * @param configDir absolute path to the modpack's config/ directory
604
+ * @param mcVersion optional MC version filter
605
+ * @param loader optional loader filter
606
+ */
607
+ export async function diffPackConfigs(configDir, mcVersion, loader) {
608
+ const resolved = normalize(configDir);
609
+ if (!isAbsolute(resolved)) {
610
+ return { error: "configDir must be an absolute path" };
611
+ }
612
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
613
+ return { error: `Config directory not found or not a directory: ${configDir}` };
614
+ }
615
+ const mods = await listModsSlim({ mcVersion, loader });
616
+ const defaults = new Map();
617
+ for (const mod of mods) {
618
+ try {
619
+ assertJarPath(mod.jarPath);
620
+ for (const prefix of ["config/", "defaultconfigs/"]) {
621
+ const entries = listEntries(mod.jarPath, prefix);
622
+ for (const entry of entries) {
623
+ if (entry.endsWith("/"))
624
+ continue;
625
+ // Normalize to relative config path (strip prefix for matching)
626
+ const relPath = entry.startsWith("defaultconfigs/")
627
+ ? entry.slice("defaultconfigs/".length)
628
+ : entry.slice("config/".length);
629
+ const buf = extractEntry(mod.jarPath, entry);
630
+ if (!buf)
631
+ continue;
632
+ const list = defaults.get(relPath) ?? [];
633
+ list.push({ modId: mod.modId, display: mod.displayName, jarEntry: entry, content: buf });
634
+ defaults.set(relPath, list);
635
+ }
636
+ }
637
+ }
638
+ catch { /* skip unreadable JARs */ }
639
+ }
640
+ // Walk the on-disk config directory
641
+ const diskFiles = walkDir(configDir);
642
+ const modified = [];
643
+ const unchanged = [];
644
+ const custom = [];
645
+ for (const diskRel of diskFiles) {
646
+ const jarDefaults = defaults.get(diskRel);
647
+ if (!jarDefaults || jarDefaults.length === 0) {
648
+ custom.push(diskRel);
649
+ continue;
650
+ }
651
+ let diskContent;
652
+ try {
653
+ diskContent = readFileSync(join(configDir, diskRel));
654
+ }
655
+ catch {
656
+ custom.push(diskRel);
657
+ continue;
658
+ }
659
+ // Compare against each mod that ships this config (typically one)
660
+ let anyDiff = false;
661
+ for (const def of jarDefaults) {
662
+ if (diskContent.equals(def.content)) {
663
+ unchanged.push({ file: diskRel, mod: def.modId, display: def.display });
664
+ }
665
+ else {
666
+ modified.push({
667
+ file: diskRel,
668
+ mod: def.modId,
669
+ display: def.display,
670
+ jarEntry: def.jarEntry,
671
+ sizeDisk: diskContent.length,
672
+ sizeJar: def.content.length,
673
+ });
674
+ anyDiff = true;
675
+ }
676
+ }
677
+ if (!anyDiff && !unchanged.some(u => u.file === diskRel)) {
678
+ custom.push(diskRel);
679
+ }
680
+ }
681
+ // Also report JAR defaults that have no on-disk counterpart (could indicate a missing config)
682
+ const missing = [];
683
+ for (const [relPath, defs] of defaults) {
684
+ if (!diskFiles.includes(relPath)) {
685
+ for (const def of defs) {
686
+ missing.push({ file: relPath, mod: def.modId, display: def.display });
687
+ }
688
+ }
689
+ }
690
+ return {
691
+ configDir,
692
+ modsScanned: mods.length,
693
+ totalDiskFiles: diskFiles.length,
694
+ totalJarDefaults: defaults.size,
695
+ summary: {
696
+ modified: modified.length,
697
+ unchanged: unchanged.length,
698
+ custom: custom.length,
699
+ missingOnDisk: missing.length,
700
+ },
701
+ note: "modified = pack config differs from JAR default. custom = on disk but no JAR default found. missing = JAR ships a default but no on-disk file exists.",
702
+ modified,
703
+ unchanged,
704
+ custom,
705
+ missing,
706
+ };
707
+ }
708
+ // ── Pack health summary ────────────────────────────────────────────────────────
709
+ /**
710
+ * One-shot modpack health overview.
711
+ * Aggregates key metrics in a single call to give agents a fast starting point:
712
+ * - mod count, loader/MC version breakdown
713
+ * - missing dependency count
714
+ * - mixin hotspot count (classes targeted by 3+ mods)
715
+ * - asset/data conflict counts
716
+ * - sidedness breakdown (client_only / server_only / common)
717
+ */
718
+ export async function packHealth(mcVersion, loader) {
719
+ const db = await getDb();
720
+ // 1. Basic stats
721
+ const where = {
722
+ ...(mcVersion ? { mcVersion: { contains: mcVersion } } : {}),
723
+ ...(loader ? { loader } : {}),
724
+ };
725
+ const mods = await db.mod.findMany({
726
+ where,
727
+ select: {
728
+ id: true, modId: true, displayName: true, version: true,
729
+ loader: true, mcVersion: true, hasMixins: true,
730
+ dependencies: true, mixinTargets: true,
731
+ modrinthId: true, curseforgeId: true,
732
+ },
733
+ });
734
+ const loaderBreakdown = {};
735
+ const versionBreakdown = {};
736
+ let withMixins = 0, linkedMR = 0, linkedCF = 0;
737
+ for (const m of mods) {
738
+ loaderBreakdown[m.loader] = (loaderBreakdown[m.loader] ?? 0) + 1;
739
+ versionBreakdown[m.mcVersion] = (versionBreakdown[m.mcVersion] ?? 0) + 1;
740
+ if (m.hasMixins)
741
+ withMixins++;
742
+ if (m.modrinthId)
743
+ linkedMR++;
744
+ if (m.curseforgeId)
745
+ linkedCF++;
746
+ }
747
+ // 2. Missing deps (lightweight inline check)
748
+ const SKIP = new Set(["minecraft", "neoforge", "forge", "fabric-api", "fabricloader", "quilt_loader", "java"]);
749
+ const ingestedIds = new Set(mods.map(m => m.modId));
750
+ let missingDeps = 0;
751
+ const missingList = [];
752
+ for (const mod of mods) {
753
+ const deps = (mod.dependencies ?? []);
754
+ for (const dep of deps) {
755
+ if (!dep.id || SKIP.has(dep.id))
756
+ continue;
757
+ if (!ingestedIds.has(dep.id)) {
758
+ missingDeps++;
759
+ if (missingList.length < 10) {
760
+ missingList.push({ dep: dep.id, requiredBy: mod.modId });
761
+ }
762
+ }
763
+ }
764
+ }
765
+ // 3. Mixin hotspots (classes targeted by 3+ mods)
766
+ const classHits = new Map();
767
+ for (const mod of mods) {
768
+ const targets = (mod.mixinTargets ?? []);
769
+ for (const cls of targets) {
770
+ const set = classHits.get(cls) ?? new Set();
771
+ set.add(mod.modId);
772
+ classHits.set(cls, set);
773
+ }
774
+ }
775
+ const hotspots = [...classHits.entries()]
776
+ .filter(([, mods]) => mods.size >= 3)
777
+ .sort((a, b) => b[1].size - a[1].size)
778
+ .slice(0, 10)
779
+ .map(([cls, modSet]) => ({ class: cls, modCount: modSet.size, mods: [...modSet].slice(0, 5) }));
780
+ // 4. Tag conflicts (lightweight — count only from indexed tags)
781
+ let tagConflicts = 0;
782
+ try {
783
+ const tags = await db.modTag.groupBy({
784
+ by: ["tagPath", "registry"],
785
+ where: { replace: true, mod: where },
786
+ _count: { modId: true },
787
+ });
788
+ tagConflicts = tags.filter(t => t._count.modId >= 2).length;
789
+ }
790
+ catch { /* tag table may not exist */ }
791
+ return {
792
+ mcVersion: mcVersion ?? "(all)",
793
+ loader: loader ?? "(all)",
794
+ totalMods: mods.length,
795
+ loaderBreakdown,
796
+ versionBreakdown,
797
+ modsWithMixins: withMixins,
798
+ platformLinks: { modrinth: linkedMR, curseforge: linkedCF, unlinked: mods.length - Math.max(linkedMR, linkedCF) },
799
+ issues: {
800
+ missingDeps,
801
+ missingDepsTop: missingList,
802
+ mixinHotspots: hotspots.length,
803
+ mixinHotspotsTop: hotspots,
804
+ tagConflicts,
805
+ },
806
+ note: "Use specific pack_tools actions for detailed analysis: asset_conflicts, data_conflicts, config_diff, pack_sidedness.",
807
+ };
808
+ }
809
+ //# sourceMappingURL=packtools.js.map