@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.
- package/README.md +938 -0
- package/dist/access-flags.d.ts +58 -0
- package/dist/access-flags.d.ts.map +1 -0
- package/dist/access-flags.js +79 -0
- package/dist/access-flags.js.map +1 -0
- package/dist/access-flags.test.d.ts +2 -0
- package/dist/access-flags.test.d.ts.map +1 -0
- package/dist/access-flags.test.js +189 -0
- package/dist/access-flags.test.js.map +1 -0
- package/dist/batch-ingest.d.ts +2 -0
- package/dist/batch-ingest.d.ts.map +1 -0
- package/dist/batch-ingest.js +54 -0
- package/dist/batch-ingest.js.map +1 -0
- package/dist/batch-resolve-mixins.d.ts +2 -0
- package/dist/batch-resolve-mixins.d.ts.map +1 -0
- package/dist/batch-resolve-mixins.js +36 -0
- package/dist/batch-resolve-mixins.js.map +1 -0
- package/dist/cache.d.ts +16 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +36 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1318 -0
- package/dist/cli.js.map +1 -0
- package/dist/curseforge.d.ts +53 -0
- package/dist/curseforge.d.ts.map +1 -0
- package/dist/curseforge.js +104 -0
- package/dist/curseforge.js.map +1 -0
- package/dist/db-backend.d.ts +17 -0
- package/dist/db-backend.d.ts.map +1 -0
- package/dist/db-backend.js +40 -0
- package/dist/db-backend.js.map +1 -0
- package/dist/db.d.ts +11 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +55 -0
- package/dist/db.js.map +1 -0
- package/dist/embed-queue.d.ts +12 -0
- package/dist/embed-queue.d.ts.map +1 -0
- package/dist/embed-queue.js +103 -0
- package/dist/embed-queue.js.map +1 -0
- package/dist/embeddings.d.ts +21 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +55 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/env-path.d.ts +19 -0
- package/dist/env-path.d.ts.map +1 -0
- package/dist/env-path.js +40 -0
- package/dist/env-path.js.map +1 -0
- package/dist/fetch-utils.d.ts +24 -0
- package/dist/fetch-utils.d.ts.map +1 -0
- package/dist/fetch-utils.js +73 -0
- package/dist/fetch-utils.js.map +1 -0
- package/dist/fetch-utils.test.d.ts +2 -0
- package/dist/fetch-utils.test.d.ts.map +1 -0
- package/dist/fetch-utils.test.js +133 -0
- package/dist/fetch-utils.test.js.map +1 -0
- package/dist/generated/sqlite/browser.d.ts +55 -0
- package/dist/generated/sqlite/browser.d.ts.map +1 -0
- package/dist/generated/sqlite/browser.js +18 -0
- package/dist/generated/sqlite/browser.js.map +1 -0
- package/dist/generated/sqlite/client.d.ts +72 -0
- package/dist/generated/sqlite/client.d.ts.map +1 -0
- package/dist/generated/sqlite/client.js +33 -0
- package/dist/generated/sqlite/client.js.map +1 -0
- package/dist/generated/sqlite/commonInputTypes.d.ts +387 -0
- package/dist/generated/sqlite/commonInputTypes.d.ts.map +1 -0
- package/dist/generated/sqlite/commonInputTypes.js +11 -0
- package/dist/generated/sqlite/commonInputTypes.js.map +1 -0
- package/dist/generated/sqlite/enums.d.ts +2 -0
- package/dist/generated/sqlite/enums.d.ts.map +1 -0
- package/dist/generated/sqlite/enums.js +11 -0
- package/dist/generated/sqlite/enums.js.map +1 -0
- package/dist/generated/sqlite/internal/class.d.ts +226 -0
- package/dist/generated/sqlite/internal/class.d.ts.map +1 -0
- package/dist/generated/sqlite/internal/class.js +84 -0
- package/dist/generated/sqlite/internal/class.js.map +1 -0
- package/dist/generated/sqlite/internal/prismaNamespace.d.ts +1344 -0
- package/dist/generated/sqlite/internal/prismaNamespace.d.ts.map +1 -0
- package/dist/generated/sqlite/internal/prismaNamespace.js +220 -0
- package/dist/generated/sqlite/internal/prismaNamespace.js.map +1 -0
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.d.ts +204 -0
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.d.ts.map +1 -0
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.js +191 -0
- package/dist/generated/sqlite/internal/prismaNamespaceBrowser.js.map +1 -0
- package/dist/generated/sqlite/models/DocEntry.d.ts +1223 -0
- package/dist/generated/sqlite/models/DocEntry.d.ts.map +1 -0
- package/dist/generated/sqlite/models/DocEntry.js +2 -0
- package/dist/generated/sqlite/models/DocEntry.js.map +1 -0
- package/dist/generated/sqlite/models/McSourceFile.d.ts +1289 -0
- package/dist/generated/sqlite/models/McSourceFile.d.ts.map +1 -0
- package/dist/generated/sqlite/models/McSourceFile.js +2 -0
- package/dist/generated/sqlite/models/McSourceFile.js.map +1 -0
- package/dist/generated/sqlite/models/McVersion.d.ts +1315 -0
- package/dist/generated/sqlite/models/McVersion.d.ts.map +1 -0
- package/dist/generated/sqlite/models/McVersion.js +2 -0
- package/dist/generated/sqlite/models/McVersion.js.map +1 -0
- package/dist/generated/sqlite/models/McVersionDiff.d.ts +1061 -0
- package/dist/generated/sqlite/models/McVersionDiff.d.ts.map +1 -0
- package/dist/generated/sqlite/models/McVersionDiff.js +2 -0
- package/dist/generated/sqlite/models/McVersionDiff.js.map +1 -0
- package/dist/generated/sqlite/models/Mod.d.ts +2396 -0
- package/dist/generated/sqlite/models/Mod.d.ts.map +1 -0
- package/dist/generated/sqlite/models/Mod.js +2 -0
- package/dist/generated/sqlite/models/Mod.js.map +1 -0
- package/dist/generated/sqlite/models/ModClass.d.ts +1254 -0
- package/dist/generated/sqlite/models/ModClass.d.ts.map +1 -0
- package/dist/generated/sqlite/models/ModClass.js +2 -0
- package/dist/generated/sqlite/models/ModClass.js.map +1 -0
- package/dist/generated/sqlite/models/ModSourceFile.d.ts +1283 -0
- package/dist/generated/sqlite/models/ModSourceFile.d.ts.map +1 -0
- package/dist/generated/sqlite/models/ModSourceFile.js +2 -0
- package/dist/generated/sqlite/models/ModSourceFile.js.map +1 -0
- package/dist/generated/sqlite/models/ModTag.d.ts +1278 -0
- package/dist/generated/sqlite/models/ModTag.d.ts.map +1 -0
- package/dist/generated/sqlite/models/ModTag.js +2 -0
- package/dist/generated/sqlite/models/ModTag.js.map +1 -0
- package/dist/generated/sqlite/models/ModVersionDiff.d.ts +1073 -0
- package/dist/generated/sqlite/models/ModVersionDiff.d.ts.map +1 -0
- package/dist/generated/sqlite/models/ModVersionDiff.js +2 -0
- package/dist/generated/sqlite/models/ModVersionDiff.js.map +1 -0
- package/dist/generated/sqlite/models/Primer.d.ts +1319 -0
- package/dist/generated/sqlite/models/Primer.d.ts.map +1 -0
- package/dist/generated/sqlite/models/Primer.js +2 -0
- package/dist/generated/sqlite/models/Primer.js.map +1 -0
- package/dist/generated/sqlite/models.d.ts +12 -0
- package/dist/generated/sqlite/models.d.ts.map +1 -0
- package/dist/generated/sqlite/models.js +2 -0
- package/dist/generated/sqlite/models.js.map +1 -0
- package/dist/jar.d.ts +7 -0
- package/dist/jar.d.ts.map +1 -0
- package/dist/jar.js +52 -0
- package/dist/jar.js.map +1 -0
- package/dist/java-tools.d.ts +46 -0
- package/dist/java-tools.d.ts.map +1 -0
- package/dist/java-tools.js +263 -0
- package/dist/java-tools.js.map +1 -0
- package/dist/launcher.d.ts +3 -0
- package/dist/launcher.d.ts.map +1 -0
- package/dist/launcher.js +105 -0
- package/dist/launcher.js.map +1 -0
- package/dist/mappings.d.ts +95 -0
- package/dist/mappings.d.ts.map +1 -0
- package/dist/mappings.js +1094 -0
- package/dist/mappings.js.map +1 -0
- package/dist/mappings.test.d.ts +2 -0
- package/dist/mappings.test.d.ts.map +1 -0
- package/dist/mappings.test.js +161 -0
- package/dist/mappings.test.js.map +1 -0
- package/dist/minecraft.d.ts +21 -0
- package/dist/minecraft.d.ts.map +1 -0
- package/dist/minecraft.js +70 -0
- package/dist/minecraft.js.map +1 -0
- package/dist/modpacks-ch.d.ts +224 -0
- package/dist/modpacks-ch.d.ts.map +1 -0
- package/dist/modpacks-ch.js +146 -0
- package/dist/modpacks-ch.js.map +1 -0
- package/dist/modrinth.d.ts +67 -0
- package/dist/modrinth.d.ts.map +1 -0
- package/dist/modrinth.js +88 -0
- package/dist/modrinth.js.map +1 -0
- package/dist/platform-adapter.d.ts +23 -0
- package/dist/platform-adapter.d.ts.map +1 -0
- package/dist/platform-adapter.js +2 -0
- package/dist/platform-adapter.js.map +1 -0
- package/dist/platform.d.ts +47 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +175 -0
- package/dist/platform.js.map +1 -0
- package/dist/processor.d.ts +51 -0
- package/dist/processor.d.ts.map +1 -0
- package/dist/processor.js +655 -0
- package/dist/processor.js.map +1 -0
- package/dist/processor.test.d.ts +2 -0
- package/dist/processor.test.d.ts.map +1 -0
- package/dist/processor.test.js +629 -0
- package/dist/processor.test.js.map +1 -0
- package/dist/repositories/embeddings-sqlite.d.ts +38 -0
- package/dist/repositories/embeddings-sqlite.d.ts.map +1 -0
- package/dist/repositories/embeddings-sqlite.js +208 -0
- package/dist/repositories/embeddings-sqlite.js.map +1 -0
- package/dist/repositories/embeddings.d.ts +40 -0
- package/dist/repositories/embeddings.d.ts.map +1 -0
- package/dist/repositories/embeddings.js +136 -0
- package/dist/repositories/embeddings.js.map +1 -0
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.d.ts.map +1 -0
- package/dist/repositories/index.js +15 -0
- package/dist/repositories/index.js.map +1 -0
- package/dist/repositories/mcVersion.d.ts +16 -0
- package/dist/repositories/mcVersion.d.ts.map +1 -0
- package/dist/repositories/mcVersion.js +52 -0
- package/dist/repositories/mcVersion.js.map +1 -0
- package/dist/repositories/mod.d.ts +241 -0
- package/dist/repositories/mod.d.ts.map +1 -0
- package/dist/repositories/mod.js +342 -0
- package/dist/repositories/mod.js.map +1 -0
- package/dist/repositories/packs.d.ts +44 -0
- package/dist/repositories/packs.d.ts.map +1 -0
- package/dist/repositories/packs.js +138 -0
- package/dist/repositories/packs.js.map +1 -0
- package/dist/search-adapter.d.ts +34 -0
- package/dist/search-adapter.d.ts.map +1 -0
- package/dist/search-adapter.js +195 -0
- package/dist/search-adapter.js.map +1 -0
- package/dist/search.d.ts +6 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +54 -0
- package/dist/search.js.map +1 -0
- package/dist/search.test.d.ts +2 -0
- package/dist/search.test.d.ts.map +1 -0
- package/dist/search.test.js +71 -0
- package/dist/search.test.js.map +1 -0
- package/dist/security.d.ts +98 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +446 -0
- package/dist/security.js.map +1 -0
- package/dist/security.test.d.ts +2 -0
- package/dist/security.test.d.ts.map +1 -0
- package/dist/security.test.js +687 -0
- package/dist/security.test.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +1465 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +1113 -0
- package/dist/setup.js.map +1 -0
- package/dist/tools/bytecode.d.ts +129 -0
- package/dist/tools/bytecode.d.ts.map +1 -0
- package/dist/tools/bytecode.js +679 -0
- package/dist/tools/bytecode.js.map +1 -0
- package/dist/tools/catalog.d.ts +54 -0
- package/dist/tools/catalog.d.ts.map +1 -0
- package/dist/tools/catalog.js +249 -0
- package/dist/tools/catalog.js.map +1 -0
- package/dist/tools/compat-check.d.ts +23 -0
- package/dist/tools/compat-check.d.ts.map +1 -0
- package/dist/tools/compat-check.js +250 -0
- package/dist/tools/compat-check.js.map +1 -0
- package/dist/tools/compat-check.test.d.ts +2 -0
- package/dist/tools/compat-check.test.d.ts.map +1 -0
- package/dist/tools/compat-check.test.js +179 -0
- package/dist/tools/compat-check.test.js.map +1 -0
- package/dist/tools/diagnostics.d.ts +10 -0
- package/dist/tools/diagnostics.d.ts.map +1 -0
- package/dist/tools/diagnostics.js +133 -0
- package/dist/tools/diagnostics.js.map +1 -0
- package/dist/tools/diagnostics.test.d.ts +2 -0
- package/dist/tools/diagnostics.test.d.ts.map +1 -0
- package/dist/tools/diagnostics.test.js +168 -0
- package/dist/tools/diagnostics.test.js.map +1 -0
- package/dist/tools/docs.d.ts +19 -0
- package/dist/tools/docs.d.ts.map +1 -0
- package/dist/tools/docs.js +230 -0
- package/dist/tools/docs.js.map +1 -0
- package/dist/tools/embed-registry.d.ts +125 -0
- package/dist/tools/embed-registry.d.ts.map +1 -0
- package/dist/tools/embed-registry.js +560 -0
- package/dist/tools/embed-registry.js.map +1 -0
- package/dist/tools/gradle.d.ts +16 -0
- package/dist/tools/gradle.d.ts.map +1 -0
- package/dist/tools/gradle.js +182 -0
- package/dist/tools/gradle.js.map +1 -0
- package/dist/tools/graphify.d.ts +144 -0
- package/dist/tools/graphify.d.ts.map +1 -0
- package/dist/tools/graphify.js +798 -0
- package/dist/tools/graphify.js.map +1 -0
- package/dist/tools/graphify.test.d.ts +2 -0
- package/dist/tools/graphify.test.d.ts.map +1 -0
- package/dist/tools/graphify.test.js +39 -0
- package/dist/tools/graphify.test.js.map +1 -0
- package/dist/tools/ingest.d.ts +85 -0
- package/dist/tools/ingest.d.ts.map +1 -0
- package/dist/tools/ingest.js +477 -0
- package/dist/tools/ingest.js.map +1 -0
- package/dist/tools/ingest.test.d.ts +2 -0
- package/dist/tools/ingest.test.d.ts.map +1 -0
- package/dist/tools/ingest.test.js +340 -0
- package/dist/tools/ingest.test.js.map +1 -0
- package/dist/tools/kubejs.d.ts +37 -0
- package/dist/tools/kubejs.d.ts.map +1 -0
- package/dist/tools/kubejs.js +245 -0
- package/dist/tools/kubejs.js.map +1 -0
- package/dist/tools/mappings.d.ts +17 -0
- package/dist/tools/mappings.d.ts.map +1 -0
- package/dist/tools/mappings.js +85 -0
- package/dist/tools/mappings.js.map +1 -0
- package/dist/tools/mc-fts.d.ts +101 -0
- package/dist/tools/mc-fts.d.ts.map +1 -0
- package/dist/tools/mc-fts.js +279 -0
- package/dist/tools/mc-fts.js.map +1 -0
- package/dist/tools/mcmeta.d.ts +41 -0
- package/dist/tools/mcmeta.d.ts.map +1 -0
- package/dist/tools/mcmeta.js +365 -0
- package/dist/tools/mcmeta.js.map +1 -0
- package/dist/tools/mixin-scan.d.ts +113 -0
- package/dist/tools/mixin-scan.d.ts.map +1 -0
- package/dist/tools/mixin-scan.js +433 -0
- package/dist/tools/mixin-scan.js.map +1 -0
- package/dist/tools/mod-data.d.ts +83 -0
- package/dist/tools/mod-data.d.ts.map +1 -0
- package/dist/tools/mod-data.js +630 -0
- package/dist/tools/mod-data.js.map +1 -0
- package/dist/tools/mod-tags.d.ts +59 -0
- package/dist/tools/mod-tags.d.ts.map +1 -0
- package/dist/tools/mod-tags.js +279 -0
- package/dist/tools/mod-tags.js.map +1 -0
- package/dist/tools/mod-tags.test.d.ts +2 -0
- package/dist/tools/mod-tags.test.d.ts.map +1 -0
- package/dist/tools/mod-tags.test.js +75 -0
- package/dist/tools/mod-tags.test.js.map +1 -0
- package/dist/tools/modpacks-ch.d.ts +257 -0
- package/dist/tools/modpacks-ch.d.ts.map +1 -0
- package/dist/tools/modpacks-ch.js +516 -0
- package/dist/tools/modpacks-ch.js.map +1 -0
- package/dist/tools/packtools.d.ts +113 -0
- package/dist/tools/packtools.d.ts.map +1 -0
- package/dist/tools/packtools.js +809 -0
- package/dist/tools/packtools.js.map +1 -0
- package/dist/tools/packtools.test.d.ts +2 -0
- package/dist/tools/packtools.test.d.ts.map +1 -0
- package/dist/tools/packtools.test.js +99 -0
- package/dist/tools/packtools.test.js.map +1 -0
- package/dist/tools/platform.d.ts +68 -0
- package/dist/tools/platform.d.ts.map +1 -0
- package/dist/tools/platform.js +452 -0
- package/dist/tools/platform.js.map +1 -0
- package/dist/tools/primers.d.ts +36 -0
- package/dist/tools/primers.d.ts.map +1 -0
- package/dist/tools/primers.js +451 -0
- package/dist/tools/primers.js.map +1 -0
- package/dist/tools/reports.d.ts +19 -0
- package/dist/tools/reports.d.ts.map +1 -0
- package/dist/tools/reports.js +573 -0
- package/dist/tools/reports.js.map +1 -0
- package/dist/tools/source.d.ts +10 -0
- package/dist/tools/source.d.ts.map +1 -0
- package/dist/tools/source.js +101 -0
- package/dist/tools/source.js.map +1 -0
- package/dist/tools/vanilla-data.d.ts +127 -0
- package/dist/tools/vanilla-data.d.ts.map +1 -0
- package/dist/tools/vanilla-data.js +824 -0
- package/dist/tools/vanilla-data.js.map +1 -0
- package/dist/tools/vanilla.d.ts +171 -0
- package/dist/tools/vanilla.d.ts.map +1 -0
- package/dist/tools/vanilla.js +612 -0
- package/dist/tools/vanilla.js.map +1 -0
- package/dist/tools/version-diff.d.ts +113 -0
- package/dist/tools/version-diff.d.ts.map +1 -0
- package/dist/tools/version-diff.js +519 -0
- package/dist/tools/version-diff.js.map +1 -0
- package/dist/tools/version-diff.test.d.ts +2 -0
- package/dist/tools/version-diff.test.d.ts.map +1 -0
- package/dist/tools/version-diff.test.js +102 -0
- package/dist/tools/version-diff.test.js.map +1 -0
- package/dist/validate.d.ts +19 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +54 -0
- package/dist/validate.js.map +1 -0
- package/dist/validate.test.d.ts +2 -0
- package/dist/validate.test.d.ts.map +1 -0
- package/dist/validate.test.js +73 -0
- package/dist/validate.test.js.map +1 -0
- package/package.json +62 -0
- package/prisma/backends/schema.sqlite.prisma +215 -0
- package/prisma/backends/template.db +0 -0
- package/prisma/schema.prisma +303 -0
- package/scripts/backup.mjs +151 -0
- package/scripts/build-template-db.mjs +38 -0
- package/scripts/check-atm10.mjs +27 -0
- package/scripts/check-sync.mjs +13 -0
- package/scripts/create-fts-index.mjs +50 -0
- package/scripts/enable-pgvector.mjs +90 -0
- package/scripts/enable-sqlite-vec.mjs +158 -0
- package/scripts/migrate-backend.mjs +188 -0
- package/scripts/migrate-embed-provenance.mjs +80 -0
- package/scripts/migrate-metadata-source.mjs +62 -0
- package/scripts/migrate-mod-source-files.mjs +67 -0
- package/scripts/post-update.mjs +77 -0
- 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
|