@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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +765 -0
- package/dist/access-widener-parser.d.ts +24 -0
- package/dist/access-widener-parser.js +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.js +178 -0
- package/dist/decompiler/vineflower.d.ts +15 -0
- package/dist/decompiler/vineflower.js +185 -0
- package/dist/errors.d.ts +50 -0
- package/dist/errors.js +49 -0
- package/dist/hash.d.ts +1 -0
- package/dist/hash.js +12 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1447 -0
- package/dist/java-process.d.ts +16 -0
- package/dist/java-process.js +120 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +21 -0
- package/dist/mapping-pipeline-service.d.ts +18 -0
- package/dist/mapping-pipeline-service.js +60 -0
- package/dist/mapping-service.d.ts +161 -0
- package/dist/mapping-service.js +1706 -0
- package/dist/maven-resolver.d.ts +22 -0
- package/dist/maven-resolver.js +122 -0
- package/dist/minecraft-explorer-service.d.ts +43 -0
- package/dist/minecraft-explorer-service.js +562 -0
- package/dist/mixin-parser.d.ts +34 -0
- package/dist/mixin-parser.js +194 -0
- package/dist/mixin-validator.d.ts +59 -0
- package/dist/mixin-validator.js +274 -0
- package/dist/mod-analyzer.d.ts +23 -0
- package/dist/mod-analyzer.js +346 -0
- package/dist/mod-decompile-service.d.ts +39 -0
- package/dist/mod-decompile-service.js +136 -0
- package/dist/mod-remap-service.d.ts +17 -0
- package/dist/mod-remap-service.js +186 -0
- package/dist/mod-search-service.d.ts +28 -0
- package/dist/mod-search-service.js +174 -0
- package/dist/mojang-tiny-mapping-service.d.ts +13 -0
- package/dist/mojang-tiny-mapping-service.js +351 -0
- package/dist/nbt/java-nbt-codec.d.ts +3 -0
- package/dist/nbt/java-nbt-codec.js +385 -0
- package/dist/nbt/json-patch.d.ts +3 -0
- package/dist/nbt/json-patch.js +352 -0
- package/dist/nbt/pipeline.d.ts +39 -0
- package/dist/nbt/pipeline.js +173 -0
- package/dist/nbt/typed-json.d.ts +10 -0
- package/dist/nbt/typed-json.js +205 -0
- package/dist/nbt/types.d.ts +66 -0
- package/dist/nbt/types.js +2 -0
- package/dist/observability.d.ts +88 -0
- package/dist/observability.js +165 -0
- package/dist/path-converter.d.ts +12 -0
- package/dist/path-converter.js +161 -0
- package/dist/path-resolver.d.ts +19 -0
- package/dist/path-resolver.js +78 -0
- package/dist/registry-service.d.ts +29 -0
- package/dist/registry-service.js +214 -0
- package/dist/repo-downloader.d.ts +15 -0
- package/dist/repo-downloader.js +111 -0
- package/dist/resources.d.ts +3 -0
- package/dist/resources.js +154 -0
- package/dist/search-hit-accumulator.d.ts +38 -0
- package/dist/search-hit-accumulator.js +153 -0
- package/dist/source-jar-reader.d.ts +13 -0
- package/dist/source-jar-reader.js +216 -0
- package/dist/source-resolver.d.ts +14 -0
- package/dist/source-resolver.js +274 -0
- package/dist/source-service.d.ts +404 -0
- package/dist/source-service.js +2881 -0
- package/dist/storage/artifacts-repo.d.ts +45 -0
- package/dist/storage/artifacts-repo.js +209 -0
- package/dist/storage/db.d.ts +14 -0
- package/dist/storage/db.js +132 -0
- package/dist/storage/files-repo.d.ts +78 -0
- package/dist/storage/files-repo.js +437 -0
- package/dist/storage/index-meta-repo.d.ts +35 -0
- package/dist/storage/index-meta-repo.js +97 -0
- package/dist/storage/migrations.d.ts +11 -0
- package/dist/storage/migrations.js +71 -0
- package/dist/storage/schema.d.ts +1 -0
- package/dist/storage/schema.js +160 -0
- package/dist/storage/sqlite.d.ts +20 -0
- package/dist/storage/sqlite.js +111 -0
- package/dist/storage/symbols-repo.d.ts +63 -0
- package/dist/storage/symbols-repo.js +401 -0
- package/dist/symbols/symbol-extractor.d.ts +7 -0
- package/dist/symbols/symbol-extractor.js +64 -0
- package/dist/tiny-remapper-resolver.d.ts +1 -0
- package/dist/tiny-remapper-resolver.js +62 -0
- package/dist/tiny-remapper-service.d.ts +16 -0
- package/dist/tiny-remapper-service.js +73 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.js +2 -0
- package/dist/version-diff-service.d.ts +41 -0
- package/dist/version-diff-service.js +222 -0
- package/dist/version-service.d.ts +70 -0
- package/dist/version-service.js +411 -0
- package/dist/vineflower-resolver.d.ts +1 -0
- package/dist/vineflower-resolver.js +62 -0
- package/dist/workspace-mapping-service.d.ts +18 -0
- package/dist/workspace-mapping-service.js +89 -0
- package/package.json +61 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { release } from "node:os";
|
|
2
|
+
import { createError, ERROR_CODES } from "./errors.js";
|
|
3
|
+
const WINDOWS_DRIVE_PATH = /^[A-Za-z]:[\\/]/;
|
|
4
|
+
const MALFORMED_WINDOWS_DRIVE_PATH = /^[A-Za-z]:(?![\\/])/;
|
|
5
|
+
const WSL_MOUNT_PATH = /^\/mnt\/[a-z](?:\/|$)/i;
|
|
6
|
+
const UNC_WSL_PATH = /^(?:\\\\wsl\$\\|\/\/wsl\$\/)/i;
|
|
7
|
+
function normalizeToUnixSlashes(value) {
|
|
8
|
+
return value.replace(/\\/g, "/");
|
|
9
|
+
}
|
|
10
|
+
function normalizeToWindowsSlashes(value) {
|
|
11
|
+
return value.replace(/\//g, "\\");
|
|
12
|
+
}
|
|
13
|
+
function parseUncWslPath(pathValue) {
|
|
14
|
+
const normalized = normalizeToUnixSlashes(pathValue);
|
|
15
|
+
if (!normalized.toLowerCase().startsWith("//wsl$/")) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const remainder = normalized.slice("//wsl$/".length).replace(/^\/+/, "");
|
|
19
|
+
const slashIndex = remainder.indexOf("/");
|
|
20
|
+
if (slashIndex < 0) {
|
|
21
|
+
return {
|
|
22
|
+
distro: remainder,
|
|
23
|
+
innerPath: ""
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
distro: remainder.slice(0, slashIndex),
|
|
28
|
+
innerPath: remainder.slice(slashIndex + 1)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function detectPathRuntime() {
|
|
32
|
+
const platform = process.platform;
|
|
33
|
+
const osRelease = release().toLowerCase();
|
|
34
|
+
const isWsl = platform === "linux" &&
|
|
35
|
+
(Boolean(process.env.WSL_DISTRO_NAME) ||
|
|
36
|
+
Boolean(process.env.WSL_INTEROP) ||
|
|
37
|
+
osRelease.includes("microsoft"));
|
|
38
|
+
return {
|
|
39
|
+
platform,
|
|
40
|
+
isWsl,
|
|
41
|
+
wslDistro: process.env.WSL_DISTRO_NAME
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function isWindowsDrivePath(pathValue) {
|
|
45
|
+
return WINDOWS_DRIVE_PATH.test(pathValue);
|
|
46
|
+
}
|
|
47
|
+
export function isWslMountPath(pathValue) {
|
|
48
|
+
return WSL_MOUNT_PATH.test(pathValue);
|
|
49
|
+
}
|
|
50
|
+
export function isUncWslPath(pathValue) {
|
|
51
|
+
return UNC_WSL_PATH.test(pathValue);
|
|
52
|
+
}
|
|
53
|
+
export function validatePathFormat(pathValue, runtimeInfo) {
|
|
54
|
+
const runtime = runtimeInfo ?? detectPathRuntime();
|
|
55
|
+
const trimmed = pathValue.trim();
|
|
56
|
+
if (!trimmed) {
|
|
57
|
+
return "Path is required.";
|
|
58
|
+
}
|
|
59
|
+
if (trimmed.includes("\0")) {
|
|
60
|
+
return "Path contains an invalid null character.";
|
|
61
|
+
}
|
|
62
|
+
if (MALFORMED_WINDOWS_DRIVE_PATH.test(trimmed)) {
|
|
63
|
+
return 'Windows drive paths must include a separator after the drive (example: "C:\\\\path").';
|
|
64
|
+
}
|
|
65
|
+
if (isUncWslPath(trimmed)) {
|
|
66
|
+
const parsed = parseUncWslPath(trimmed);
|
|
67
|
+
if (!parsed?.distro) {
|
|
68
|
+
return 'UNC WSL paths must include a distro segment (example: "\\\\\\\\wsl$\\\\Ubuntu\\\\path").';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const usesWindowsStyle = isWindowsDrivePath(trimmed) || isUncWslPath(trimmed);
|
|
72
|
+
if (usesWindowsStyle && runtime.platform !== "win32" && !runtime.isWsl) {
|
|
73
|
+
return "Windows-style paths are not supported on this host.";
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
function throwInvalidPath(pathValue, reason, runtime, field) {
|
|
78
|
+
throw createError({
|
|
79
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
80
|
+
message: reason,
|
|
81
|
+
details: {
|
|
82
|
+
field: field ?? "path",
|
|
83
|
+
path: pathValue,
|
|
84
|
+
platform: runtime.platform,
|
|
85
|
+
isWsl: runtime.isWsl
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function toWslPath(pathValue, runtime, field) {
|
|
90
|
+
if (isWindowsDrivePath(pathValue)) {
|
|
91
|
+
const normalized = normalizeToUnixSlashes(pathValue);
|
|
92
|
+
const match = normalized.match(/^([A-Za-z]):(?:\/(.*))?$/);
|
|
93
|
+
if (!match?.[1]) {
|
|
94
|
+
throwInvalidPath(pathValue, "Failed to parse Windows drive path.", runtime, field);
|
|
95
|
+
}
|
|
96
|
+
const drive = match[1].toLowerCase();
|
|
97
|
+
const rest = (match[2] ?? "").replace(/^\/+/, "");
|
|
98
|
+
return rest ? `/mnt/${drive}/${rest}` : `/mnt/${drive}`;
|
|
99
|
+
}
|
|
100
|
+
const parsed = parseUncWslPath(pathValue);
|
|
101
|
+
if (parsed) {
|
|
102
|
+
const rest = parsed.innerPath.replace(/^\/+/, "");
|
|
103
|
+
return rest ? `/${rest}` : "/";
|
|
104
|
+
}
|
|
105
|
+
return pathValue;
|
|
106
|
+
}
|
|
107
|
+
function toWindowsPath(pathValue, runtime, field) {
|
|
108
|
+
if (isWslMountPath(pathValue)) {
|
|
109
|
+
const normalized = normalizeToUnixSlashes(pathValue);
|
|
110
|
+
const match = normalized.match(/^\/mnt\/([A-Za-z])(?:\/(.*))?$/);
|
|
111
|
+
if (!match?.[1]) {
|
|
112
|
+
throwInvalidPath(pathValue, "Failed to parse WSL mount path.", runtime, field);
|
|
113
|
+
}
|
|
114
|
+
const drive = match[1].toUpperCase();
|
|
115
|
+
const rest = (match[2] ?? "").replace(/^\/+/, "").replace(/\//g, "\\");
|
|
116
|
+
return rest ? `${drive}:\\${rest}` : `${drive}:\\`;
|
|
117
|
+
}
|
|
118
|
+
if (pathValue.startsWith("/")) {
|
|
119
|
+
const distro = runtime.wslDistro?.trim();
|
|
120
|
+
if (!distro) {
|
|
121
|
+
throwInvalidPath(pathValue, "Cannot convert Unix path to Windows path without WSL distro name.", runtime, field);
|
|
122
|
+
}
|
|
123
|
+
return `\\\\wsl$\\${distro}${pathValue.replace(/\//g, "\\")}`;
|
|
124
|
+
}
|
|
125
|
+
return normalizeToWindowsSlashes(pathValue);
|
|
126
|
+
}
|
|
127
|
+
export function normalizePathForHost(pathValue, runtimeInfo, field) {
|
|
128
|
+
const runtime = runtimeInfo ?? detectPathRuntime();
|
|
129
|
+
const validationError = validatePathFormat(pathValue, runtime);
|
|
130
|
+
if (validationError) {
|
|
131
|
+
throwInvalidPath(pathValue, validationError, runtime, field);
|
|
132
|
+
}
|
|
133
|
+
const trimmed = pathValue.trim();
|
|
134
|
+
if (runtime.isWsl) {
|
|
135
|
+
if (isWindowsDrivePath(trimmed) || isUncWslPath(trimmed)) {
|
|
136
|
+
return toWslPath(trimmed, runtime, field);
|
|
137
|
+
}
|
|
138
|
+
return trimmed;
|
|
139
|
+
}
|
|
140
|
+
if (runtime.platform === "win32") {
|
|
141
|
+
if (isWindowsDrivePath(trimmed) || isUncWslPath(trimmed)) {
|
|
142
|
+
return normalizeToWindowsSlashes(trimmed);
|
|
143
|
+
}
|
|
144
|
+
if (isWslMountPath(trimmed) || trimmed.startsWith("/")) {
|
|
145
|
+
return toWindowsPath(trimmed, runtime, field);
|
|
146
|
+
}
|
|
147
|
+
return trimmed;
|
|
148
|
+
}
|
|
149
|
+
return trimmed;
|
|
150
|
+
}
|
|
151
|
+
export function normalizeOptionalPathForHost(pathValue, runtimeInfo, field) {
|
|
152
|
+
if (!pathValue) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
const trimmed = pathValue.trim();
|
|
156
|
+
if (!trimmed) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
return normalizePathForHost(trimmed, runtimeInfo, field);
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=path-converter.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ArtifactSignature } from "./types.js";
|
|
2
|
+
export interface ResolvedJarInfo {
|
|
3
|
+
originalPath: string;
|
|
4
|
+
resolvedPath: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function normalizeJarPath(jarPath: string): string;
|
|
7
|
+
export declare function resolveJarPathWithSymlinkCheck(jarPath: string): ResolvedJarInfo;
|
|
8
|
+
export declare function buildJarSignature(stats: {
|
|
9
|
+
mtimeMs: number;
|
|
10
|
+
size: number;
|
|
11
|
+
}): string;
|
|
12
|
+
export declare function artifactSignatureFromFile(jarPath: string): ArtifactSignature;
|
|
13
|
+
export declare function isSecureJarEntryPath(entryPath: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Validate and normalize a user-supplied jar path input.
|
|
16
|
+
* Trims whitespace, validates non-empty, resolves symlinks, and wraps
|
|
17
|
+
* any filesystem error as ERR_INVALID_INPUT.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateAndNormalizeJarPath(jarPathInput: string): string;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { extname, resolve } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { normalizePathForHost } from "./path-converter.js";
|
|
5
|
+
import { createError, ERROR_CODES } from "./errors.js";
|
|
6
|
+
const INVALID_ENTRY = /(^|\/|\\)\.\.(\/|\\|$)/;
|
|
7
|
+
export function normalizeJarPath(jarPath) {
|
|
8
|
+
const normalizedInput = normalizePathForHost(jarPath, undefined, "jarPath");
|
|
9
|
+
const absolute = resolve(normalizedInput);
|
|
10
|
+
const stats = statSync(absolute);
|
|
11
|
+
if (!stats.isFile()) {
|
|
12
|
+
throw createError({
|
|
13
|
+
code: ERROR_CODES.JAR_NOT_FOUND,
|
|
14
|
+
message: `Expected a file path for jar, got "${normalizedInput}".`,
|
|
15
|
+
details: { jarPath: normalizedInput }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
if (extname(absolute).toLowerCase() !== ".jar") {
|
|
19
|
+
throw createError({
|
|
20
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
21
|
+
message: `Expected a .jar file, got "${normalizedInput}".`,
|
|
22
|
+
details: { jarPath: normalizedInput }
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
const resolved = realpathSync(absolute);
|
|
26
|
+
return resolved;
|
|
27
|
+
}
|
|
28
|
+
export function resolveJarPathWithSymlinkCheck(jarPath) {
|
|
29
|
+
const resolvedPath = normalizeJarPath(jarPath);
|
|
30
|
+
return {
|
|
31
|
+
originalPath: jarPath,
|
|
32
|
+
resolvedPath
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function buildJarSignature(stats) {
|
|
36
|
+
return `${Math.trunc(stats.mtimeMs)}:${stats.size}`;
|
|
37
|
+
}
|
|
38
|
+
export function artifactSignatureFromFile(jarPath) {
|
|
39
|
+
const resolvedPath = resolveJarPathWithSymlinkCheck(jarPath).resolvedPath;
|
|
40
|
+
const stats = statSync(resolvedPath);
|
|
41
|
+
return {
|
|
42
|
+
sourcePath: resolvedPath,
|
|
43
|
+
sourceArtifactId: createHash("sha256").update(`jar|${resolvedPath}|${buildJarSignature(stats)}`).digest("hex"),
|
|
44
|
+
signature: buildJarSignature(stats),
|
|
45
|
+
signatureParts: {
|
|
46
|
+
mtimeMs: stats.mtimeMs,
|
|
47
|
+
size: stats.size
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function isSecureJarEntryPath(entryPath) {
|
|
52
|
+
return !INVALID_ENTRY.test(entryPath.replaceAll("\\", "/"));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate and normalize a user-supplied jar path input.
|
|
56
|
+
* Trims whitespace, validates non-empty, resolves symlinks, and wraps
|
|
57
|
+
* any filesystem error as ERR_INVALID_INPUT.
|
|
58
|
+
*/
|
|
59
|
+
export function validateAndNormalizeJarPath(jarPathInput) {
|
|
60
|
+
const jarPath = jarPathInput.trim();
|
|
61
|
+
if (!jarPath) {
|
|
62
|
+
throw createError({
|
|
63
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
64
|
+
message: "jarPath must be non-empty."
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return normalizeJarPath(jarPath);
|
|
69
|
+
}
|
|
70
|
+
catch (cause) {
|
|
71
|
+
throw createError({
|
|
72
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
73
|
+
message: cause instanceof Error ? cause.message : `Invalid jar path: "${jarPath}".`,
|
|
74
|
+
details: { jarPath }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=path-resolver.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { VersionService } from "./version-service.js";
|
|
2
|
+
import type { Config } from "./types.js";
|
|
3
|
+
export type GetRegistryDataInput = {
|
|
4
|
+
version: string;
|
|
5
|
+
registry?: string;
|
|
6
|
+
};
|
|
7
|
+
export type RegistryEntry = {
|
|
8
|
+
protocol_id: number;
|
|
9
|
+
};
|
|
10
|
+
export type RegistryData = {
|
|
11
|
+
default?: string;
|
|
12
|
+
entries: Record<string, RegistryEntry>;
|
|
13
|
+
};
|
|
14
|
+
export type GetRegistryDataOutput = {
|
|
15
|
+
version: string;
|
|
16
|
+
registry?: string;
|
|
17
|
+
registries?: string[];
|
|
18
|
+
data: Record<string, RegistryData> | RegistryData;
|
|
19
|
+
entryCount: number;
|
|
20
|
+
warnings: string[];
|
|
21
|
+
};
|
|
22
|
+
export declare class RegistryService {
|
|
23
|
+
private readonly config;
|
|
24
|
+
private readonly versionService;
|
|
25
|
+
private readonly registryCache;
|
|
26
|
+
constructor(config: Config, versionService: VersionService);
|
|
27
|
+
getRegistryData(input: GetRegistryDataInput): Promise<GetRegistryDataOutput>;
|
|
28
|
+
private loadRegistries;
|
|
29
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createError, ERROR_CODES } from "./errors.js";
|
|
6
|
+
import { log } from "./logger.js";
|
|
7
|
+
const DATAGEN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
8
|
+
const MAX_STDIO_SNAPSHOT = 6_240;
|
|
9
|
+
function limitOutput(text) {
|
|
10
|
+
if (text.length <= MAX_STDIO_SNAPSHOT)
|
|
11
|
+
return text;
|
|
12
|
+
return text.slice(-MAX_STDIO_SNAPSHOT);
|
|
13
|
+
}
|
|
14
|
+
function resolveRegistryPaths(registryDir) {
|
|
15
|
+
return [
|
|
16
|
+
join(registryDir, "reports", "registries.json"),
|
|
17
|
+
join(registryDir, "generated", "reports", "registries.json"),
|
|
18
|
+
join(registryDir, "registries.json")
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
function findRegistryFile(registryDir) {
|
|
22
|
+
for (const candidate of resolveRegistryPaths(registryDir)) {
|
|
23
|
+
if (existsSync(candidate))
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
function runDataGen(serverJarPath, outputDir, version) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
// MC 1.18+ uses bundler format, older versions use -cp with main class directly.
|
|
31
|
+
// The bundler approach works for 1.18+ and the -cp approach for older versions.
|
|
32
|
+
// We try bundler first since most modern versions use it.
|
|
33
|
+
const isLegacy = isLegacyVersion(version);
|
|
34
|
+
const args = isLegacy
|
|
35
|
+
? [
|
|
36
|
+
"-cp", serverJarPath,
|
|
37
|
+
"-Xmx2G", "-Xms512M",
|
|
38
|
+
"net.minecraft.data.Main",
|
|
39
|
+
"--reports", "--all", "--server",
|
|
40
|
+
"--output", outputDir
|
|
41
|
+
]
|
|
42
|
+
: [
|
|
43
|
+
"-DbundlerMainClass=net.minecraft.data.Main",
|
|
44
|
+
"-Xmx2G", "-Xms512M",
|
|
45
|
+
"-jar", serverJarPath,
|
|
46
|
+
"--reports", "--all", "--server",
|
|
47
|
+
"--output", outputDir
|
|
48
|
+
];
|
|
49
|
+
log("info", "registry.datagen.start", { version, isLegacy, serverJarPath, outputDir });
|
|
50
|
+
const proc = spawn("java", args, {
|
|
51
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
52
|
+
cwd: outputDir
|
|
53
|
+
});
|
|
54
|
+
let stderr = "";
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
proc.kill();
|
|
57
|
+
reject(createError({
|
|
58
|
+
code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
|
|
59
|
+
message: `Registry data generation timed out for version "${version}".`,
|
|
60
|
+
details: { version, timeoutMs: DATAGEN_TIMEOUT_MS, stderrTail: limitOutput(stderr) }
|
|
61
|
+
}));
|
|
62
|
+
}, DATAGEN_TIMEOUT_MS);
|
|
63
|
+
proc.stderr?.on("data", (chunk) => {
|
|
64
|
+
stderr += chunk.toString("utf8");
|
|
65
|
+
stderr = limitOutput(stderr);
|
|
66
|
+
});
|
|
67
|
+
proc.once("error", (error) => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
reject(createError({
|
|
70
|
+
code: ERROR_CODES.JAVA_UNAVAILABLE,
|
|
71
|
+
message: "java command is not available for data generation.",
|
|
72
|
+
details: { error: error instanceof Error ? error.message : String(error) }
|
|
73
|
+
}));
|
|
74
|
+
});
|
|
75
|
+
proc.once("exit", (code) => {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
if (code !== 0) {
|
|
78
|
+
reject(createError({
|
|
79
|
+
code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
|
|
80
|
+
message: `Data generation exited with code ${code} for version "${version}".`,
|
|
81
|
+
details: { version, code, stderrTail: limitOutput(stderr) }
|
|
82
|
+
}));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
log("info", "registry.datagen.done", { version });
|
|
86
|
+
resolve();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* MC versions before 1.18 use -cp instead of -jar bundler format for data gen.
|
|
92
|
+
* 1.18 snapshots start with "21w" (2021 weekly), release is "1.18".
|
|
93
|
+
*/
|
|
94
|
+
function isLegacyVersion(version) {
|
|
95
|
+
// Snapshot format: YYwNNa
|
|
96
|
+
const snapshotMatch = version.match(/^(\d{2})w(\d{2})[a-z]$/);
|
|
97
|
+
if (snapshotMatch) {
|
|
98
|
+
const year = Number(snapshotMatch[1]);
|
|
99
|
+
const week = Number(snapshotMatch[2]);
|
|
100
|
+
// 1.18 started in 21w37a
|
|
101
|
+
if (year < 21)
|
|
102
|
+
return true;
|
|
103
|
+
if (year === 21 && week < 37)
|
|
104
|
+
return true;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
// Release format: 1.X or 1.X.Y
|
|
108
|
+
const releaseMatch = version.match(/^1\.(\d+)(?:\.\d+)?(?:-.*)?$/);
|
|
109
|
+
if (releaseMatch) {
|
|
110
|
+
return Number(releaseMatch[1]) < 18;
|
|
111
|
+
}
|
|
112
|
+
// New format (26.1+) is never legacy
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
export class RegistryService {
|
|
116
|
+
config;
|
|
117
|
+
versionService;
|
|
118
|
+
registryCache = new Map();
|
|
119
|
+
constructor(config, versionService) {
|
|
120
|
+
this.config = config;
|
|
121
|
+
this.versionService = versionService;
|
|
122
|
+
}
|
|
123
|
+
async getRegistryData(input) {
|
|
124
|
+
const version = input.version.trim();
|
|
125
|
+
if (!version) {
|
|
126
|
+
throw createError({
|
|
127
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
128
|
+
message: "version must be non-empty."
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const warnings = [];
|
|
132
|
+
const allRegistries = await this.loadRegistries(version, warnings);
|
|
133
|
+
const registryNames = Object.keys(allRegistries).sort();
|
|
134
|
+
if (input.registry) {
|
|
135
|
+
const registryName = normalizeRegistryName(input.registry);
|
|
136
|
+
const data = allRegistries[registryName];
|
|
137
|
+
if (!data) {
|
|
138
|
+
throw createError({
|
|
139
|
+
code: ERROR_CODES.SOURCE_NOT_FOUND,
|
|
140
|
+
message: `Registry "${registryName}" not found for version "${version}".`,
|
|
141
|
+
details: {
|
|
142
|
+
version,
|
|
143
|
+
registry: registryName,
|
|
144
|
+
available: registryNames.slice(0, 20)
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
version,
|
|
150
|
+
registry: registryName,
|
|
151
|
+
data,
|
|
152
|
+
entryCount: Object.keys(data.entries).length,
|
|
153
|
+
warnings
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
let totalEntries = 0;
|
|
157
|
+
for (const registry of Object.values(allRegistries)) {
|
|
158
|
+
totalEntries += Object.keys(registry.entries).length;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
version,
|
|
162
|
+
registries: registryNames,
|
|
163
|
+
data: allRegistries,
|
|
164
|
+
entryCount: totalEntries,
|
|
165
|
+
warnings
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async loadRegistries(version, warnings) {
|
|
169
|
+
const cached = this.registryCache.get(version);
|
|
170
|
+
if (cached)
|
|
171
|
+
return cached;
|
|
172
|
+
const registryDir = join(this.config.cacheDir, "registries", version);
|
|
173
|
+
// Check if we already have generated data
|
|
174
|
+
let registryFile = findRegistryFile(registryDir);
|
|
175
|
+
if (!registryFile) {
|
|
176
|
+
await mkdir(registryDir, { recursive: true });
|
|
177
|
+
const serverJar = await this.versionService.resolveServerJar(version);
|
|
178
|
+
await runDataGen(serverJar.jarPath, registryDir, version);
|
|
179
|
+
registryFile = findRegistryFile(registryDir);
|
|
180
|
+
if (!registryFile) {
|
|
181
|
+
throw createError({
|
|
182
|
+
code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
|
|
183
|
+
message: `Registry data generation did not produce registries.json for version "${version}".`,
|
|
184
|
+
details: { version, registryDir }
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const raw = readFileSync(registryFile, "utf8");
|
|
189
|
+
const parsed = JSON.parse(raw);
|
|
190
|
+
// Validate structure
|
|
191
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
192
|
+
throw createError({
|
|
193
|
+
code: ERROR_CODES.REGISTRY_GENERATION_FAILED,
|
|
194
|
+
message: `registries.json for version "${version}" has invalid structure.`,
|
|
195
|
+
details: { version }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
this.registryCache.set(version, parsed);
|
|
199
|
+
// Trim cache to avoid unbounded growth
|
|
200
|
+
if (this.registryCache.size > 8) {
|
|
201
|
+
const oldest = this.registryCache.keys().next().value;
|
|
202
|
+
if (oldest !== undefined)
|
|
203
|
+
this.registryCache.delete(oldest);
|
|
204
|
+
}
|
|
205
|
+
return parsed;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function normalizeRegistryName(name) {
|
|
209
|
+
const trimmed = name.trim();
|
|
210
|
+
if (trimmed.includes(":"))
|
|
211
|
+
return trimmed;
|
|
212
|
+
return `minecraft:${trimmed}`;
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=registry-service.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface DownloadResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
statusCode?: number;
|
|
4
|
+
etag?: string;
|
|
5
|
+
lastModified?: string;
|
|
6
|
+
contentLength?: number;
|
|
7
|
+
path?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface DownloadOptions {
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
retries?: number;
|
|
12
|
+
fetchFn?: typeof fetch;
|
|
13
|
+
}
|
|
14
|
+
export declare function downloadToCache(url: string, destinationPath: string, opts?: DownloadOptions): Promise<DownloadResult>;
|
|
15
|
+
export declare function defaultDownloadPath(cacheDir: string, url: string): string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createWriteStream, mkdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { createError, ERROR_CODES } from "./errors.js";
|
|
7
|
+
function isHttpUrl(url) {
|
|
8
|
+
try {
|
|
9
|
+
const parsed = new URL(url);
|
|
10
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function sleep(ms) {
|
|
17
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
}
|
|
19
|
+
function sha256(input) {
|
|
20
|
+
return createHash("sha256").update(input).digest("hex");
|
|
21
|
+
}
|
|
22
|
+
function retryDelay(baseMs, attempt) {
|
|
23
|
+
return Math.floor(baseMs * 2 ** attempt + Math.random() * 128);
|
|
24
|
+
}
|
|
25
|
+
export async function downloadToCache(url, destinationPath, opts = {}) {
|
|
26
|
+
const timeoutMs = opts.timeoutMs ?? 15000;
|
|
27
|
+
const maxRetries = opts.retries ?? 2;
|
|
28
|
+
const fetchFn = opts.fetchFn ?? globalThis.fetch;
|
|
29
|
+
if (!isHttpUrl(url)) {
|
|
30
|
+
throw createError({
|
|
31
|
+
code: ERROR_CODES.INVALID_INPUT,
|
|
32
|
+
message: `Unsupported scheme for download URL: ${url}`,
|
|
33
|
+
details: { url }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
mkdirSync(dirname(destinationPath), { recursive: true });
|
|
37
|
+
let attempt = 0;
|
|
38
|
+
while (true) {
|
|
39
|
+
const timeout = new AbortController();
|
|
40
|
+
const timer = setTimeout(() => timeout.abort(), timeoutMs);
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetchFn(url, { signal: timeout.signal });
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
const status = response.status;
|
|
45
|
+
if (status === 404) {
|
|
46
|
+
return { ok: false, statusCode: status };
|
|
47
|
+
}
|
|
48
|
+
if (status === 429 || (status >= 500 && status < 600)) {
|
|
49
|
+
if (attempt >= maxRetries) {
|
|
50
|
+
return { ok: false, statusCode: status };
|
|
51
|
+
}
|
|
52
|
+
const retryAfter = Number.parseInt(response.headers.get("retry-after") ?? "", 10);
|
|
53
|
+
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : retryDelay(200, attempt);
|
|
54
|
+
await sleep(waitMs);
|
|
55
|
+
attempt += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
statusCode: status,
|
|
62
|
+
etag: response.headers.get("etag") ?? undefined,
|
|
63
|
+
lastModified: response.headers.get("last-modified") ?? undefined,
|
|
64
|
+
contentLength: Number.parseInt(response.headers.get("content-length") ?? "0", 10)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const tempPath = `${destinationPath}.${randomBytes(4).toString("hex")}.tmp`;
|
|
68
|
+
try {
|
|
69
|
+
if (!response.body) {
|
|
70
|
+
writeFileSync(tempPath, Buffer.alloc(0));
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const readable = Readable.fromWeb(response.body);
|
|
74
|
+
await pipeline(readable, createWriteStream(tempPath));
|
|
75
|
+
}
|
|
76
|
+
const contentLength = statSync(tempPath).size;
|
|
77
|
+
renameSync(tempPath, destinationPath);
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
statusCode: status,
|
|
81
|
+
etag: response.headers.get("etag") ?? undefined,
|
|
82
|
+
lastModified: response.headers.get("last-modified") ?? undefined,
|
|
83
|
+
contentLength,
|
|
84
|
+
path: destinationPath
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (streamError) {
|
|
88
|
+
try {
|
|
89
|
+
unlinkSync(tempPath);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// best-effort cleanup
|
|
93
|
+
}
|
|
94
|
+
throw streamError instanceof Error ? streamError : new Error(String(streamError));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (caughtError) {
|
|
98
|
+
clearTimeout(timer);
|
|
99
|
+
if (attempt >= maxRetries) {
|
|
100
|
+
throw caughtError instanceof Error ? caughtError : new Error(String(caughtError));
|
|
101
|
+
}
|
|
102
|
+
await sleep(retryDelay(200, attempt));
|
|
103
|
+
attempt += 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export function defaultDownloadPath(cacheDir, url) {
|
|
108
|
+
const filename = `${sha256(url)}.jar`;
|
|
109
|
+
return `${cacheDir}/downloads/${filename}`;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=repo-downloader.js.map
|