@adhisang/minecraft-modding-mcp 2.0.0 → 2.1.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/CHANGELOG.md +44 -0
- package/README.md +109 -29
- package/dist/cli.js +31 -4
- package/dist/compat-stdio-transport.d.ts +2 -7
- package/dist/compat-stdio-transport.js +12 -154
- package/dist/index.js +392 -33
- package/dist/json-rpc-framing.d.ts +22 -0
- package/dist/json-rpc-framing.js +168 -0
- package/dist/mapping-pipeline-service.js +9 -1
- package/dist/mapping-service.d.ts +9 -0
- package/dist/mapping-service.js +183 -60
- package/dist/minecraft-explorer-service.d.ts +0 -1
- package/dist/minecraft-explorer-service.js +119 -23
- package/dist/mixin-validator.d.ts +24 -2
- package/dist/mixin-validator.js +223 -98
- package/dist/mod-decompile-service.d.ts +5 -0
- package/dist/mod-decompile-service.js +40 -5
- package/dist/mod-remap-service.js +142 -30
- package/dist/path-resolver.js +41 -4
- package/dist/registry-service.d.ts +10 -1
- package/dist/registry-service.js +154 -22
- package/dist/search-hit-accumulator.js +23 -2
- package/dist/source-jar-reader.js +16 -2
- package/dist/source-resolver.js +6 -7
- package/dist/source-service.d.ts +42 -4
- package/dist/source-service.js +781 -127
- package/dist/stdio-supervisor.d.ts +46 -0
- package/dist/stdio-supervisor.js +349 -0
- package/dist/storage/files-repo.d.ts +3 -9
- package/dist/storage/files-repo.js +66 -43
- package/dist/symbols/symbol-extractor.js +6 -4
- package/dist/tool-execution-gate.d.ts +15 -0
- package/dist/tool-execution-gate.js +58 -0
- package/dist/version-diff-service.js +10 -5
- package/dist/version-service.js +7 -2
- package/dist/workspace-mapping-service.js +12 -0
- package/package.json +1 -1
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync,
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { createError, ERROR_CODES } from "./errors.js";
|
|
5
|
+
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
6
6
|
import { log } from "./logger.js";
|
|
7
7
|
import { resolveTinyMappingFile } from "./mapping-service.js";
|
|
8
8
|
import { resolveMojangTinyFile } from "./mojang-tiny-mapping-service.js";
|
|
9
9
|
import { analyzeModJar } from "./mod-analyzer.js";
|
|
10
10
|
import { normalizePathForHost } from "./path-converter.js";
|
|
11
|
+
import { listJarEntries, readJarEntryAsBuffer } from "./source-jar-reader.js";
|
|
11
12
|
import { remapJar } from "./tiny-remapper-service.js";
|
|
12
13
|
import { resolveTinyRemapperJar } from "./tiny-remapper-resolver.js";
|
|
13
14
|
function normalizeTargetNamespace(target) {
|
|
14
15
|
return target === "yarn" ? "yarn" : "mojang";
|
|
15
16
|
}
|
|
16
|
-
function
|
|
17
|
+
function defaultSourceNamespaceForLoader(loader) {
|
|
17
18
|
if (loader === "fabric" || loader === "quilt") {
|
|
18
19
|
return "intermediary";
|
|
19
20
|
}
|
|
@@ -35,6 +36,81 @@ function extractMinecraftVersion(dependencies) {
|
|
|
35
36
|
const match = mcDep.versionRange.match(/(\d+\.\d+(?:\.\d+)?)/);
|
|
36
37
|
return match?.[1];
|
|
37
38
|
}
|
|
39
|
+
function countMatches(input, pattern) {
|
|
40
|
+
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
41
|
+
const globalPattern = new RegExp(pattern.source, flags);
|
|
42
|
+
let count = 0;
|
|
43
|
+
while (globalPattern.exec(input)) {
|
|
44
|
+
count += 1;
|
|
45
|
+
}
|
|
46
|
+
return count;
|
|
47
|
+
}
|
|
48
|
+
async function detectFabricLikeInputNamespace(inputJar) {
|
|
49
|
+
const warnings = [];
|
|
50
|
+
const classEntries = (await listJarEntries(inputJar))
|
|
51
|
+
.filter((entry) => entry.endsWith(".class"))
|
|
52
|
+
.slice(0, 24);
|
|
53
|
+
if (classEntries.length === 0) {
|
|
54
|
+
warnings.push("Could not inspect class entries to detect input mapping; assuming intermediary.");
|
|
55
|
+
return {
|
|
56
|
+
fromNamespace: "intermediary",
|
|
57
|
+
warnings
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
let mojangScore = 0;
|
|
61
|
+
let intermediaryScore = 0;
|
|
62
|
+
for (const entry of classEntries) {
|
|
63
|
+
let text = "";
|
|
64
|
+
try {
|
|
65
|
+
text = (await readJarEntryAsBuffer(inputJar, entry)).toString("latin1");
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
mojangScore += countMatches(text, /net\/minecraft\/(?:advancements|client|commands|core|data|gametest|nbt|network|recipe|resources|server|sounds|stats|tags|util|world)\//g) * 3;
|
|
71
|
+
intermediaryScore += countMatches(text, /net\/minecraft\/class_\d+/g) * 3;
|
|
72
|
+
intermediaryScore += countMatches(text, /\b(?:method|field)_\d+\b/g);
|
|
73
|
+
}
|
|
74
|
+
if (mojangScore > intermediaryScore && mojangScore > 0) {
|
|
75
|
+
return {
|
|
76
|
+
fromNamespace: "mojang",
|
|
77
|
+
warnings
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (intermediaryScore > mojangScore && intermediaryScore > 0) {
|
|
81
|
+
return {
|
|
82
|
+
fromNamespace: "intermediary",
|
|
83
|
+
warnings
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
warnings.push("Could not confidently detect whether the input jar uses intermediary or mojang names; assuming intermediary.");
|
|
87
|
+
return {
|
|
88
|
+
fromNamespace: "intermediary",
|
|
89
|
+
warnings
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function detectInputNamespaceForLoader(inputJar, loader) {
|
|
93
|
+
if (loader === "fabric" || loader === "quilt") {
|
|
94
|
+
return detectFabricLikeInputNamespace(inputJar);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
fromNamespace: defaultSourceNamespaceForLoader(loader),
|
|
98
|
+
warnings: []
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function resolveOutputJarPath(input, normalizedInput, modId, modVersion) {
|
|
102
|
+
const defaultOutputName = `${modId ?? "mod"}-${modVersion ?? "0"}-${input.targetMapping}.jar`;
|
|
103
|
+
return input.outputJar
|
|
104
|
+
? normalizePathForHost(input.outputJar, undefined, "outputJar")
|
|
105
|
+
: join(dirname(normalizedInput), defaultOutputName);
|
|
106
|
+
}
|
|
107
|
+
function copyJarToDestination(sourceJar, destinationJar) {
|
|
108
|
+
if (sourceJar === destinationJar) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
mkdirSync(dirname(destinationJar), { recursive: true });
|
|
112
|
+
copyFileSync(sourceJar, destinationJar);
|
|
113
|
+
}
|
|
38
114
|
function buildCacheKey(inputJar, fromNamespace, targetNamespace, mcVersion) {
|
|
39
115
|
const stat = statSync(inputJar, { throwIfNoEntry: false });
|
|
40
116
|
const signature = stat ? `${stat.mtimeMs}:${stat.size}` : "unknown";
|
|
@@ -71,7 +147,9 @@ export async function remapModJar(input, config) {
|
|
|
71
147
|
details: { inputJar: normalizedInput }
|
|
72
148
|
});
|
|
73
149
|
}
|
|
74
|
-
const
|
|
150
|
+
const namespaceDetection = await detectInputNamespaceForLoader(normalizedInput, analysis.loader);
|
|
151
|
+
warnings.push(...namespaceDetection.warnings);
|
|
152
|
+
const fromNamespace = namespaceDetection.fromNamespace;
|
|
75
153
|
// 3. Determine MC version
|
|
76
154
|
const mcVersion = input.mcVersion ?? extractMinecraftVersion(analysis.dependencies);
|
|
77
155
|
if (!mcVersion) {
|
|
@@ -85,21 +163,32 @@ export async function remapModJar(input, config) {
|
|
|
85
163
|
}
|
|
86
164
|
});
|
|
87
165
|
}
|
|
166
|
+
const outputJar = resolveOutputJarPath(input, normalizedInput, analysis.modId, analysis.modVersion);
|
|
88
167
|
// 4. Check cache after mapping context is known
|
|
89
168
|
const cacheKey = buildCacheKey(normalizedInput, fromNamespace, resolvedTargetNamespace, mcVersion);
|
|
90
169
|
const cacheDir = join(config.cacheDir, "remapped-mods");
|
|
91
170
|
mkdirSync(cacheDir, { recursive: true });
|
|
92
171
|
const cachedOutput = join(cacheDir, `${cacheKey}.jar`);
|
|
93
172
|
if (existsSync(cachedOutput)) {
|
|
94
|
-
const
|
|
95
|
-
?
|
|
173
|
+
const cacheHitOutputJar = input.outputJar
|
|
174
|
+
? outputJar
|
|
96
175
|
: cachedOutput;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
176
|
+
copyJarToDestination(cachedOutput, cacheHitOutputJar);
|
|
177
|
+
log("info", "remap.cache-hit", { inputJar: normalizedInput, outputJar: cacheHitOutputJar });
|
|
178
|
+
return {
|
|
179
|
+
outputJar: cacheHitOutputJar,
|
|
180
|
+
mcVersion,
|
|
181
|
+
fromMapping: fromNamespace,
|
|
182
|
+
targetMapping: input.targetMapping,
|
|
183
|
+
resolvedTargetNamespace,
|
|
184
|
+
durationMs: Date.now() - startedAt,
|
|
185
|
+
warnings: [...warnings, "Result served from cache."]
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (fromNamespace === resolvedTargetNamespace) {
|
|
189
|
+
copyJarToDestination(normalizedInput, outputJar);
|
|
190
|
+
copyJarToDestination(normalizedInput, cachedOutput);
|
|
191
|
+
warnings.push(`Input JAR already uses ${fromNamespace} names; output is a copy of the input JAR.`);
|
|
103
192
|
return {
|
|
104
193
|
outputJar,
|
|
105
194
|
mcVersion,
|
|
@@ -107,9 +196,23 @@ export async function remapModJar(input, config) {
|
|
|
107
196
|
targetMapping: input.targetMapping,
|
|
108
197
|
resolvedTargetNamespace,
|
|
109
198
|
durationMs: Date.now() - startedAt,
|
|
110
|
-
warnings
|
|
199
|
+
warnings
|
|
111
200
|
};
|
|
112
201
|
}
|
|
202
|
+
if (fromNamespace === "mojang" && resolvedTargetNamespace === "yarn") {
|
|
203
|
+
throw createError({
|
|
204
|
+
code: ERROR_CODES.REMAP_FAILED,
|
|
205
|
+
message: "Mojang-mapped Fabric/Quilt input jars cannot be remapped to yarn with the available mapping files.",
|
|
206
|
+
details: {
|
|
207
|
+
inputJar: normalizedInput,
|
|
208
|
+
mcVersion,
|
|
209
|
+
fromMapping: fromNamespace,
|
|
210
|
+
targetMapping: input.targetMapping,
|
|
211
|
+
resolvedTargetNamespace,
|
|
212
|
+
nextAction: 'Use targetMapping="mojang" for Mojang-mapped inputs, or rebuild the mod against intermediary mappings before requesting yarn output.'
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
113
216
|
// 5. Resolve tiny-remapper
|
|
114
217
|
const tinyRemapperJar = await resolveTinyRemapperJar(config.cacheDir, config.tinyRemapperJarPath);
|
|
115
218
|
// 6. Resolve mapping file and remap
|
|
@@ -125,30 +228,39 @@ export async function remapModJar(input, config) {
|
|
|
125
228
|
toNamespace = "mojang";
|
|
126
229
|
warnings.push(...mojangTiny.warnings);
|
|
127
230
|
}
|
|
128
|
-
// 7. Determine output path
|
|
129
|
-
const modId = analysis.modId ?? "mod";
|
|
130
|
-
const modVersion = analysis.modVersion ?? "0";
|
|
131
|
-
const defaultOutputName = `${modId}-${modVersion}-${input.targetMapping}.jar`;
|
|
132
|
-
const outputJar = input.outputJar
|
|
133
|
-
? normalizePathForHost(input.outputJar, undefined, "outputJar")
|
|
134
|
-
: join(dirname(normalizedInput), defaultOutputName);
|
|
135
231
|
mkdirSync(dirname(outputJar), { recursive: true });
|
|
136
232
|
// 8. Use temporary directory for intermediate work
|
|
137
233
|
const tempDir = join(tmpdir(), `mcp-remap-${cacheKey.slice(0, 12)}`);
|
|
138
234
|
mkdirSync(tempDir, { recursive: true });
|
|
139
235
|
try {
|
|
140
236
|
const tempOutput = join(tempDir, "remapped.jar");
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
237
|
+
try {
|
|
238
|
+
await remapJar(tinyRemapperJar, {
|
|
239
|
+
inputJar: normalizedInput,
|
|
240
|
+
outputJar: tempOutput,
|
|
241
|
+
mappingsFile,
|
|
242
|
+
fromNamespace,
|
|
243
|
+
toNamespace,
|
|
244
|
+
timeoutMs: config.remapTimeoutMs,
|
|
245
|
+
maxMemoryMb: config.remapMaxMemoryMb
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch (caughtError) {
|
|
249
|
+
if (isAppError(caughtError)) {
|
|
250
|
+
throw createError({
|
|
251
|
+
code: caughtError.code,
|
|
252
|
+
message: caughtError.message,
|
|
253
|
+
details: {
|
|
254
|
+
...(caughtError.details ?? {}),
|
|
255
|
+
fromMapping: fromNamespace,
|
|
256
|
+
targetMapping: input.targetMapping,
|
|
257
|
+
resolvedTargetNamespace
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
throw caughtError;
|
|
262
|
+
}
|
|
150
263
|
// Copy to final destination and cache
|
|
151
|
-
const { copyFileSync } = await import("node:fs");
|
|
152
264
|
copyFileSync(tempOutput, outputJar);
|
|
153
265
|
if (outputJar !== cachedOutput) {
|
|
154
266
|
mkdirSync(dirname(cachedOutput), { recursive: true });
|
package/dist/path-resolver.js
CHANGED
|
@@ -2,12 +2,35 @@ import { realpathSync, statSync } from "node:fs";
|
|
|
2
2
|
import { extname, resolve } from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { normalizePathForHost } from "./path-converter.js";
|
|
5
|
-
import { createError, ERROR_CODES } from "./errors.js";
|
|
5
|
+
import { createError, ERROR_CODES, isAppError } from "./errors.js";
|
|
6
6
|
const INVALID_ENTRY = /(^|\/|\\)\.\.(\/|\\|$)/;
|
|
7
7
|
export function normalizeJarPath(jarPath) {
|
|
8
8
|
const normalizedInput = normalizePathForHost(jarPath, undefined, "jarPath");
|
|
9
9
|
const absolute = resolve(normalizedInput);
|
|
10
|
-
|
|
10
|
+
let stats;
|
|
11
|
+
try {
|
|
12
|
+
stats = statSync(absolute, { throwIfNoEntry: false });
|
|
13
|
+
}
|
|
14
|
+
catch (cause) {
|
|
15
|
+
if (isAppError(cause)) {
|
|
16
|
+
throw cause;
|
|
17
|
+
}
|
|
18
|
+
throw createError({
|
|
19
|
+
code: ERROR_CODES.JAR_NOT_FOUND,
|
|
20
|
+
message: `Could not access jar "${normalizedInput}".`,
|
|
21
|
+
details: {
|
|
22
|
+
jarPath: normalizedInput,
|
|
23
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (!stats) {
|
|
28
|
+
throw createError({
|
|
29
|
+
code: ERROR_CODES.JAR_NOT_FOUND,
|
|
30
|
+
message: `Jar not found: "${normalizedInput}".`,
|
|
31
|
+
details: { jarPath: normalizedInput }
|
|
32
|
+
});
|
|
33
|
+
}
|
|
11
34
|
if (!stats.isFile()) {
|
|
12
35
|
throw createError({
|
|
13
36
|
code: ERROR_CODES.JAR_NOT_FOUND,
|
|
@@ -22,8 +45,22 @@ export function normalizeJarPath(jarPath) {
|
|
|
22
45
|
details: { jarPath: normalizedInput }
|
|
23
46
|
});
|
|
24
47
|
}
|
|
25
|
-
|
|
26
|
-
|
|
48
|
+
try {
|
|
49
|
+
return realpathSync(absolute);
|
|
50
|
+
}
|
|
51
|
+
catch (cause) {
|
|
52
|
+
if (isAppError(cause)) {
|
|
53
|
+
throw cause;
|
|
54
|
+
}
|
|
55
|
+
throw createError({
|
|
56
|
+
code: ERROR_CODES.JAR_NOT_FOUND,
|
|
57
|
+
message: `Could not resolve jar "${normalizedInput}".`,
|
|
58
|
+
details: {
|
|
59
|
+
jarPath: normalizedInput,
|
|
60
|
+
reason: cause instanceof Error ? cause.message : String(cause)
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
27
64
|
}
|
|
28
65
|
export function resolveJarPathWithSymlinkCheck(jarPath) {
|
|
29
66
|
const resolvedPath = normalizeJarPath(jarPath);
|
|
@@ -3,6 +3,8 @@ import type { Config } from "./types.js";
|
|
|
3
3
|
export type GetRegistryDataInput = {
|
|
4
4
|
version: string;
|
|
5
5
|
registry?: string;
|
|
6
|
+
includeData?: boolean;
|
|
7
|
+
maxEntriesPerRegistry?: number;
|
|
6
8
|
};
|
|
7
9
|
export type RegistryEntry = {
|
|
8
10
|
protocol_id: number;
|
|
@@ -15,8 +17,11 @@ export type GetRegistryDataOutput = {
|
|
|
15
17
|
version: string;
|
|
16
18
|
registry?: string;
|
|
17
19
|
registries?: string[];
|
|
18
|
-
data
|
|
20
|
+
data?: Record<string, RegistryData> | RegistryData;
|
|
19
21
|
entryCount: number;
|
|
22
|
+
returnedEntryCount?: number;
|
|
23
|
+
registryEntryCounts?: Record<string, number>;
|
|
24
|
+
dataTruncated?: boolean;
|
|
20
25
|
warnings: string[];
|
|
21
26
|
};
|
|
22
27
|
export declare class RegistryService {
|
|
@@ -26,4 +31,8 @@ export declare class RegistryService {
|
|
|
26
31
|
constructor(config: Config, versionService: VersionService);
|
|
27
32
|
getRegistryData(input: GetRegistryDataInput): Promise<GetRegistryDataOutput>;
|
|
28
33
|
private loadRegistries;
|
|
34
|
+
private loadExistingRegistries;
|
|
35
|
+
private readRegistryFileOrThrow;
|
|
36
|
+
private isInvalidRegistrySnapshot;
|
|
37
|
+
private cacheRegistries;
|
|
29
38
|
}
|
package/dist/registry-service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
@@ -6,6 +6,40 @@ import { createError, ERROR_CODES } from "./errors.js";
|
|
|
6
6
|
import { log } from "./logger.js";
|
|
7
7
|
const DATAGEN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
8
|
const MAX_STDIO_SNAPSHOT = 6_240;
|
|
9
|
+
function clampPositiveInt(value) {
|
|
10
|
+
if (!Number.isFinite(value) || value == null) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return Math.max(1, Math.trunc(value));
|
|
14
|
+
}
|
|
15
|
+
function sortedRegistryEntryNames(entries) {
|
|
16
|
+
return Object.keys(entries).sort((left, right) => left.localeCompare(right));
|
|
17
|
+
}
|
|
18
|
+
function limitRegistryEntries(data, maxEntries) {
|
|
19
|
+
const entryNames = sortedRegistryEntryNames(data.entries);
|
|
20
|
+
const total = entryNames.length;
|
|
21
|
+
if (maxEntries == null || total <= maxEntries) {
|
|
22
|
+
return {
|
|
23
|
+
data,
|
|
24
|
+
total,
|
|
25
|
+
returned: total,
|
|
26
|
+
truncated: false
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const limitedEntries = {};
|
|
30
|
+
for (const entryName of entryNames.slice(0, maxEntries)) {
|
|
31
|
+
limitedEntries[entryName] = data.entries[entryName];
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
data: {
|
|
35
|
+
default: data.default,
|
|
36
|
+
entries: limitedEntries
|
|
37
|
+
},
|
|
38
|
+
total,
|
|
39
|
+
returned: maxEntries,
|
|
40
|
+
truncated: true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
9
43
|
function limitOutput(text) {
|
|
10
44
|
if (text.length <= MAX_STDIO_SNAPSHOT)
|
|
11
45
|
return text;
|
|
@@ -25,6 +59,31 @@ function findRegistryFile(registryDir) {
|
|
|
25
59
|
}
|
|
26
60
|
return undefined;
|
|
27
61
|
}
|
|
62
|
+
function parseRegistrySnapshot(raw, version, registryFile) {
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(raw);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw createError({
|
|
69
|
+
code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
|
|
70
|
+
message: `Failed to parse registries.json for version "${version}".`,
|
|
71
|
+
details: {
|
|
72
|
+
version,
|
|
73
|
+
registryFile,
|
|
74
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
79
|
+
throw createError({
|
|
80
|
+
code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
|
|
81
|
+
message: `registries.json for version "${version}" has invalid structure.`,
|
|
82
|
+
details: { version, registryFile }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
28
87
|
function runDataGen(serverJarPath, outputDir, version) {
|
|
29
88
|
return new Promise((resolve, reject) => {
|
|
30
89
|
// MC 1.18+ uses bundler format, older versions use -cp with main class directly.
|
|
@@ -131,6 +190,12 @@ export class RegistryService {
|
|
|
131
190
|
const warnings = [];
|
|
132
191
|
const allRegistries = await this.loadRegistries(version, warnings);
|
|
133
192
|
const registryNames = Object.keys(allRegistries).sort();
|
|
193
|
+
const includeData = input.includeData ?? true;
|
|
194
|
+
const maxEntriesPerRegistry = clampPositiveInt(input.maxEntriesPerRegistry);
|
|
195
|
+
const registryEntryCounts = Object.fromEntries(registryNames.map((registryName) => [
|
|
196
|
+
registryName,
|
|
197
|
+
Object.keys(allRegistries[registryName].entries).length
|
|
198
|
+
]));
|
|
134
199
|
if (input.registry) {
|
|
135
200
|
const registryName = normalizeRegistryName(input.registry);
|
|
136
201
|
const data = allRegistries[registryName];
|
|
@@ -145,11 +210,17 @@ export class RegistryService {
|
|
|
145
210
|
}
|
|
146
211
|
});
|
|
147
212
|
}
|
|
213
|
+
const limited = limitRegistryEntries(data, maxEntriesPerRegistry);
|
|
148
214
|
return {
|
|
149
215
|
version,
|
|
150
216
|
registry: registryName,
|
|
151
|
-
data,
|
|
152
|
-
entryCount:
|
|
217
|
+
...(includeData ? { data: limited.data } : {}),
|
|
218
|
+
entryCount: limited.total,
|
|
219
|
+
returnedEntryCount: includeData ? limited.returned : 0,
|
|
220
|
+
registryEntryCounts: {
|
|
221
|
+
[registryName]: limited.total
|
|
222
|
+
},
|
|
223
|
+
...(limited.truncated ? { dataTruncated: true } : {}),
|
|
153
224
|
warnings
|
|
154
225
|
};
|
|
155
226
|
}
|
|
@@ -157,11 +228,25 @@ export class RegistryService {
|
|
|
157
228
|
for (const registry of Object.values(allRegistries)) {
|
|
158
229
|
totalEntries += Object.keys(registry.entries).length;
|
|
159
230
|
}
|
|
231
|
+
let returnedEntryCount = 0;
|
|
232
|
+
let dataTruncated = false;
|
|
233
|
+
const limitedData = {};
|
|
234
|
+
if (includeData) {
|
|
235
|
+
for (const registryName of registryNames) {
|
|
236
|
+
const limited = limitRegistryEntries(allRegistries[registryName], maxEntriesPerRegistry);
|
|
237
|
+
limitedData[registryName] = limited.data;
|
|
238
|
+
returnedEntryCount += limited.returned;
|
|
239
|
+
dataTruncated ||= limited.truncated;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
160
242
|
return {
|
|
161
243
|
version,
|
|
162
244
|
registries: registryNames,
|
|
163
|
-
data:
|
|
245
|
+
...(includeData ? { data: limitedData } : {}),
|
|
164
246
|
entryCount: totalEntries,
|
|
247
|
+
returnedEntryCount: includeData ? returnedEntryCount : 0,
|
|
248
|
+
registryEntryCounts,
|
|
249
|
+
...(dataTruncated ? { dataTruncated: true } : {}),
|
|
165
250
|
warnings
|
|
166
251
|
};
|
|
167
252
|
}
|
|
@@ -170,31 +255,79 @@ export class RegistryService {
|
|
|
170
255
|
if (cached)
|
|
171
256
|
return cached;
|
|
172
257
|
const registryDir = join(this.config.cacheDir, "registries", version);
|
|
173
|
-
|
|
174
|
-
|
|
258
|
+
const cachedRegistries = this.loadExistingRegistries(registryDir, version, warnings);
|
|
259
|
+
if (cachedRegistries) {
|
|
260
|
+
this.cacheRegistries(version, cachedRegistries);
|
|
261
|
+
return cachedRegistries;
|
|
262
|
+
}
|
|
263
|
+
await mkdir(registryDir, { recursive: true });
|
|
264
|
+
const serverJar = await this.versionService.resolveServerJar(version);
|
|
265
|
+
await runDataGen(serverJar.jarPath, registryDir, version);
|
|
266
|
+
const registryFile = findRegistryFile(registryDir);
|
|
175
267
|
if (!registryFile) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
268
|
+
throw createError({
|
|
269
|
+
code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
|
|
270
|
+
message: `Registry data generation did not produce registries.json for version "${version}".`,
|
|
271
|
+
details: { version, registryDir }
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const parsed = this.readRegistryFileOrThrow(registryFile, version);
|
|
275
|
+
this.cacheRegistries(version, parsed);
|
|
276
|
+
return parsed;
|
|
277
|
+
}
|
|
278
|
+
loadExistingRegistries(registryDir, version, warnings) {
|
|
279
|
+
for (const candidate of resolveRegistryPaths(registryDir)) {
|
|
280
|
+
if (!existsSync(candidate)) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
return this.readRegistryFileOrThrow(candidate, version);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (!this.isInvalidRegistrySnapshot(error)) {
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
warnings.push(`Ignored corrupt cached registry snapshot and regenerated it: ${candidate}`);
|
|
291
|
+
log("warn", "registry.cache.invalidated", {
|
|
292
|
+
version,
|
|
293
|
+
registryFile: candidate,
|
|
294
|
+
reason: error.message
|
|
185
295
|
});
|
|
296
|
+
try {
|
|
297
|
+
rmSync(candidate, { force: true });
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// best-effort cleanup
|
|
301
|
+
}
|
|
186
302
|
}
|
|
187
303
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
readRegistryFileOrThrow(registryFile, version) {
|
|
307
|
+
let raw;
|
|
308
|
+
try {
|
|
309
|
+
raw = readFileSync(registryFile, "utf8");
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
192
312
|
throw createError({
|
|
193
313
|
code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
|
|
194
|
-
message: `registries.json for version "${version}"
|
|
195
|
-
details: {
|
|
314
|
+
message: `Failed to read registries.json for version "${version}".`,
|
|
315
|
+
details: {
|
|
316
|
+
version,
|
|
317
|
+
registryFile,
|
|
318
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
319
|
+
}
|
|
196
320
|
});
|
|
197
321
|
}
|
|
322
|
+
return parseRegistrySnapshot(raw, version, registryFile);
|
|
323
|
+
}
|
|
324
|
+
isInvalidRegistrySnapshot(error) {
|
|
325
|
+
return (typeof error === "object" &&
|
|
326
|
+
error !== null &&
|
|
327
|
+
"code" in error &&
|
|
328
|
+
error.code === ERROR_CODES.REGISTRY_GENERATION_FAILED);
|
|
329
|
+
}
|
|
330
|
+
cacheRegistries(version, parsed) {
|
|
198
331
|
this.registryCache.set(version, parsed);
|
|
199
332
|
// Trim cache to avoid unbounded growth
|
|
200
333
|
if (this.registryCache.size > 8) {
|
|
@@ -202,7 +335,6 @@ export class RegistryService {
|
|
|
202
335
|
if (oldest !== undefined)
|
|
203
336
|
this.registryCache.delete(oldest);
|
|
204
337
|
}
|
|
205
|
-
return parsed;
|
|
206
338
|
}
|
|
207
339
|
}
|
|
208
340
|
function normalizeRegistryName(name) {
|
|
@@ -107,6 +107,21 @@ function heapSiftUp(heap, index) {
|
|
|
107
107
|
index = parent;
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
+
function heapPopWorst(heap) {
|
|
111
|
+
if (heap.length === 0) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const root = heap[0];
|
|
115
|
+
const tail = heap.pop();
|
|
116
|
+
if (heap.length === 0) {
|
|
117
|
+
return root;
|
|
118
|
+
}
|
|
119
|
+
if (tail) {
|
|
120
|
+
heap[0] = tail;
|
|
121
|
+
heapSiftDown(heap, 0, heap.length);
|
|
122
|
+
}
|
|
123
|
+
return root;
|
|
124
|
+
}
|
|
110
125
|
export function createSearchHitAccumulator(limit, cursor) {
|
|
111
126
|
const pageLimit = Math.max(1, limit);
|
|
112
127
|
const keepLimit = pageLimit + 1;
|
|
@@ -141,8 +156,14 @@ export function createSearchHitAccumulator(limit, cursor) {
|
|
|
141
156
|
return heap.length;
|
|
142
157
|
},
|
|
143
158
|
finalize() {
|
|
144
|
-
|
|
145
|
-
const
|
|
159
|
+
const sorted = new Array(heap.length);
|
|
160
|
+
const heapCopy = heap.slice();
|
|
161
|
+
for (let index = heapCopy.length - 1; index >= 0; index -= 1) {
|
|
162
|
+
const next = heapPopWorst(heapCopy);
|
|
163
|
+
if (next) {
|
|
164
|
+
sorted[index] = next;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
146
167
|
const page = sorted.slice(0, pageLimit);
|
|
147
168
|
const hasMore = totalAfterCursor > page.length;
|
|
148
169
|
return {
|
|
@@ -9,6 +9,20 @@ function toErrorMessage(value) {
|
|
|
9
9
|
}
|
|
10
10
|
return String(value);
|
|
11
11
|
}
|
|
12
|
+
function hasJavaSourceExtension(entryPath) {
|
|
13
|
+
const suffix = ".java";
|
|
14
|
+
if (entryPath.length < suffix.length) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
for (let index = 0; index < suffix.length; index += 1) {
|
|
18
|
+
const charCode = entryPath.charCodeAt(entryPath.length - suffix.length + index);
|
|
19
|
+
const normalizedCharCode = charCode >= 65 && charCode <= 90 ? charCode + 32 : charCode;
|
|
20
|
+
if (normalizedCharCode !== suffix.charCodeAt(index)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
12
26
|
function openZipFile(jarPath) {
|
|
13
27
|
return new Promise((resolve, reject) => {
|
|
14
28
|
yauzl.open(jarPath, {
|
|
@@ -128,7 +142,7 @@ export async function listJarEntries(jarPath) {
|
|
|
128
142
|
}
|
|
129
143
|
export async function listJavaEntries(jarPath) {
|
|
130
144
|
const entries = await listJarEntries(jarPath);
|
|
131
|
-
return entries.filter((entry) => entry
|
|
145
|
+
return entries.filter((entry) => hasJavaSourceExtension(entry) && isSecureJarEntryPath(entry));
|
|
132
146
|
}
|
|
133
147
|
export async function readJarEntryAsUtf8(jarPath, entryPath) {
|
|
134
148
|
const contentBuffer = await readJarEntryAsBuffer(jarPath, entryPath);
|
|
@@ -171,7 +185,7 @@ export async function* iterateJavaEntriesAsUtf8(jarPath, maxBytes) {
|
|
|
171
185
|
if (!entry) {
|
|
172
186
|
break;
|
|
173
187
|
}
|
|
174
|
-
if (!entry.fileName
|
|
188
|
+
if (!hasJavaSourceExtension(entry.fileName)) {
|
|
175
189
|
continue;
|
|
176
190
|
}
|
|
177
191
|
if (!isSecureJarEntryPath(entry.fileName)) {
|
package/dist/source-resolver.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, resolve as resolvePath } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import fastGlob from "fast-glob";
|
|
4
5
|
import { createError, ERROR_CODES } from "./errors.js";
|
|
5
6
|
import { buildRemoteBinaryUrls, buildRemoteSourceUrls, hasExistingJar, parseCoordinate, normalizedCoordinateValue } from "./maven-resolver.js";
|
|
6
7
|
import { defaultDownloadPath, downloadToCache } from "./repo-downloader.js";
|
|
@@ -104,13 +105,11 @@ function resolveGradleCacheCoordinateCandidate(coordinate) {
|
|
|
104
105
|
];
|
|
105
106
|
let discoveredFiles = [];
|
|
106
107
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
}
|
|
108
|
+
discoveredFiles = fastGlob.sync("*/*", {
|
|
109
|
+
cwd: baseDir,
|
|
110
|
+
absolute: true,
|
|
111
|
+
onlyFiles: true
|
|
112
|
+
});
|
|
114
113
|
}
|
|
115
114
|
catch {
|
|
116
115
|
return undefined;
|