@adhisang/minecraft-modding-mcp 1.0.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 (106) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +765 -0
  4. package/dist/access-widener-parser.d.ts +24 -0
  5. package/dist/access-widener-parser.js +77 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +4 -0
  8. package/dist/config.d.ts +27 -0
  9. package/dist/config.js +178 -0
  10. package/dist/decompiler/vineflower.d.ts +15 -0
  11. package/dist/decompiler/vineflower.js +185 -0
  12. package/dist/errors.d.ts +50 -0
  13. package/dist/errors.js +49 -0
  14. package/dist/hash.d.ts +1 -0
  15. package/dist/hash.js +12 -0
  16. package/dist/index.d.ts +7 -0
  17. package/dist/index.js +1447 -0
  18. package/dist/java-process.d.ts +16 -0
  19. package/dist/java-process.js +120 -0
  20. package/dist/logger.d.ts +3 -0
  21. package/dist/logger.js +21 -0
  22. package/dist/mapping-pipeline-service.d.ts +18 -0
  23. package/dist/mapping-pipeline-service.js +60 -0
  24. package/dist/mapping-service.d.ts +161 -0
  25. package/dist/mapping-service.js +1706 -0
  26. package/dist/maven-resolver.d.ts +22 -0
  27. package/dist/maven-resolver.js +122 -0
  28. package/dist/minecraft-explorer-service.d.ts +43 -0
  29. package/dist/minecraft-explorer-service.js +562 -0
  30. package/dist/mixin-parser.d.ts +34 -0
  31. package/dist/mixin-parser.js +194 -0
  32. package/dist/mixin-validator.d.ts +59 -0
  33. package/dist/mixin-validator.js +274 -0
  34. package/dist/mod-analyzer.d.ts +23 -0
  35. package/dist/mod-analyzer.js +346 -0
  36. package/dist/mod-decompile-service.d.ts +39 -0
  37. package/dist/mod-decompile-service.js +136 -0
  38. package/dist/mod-remap-service.d.ts +17 -0
  39. package/dist/mod-remap-service.js +186 -0
  40. package/dist/mod-search-service.d.ts +28 -0
  41. package/dist/mod-search-service.js +174 -0
  42. package/dist/mojang-tiny-mapping-service.d.ts +13 -0
  43. package/dist/mojang-tiny-mapping-service.js +351 -0
  44. package/dist/nbt/java-nbt-codec.d.ts +3 -0
  45. package/dist/nbt/java-nbt-codec.js +385 -0
  46. package/dist/nbt/json-patch.d.ts +3 -0
  47. package/dist/nbt/json-patch.js +352 -0
  48. package/dist/nbt/pipeline.d.ts +39 -0
  49. package/dist/nbt/pipeline.js +173 -0
  50. package/dist/nbt/typed-json.d.ts +10 -0
  51. package/dist/nbt/typed-json.js +205 -0
  52. package/dist/nbt/types.d.ts +66 -0
  53. package/dist/nbt/types.js +2 -0
  54. package/dist/observability.d.ts +88 -0
  55. package/dist/observability.js +165 -0
  56. package/dist/path-converter.d.ts +12 -0
  57. package/dist/path-converter.js +161 -0
  58. package/dist/path-resolver.d.ts +19 -0
  59. package/dist/path-resolver.js +78 -0
  60. package/dist/registry-service.d.ts +29 -0
  61. package/dist/registry-service.js +214 -0
  62. package/dist/repo-downloader.d.ts +15 -0
  63. package/dist/repo-downloader.js +111 -0
  64. package/dist/resources.d.ts +3 -0
  65. package/dist/resources.js +154 -0
  66. package/dist/search-hit-accumulator.d.ts +38 -0
  67. package/dist/search-hit-accumulator.js +153 -0
  68. package/dist/source-jar-reader.d.ts +13 -0
  69. package/dist/source-jar-reader.js +216 -0
  70. package/dist/source-resolver.d.ts +14 -0
  71. package/dist/source-resolver.js +274 -0
  72. package/dist/source-service.d.ts +404 -0
  73. package/dist/source-service.js +2881 -0
  74. package/dist/storage/artifacts-repo.d.ts +45 -0
  75. package/dist/storage/artifacts-repo.js +209 -0
  76. package/dist/storage/db.d.ts +14 -0
  77. package/dist/storage/db.js +132 -0
  78. package/dist/storage/files-repo.d.ts +78 -0
  79. package/dist/storage/files-repo.js +437 -0
  80. package/dist/storage/index-meta-repo.d.ts +35 -0
  81. package/dist/storage/index-meta-repo.js +97 -0
  82. package/dist/storage/migrations.d.ts +11 -0
  83. package/dist/storage/migrations.js +71 -0
  84. package/dist/storage/schema.d.ts +1 -0
  85. package/dist/storage/schema.js +160 -0
  86. package/dist/storage/sqlite.d.ts +20 -0
  87. package/dist/storage/sqlite.js +111 -0
  88. package/dist/storage/symbols-repo.d.ts +63 -0
  89. package/dist/storage/symbols-repo.js +401 -0
  90. package/dist/symbols/symbol-extractor.d.ts +7 -0
  91. package/dist/symbols/symbol-extractor.js +64 -0
  92. package/dist/tiny-remapper-resolver.d.ts +1 -0
  93. package/dist/tiny-remapper-resolver.js +62 -0
  94. package/dist/tiny-remapper-service.d.ts +16 -0
  95. package/dist/tiny-remapper-service.js +73 -0
  96. package/dist/types.d.ts +120 -0
  97. package/dist/types.js +2 -0
  98. package/dist/version-diff-service.d.ts +41 -0
  99. package/dist/version-diff-service.js +222 -0
  100. package/dist/version-service.d.ts +70 -0
  101. package/dist/version-service.js +411 -0
  102. package/dist/vineflower-resolver.d.ts +1 -0
  103. package/dist/vineflower-resolver.js +62 -0
  104. package/dist/workspace-mapping-service.d.ts +18 -0
  105. package/dist/workspace-mapping-service.js +89 -0
  106. package/package.json +61 -0
@@ -0,0 +1,346 @@
1
+ import { parse as parseToml } from "smol-toml";
2
+ import { z } from "zod";
3
+ import { AppError, ERROR_CODES } from "./errors.js";
4
+ import { normalizeJarPath } from "./path-resolver.js";
5
+ import { listJarEntries, readJarEntryAsUtf8 } from "./source-jar-reader.js";
6
+ function toErrorMessage(value) {
7
+ if (value instanceof Error) {
8
+ return value.message;
9
+ }
10
+ return String(value);
11
+ }
12
+ // ---------------------------------------------------------------------------
13
+ // Zod schemas — .passthrough() for lenient parsing
14
+ // ---------------------------------------------------------------------------
15
+ const stringOrEntrypoint = z.union([
16
+ z.string(),
17
+ z.object({ value: z.string() }).passthrough()
18
+ ]);
19
+ const stringOrMixinRef = z.union([
20
+ z.string(),
21
+ z.object({ config: z.string() }).passthrough()
22
+ ]);
23
+ const fabricModJsonSchema = z
24
+ .object({
25
+ id: z.string().optional(),
26
+ name: z.string().optional(),
27
+ version: z.string().optional(),
28
+ description: z.string().optional(),
29
+ entrypoints: z.record(z.array(stringOrEntrypoint)).optional(),
30
+ mixins: z.array(stringOrMixinRef).optional(),
31
+ accessWidener: z.string().optional(),
32
+ depends: z.record(z.union([z.string(), z.array(z.string())])).optional(),
33
+ recommends: z.record(z.union([z.string(), z.array(z.string())])).optional(),
34
+ conflicts: z.record(z.union([z.string(), z.array(z.string())])).optional(),
35
+ suggests: z.record(z.union([z.string(), z.array(z.string())])).optional()
36
+ })
37
+ .passthrough();
38
+ const quiltModJsonSchema = z
39
+ .object({
40
+ schema_version: z.number().optional(),
41
+ quilt_loader: z
42
+ .object({
43
+ id: z.string().optional(),
44
+ version: z.string().optional(),
45
+ metadata: z
46
+ .object({
47
+ name: z.string().optional(),
48
+ description: z.string().optional()
49
+ })
50
+ .passthrough()
51
+ .optional(),
52
+ entrypoints: z.record(z.array(stringOrEntrypoint)).optional(),
53
+ depends: z.array(z.unknown()).optional()
54
+ })
55
+ .passthrough()
56
+ .optional(),
57
+ mixin: z.union([z.string(), z.array(z.string())]).optional(),
58
+ access_widener: z.string().optional()
59
+ })
60
+ .passthrough();
61
+ const forgeModsTomlSchema = z
62
+ .object({
63
+ modLoader: z.string().optional(),
64
+ mods: z
65
+ .array(z
66
+ .object({
67
+ modId: z.string().optional(),
68
+ displayName: z.string().optional(),
69
+ version: z.string().optional(),
70
+ description: z.string().optional()
71
+ })
72
+ .passthrough())
73
+ .optional(),
74
+ dependencies: z.record(z.array(z.unknown())).optional(),
75
+ mixins: z.array(z.object({ config: z.string() }).passthrough()).optional()
76
+ })
77
+ .passthrough();
78
+ const legacyForgeSchema = z.array(z
79
+ .object({
80
+ modid: z.string().optional(),
81
+ name: z.string().optional(),
82
+ version: z.string().optional(),
83
+ description: z.string().optional()
84
+ })
85
+ .passthrough());
86
+ // ---------------------------------------------------------------------------
87
+ // Parser: Fabric
88
+ // ---------------------------------------------------------------------------
89
+ function normalizeFabricEntrypoint(v) {
90
+ return typeof v === "string" ? v : v.value;
91
+ }
92
+ function normalizeMixinRef(v) {
93
+ return typeof v === "string" ? v : v.config;
94
+ }
95
+ function collectFabricDeps(record, kind) {
96
+ if (!record)
97
+ return [];
98
+ return Object.entries(record).map(([modId, versionRange]) => ({
99
+ modId,
100
+ versionRange: Array.isArray(versionRange) ? versionRange.join(" || ") : versionRange,
101
+ kind
102
+ }));
103
+ }
104
+ function parseFabricMod(content) {
105
+ const parsed = fabricModJsonSchema.safeParse(JSON.parse(content));
106
+ if (!parsed.success)
107
+ return {};
108
+ const mod = parsed.data;
109
+ const entrypoints = mod.entrypoints
110
+ ? Object.fromEntries(Object.entries(mod.entrypoints).map(([key, values]) => [
111
+ key,
112
+ values.map(normalizeFabricEntrypoint)
113
+ ]))
114
+ : undefined;
115
+ const mixinConfigs = mod.mixins?.map(normalizeMixinRef);
116
+ const dependencies = [
117
+ ...collectFabricDeps(mod.depends, "required"),
118
+ ...collectFabricDeps(mod.recommends, "recommends"),
119
+ ...collectFabricDeps(mod.conflicts, "conflicts"),
120
+ ...collectFabricDeps(mod.suggests, "optional")
121
+ ];
122
+ return {
123
+ modId: mod.id,
124
+ modName: mod.name,
125
+ modVersion: mod.version,
126
+ description: mod.description,
127
+ entrypoints,
128
+ mixinConfigs,
129
+ accessWidener: mod.accessWidener,
130
+ dependencies: dependencies.length > 0 ? dependencies : undefined
131
+ };
132
+ }
133
+ // ---------------------------------------------------------------------------
134
+ // Parser: Quilt
135
+ // ---------------------------------------------------------------------------
136
+ function parseQuiltMod(content) {
137
+ const parsed = quiltModJsonSchema.safeParse(JSON.parse(content));
138
+ if (!parsed.success)
139
+ return {};
140
+ const mod = parsed.data;
141
+ const loader = mod.quilt_loader;
142
+ const entrypoints = loader?.entrypoints
143
+ ? Object.fromEntries(Object.entries(loader.entrypoints).map(([key, values]) => [
144
+ key,
145
+ values.map(normalizeFabricEntrypoint)
146
+ ]))
147
+ : undefined;
148
+ const rawMixin = mod.mixin;
149
+ const mixinConfigs = rawMixin
150
+ ? Array.isArray(rawMixin)
151
+ ? rawMixin
152
+ : [rawMixin]
153
+ : undefined;
154
+ const dependencies = [];
155
+ if (loader?.depends && Array.isArray(loader.depends)) {
156
+ for (const dep of loader.depends) {
157
+ if (typeof dep === "string") {
158
+ dependencies.push({ modId: dep, kind: "required" });
159
+ }
160
+ else if (dep && typeof dep === "object" && "id" in dep) {
161
+ const d = dep;
162
+ dependencies.push({
163
+ modId: d.id,
164
+ versionRange: d.versions,
165
+ kind: d.optional ? "optional" : "required"
166
+ });
167
+ }
168
+ }
169
+ }
170
+ return {
171
+ modId: loader?.id,
172
+ modName: loader?.metadata?.name,
173
+ modVersion: loader?.version,
174
+ description: loader?.metadata?.description,
175
+ entrypoints,
176
+ mixinConfigs,
177
+ accessWidener: mod.access_widener,
178
+ dependencies: dependencies.length > 0 ? dependencies : undefined
179
+ };
180
+ }
181
+ // ---------------------------------------------------------------------------
182
+ // Parser: Forge / NeoForge (TOML)
183
+ // ---------------------------------------------------------------------------
184
+ function parseForgeMod(content, entries) {
185
+ const raw = parseToml(content);
186
+ const parsed = forgeModsTomlSchema.safeParse(raw);
187
+ if (!parsed.success)
188
+ return {};
189
+ const toml = parsed.data;
190
+ const firstMod = toml.mods?.[0];
191
+ // Determine Forge vs NeoForge from modLoader field
192
+ const modLoaderValue = toml.modLoader?.toLowerCase() ?? "";
193
+ const isNeoForge = modLoaderValue.includes("neoforge") || modLoaderValue.includes("lowcodefml");
194
+ // Also check for neoforge.mods.toml presence
195
+ const hasNeoforgeToml = entries.includes("META-INF/neoforge.mods.toml");
196
+ const detectedLoader = isNeoForge || hasNeoforgeToml ? "neoforge" : "forge";
197
+ // Dependencies
198
+ const dependencies = [];
199
+ if (toml.dependencies) {
200
+ for (const depArray of Object.values(toml.dependencies)) {
201
+ for (const dep of depArray) {
202
+ if (dep && typeof dep === "object") {
203
+ const d = dep;
204
+ if (d.modId) {
205
+ dependencies.push({
206
+ modId: d.modId,
207
+ versionRange: d.versionRange,
208
+ kind: d.mandatory === false ? "optional" : "required"
209
+ });
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ // Mixin configs
216
+ const mixinConfigs = toml.mixins?.map((m) => m.config);
217
+ return {
218
+ detectedLoader,
219
+ modId: firstMod?.modId,
220
+ modName: firstMod?.displayName,
221
+ modVersion: firstMod?.version,
222
+ description: firstMod?.description,
223
+ mixinConfigs: mixinConfigs && mixinConfigs.length > 0 ? mixinConfigs : undefined,
224
+ dependencies: dependencies.length > 0 ? dependencies : undefined
225
+ };
226
+ }
227
+ // ---------------------------------------------------------------------------
228
+ // Parser: Legacy Forge (mcmod.info)
229
+ // ---------------------------------------------------------------------------
230
+ function parseLegacyForgeMod(content) {
231
+ // mcmod.info may be wrapped in an extra array or be a direct array
232
+ let rawArray = JSON.parse(content);
233
+ if (Array.isArray(rawArray) && rawArray.length === 1 && Array.isArray(rawArray[0])) {
234
+ rawArray = rawArray[0];
235
+ }
236
+ const parsed = legacyForgeSchema.safeParse(rawArray);
237
+ if (!parsed.success)
238
+ return {};
239
+ const first = parsed.data[0];
240
+ if (!first)
241
+ return {};
242
+ return {
243
+ modId: first.modid,
244
+ modName: first.name,
245
+ modVersion: first.version,
246
+ description: first.description
247
+ };
248
+ }
249
+ // ---------------------------------------------------------------------------
250
+ // Main export
251
+ // ---------------------------------------------------------------------------
252
+ export async function analyzeModJar(jarPath, options) {
253
+ let resolvedPath;
254
+ try {
255
+ resolvedPath = normalizeJarPath(jarPath);
256
+ }
257
+ catch (cause) {
258
+ throw new AppError({
259
+ code: ERROR_CODES.INVALID_INPUT,
260
+ message: cause instanceof Error ? cause.message : `Invalid jar path: ${jarPath}`,
261
+ details: { jarPath }
262
+ });
263
+ }
264
+ let entries;
265
+ try {
266
+ entries = await listJarEntries(resolvedPath);
267
+ }
268
+ catch (cause) {
269
+ throw new AppError({
270
+ code: ERROR_CODES.SOURCE_NOT_FOUND,
271
+ message: `Failed to read jar "${resolvedPath}".`,
272
+ details: {
273
+ jarPath: resolvedPath,
274
+ reason: toErrorMessage(cause)
275
+ }
276
+ });
277
+ }
278
+ // Class counting
279
+ const classEntries = entries.filter((e) => e.endsWith(".class"));
280
+ const classCount = classEntries.length;
281
+ const classes = options?.includeClasses ? classEntries : undefined;
282
+ // Detect loader and parse metadata
283
+ let loader = "unknown";
284
+ let metadata = {};
285
+ if (entries.includes("fabric.mod.json")) {
286
+ loader = "fabric";
287
+ try {
288
+ const content = await readJarEntryAsUtf8(resolvedPath, "fabric.mod.json");
289
+ metadata = parseFabricMod(content);
290
+ }
291
+ catch {
292
+ // graceful fallback
293
+ }
294
+ }
295
+ else if (entries.includes("quilt.mod.json")) {
296
+ loader = "quilt";
297
+ try {
298
+ const content = await readJarEntryAsUtf8(resolvedPath, "quilt.mod.json");
299
+ metadata = parseQuiltMod(content);
300
+ }
301
+ catch {
302
+ // graceful fallback
303
+ }
304
+ }
305
+ else if (entries.includes("META-INF/neoforge.mods.toml")) {
306
+ loader = "neoforge";
307
+ try {
308
+ const content = await readJarEntryAsUtf8(resolvedPath, "META-INF/neoforge.mods.toml");
309
+ const result = parseForgeMod(content, entries);
310
+ const { detectedLoader: _, ...rest } = result;
311
+ metadata = rest;
312
+ }
313
+ catch {
314
+ // graceful fallback
315
+ }
316
+ }
317
+ else if (entries.includes("META-INF/mods.toml")) {
318
+ try {
319
+ const content = await readJarEntryAsUtf8(resolvedPath, "META-INF/mods.toml");
320
+ const result = parseForgeMod(content, entries);
321
+ loader = result.detectedLoader ?? "forge";
322
+ const { detectedLoader: _, ...rest } = result;
323
+ metadata = rest;
324
+ }
325
+ catch {
326
+ loader = "forge";
327
+ }
328
+ }
329
+ else if (entries.includes("mcmod.info")) {
330
+ loader = "forge";
331
+ try {
332
+ const content = await readJarEntryAsUtf8(resolvedPath, "mcmod.info");
333
+ metadata = parseLegacyForgeMod(content);
334
+ }
335
+ catch {
336
+ // graceful fallback
337
+ }
338
+ }
339
+ return {
340
+ loader,
341
+ ...metadata,
342
+ classCount,
343
+ ...(classes !== undefined ? { classes } : {})
344
+ };
345
+ }
346
+ //# sourceMappingURL=mod-analyzer.js.map
@@ -0,0 +1,39 @@
1
+ import type { Config } from "./types.js";
2
+ export type DecompileModJarInput = {
3
+ jarPath: string;
4
+ className?: string;
5
+ };
6
+ export type DecompileModJarOutput = {
7
+ modId: string;
8
+ modName?: string;
9
+ modVersion?: string;
10
+ loader: string;
11
+ outputDir: string;
12
+ fileCount: number;
13
+ files?: string[];
14
+ source?: {
15
+ className: string;
16
+ content: string;
17
+ totalLines: number;
18
+ };
19
+ warnings: string[];
20
+ };
21
+ export type GetModClassSourceInput = {
22
+ jarPath: string;
23
+ className: string;
24
+ };
25
+ export type GetModClassSourceOutput = {
26
+ className: string;
27
+ content: string;
28
+ totalLines: number;
29
+ modId?: string;
30
+ warnings: string[];
31
+ };
32
+ export declare class ModDecompileService {
33
+ private readonly config;
34
+ private readonly decompileCache;
35
+ constructor(config: Config);
36
+ decompileModJar(input: DecompileModJarInput): Promise<DecompileModJarOutput>;
37
+ getModClassSource(input: GetModClassSourceInput): Promise<GetModClassSourceOutput>;
38
+ private ensureDecompiled;
39
+ }
@@ -0,0 +1,136 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createError, ERROR_CODES } from "./errors.js";
5
+ import { log } from "./logger.js";
6
+ import { decompileBinaryJar } from "./decompiler/vineflower.js";
7
+ import { resolveVineflowerJar } from "./vineflower-resolver.js";
8
+ import { analyzeModJar } from "./mod-analyzer.js";
9
+ import { validateAndNormalizeJarPath } from "./path-resolver.js";
10
+ const DECOMPILE_TIMEOUT_MS = 300_000;
11
+ function modDecompileCacheKey(jarPath) {
12
+ return createHash("sha256").update(jarPath).digest("hex");
13
+ }
14
+ function classNameToFilePath(className) {
15
+ return className.replaceAll(".", "/") + ".java";
16
+ }
17
+ function filePathToClassName(filePath) {
18
+ return filePath.replace(/\.java$/, "").replaceAll("/", ".");
19
+ }
20
+ export class ModDecompileService {
21
+ config;
22
+ // Cache: jarPath hash → decompiled output dir + file list
23
+ decompileCache = new Map();
24
+ constructor(config) {
25
+ this.config = config;
26
+ }
27
+ async decompileModJar(input) {
28
+ const jarPath = validateAndNormalizeJarPath(input.jarPath);
29
+ const warnings = [];
30
+ const { outputDir, files, analysis } = await this.ensureDecompiled(jarPath, warnings);
31
+ let sourceResult;
32
+ if (input.className) {
33
+ const targetFile = classNameToFilePath(input.className);
34
+ const matched = files.find((f) => f === targetFile || f.endsWith(`/${targetFile}`) || f === input.className);
35
+ if (matched) {
36
+ const content = readFileSync(join(outputDir, matched), "utf8");
37
+ sourceResult = {
38
+ className: filePathToClassName(matched),
39
+ content,
40
+ totalLines: content.split("\n").length
41
+ };
42
+ }
43
+ else {
44
+ warnings.push(`Class "${input.className}" not found in decompiled output. Use the files list to find available classes.`);
45
+ }
46
+ }
47
+ return {
48
+ modId: analysis.modId ?? "unknown",
49
+ modName: analysis.modName,
50
+ modVersion: analysis.modVersion,
51
+ loader: analysis.loader,
52
+ outputDir,
53
+ fileCount: files.length,
54
+ files: input.className ? undefined : files.map(filePathToClassName),
55
+ source: sourceResult,
56
+ warnings
57
+ };
58
+ }
59
+ async getModClassSource(input) {
60
+ const jarPath = validateAndNormalizeJarPath(input.jarPath);
61
+ const className = input.className.trim();
62
+ if (!className) {
63
+ throw createError({
64
+ code: ERROR_CODES.INVALID_INPUT,
65
+ message: "className must be non-empty."
66
+ });
67
+ }
68
+ const warnings = [];
69
+ const { outputDir, files, analysis } = await this.ensureDecompiled(jarPath, warnings);
70
+ const targetFile = classNameToFilePath(className);
71
+ const matched = files.find((f) => f === targetFile || f.endsWith(`/${targetFile}`));
72
+ if (!matched) {
73
+ throw createError({
74
+ code: ERROR_CODES.CLASS_NOT_FOUND,
75
+ message: `Class "${className}" not found in decompiled mod JAR.`,
76
+ details: { className, jarPath, availableCount: files.length }
77
+ });
78
+ }
79
+ const content = readFileSync(join(outputDir, matched), "utf8");
80
+ return {
81
+ className: filePathToClassName(matched),
82
+ content,
83
+ totalLines: content.split("\n").length,
84
+ modId: analysis.modId,
85
+ warnings
86
+ };
87
+ }
88
+ async ensureDecompiled(jarPath, warnings) {
89
+ const cacheKey = modDecompileCacheKey(jarPath);
90
+ const cached = this.decompileCache.get(cacheKey);
91
+ if (cached)
92
+ return cached;
93
+ log("info", "mod-decompile.start", { jarPath });
94
+ const startedAt = Date.now();
95
+ // Analyze mod metadata
96
+ let analysis;
97
+ try {
98
+ analysis = await analyzeModJar(jarPath, { includeClasses: false });
99
+ }
100
+ catch {
101
+ analysis = {
102
+ loader: "unknown",
103
+ classCount: 0
104
+ };
105
+ warnings.push("Could not extract mod metadata from JAR.");
106
+ }
107
+ // Resolve Vineflower
108
+ const vineflowerPath = await resolveVineflowerJar(this.config.cacheDir, this.config.vineflowerJarPath);
109
+ // Decompile
110
+ const decompileResult = await decompileBinaryJar(jarPath, this.config.cacheDir, {
111
+ vineflowerJarPath: vineflowerPath,
112
+ timeoutMs: DECOMPILE_TIMEOUT_MS,
113
+ signature: cacheKey
114
+ });
115
+ const files = decompileResult.javaFiles.map((entry) => entry.filePath);
116
+ const result = {
117
+ outputDir: decompileResult.outputDir,
118
+ files,
119
+ analysis
120
+ };
121
+ this.decompileCache.set(cacheKey, result);
122
+ // Trim cache
123
+ if (this.decompileCache.size > 8) {
124
+ const oldest = this.decompileCache.keys().next().value;
125
+ if (oldest !== undefined)
126
+ this.decompileCache.delete(oldest);
127
+ }
128
+ log("info", "mod-decompile.done", {
129
+ jarPath,
130
+ fileCount: files.length,
131
+ durationMs: Date.now() - startedAt
132
+ });
133
+ return result;
134
+ }
135
+ }
136
+ //# sourceMappingURL=mod-decompile-service.js.map
@@ -0,0 +1,17 @@
1
+ import type { Config } from "./types.js";
2
+ export interface ModRemapInput {
3
+ inputJar: string;
4
+ outputJar?: string;
5
+ mcVersion?: string;
6
+ targetMapping: "yarn" | "mojang";
7
+ }
8
+ export interface ModRemapResult {
9
+ outputJar: string;
10
+ mcVersion: string;
11
+ fromMapping: string;
12
+ targetMapping: string;
13
+ resolvedTargetNamespace: "yarn" | "mojang";
14
+ durationMs: number;
15
+ warnings: string[];
16
+ }
17
+ export declare function remapModJar(input: ModRemapInput, config: Config): Promise<ModRemapResult>;