@loontail/minecraft-kit 0.3.0 → 0.5.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 +1 -1
- package/dist/cli/index.cjs +5037 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +2 -0
- package/dist/cli/{index.js → index.mjs} +2 -2
- package/dist/cli/index.mjs.map +1 -0
- package/dist/index.cjs +3804 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1714 -0
- package/dist/{index.js → index.mjs} +2 -2
- package/dist/index.mjs.map +1 -0
- package/package.json +26 -26
- package/dist/cli/index.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3804 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var os = require('os');
|
|
4
|
+
var lruCache = require('lru-cache');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var fs$1 = require('fs');
|
|
7
|
+
var promises = require('stream/promises');
|
|
8
|
+
var yauzl = require('yauzl');
|
|
9
|
+
var crypto2 = require('crypto');
|
|
10
|
+
var fs = require('fs/promises');
|
|
11
|
+
var stream = require('stream');
|
|
12
|
+
var pLimit = require('p-limit');
|
|
13
|
+
var buffer = require('buffer');
|
|
14
|
+
var child_process = require('child_process');
|
|
15
|
+
|
|
16
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
17
|
+
|
|
18
|
+
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
19
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
20
|
+
var yauzl__default = /*#__PURE__*/_interopDefault(yauzl);
|
|
21
|
+
var crypto2__default = /*#__PURE__*/_interopDefault(crypto2);
|
|
22
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
23
|
+
var pLimit__default = /*#__PURE__*/_interopDefault(pLimit);
|
|
24
|
+
|
|
25
|
+
// src/types/logger.ts
|
|
26
|
+
var LogLevels = {
|
|
27
|
+
DEBUG: "debug",
|
|
28
|
+
INFO: "info",
|
|
29
|
+
WARN: "warn",
|
|
30
|
+
ERROR: "error"
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/core/logger.ts
|
|
34
|
+
var silentLogger = {
|
|
35
|
+
log() {
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var consoleLogger = {
|
|
39
|
+
log(level, message, fields) {
|
|
40
|
+
const target = pickConsole(level);
|
|
41
|
+
if (fields !== void 0) {
|
|
42
|
+
target(`[${level}] ${message}`, fields);
|
|
43
|
+
} else {
|
|
44
|
+
target(`[${level}] ${message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function pickConsole(level) {
|
|
49
|
+
if (level === LogLevels.ERROR) return console.error.bind(console);
|
|
50
|
+
if (level === LogLevels.WARN) return console.warn.bind(console);
|
|
51
|
+
if (level === LogLevels.INFO) return console.info.bind(console);
|
|
52
|
+
return console.debug.bind(console);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/constants/platform.ts
|
|
56
|
+
var NODE_PLATFORM_TO_MOJANG_OS = {
|
|
57
|
+
win32: "windows",
|
|
58
|
+
darwin: "osx",
|
|
59
|
+
linux: "linux"
|
|
60
|
+
};
|
|
61
|
+
var NODE_ARCH_TO_MOJANG_ARCH = {
|
|
62
|
+
x64: "x64",
|
|
63
|
+
ia32: "x86",
|
|
64
|
+
arm64: "arm64"
|
|
65
|
+
};
|
|
66
|
+
var RUNTIME_PLATFORM_KEYS = {
|
|
67
|
+
windows: { x64: "windows-x64", x86: "windows-x86", arm64: "windows-arm64" },
|
|
68
|
+
osx: { x64: "mac-os", arm64: "mac-os-arm64", x86: "mac-os" },
|
|
69
|
+
linux: { x64: "linux", x86: "linux-i386", arm64: "linux" }
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/core/errors.ts
|
|
73
|
+
var MinecraftKitError = class extends Error {
|
|
74
|
+
name = "MinecraftKitError";
|
|
75
|
+
/** Stable discriminator. */
|
|
76
|
+
code;
|
|
77
|
+
/** Structured context; safe to serialize. */
|
|
78
|
+
context;
|
|
79
|
+
constructor(code, message, options = {}) {
|
|
80
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
81
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
82
|
+
this.code = code;
|
|
83
|
+
this.context = Object.freeze({ ...options.context ?? {} });
|
|
84
|
+
}
|
|
85
|
+
/** JSON-friendly representation. */
|
|
86
|
+
toJSON() {
|
|
87
|
+
return {
|
|
88
|
+
name: this.name,
|
|
89
|
+
code: this.code,
|
|
90
|
+
message: this.message,
|
|
91
|
+
context: this.context,
|
|
92
|
+
cause: this.cause instanceof Error ? { name: this.cause.name, message: this.cause.message } : this.cause
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
function isMinecraftKitError(e) {
|
|
97
|
+
return e instanceof MinecraftKitError;
|
|
98
|
+
}
|
|
99
|
+
function isErrorCode(e, code) {
|
|
100
|
+
return isMinecraftKitError(e) && e.code === code;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/core/system.ts
|
|
104
|
+
function detectSystem(input = {}) {
|
|
105
|
+
const platform = input.platform ?? process.platform;
|
|
106
|
+
const arch = input.arch ?? process.arch;
|
|
107
|
+
const osVersion = input.osVersion ?? os__default.default.release();
|
|
108
|
+
const mojangOs = NODE_PLATFORM_TO_MOJANG_OS[platform];
|
|
109
|
+
const mojangArch = NODE_ARCH_TO_MOJANG_ARCH[arch];
|
|
110
|
+
if (mojangOs === void 0 || mojangArch === void 0) {
|
|
111
|
+
throw new MinecraftKitError(
|
|
112
|
+
"RUNTIME_UNSUPPORTED_PLATFORM",
|
|
113
|
+
`Unsupported platform/arch combination: ${platform}/${arch}`,
|
|
114
|
+
{ context: { platform, arch: String(arch) } }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return { os: mojangOs, arch: mojangArch, osVersion };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/constants/defaults.ts
|
|
121
|
+
var HTTP_TIMEOUT_MS = 3e4;
|
|
122
|
+
var HTTP_RETRY_MAX = 4;
|
|
123
|
+
var HTTP_RETRY_BACKOFF_BASE_MS = 500;
|
|
124
|
+
var HTTP_RETRY_BACKOFF_CAP_MS = 3e4;
|
|
125
|
+
var DOWNLOAD_CONCURRENCY = 32;
|
|
126
|
+
var CACHE_TTL_MS = 5 * 6e4;
|
|
127
|
+
var CACHE_MAX_ENTRIES = 256;
|
|
128
|
+
var USER_AGENT = "minecraft-kit/0.1";
|
|
129
|
+
var DEFAULT_LAUNCHER_NAME = "minecraft-kit";
|
|
130
|
+
var DEFAULT_LAUNCHER_VERSION = "0.1.0";
|
|
131
|
+
var DEFAULT_MIN_MB = 1024;
|
|
132
|
+
var DEFAULT_MAX_MB = 4096;
|
|
133
|
+
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
134
|
+
var PROGRESS_EVENT_INTERVAL_MS = 100;
|
|
135
|
+
var MAX_PROCESSOR_STDERR_LINES = 20;
|
|
136
|
+
var SPAWNER_MAX_LINE_BYTES = 64 * 1024;
|
|
137
|
+
|
|
138
|
+
// src/http/cache.ts
|
|
139
|
+
function createMemoryCache(options = {}) {
|
|
140
|
+
const cache = new lruCache.LRUCache({
|
|
141
|
+
max: options.maxEntries ?? CACHE_MAX_ENTRIES,
|
|
142
|
+
ttl: options.ttlMs ?? CACHE_TTL_MS
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
get(key) {
|
|
146
|
+
const wrapped = cache.get(key);
|
|
147
|
+
return wrapped === void 0 ? void 0 : wrapped.value;
|
|
148
|
+
},
|
|
149
|
+
set(key, value, ttlMs) {
|
|
150
|
+
const wrapped = { value };
|
|
151
|
+
if (ttlMs === void 0) {
|
|
152
|
+
cache.set(key, wrapped);
|
|
153
|
+
} else {
|
|
154
|
+
cache.set(key, wrapped, { ttl: ttlMs });
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
delete(key) {
|
|
158
|
+
cache.delete(key);
|
|
159
|
+
},
|
|
160
|
+
clear() {
|
|
161
|
+
cache.clear();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/http/client.ts
|
|
167
|
+
var TIMEOUT_REASON = /* @__PURE__ */ Symbol("http-timeout");
|
|
168
|
+
var FetchHttpClient = class {
|
|
169
|
+
async request(url, options = {}) {
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
const timeoutMs = options.timeoutMs ?? HTTP_TIMEOUT_MS;
|
|
172
|
+
const onParentAbort = () => controller.abort(options.signal?.reason);
|
|
173
|
+
if (options.signal) {
|
|
174
|
+
if (options.signal.aborted) {
|
|
175
|
+
controller.abort(options.signal.reason);
|
|
176
|
+
} else {
|
|
177
|
+
options.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const timer = setTimeout(() => controller.abort(TIMEOUT_REASON), timeoutMs);
|
|
181
|
+
let response;
|
|
182
|
+
try {
|
|
183
|
+
response = await fetch(url, {
|
|
184
|
+
method: "GET",
|
|
185
|
+
headers: { "user-agent": USER_AGENT, ...options.headers ?? {} },
|
|
186
|
+
signal: controller.signal,
|
|
187
|
+
redirect: "follow"
|
|
188
|
+
});
|
|
189
|
+
} catch (cause) {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
options.signal?.removeEventListener("abort", onParentAbort);
|
|
192
|
+
if (controller.signal.reason === TIMEOUT_REASON) {
|
|
193
|
+
throw new MinecraftKitError("NETWORK_TIMEOUT", `Request timed out: ${url}`, {
|
|
194
|
+
cause,
|
|
195
|
+
context: { url, timeoutMs }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (options.signal?.aborted) {
|
|
199
|
+
throw new MinecraftKitError("NETWORK_ABORTED", `Request aborted: ${url}`, {
|
|
200
|
+
cause,
|
|
201
|
+
context: { url }
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
throw new MinecraftKitError("NETWORK_HTTP_ERROR", `Network request failed: ${url}`, {
|
|
205
|
+
cause,
|
|
206
|
+
context: { url }
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
options.signal?.removeEventListener("abort", onParentAbort);
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
throw new MinecraftKitError("NETWORK_HTTP_ERROR", `HTTP ${response.status} for ${url}`, {
|
|
213
|
+
context: { url, httpStatus: response.status }
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return new FetchHttpResponse(response, url);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
var FetchHttpResponse = class {
|
|
220
|
+
constructor(response, url) {
|
|
221
|
+
this.response = response;
|
|
222
|
+
this.status = response.status;
|
|
223
|
+
this.url = url;
|
|
224
|
+
const headers = {};
|
|
225
|
+
response.headers.forEach((value, key) => {
|
|
226
|
+
headers[key.toLowerCase()] = value;
|
|
227
|
+
});
|
|
228
|
+
this.headers = headers;
|
|
229
|
+
}
|
|
230
|
+
response;
|
|
231
|
+
status;
|
|
232
|
+
headers;
|
|
233
|
+
url;
|
|
234
|
+
async text() {
|
|
235
|
+
return this.response.text();
|
|
236
|
+
}
|
|
237
|
+
async json() {
|
|
238
|
+
return await this.response.json();
|
|
239
|
+
}
|
|
240
|
+
async bytes() {
|
|
241
|
+
const buf = await this.response.arrayBuffer();
|
|
242
|
+
return new Uint8Array(buf);
|
|
243
|
+
}
|
|
244
|
+
async *stream() {
|
|
245
|
+
const body = this.response.body;
|
|
246
|
+
if (!body) {
|
|
247
|
+
const buf = await this.bytes();
|
|
248
|
+
yield buf;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const reader = body.getReader();
|
|
252
|
+
try {
|
|
253
|
+
while (true) {
|
|
254
|
+
const { value, done } = await reader.read();
|
|
255
|
+
if (done) return;
|
|
256
|
+
if (value) yield value;
|
|
257
|
+
}
|
|
258
|
+
} finally {
|
|
259
|
+
reader.releaseLock();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// src/constants/files.ts
|
|
265
|
+
var VERSIONS_DIR = "versions";
|
|
266
|
+
var LIBRARIES_DIR = "libraries";
|
|
267
|
+
var ASSETS_DIR = "assets";
|
|
268
|
+
var ASSETS_OBJECTS_DIR = "assets/objects";
|
|
269
|
+
var ASSETS_INDEXES_DIR = "assets/indexes";
|
|
270
|
+
var ASSETS_VIRTUAL_DIR = "assets/virtual";
|
|
271
|
+
var ASSETS_LEGACY_DIR = "assets/virtual/legacy";
|
|
272
|
+
var ASSETS_RESOURCES_DIR = "resources";
|
|
273
|
+
var ASSETS_LOG_CONFIGS_DIR = "assets/log_configs";
|
|
274
|
+
var RUNTIMES_DIR = "runtime";
|
|
275
|
+
var NATIVES_DIR_NAME = "natives";
|
|
276
|
+
var FORGE_INSTALLERS_DIR = "forge-installers";
|
|
277
|
+
var JAVA_EXECUTABLE = {
|
|
278
|
+
windows: "bin/javaw.exe",
|
|
279
|
+
windowsConsole: "bin/java.exe",
|
|
280
|
+
linux: "bin/java",
|
|
281
|
+
/** Note: macOS uses an extra `jre.bundle/Contents/Home/` prefix above this. */
|
|
282
|
+
osx: "bin/java"
|
|
283
|
+
};
|
|
284
|
+
var MAC_RUNTIME_PREFIX = "jre.bundle/Contents/Home";
|
|
285
|
+
|
|
286
|
+
// src/core/paths.ts
|
|
287
|
+
var targetPaths = {
|
|
288
|
+
versionsDir: (root) => path__default.default.join(root, VERSIONS_DIR),
|
|
289
|
+
versionDir: (root, versionId) => path__default.default.join(root, VERSIONS_DIR, versionId),
|
|
290
|
+
versionJar: (root, versionId) => path__default.default.join(root, VERSIONS_DIR, versionId, `${versionId}.jar`),
|
|
291
|
+
versionJson: (root, versionId) => path__default.default.join(root, VERSIONS_DIR, versionId, `${versionId}.json`),
|
|
292
|
+
librariesDir: (root) => path__default.default.join(root, LIBRARIES_DIR),
|
|
293
|
+
libraryFile: (root, libraryPath) => path__default.default.join(root, LIBRARIES_DIR, libraryPath),
|
|
294
|
+
assetIndex: (root, indexId) => path__default.default.join(root, ASSETS_INDEXES_DIR, `${indexId}.json`),
|
|
295
|
+
assetObject: (root, hash) => path__default.default.join(root, ASSETS_OBJECTS_DIR, hash.slice(0, 2), hash),
|
|
296
|
+
assetVirtual: (root, virtualPath) => path__default.default.join(root, ASSETS_VIRTUAL_DIR, virtualPath),
|
|
297
|
+
assetLegacy: (root, virtualPath) => path__default.default.join(root, ASSETS_LEGACY_DIR, virtualPath),
|
|
298
|
+
assetResource: (root, virtualPath) => path__default.default.join(root, ASSETS_RESOURCES_DIR, virtualPath),
|
|
299
|
+
loggingConfig: (root, id) => path__default.default.join(root, ASSETS_LOG_CONFIGS_DIR, id),
|
|
300
|
+
nativesDir: (root, versionId) => path__default.default.join(root, VERSIONS_DIR, versionId, NATIVES_DIR_NAME),
|
|
301
|
+
/**
|
|
302
|
+
* Path to a runtime component's root directory. Honours `installRoot` (custom global
|
|
303
|
+
* runtime location) when present; otherwise falls back to `<directory>/runtime/<component>`.
|
|
304
|
+
*/
|
|
305
|
+
runtimeRoot: (directory, component, installRoot) => installRoot !== void 0 ? path__default.default.join(installRoot, component) : path__default.default.join(directory, RUNTIMES_DIR, component),
|
|
306
|
+
runtimeJavaExecutable: (directory, component, os2, installRoot) => {
|
|
307
|
+
const runtime = targetPaths.runtimeRoot(directory, component, installRoot);
|
|
308
|
+
if (os2 === "windows") return path__default.default.join(runtime, JAVA_EXECUTABLE.windows);
|
|
309
|
+
if (os2 === "osx") return path__default.default.join(runtime, MAC_RUNTIME_PREFIX, JAVA_EXECUTABLE.osx);
|
|
310
|
+
return path__default.default.join(runtime, JAVA_EXECUTABLE.linux);
|
|
311
|
+
},
|
|
312
|
+
forgeInstaller: (root, mavenVersion) => path__default.default.join(root, FORGE_INSTALLERS_DIR, `forge-${mavenVersion}-installer.jar`)
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// src/types/install.ts
|
|
316
|
+
var InstallPhases = {
|
|
317
|
+
PLANNING: "planning",
|
|
318
|
+
DOWNLOADING_VERSION_MANIFEST: "downloading-version-manifest",
|
|
319
|
+
DOWNLOADING_CLIENT_JAR: "downloading-client-jar",
|
|
320
|
+
DOWNLOADING_LIBRARIES: "downloading-libraries",
|
|
321
|
+
DOWNLOADING_ASSET_INDEX: "downloading-asset-index",
|
|
322
|
+
DOWNLOADING_ASSETS: "downloading-assets",
|
|
323
|
+
EXTRACTING_NATIVES: "extracting-natives",
|
|
324
|
+
INSTALLING_RUNTIME: "installing-runtime",
|
|
325
|
+
INSTALLING_FABRIC: "installing-fabric",
|
|
326
|
+
INSTALLING_FORGE: "installing-forge",
|
|
327
|
+
RUNNING_FORGE_PROCESSORS: "running-forge-processors",
|
|
328
|
+
WRITING_FILES: "writing-files",
|
|
329
|
+
COMPLETED: "completed"
|
|
330
|
+
};
|
|
331
|
+
var InstallActionKinds = {
|
|
332
|
+
DOWNLOAD_FILE: "download-file",
|
|
333
|
+
EXTRACT_NATIVE: "extract-native",
|
|
334
|
+
RUN_FORGE_PROCESSOR: "run-forge-processor",
|
|
335
|
+
WRITE_VERSION_JSON: "write-version-json",
|
|
336
|
+
WRITE_LOGGING_CONFIG: "write-logging-config"
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// src/types/loader.ts
|
|
340
|
+
var Loaders = {
|
|
341
|
+
/** Plain vanilla Minecraft, no mod loader. */
|
|
342
|
+
VANILLA: "vanilla",
|
|
343
|
+
/** Fabric mod loader. */
|
|
344
|
+
FABRIC: "fabric",
|
|
345
|
+
/** Modern (1.13+) Forge mod loader. */
|
|
346
|
+
FORGE: "forge"
|
|
347
|
+
};
|
|
348
|
+
var VersionPreference = {
|
|
349
|
+
LATEST: "latest",
|
|
350
|
+
RECOMMENDED: "recommended"
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// src/constants/api.ts
|
|
354
|
+
var PISTON_META = "https://piston-meta.mojang.com";
|
|
355
|
+
var RESOURCES = "https://resources.download.minecraft.net";
|
|
356
|
+
var FABRIC_META = "https://meta.fabricmc.net";
|
|
357
|
+
var FORGE_MAVEN = "https://maven.minecraftforge.net";
|
|
358
|
+
var FORGE_FILES = "https://files.minecraftforge.net";
|
|
359
|
+
var RUNTIME_INDEX_DIGEST = "2ec0cc96c44e5a76b9c8b7c39df7210883d12871";
|
|
360
|
+
var ApiEndpoints = {
|
|
361
|
+
mojang: {
|
|
362
|
+
/** Top-level Minecraft version manifest (v2). */
|
|
363
|
+
versionManifest: () => `${PISTON_META}/mc/game/version_manifest_v2.json`,
|
|
364
|
+
/** Mojang Java-runtime index. */
|
|
365
|
+
runtimeIndex: () => `${PISTON_META}/v1/products/java-runtime/${RUNTIME_INDEX_DIGEST}/all.json`
|
|
366
|
+
},
|
|
367
|
+
resources: {
|
|
368
|
+
/** Hash-addressed Minecraft asset object. */
|
|
369
|
+
asset: (hash) => `${RESOURCES}/${hash.slice(0, 2)}/${hash}`
|
|
370
|
+
},
|
|
371
|
+
fabric: {
|
|
372
|
+
gameVersions: () => `${FABRIC_META}/v2/versions/game`,
|
|
373
|
+
loaderVersions: () => `${FABRIC_META}/v2/versions/loader`,
|
|
374
|
+
loaderForGame: (minecraftVersion) => `${FABRIC_META}/v2/versions/loader/${encodeURIComponent(minecraftVersion)}`,
|
|
375
|
+
profile: (minecraftVersion, loaderVersion) => `${FABRIC_META}/v2/versions/loader/${encodeURIComponent(minecraftVersion)}/${encodeURIComponent(loaderVersion)}/profile/json`
|
|
376
|
+
},
|
|
377
|
+
forge: {
|
|
378
|
+
/** Forge Maven listing of all builds across all MC versions. */
|
|
379
|
+
mavenMetadata: () => `${FORGE_MAVEN}/net/minecraftforge/forge/maven-metadata.xml`,
|
|
380
|
+
/** Slim "recommended" / "latest" promotion mapping. */
|
|
381
|
+
promotions: () => `${FORGE_FILES}/net/minecraftforge/forge/promotions_slim.json`,
|
|
382
|
+
/** URL of the modern installer JAR for a Maven version (e.g. `1.20.1-47.2.0`). */
|
|
383
|
+
installer: (mavenVersion) => {
|
|
384
|
+
const filename = `forge-${mavenVersion}-installer.jar`;
|
|
385
|
+
return `${FORGE_MAVEN}/net/minecraftforge/forge/${mavenVersion}/${filename}`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// src/http/metadata.ts
|
|
391
|
+
async function fetchJson(http, cache, input) {
|
|
392
|
+
const key = input.cacheKey ?? `json:${input.url}`;
|
|
393
|
+
const cached = cache.get(key);
|
|
394
|
+
if (cached !== void 0) {
|
|
395
|
+
return cached;
|
|
396
|
+
}
|
|
397
|
+
const requestOptions = {};
|
|
398
|
+
if (input.signal !== void 0) requestOptions.signal = input.signal;
|
|
399
|
+
const response = await http.request(input.url, requestOptions);
|
|
400
|
+
const value = await response.json();
|
|
401
|
+
cache.set(key, value, input.ttlMs ?? CACHE_TTL_MS);
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
async function fetchText(http, cache, input) {
|
|
405
|
+
const key = input.cacheKey ?? `text:${input.url}`;
|
|
406
|
+
const cached = cache.get(key);
|
|
407
|
+
if (cached !== void 0) {
|
|
408
|
+
return cached;
|
|
409
|
+
}
|
|
410
|
+
const requestOptions = {};
|
|
411
|
+
if (input.signal !== void 0) requestOptions.signal = input.signal;
|
|
412
|
+
const response = await http.request(input.url, requestOptions);
|
|
413
|
+
const text = await response.text();
|
|
414
|
+
cache.set(key, text, input.ttlMs ?? CACHE_TTL_MS);
|
|
415
|
+
return text;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/install/assets.ts
|
|
419
|
+
async function planAssetDownloads(input) {
|
|
420
|
+
const indexUrl = input.assetIndex.url;
|
|
421
|
+
const indexPath = targetPaths.assetIndex(input.directory, input.assetIndex.id);
|
|
422
|
+
const indexDocument = await fetchJson(input.http, input.cache, {
|
|
423
|
+
url: indexUrl,
|
|
424
|
+
cacheKey: `asset-index:${input.assetIndex.id}:${input.assetIndex.sha1}`,
|
|
425
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
426
|
+
});
|
|
427
|
+
const actions = [
|
|
428
|
+
{
|
|
429
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
430
|
+
url: indexUrl,
|
|
431
|
+
target: indexPath,
|
|
432
|
+
expectedSha1: input.assetIndex.sha1,
|
|
433
|
+
expectedSize: input.assetIndex.size,
|
|
434
|
+
category: "asset-index"
|
|
435
|
+
}
|
|
436
|
+
];
|
|
437
|
+
const seen = /* @__PURE__ */ new Set();
|
|
438
|
+
for (const entry of Object.values(indexDocument.objects)) {
|
|
439
|
+
if (seen.has(entry.hash)) continue;
|
|
440
|
+
seen.add(entry.hash);
|
|
441
|
+
actions.push({
|
|
442
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
443
|
+
url: ApiEndpoints.resources.asset(entry.hash),
|
|
444
|
+
target: targetPaths.assetObject(input.directory, entry.hash),
|
|
445
|
+
expectedSha1: entry.hash,
|
|
446
|
+
expectedSize: entry.size,
|
|
447
|
+
category: "asset"
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return { actions, indexDocument };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/constants/maven.ts
|
|
454
|
+
var DEFAULT_LIBRARY_REPOSITORY = "https://libraries.minecraft.net/";
|
|
455
|
+
var FABRIC_MAVEN_BASE = "https://maven.fabricmc.net/";
|
|
456
|
+
var FORGE_MAVEN_BASE = "https://maven.minecraftforge.net/";
|
|
457
|
+
|
|
458
|
+
// src/core/maven.ts
|
|
459
|
+
function parseMavenCoordinate(input) {
|
|
460
|
+
const trimmed = input.startsWith("[") && input.endsWith("]") ? input.slice(1, -1) : input;
|
|
461
|
+
const atIndex = trimmed.indexOf("@");
|
|
462
|
+
const extension = atIndex === -1 ? "jar" : trimmed.slice(atIndex + 1);
|
|
463
|
+
const body = atIndex === -1 ? trimmed : trimmed.slice(0, atIndex);
|
|
464
|
+
const parts = body.split(":");
|
|
465
|
+
if (parts.length < 3 || parts.length > 4) {
|
|
466
|
+
throw new MinecraftKitError("INVALID_INPUT", `Invalid Maven coordinate: ${input}`, {
|
|
467
|
+
context: { input }
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
const [group, artifact, version, classifier] = parts;
|
|
471
|
+
if (!group || !artifact || !version) {
|
|
472
|
+
throw new MinecraftKitError(
|
|
473
|
+
"INVALID_INPUT",
|
|
474
|
+
`Invalid Maven coordinate (missing component): ${input}`,
|
|
475
|
+
{ context: { input } }
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
if (classifier === void 0) {
|
|
479
|
+
return { group, artifact, version, extension };
|
|
480
|
+
}
|
|
481
|
+
return { group, artifact, version, classifier, extension };
|
|
482
|
+
}
|
|
483
|
+
function mavenRelativePath(coord) {
|
|
484
|
+
const groupPath = coord.group.replaceAll(".", "/");
|
|
485
|
+
const classifierSegment = coord.classifier === void 0 ? "" : `-${coord.classifier}`;
|
|
486
|
+
const filename = `${coord.artifact}-${coord.version}${classifierSegment}.${coord.extension}`;
|
|
487
|
+
return `${groupPath}/${coord.artifact}/${coord.version}/${filename}`;
|
|
488
|
+
}
|
|
489
|
+
function mavenRelativePathFor(input) {
|
|
490
|
+
return mavenRelativePath(parseMavenCoordinate(input));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/core/rules.ts
|
|
494
|
+
function evaluateRules(rules, context) {
|
|
495
|
+
if (!rules || rules.length === 0) {
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
let allowed = false;
|
|
499
|
+
for (const rule of rules) {
|
|
500
|
+
if (matchesRule(rule, context)) {
|
|
501
|
+
allowed = rule.action === "allow";
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return allowed;
|
|
505
|
+
}
|
|
506
|
+
function matchesRule(rule, context) {
|
|
507
|
+
if (rule.os !== void 0) {
|
|
508
|
+
if (rule.os.name !== void 0 && rule.os.name !== context.system.os) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
if (rule.os.arch !== void 0 && normalizeArch(rule.os.arch) !== context.system.arch) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
if (rule.os.version !== void 0) {
|
|
515
|
+
try {
|
|
516
|
+
if (!new RegExp(rule.os.version).test(context.system.osVersion)) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
} catch {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (rule.features !== void 0) {
|
|
525
|
+
const features = context.features ?? {};
|
|
526
|
+
for (const [key, expected] of Object.entries(rule.features)) {
|
|
527
|
+
const actual = features[key] === true;
|
|
528
|
+
if (expected !== actual) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
function normalizeArch(arch) {
|
|
536
|
+
return arch === "ia32" ? "x86" : arch;
|
|
537
|
+
}
|
|
538
|
+
function resolveArchPlaceholder(template, archDigit2) {
|
|
539
|
+
return template.replaceAll("${arch}", archDigit2);
|
|
540
|
+
}
|
|
541
|
+
function archDigit(arch) {
|
|
542
|
+
if (arch === "x86") return "32";
|
|
543
|
+
if (arch === "x64") return "64";
|
|
544
|
+
return "64";
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/install/libraries.ts
|
|
548
|
+
function planLibraryDownloads(input) {
|
|
549
|
+
const downloads = [];
|
|
550
|
+
const nativeExtractions = [];
|
|
551
|
+
const classpathFiles = [];
|
|
552
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
553
|
+
const nativesDir = targetPaths.nativesDir(input.directory, input.versionId);
|
|
554
|
+
for (const library of input.libraries) {
|
|
555
|
+
if (!evaluateRules(library.rules, { system: input.system })) continue;
|
|
556
|
+
const artifact = pickPrimaryArtifact(library);
|
|
557
|
+
if (artifact) {
|
|
558
|
+
const targetPath = path__default.default.join(
|
|
559
|
+
targetPaths.librariesDir(input.directory),
|
|
560
|
+
artifact.relativePath
|
|
561
|
+
);
|
|
562
|
+
if (!seenPaths.has(targetPath)) {
|
|
563
|
+
seenPaths.add(targetPath);
|
|
564
|
+
if (artifact.url) {
|
|
565
|
+
downloads.push({
|
|
566
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
567
|
+
url: artifact.url,
|
|
568
|
+
target: targetPath,
|
|
569
|
+
...artifact.sha1 !== void 0 ? { expectedSha1: artifact.sha1 } : {},
|
|
570
|
+
...artifact.size !== void 0 ? { expectedSize: artifact.size } : {},
|
|
571
|
+
category: input.category
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
classpathFiles.push(targetPath);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const native = pickNative(library, input.system);
|
|
578
|
+
if (native) {
|
|
579
|
+
const targetPath = path__default.default.join(targetPaths.librariesDir(input.directory), native.relativePath);
|
|
580
|
+
if (!seenPaths.has(targetPath)) {
|
|
581
|
+
seenPaths.add(targetPath);
|
|
582
|
+
if (native.url) {
|
|
583
|
+
downloads.push({
|
|
584
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
585
|
+
url: native.url,
|
|
586
|
+
target: targetPath,
|
|
587
|
+
...native.sha1 !== void 0 ? { expectedSha1: native.sha1 } : {},
|
|
588
|
+
...native.size !== void 0 ? { expectedSize: native.size } : {},
|
|
589
|
+
category: input.category
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
nativeExtractions.push({
|
|
594
|
+
kind: InstallActionKinds.EXTRACT_NATIVE,
|
|
595
|
+
source: targetPath,
|
|
596
|
+
destination: nativesDir,
|
|
597
|
+
exclude: library.extract?.exclude ?? ["META-INF/"]
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return { downloads, nativeExtractions, classpathFiles };
|
|
602
|
+
}
|
|
603
|
+
function pickPrimaryArtifact(library) {
|
|
604
|
+
if (library.downloads?.artifact) {
|
|
605
|
+
return artifactFromDownload(library.downloads.artifact);
|
|
606
|
+
}
|
|
607
|
+
if (library.url) {
|
|
608
|
+
return mavenArtifactFromCoord(library.name, library.url);
|
|
609
|
+
}
|
|
610
|
+
if (library.name && !library.natives) {
|
|
611
|
+
return mavenArtifactFromCoord(library.name, DEFAULT_LIBRARY_REPOSITORY);
|
|
612
|
+
}
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
function pickNative(library, system) {
|
|
616
|
+
if (!library.natives) return null;
|
|
617
|
+
const classifierTemplate = library.natives[system.os];
|
|
618
|
+
if (!classifierTemplate) return null;
|
|
619
|
+
const classifier = resolveArchPlaceholder(classifierTemplate, archDigit(system.arch));
|
|
620
|
+
const classifierArtifact = library.downloads?.classifiers?.[classifier];
|
|
621
|
+
if (classifierArtifact) {
|
|
622
|
+
return artifactFromDownload(classifierArtifact);
|
|
623
|
+
}
|
|
624
|
+
if (library.url || library.name) {
|
|
625
|
+
const coord = parseMavenCoordinate(library.name);
|
|
626
|
+
const withClassifier = `${coord.group}:${coord.artifact}:${coord.version}:${classifier}`;
|
|
627
|
+
return mavenArtifactFromCoord(withClassifier, library.url ?? DEFAULT_LIBRARY_REPOSITORY);
|
|
628
|
+
}
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
function artifactFromDownload(artifact) {
|
|
632
|
+
return {
|
|
633
|
+
relativePath: artifact.path,
|
|
634
|
+
url: artifact.url,
|
|
635
|
+
sha1: artifact.sha1,
|
|
636
|
+
size: artifact.size
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
function mavenArtifactFromCoord(coord, baseUrl) {
|
|
640
|
+
const relativePath = mavenRelativePathFor(coord);
|
|
641
|
+
const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
642
|
+
if (!relativePath) {
|
|
643
|
+
throw new MinecraftKitError("MANIFEST_INVALID", `Invalid library coordinate: ${coord}`, {
|
|
644
|
+
context: { input: coord }
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
relativePath,
|
|
649
|
+
url: `${normalizedBase}${relativePath}`,
|
|
650
|
+
sha1: void 0,
|
|
651
|
+
size: void 0
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/install/fabric-install.ts
|
|
656
|
+
function planFabricInstall(input) {
|
|
657
|
+
const versionId = input.loader.profile.id;
|
|
658
|
+
const versionJsonPath = targetPaths.versionJson(input.directory, versionId);
|
|
659
|
+
const versionJson = {
|
|
660
|
+
kind: InstallActionKinds.WRITE_VERSION_JSON,
|
|
661
|
+
path: versionJsonPath,
|
|
662
|
+
content: `${JSON.stringify(input.loader.profile, null, 2)}
|
|
663
|
+
`
|
|
664
|
+
};
|
|
665
|
+
const plan = planLibraryDownloads({
|
|
666
|
+
libraries: input.loader.profile.libraries,
|
|
667
|
+
directory: input.directory,
|
|
668
|
+
system: input.system,
|
|
669
|
+
versionId: input.minecraft.version,
|
|
670
|
+
category: "fabric-library"
|
|
671
|
+
});
|
|
672
|
+
return {
|
|
673
|
+
versionJson,
|
|
674
|
+
libraryDownloads: plan.downloads,
|
|
675
|
+
classpathFiles: plan.classpathFiles,
|
|
676
|
+
versionId
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// src/constants/limits.ts
|
|
681
|
+
var EXTRACTION_MAX_FILE_SIZE = 256 * 1024 * 1024;
|
|
682
|
+
var EXTRACTION_MAX_TOTAL_SIZE = 2 * 1024 * 1024 * 1024;
|
|
683
|
+
var EXTRACTION_MAX_COMPRESSION_RATIO = 200;
|
|
684
|
+
var EXTRACTION_MAX_ENTRY_COUNT = 1e5;
|
|
685
|
+
var FORGE_INSTALLER_MAX_SIZE = 256 * 1024 * 1024;
|
|
686
|
+
async function ensureDir(directory) {
|
|
687
|
+
try {
|
|
688
|
+
await fs__default.default.mkdir(directory, { recursive: true });
|
|
689
|
+
} catch (cause) {
|
|
690
|
+
throw new MinecraftKitError(
|
|
691
|
+
"FILESYSTEM_WRITE_ERROR",
|
|
692
|
+
`Failed to create directory: ${directory}`,
|
|
693
|
+
{ cause, context: { filePath: directory } }
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async function fileExists(filePath) {
|
|
698
|
+
try {
|
|
699
|
+
const stat = await fs__default.default.stat(filePath);
|
|
700
|
+
return stat.isFile();
|
|
701
|
+
} catch {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async function dirExists(filePath) {
|
|
706
|
+
try {
|
|
707
|
+
const stat = await fs__default.default.stat(filePath);
|
|
708
|
+
return stat.isDirectory();
|
|
709
|
+
} catch {
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
async function fileSize(filePath) {
|
|
714
|
+
try {
|
|
715
|
+
const stat = await fs__default.default.stat(filePath);
|
|
716
|
+
return stat.size;
|
|
717
|
+
} catch {
|
|
718
|
+
return -1;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
async function atomicWrite(target, data) {
|
|
722
|
+
await ensureDir(path__default.default.dirname(target));
|
|
723
|
+
const tmp = `${target}.${crypto2__default.default.randomBytes(4).toString("hex")}.tmp`;
|
|
724
|
+
try {
|
|
725
|
+
if (typeof data === "string") {
|
|
726
|
+
await fs__default.default.writeFile(tmp, data, "utf8");
|
|
727
|
+
} else {
|
|
728
|
+
await fs__default.default.writeFile(tmp, data);
|
|
729
|
+
}
|
|
730
|
+
await fs__default.default.rename(tmp, target);
|
|
731
|
+
} catch (cause) {
|
|
732
|
+
try {
|
|
733
|
+
await fs__default.default.unlink(tmp);
|
|
734
|
+
} catch {
|
|
735
|
+
}
|
|
736
|
+
throw new MinecraftKitError("FILESYSTEM_WRITE_ERROR", `Failed to write file: ${target}`, {
|
|
737
|
+
cause,
|
|
738
|
+
context: { filePath: target }
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async function readText(filePath) {
|
|
743
|
+
try {
|
|
744
|
+
return await fs__default.default.readFile(filePath, "utf8");
|
|
745
|
+
} catch (cause) {
|
|
746
|
+
throw new MinecraftKitError("FILESYSTEM_READ_ERROR", `Failed to read file: ${filePath}`, {
|
|
747
|
+
cause,
|
|
748
|
+
context: { filePath }
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async function listChildDirectories(directory) {
|
|
753
|
+
try {
|
|
754
|
+
const entries = await fs__default.default.readdir(directory, { withFileTypes: true });
|
|
755
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
756
|
+
} catch {
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
async function chmodExecutable(filePath) {
|
|
761
|
+
if (process.platform === "win32") {
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
await fs__default.default.chmod(filePath, 493);
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function assertWithinRoot(root, child) {
|
|
770
|
+
const normalizedRoot = path__default.default.resolve(root);
|
|
771
|
+
const normalizedChild = path__default.default.resolve(root, child);
|
|
772
|
+
const sep = path__default.default.sep;
|
|
773
|
+
if (normalizedChild !== normalizedRoot && !normalizedChild.startsWith(normalizedRoot + sep)) {
|
|
774
|
+
throw new MinecraftKitError("FILESYSTEM_PATH_TRAVERSAL", `Path escapes root: ${child}`, {
|
|
775
|
+
context: { filePath: child, rootDirectory: root }
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/core/archive.ts
|
|
781
|
+
function openZip(filePath) {
|
|
782
|
+
return new Promise((resolve, reject) => {
|
|
783
|
+
yauzl__default.default.open(filePath, { lazyEntries: true, autoClose: false }, (err, zipFile) => {
|
|
784
|
+
if (err || !zipFile) {
|
|
785
|
+
reject(
|
|
786
|
+
new MinecraftKitError("ARCHIVE_INVALID", `Failed to open archive: ${filePath}`, {
|
|
787
|
+
cause: err,
|
|
788
|
+
context: { filePath }
|
|
789
|
+
})
|
|
790
|
+
);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
resolve(new ZipReader(zipFile, filePath));
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
var ZipReader = class {
|
|
798
|
+
constructor(file, filePath) {
|
|
799
|
+
this.file = file;
|
|
800
|
+
this.filePath = filePath;
|
|
801
|
+
}
|
|
802
|
+
file;
|
|
803
|
+
filePath;
|
|
804
|
+
/** Iterate every entry. Caller may break out of the loop early. */
|
|
805
|
+
async *entries() {
|
|
806
|
+
const file = this.file;
|
|
807
|
+
let count = 0;
|
|
808
|
+
while (true) {
|
|
809
|
+
const entry = await this.readNext();
|
|
810
|
+
if (entry === null) return;
|
|
811
|
+
count++;
|
|
812
|
+
if (count > EXTRACTION_MAX_ENTRY_COUNT) {
|
|
813
|
+
throw new MinecraftKitError(
|
|
814
|
+
"ARCHIVE_TOO_LARGE",
|
|
815
|
+
`Archive contains too many entries: ${this.filePath}`,
|
|
816
|
+
{ context: { filePath: this.filePath } }
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
yield this.toZipEntry(entry, file);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/** Find a single entry by name. Returns undefined if absent. */
|
|
823
|
+
async findEntry(name) {
|
|
824
|
+
for await (const entry of this.entries()) {
|
|
825
|
+
if (entry.name === name) return entry;
|
|
826
|
+
}
|
|
827
|
+
return void 0;
|
|
828
|
+
}
|
|
829
|
+
/** Close the reader. */
|
|
830
|
+
close() {
|
|
831
|
+
this.file.close();
|
|
832
|
+
}
|
|
833
|
+
readNext() {
|
|
834
|
+
return new Promise((resolve, reject) => {
|
|
835
|
+
const onEntry = (entry) => {
|
|
836
|
+
cleanup();
|
|
837
|
+
resolve(entry);
|
|
838
|
+
};
|
|
839
|
+
const onEnd = () => {
|
|
840
|
+
cleanup();
|
|
841
|
+
resolve(null);
|
|
842
|
+
};
|
|
843
|
+
const onError = (err) => {
|
|
844
|
+
cleanup();
|
|
845
|
+
reject(
|
|
846
|
+
new MinecraftKitError("ARCHIVE_INVALID", "Failed to read archive entry", {
|
|
847
|
+
cause: err,
|
|
848
|
+
context: { filePath: this.filePath }
|
|
849
|
+
})
|
|
850
|
+
);
|
|
851
|
+
};
|
|
852
|
+
const cleanup = () => {
|
|
853
|
+
this.file.removeListener("entry", onEntry);
|
|
854
|
+
this.file.removeListener("end", onEnd);
|
|
855
|
+
this.file.removeListener("error", onError);
|
|
856
|
+
};
|
|
857
|
+
this.file.once("entry", onEntry);
|
|
858
|
+
this.file.once("end", onEnd);
|
|
859
|
+
this.file.once("error", onError);
|
|
860
|
+
this.file.readEntry();
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
toZipEntry(entry, file) {
|
|
864
|
+
const name = entry.fileName;
|
|
865
|
+
const isDirectory = name.endsWith("/");
|
|
866
|
+
return {
|
|
867
|
+
name,
|
|
868
|
+
compressedSize: entry.compressedSize,
|
|
869
|
+
uncompressedSize: entry.uncompressedSize,
|
|
870
|
+
isDirectory,
|
|
871
|
+
readBuffer: async () => {
|
|
872
|
+
if (entry.uncompressedSize > EXTRACTION_MAX_FILE_SIZE) {
|
|
873
|
+
throw new MinecraftKitError(
|
|
874
|
+
"ARCHIVE_TOO_LARGE",
|
|
875
|
+
`Archive entry exceeds size cap: ${name}`,
|
|
876
|
+
{ context: { filePath: this.filePath, entryName: name, size: entry.uncompressedSize } }
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
if (entry.compressedSize > 0 && entry.uncompressedSize / entry.compressedSize > EXTRACTION_MAX_COMPRESSION_RATIO) {
|
|
880
|
+
throw new MinecraftKitError(
|
|
881
|
+
"ARCHIVE_TOO_LARGE",
|
|
882
|
+
`Archive entry exceeds compression-ratio cap: ${name}`,
|
|
883
|
+
{ context: { filePath: this.filePath, entryName: name } }
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
const stream = await openStream(file, entry, this.filePath);
|
|
887
|
+
const chunks = [];
|
|
888
|
+
for await (const chunk of stream) {
|
|
889
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
890
|
+
}
|
|
891
|
+
return Buffer.concat(chunks);
|
|
892
|
+
},
|
|
893
|
+
openReadStream: () => openStream(file, entry, this.filePath)
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
function openStream(file, entry, archivePath) {
|
|
898
|
+
return new Promise((resolve, reject) => {
|
|
899
|
+
file.openReadStream(entry, (err, stream) => {
|
|
900
|
+
if (err || !stream) {
|
|
901
|
+
reject(
|
|
902
|
+
new MinecraftKitError(
|
|
903
|
+
"ARCHIVE_INVALID",
|
|
904
|
+
`Failed to open archive entry: ${entry.fileName}`,
|
|
905
|
+
{ cause: err, context: { filePath: archivePath, entryName: entry.fileName } }
|
|
906
|
+
)
|
|
907
|
+
);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
resolve(stream);
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
async function extractAllToDir(zipPath, targetDir, options = {}) {
|
|
915
|
+
const exclude = options.excludePrefixes ?? ["META-INF/"];
|
|
916
|
+
let fileCount = 0;
|
|
917
|
+
let totalSize = 0;
|
|
918
|
+
await ensureDir(targetDir);
|
|
919
|
+
const reader = await openZip(zipPath);
|
|
920
|
+
try {
|
|
921
|
+
for await (const entry of reader.entries()) {
|
|
922
|
+
if (entry.isDirectory) continue;
|
|
923
|
+
if (exclude.some((prefix) => entry.name.startsWith(prefix))) continue;
|
|
924
|
+
assertSafeEntryName(entry.name);
|
|
925
|
+
const destination = path__default.default.join(targetDir, entry.name);
|
|
926
|
+
assertWithinRoot(targetDir, entry.name);
|
|
927
|
+
totalSize += entry.uncompressedSize;
|
|
928
|
+
if (totalSize > EXTRACTION_MAX_TOTAL_SIZE) {
|
|
929
|
+
throw new MinecraftKitError(
|
|
930
|
+
"ARCHIVE_TOO_LARGE",
|
|
931
|
+
`Archive total size cap exceeded: ${zipPath}`,
|
|
932
|
+
{ context: { filePath: zipPath } }
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
if (entry.uncompressedSize > EXTRACTION_MAX_FILE_SIZE) {
|
|
936
|
+
throw new MinecraftKitError(
|
|
937
|
+
"ARCHIVE_TOO_LARGE",
|
|
938
|
+
`Archive entry exceeds size cap: ${entry.name}`,
|
|
939
|
+
{ context: { filePath: zipPath, entryName: entry.name } }
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
await ensureDir(path__default.default.dirname(destination));
|
|
943
|
+
const stream = await entry.openReadStream();
|
|
944
|
+
await promises.pipeline(stream, fs$1.createWriteStream(destination));
|
|
945
|
+
if (entry.name.endsWith(".so") || entry.name.endsWith(".dylib") || entry.name.endsWith(".jnilib")) {
|
|
946
|
+
await chmodExecutable(destination);
|
|
947
|
+
}
|
|
948
|
+
fileCount++;
|
|
949
|
+
}
|
|
950
|
+
} finally {
|
|
951
|
+
reader.close();
|
|
952
|
+
}
|
|
953
|
+
return { fileCount };
|
|
954
|
+
}
|
|
955
|
+
var RESERVED_NAME = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$/i;
|
|
956
|
+
function assertSafeEntryName(name) {
|
|
957
|
+
if (!name) {
|
|
958
|
+
throw rejectEntry(name, "empty entry name");
|
|
959
|
+
}
|
|
960
|
+
if (name.includes(String.fromCharCode(0))) {
|
|
961
|
+
throw rejectEntry(name, "null byte");
|
|
962
|
+
}
|
|
963
|
+
if (path__default.default.posix.isAbsolute(name) || /^[a-zA-Z]:/.test(name) || name.startsWith("\\")) {
|
|
964
|
+
throw rejectEntry(name, "absolute path");
|
|
965
|
+
}
|
|
966
|
+
const segments = name.split("/");
|
|
967
|
+
for (const segment of segments) {
|
|
968
|
+
if (segment === "..") {
|
|
969
|
+
throw rejectEntry(name, "parent traversal");
|
|
970
|
+
}
|
|
971
|
+
if (RESERVED_NAME.test(segment)) {
|
|
972
|
+
throw rejectEntry(name, "reserved Windows name");
|
|
973
|
+
}
|
|
974
|
+
if (/[\s.]$/.test(segment)) {
|
|
975
|
+
throw rejectEntry(name, "trailing dot or whitespace");
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function rejectEntry(name, reason) {
|
|
980
|
+
return new MinecraftKitError(
|
|
981
|
+
"ARCHIVE_ENTRY_REJECTED",
|
|
982
|
+
`Archive entry rejected (${reason}): ${name}`,
|
|
983
|
+
{ context: { entryName: name, reason } }
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
async function readEntryBuffer(zipPath, entryName) {
|
|
987
|
+
const reader = await openZip(zipPath);
|
|
988
|
+
try {
|
|
989
|
+
const entry = await reader.findEntry(entryName);
|
|
990
|
+
if (!entry) return void 0;
|
|
991
|
+
return await entry.readBuffer();
|
|
992
|
+
} finally {
|
|
993
|
+
reader.close();
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async function extractSingleEntry(zipPath, entryName, destination) {
|
|
997
|
+
const buffer = await readEntryBuffer(zipPath, entryName);
|
|
998
|
+
if (!buffer) {
|
|
999
|
+
throw new MinecraftKitError("ARCHIVE_INVALID", `Archive entry not found: ${entryName}`, {
|
|
1000
|
+
context: { filePath: zipPath, entryName }
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
await atomicWrite(destination, buffer);
|
|
1004
|
+
}
|
|
1005
|
+
var MANIFEST_LINE_CONTINUATION = /\r?\n[ \t]/g;
|
|
1006
|
+
var MANIFEST_MAIN_CLASS = /^Main-Class:\s*(.+)$/i;
|
|
1007
|
+
async function readJarMainClass(zipPath) {
|
|
1008
|
+
const buf = await readEntryBuffer(zipPath, "META-INF/MANIFEST.MF");
|
|
1009
|
+
if (!buf) return void 0;
|
|
1010
|
+
const text = buf.toString("utf8").replaceAll(MANIFEST_LINE_CONTINUATION, "");
|
|
1011
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1012
|
+
const match = MANIFEST_MAIN_CLASS.exec(line);
|
|
1013
|
+
if (match?.[1]) return match[1].trim();
|
|
1014
|
+
}
|
|
1015
|
+
return void 0;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
// src/core/collections.ts
|
|
1019
|
+
function dedupeBy(values, key) {
|
|
1020
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1021
|
+
const result = [];
|
|
1022
|
+
for (const value of values) {
|
|
1023
|
+
const k = key(value);
|
|
1024
|
+
if (seen.has(k)) continue;
|
|
1025
|
+
seen.add(k);
|
|
1026
|
+
result.push(value);
|
|
1027
|
+
}
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
function dedupe(values) {
|
|
1031
|
+
return dedupeBy(values, (v) => v);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// src/core/retry.ts
|
|
1035
|
+
function abortableSleep(ms, signal) {
|
|
1036
|
+
return new Promise((resolve, reject) => {
|
|
1037
|
+
if (signal?.aborted) {
|
|
1038
|
+
reject(toAbortError(signal));
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const timer = setTimeout(() => {
|
|
1042
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1043
|
+
resolve();
|
|
1044
|
+
}, ms);
|
|
1045
|
+
const onAbort = () => {
|
|
1046
|
+
clearTimeout(timer);
|
|
1047
|
+
reject(toAbortError(signal));
|
|
1048
|
+
};
|
|
1049
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
function toAbortError(signal) {
|
|
1053
|
+
return new MinecraftKitError("NETWORK_ABORTED", "Operation aborted", {
|
|
1054
|
+
context: { reason: signal?.reason }
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
async function withRetry(op, isRetryable, options = {}) {
|
|
1058
|
+
const max = options.maxAttempts ?? HTTP_RETRY_MAX;
|
|
1059
|
+
const base = options.baseMs ?? HTTP_RETRY_BACKOFF_BASE_MS;
|
|
1060
|
+
const cap = options.capMs ?? HTTP_RETRY_BACKOFF_CAP_MS;
|
|
1061
|
+
const sleep = options.sleep ?? abortableSleep;
|
|
1062
|
+
const random = options.random ?? Math.random;
|
|
1063
|
+
let lastError;
|
|
1064
|
+
for (let attempt = 0; attempt < max; attempt++) {
|
|
1065
|
+
if (options.signal?.aborted) {
|
|
1066
|
+
throw toAbortError(options.signal);
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
return await op(attempt);
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
lastError = error;
|
|
1072
|
+
options.onAttemptFailed?.(error, attempt);
|
|
1073
|
+
if (!isRetryable(error) || attempt === max - 1) {
|
|
1074
|
+
throw error;
|
|
1075
|
+
}
|
|
1076
|
+
const delayCap = Math.min(cap, base * 2 ** attempt);
|
|
1077
|
+
const delay = Math.floor(random() * delayCap);
|
|
1078
|
+
await sleep(delay, options.signal);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
throw lastError ?? new Error("withRetry exhausted attempts");
|
|
1082
|
+
}
|
|
1083
|
+
function isHttpRetryable(error) {
|
|
1084
|
+
if (!isMinecraftKitError(error)) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
if (error.code === "NETWORK_ABORTED") return false;
|
|
1088
|
+
if (error.code === "NETWORK_TIMEOUT") return true;
|
|
1089
|
+
if (error.code === "NETWORK_HTTP_ERROR") {
|
|
1090
|
+
const status = typeof error.context.httpStatus === "number" ? error.context.httpStatus : 0;
|
|
1091
|
+
if (status === 408 || status === 425 || status === 429) return true;
|
|
1092
|
+
if (status >= 500 && status < 600) return true;
|
|
1093
|
+
return status === 0;
|
|
1094
|
+
}
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/http/download.ts
|
|
1099
|
+
async function downloadFile(http, input) {
|
|
1100
|
+
const fileRef = { url: input.url, target: input.target, category: input.category };
|
|
1101
|
+
if (input.expectedSha1 !== void 0) {
|
|
1102
|
+
const existing = await checkExistingFile(input.target, input.expectedSha1, input.expectedSize);
|
|
1103
|
+
if (existing.matches) {
|
|
1104
|
+
input.onEvent?.({ type: "download:skipped", file: fileRef });
|
|
1105
|
+
return { bytesDownloaded: 0, sha1: existing.sha1, skipped: true };
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
await ensureDir(path__default.default.dirname(input.target));
|
|
1109
|
+
const tmp = `${input.target}.${crypto2__default.default.randomBytes(4).toString("hex")}.download`;
|
|
1110
|
+
return withRetry(
|
|
1111
|
+
async () => {
|
|
1112
|
+
input.onEvent?.({
|
|
1113
|
+
type: "download:started",
|
|
1114
|
+
file: fileRef,
|
|
1115
|
+
expectedSize: input.expectedSize ?? 0
|
|
1116
|
+
});
|
|
1117
|
+
const startedAt = Date.now();
|
|
1118
|
+
let bytesDownloaded = 0;
|
|
1119
|
+
const hash = crypto2__default.default.createHash("sha1");
|
|
1120
|
+
const response = await http.request(input.url, { signal: input.signal });
|
|
1121
|
+
const contentLength = Number(response.headers["content-length"] ?? "0");
|
|
1122
|
+
const total = input.expectedSize ?? (Number.isFinite(contentLength) ? contentLength : 0);
|
|
1123
|
+
const sourceIterable = response.stream();
|
|
1124
|
+
const counting = (async function* () {
|
|
1125
|
+
for await (const chunk of sourceIterable) {
|
|
1126
|
+
bytesDownloaded += chunk.byteLength;
|
|
1127
|
+
hash.update(chunk);
|
|
1128
|
+
input.onEvent?.({
|
|
1129
|
+
type: "download:progress",
|
|
1130
|
+
file: fileRef,
|
|
1131
|
+
bytesDownloaded,
|
|
1132
|
+
totalBytes: total
|
|
1133
|
+
});
|
|
1134
|
+
yield chunk;
|
|
1135
|
+
}
|
|
1136
|
+
})();
|
|
1137
|
+
try {
|
|
1138
|
+
await promises.pipeline(stream.Readable.from(counting), fs$1.createWriteStream(tmp));
|
|
1139
|
+
} catch (cause) {
|
|
1140
|
+
await safeUnlink(tmp);
|
|
1141
|
+
throw new MinecraftKitError(
|
|
1142
|
+
"FILESYSTEM_WRITE_ERROR",
|
|
1143
|
+
`Failed to write download: ${input.target}`,
|
|
1144
|
+
{ cause, context: { filePath: input.target, url: input.url } }
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
const computedSha1 = hash.digest("hex");
|
|
1148
|
+
if (input.expectedSize !== void 0 && bytesDownloaded !== input.expectedSize) {
|
|
1149
|
+
await safeUnlink(tmp);
|
|
1150
|
+
throw new MinecraftKitError("INTEGRITY_SIZE_MISMATCH", `Size mismatch for ${input.url}`, {
|
|
1151
|
+
context: {
|
|
1152
|
+
url: input.url,
|
|
1153
|
+
expectedSize: input.expectedSize,
|
|
1154
|
+
actualSize: bytesDownloaded
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
if (input.expectedSha1 !== void 0 && computedSha1 !== input.expectedSha1) {
|
|
1159
|
+
await safeUnlink(tmp);
|
|
1160
|
+
input.onEvent?.({
|
|
1161
|
+
type: "integrity:mismatch",
|
|
1162
|
+
file: fileRef,
|
|
1163
|
+
algorithm: "sha1",
|
|
1164
|
+
expected: input.expectedSha1,
|
|
1165
|
+
actual: computedSha1
|
|
1166
|
+
});
|
|
1167
|
+
throw new MinecraftKitError("INTEGRITY_HASH_MISMATCH", `SHA-1 mismatch for ${input.url}`, {
|
|
1168
|
+
context: {
|
|
1169
|
+
url: input.url,
|
|
1170
|
+
expectedHash: input.expectedSha1,
|
|
1171
|
+
actualHash: computedSha1
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
try {
|
|
1176
|
+
await fs__default.default.rename(tmp, input.target);
|
|
1177
|
+
} catch (cause) {
|
|
1178
|
+
await safeUnlink(tmp);
|
|
1179
|
+
throw new MinecraftKitError(
|
|
1180
|
+
"FILESYSTEM_WRITE_ERROR",
|
|
1181
|
+
`Failed to finalize download: ${input.target}`,
|
|
1182
|
+
{ cause, context: { filePath: input.target } }
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
input.onEvent?.({
|
|
1186
|
+
type: "download:completed",
|
|
1187
|
+
file: fileRef,
|
|
1188
|
+
durationMs: Date.now() - startedAt,
|
|
1189
|
+
bytes: bytesDownloaded
|
|
1190
|
+
});
|
|
1191
|
+
if (input.expectedSha1 !== void 0) {
|
|
1192
|
+
input.onEvent?.({
|
|
1193
|
+
type: "integrity:verified",
|
|
1194
|
+
file: fileRef,
|
|
1195
|
+
algorithm: "sha1",
|
|
1196
|
+
hash: computedSha1
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
return { bytesDownloaded, sha1: computedSha1, skipped: false };
|
|
1200
|
+
},
|
|
1201
|
+
isHttpRetryable,
|
|
1202
|
+
{
|
|
1203
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
1204
|
+
onAttemptFailed: (error, attempt) => {
|
|
1205
|
+
input.onEvent?.({
|
|
1206
|
+
type: "download:failed",
|
|
1207
|
+
file: fileRef,
|
|
1208
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1209
|
+
willRetry: isHttpRetryable(error) && attempt < HTTP_RETRY_MAX - 1
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
async function checkExistingFile(target, expectedSha1, expectedSize) {
|
|
1216
|
+
let stat;
|
|
1217
|
+
try {
|
|
1218
|
+
stat = await fs__default.default.stat(target);
|
|
1219
|
+
} catch {
|
|
1220
|
+
return { matches: false, sha1: "" };
|
|
1221
|
+
}
|
|
1222
|
+
if (!stat.isFile()) {
|
|
1223
|
+
return { matches: false, sha1: "" };
|
|
1224
|
+
}
|
|
1225
|
+
if (expectedSize !== void 0 && stat.size !== expectedSize) {
|
|
1226
|
+
return { matches: false, sha1: "" };
|
|
1227
|
+
}
|
|
1228
|
+
const buf = await fs__default.default.readFile(target);
|
|
1229
|
+
const sha1 = crypto2__default.default.createHash("sha1").update(buf).digest("hex");
|
|
1230
|
+
return { matches: sha1 === expectedSha1, sha1 };
|
|
1231
|
+
}
|
|
1232
|
+
async function safeUnlink(filePath) {
|
|
1233
|
+
try {
|
|
1234
|
+
await fs__default.default.unlink(filePath);
|
|
1235
|
+
} catch {
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// src/install/forge-install.ts
|
|
1240
|
+
async function planForgeInstall(input) {
|
|
1241
|
+
const installerPath = targetPaths.forgeInstaller(input.directory, input.loader.fullVersion);
|
|
1242
|
+
await downloadFile(input.http, {
|
|
1243
|
+
url: input.loader.installerUrl,
|
|
1244
|
+
target: installerPath,
|
|
1245
|
+
category: "forge-installer",
|
|
1246
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
1247
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
1248
|
+
});
|
|
1249
|
+
const installerDownload = {
|
|
1250
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
1251
|
+
url: input.loader.installerUrl,
|
|
1252
|
+
target: installerPath,
|
|
1253
|
+
category: "forge-installer"
|
|
1254
|
+
};
|
|
1255
|
+
const profile = await readJsonEntry(installerPath, "install_profile.json");
|
|
1256
|
+
const versionRelative = profile.json.startsWith("/") ? profile.json.slice(1) : profile.json;
|
|
1257
|
+
const version = await readJsonEntry(installerPath, versionRelative);
|
|
1258
|
+
await extractInstallerMavenEntries(installerPath, input.directory);
|
|
1259
|
+
const dataResolved = await resolveProfileData({
|
|
1260
|
+
profile,
|
|
1261
|
+
installerPath,
|
|
1262
|
+
directory: input.directory
|
|
1263
|
+
});
|
|
1264
|
+
const installerLibraries = planLibraryDownloads({
|
|
1265
|
+
libraries: profile.libraries,
|
|
1266
|
+
directory: input.directory,
|
|
1267
|
+
system: input.system,
|
|
1268
|
+
versionId: input.minecraft.version,
|
|
1269
|
+
category: "forge-library"
|
|
1270
|
+
});
|
|
1271
|
+
const versionLibraries = planLibraryDownloads({
|
|
1272
|
+
libraries: version.libraries,
|
|
1273
|
+
directory: input.directory,
|
|
1274
|
+
system: input.system,
|
|
1275
|
+
versionId: version.id,
|
|
1276
|
+
category: "forge-library"
|
|
1277
|
+
});
|
|
1278
|
+
const dedupedDownloads = dedupeBy(
|
|
1279
|
+
[...installerLibraries.downloads, ...versionLibraries.downloads],
|
|
1280
|
+
(action) => action.target
|
|
1281
|
+
);
|
|
1282
|
+
const classpathFiles = dedupe([
|
|
1283
|
+
...installerLibraries.classpathFiles,
|
|
1284
|
+
...versionLibraries.classpathFiles
|
|
1285
|
+
]);
|
|
1286
|
+
const processorActions = await buildProcessorActions({
|
|
1287
|
+
profile,
|
|
1288
|
+
minecraft: input.minecraft,
|
|
1289
|
+
installerPath,
|
|
1290
|
+
directory: input.directory,
|
|
1291
|
+
system: input.system,
|
|
1292
|
+
dataResolved
|
|
1293
|
+
});
|
|
1294
|
+
const versionJsonPath = targetPaths.versionJson(input.directory, version.id);
|
|
1295
|
+
const versionJson = {
|
|
1296
|
+
kind: InstallActionKinds.WRITE_VERSION_JSON,
|
|
1297
|
+
path: versionJsonPath,
|
|
1298
|
+
content: `${JSON.stringify(version, null, 2)}
|
|
1299
|
+
`
|
|
1300
|
+
};
|
|
1301
|
+
return {
|
|
1302
|
+
installerDownload,
|
|
1303
|
+
libraryDownloads: dedupedDownloads,
|
|
1304
|
+
classpathFiles,
|
|
1305
|
+
processorActions,
|
|
1306
|
+
versionJson,
|
|
1307
|
+
versionId: version.id,
|
|
1308
|
+
profile,
|
|
1309
|
+
version
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
async function readJsonEntry(zipPath, entryName) {
|
|
1313
|
+
const buffer = await readEntryBuffer(zipPath, entryName);
|
|
1314
|
+
if (!buffer) {
|
|
1315
|
+
throw new MinecraftKitError(
|
|
1316
|
+
"FORGE_INSTALLER_INVALID",
|
|
1317
|
+
`Forge installer is missing required entry: ${entryName}`,
|
|
1318
|
+
{ context: { filePath: zipPath, entryName } }
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
try {
|
|
1322
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
1323
|
+
} catch (cause) {
|
|
1324
|
+
throw new MinecraftKitError(
|
|
1325
|
+
"FORGE_INSTALLER_INVALID",
|
|
1326
|
+
`Forge installer entry is not valid JSON: ${entryName}`,
|
|
1327
|
+
{ cause, context: { filePath: zipPath, entryName } }
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
async function extractInstallerMavenEntries(installerPath, directory) {
|
|
1332
|
+
const reader = await openZip(installerPath);
|
|
1333
|
+
try {
|
|
1334
|
+
for await (const entry of reader.entries()) {
|
|
1335
|
+
if (!entry.name.startsWith("maven/") || entry.isDirectory) continue;
|
|
1336
|
+
const relativeWithinLibraries = entry.name.slice("maven/".length);
|
|
1337
|
+
const destination = path__default.default.join(targetPaths.librariesDir(directory), relativeWithinLibraries);
|
|
1338
|
+
const buffer = await entry.readBuffer();
|
|
1339
|
+
await atomicWrite(destination, buffer);
|
|
1340
|
+
}
|
|
1341
|
+
} finally {
|
|
1342
|
+
reader.close();
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
async function resolveProfileData(input) {
|
|
1346
|
+
const tokens = {};
|
|
1347
|
+
for (const [key, sided] of Object.entries(input.profile.data)) {
|
|
1348
|
+
const raw = sided.client;
|
|
1349
|
+
tokens[key] = await resolveDataValue(raw, input.installerPath, input.directory);
|
|
1350
|
+
}
|
|
1351
|
+
return { tokens };
|
|
1352
|
+
}
|
|
1353
|
+
async function resolveDataValue(raw, installerPath, directory) {
|
|
1354
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
1355
|
+
const coord = raw.slice(1, -1);
|
|
1356
|
+
const relativePath = mavenRelativePathFor(coord);
|
|
1357
|
+
return {
|
|
1358
|
+
value: path__default.default.join(targetPaths.librariesDir(directory), relativePath),
|
|
1359
|
+
isPath: true
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
if (raw.startsWith("'")) {
|
|
1363
|
+
return { value: raw.slice(1), isPath: false };
|
|
1364
|
+
}
|
|
1365
|
+
if (raw.startsWith("/")) {
|
|
1366
|
+
const entryName = raw.slice(1);
|
|
1367
|
+
const destination = path__default.default.join(targetPaths.librariesDir(directory), "forge-data", entryName);
|
|
1368
|
+
await extractSingleEntry(installerPath, entryName, destination);
|
|
1369
|
+
return { value: destination, isPath: true };
|
|
1370
|
+
}
|
|
1371
|
+
return { value: raw, isPath: false };
|
|
1372
|
+
}
|
|
1373
|
+
async function buildProcessorActions(input) {
|
|
1374
|
+
const builtIns = {
|
|
1375
|
+
SIDE: { value: "client", isPath: false },
|
|
1376
|
+
MINECRAFT_JAR: {
|
|
1377
|
+
value: targetPaths.versionJar(input.directory, input.minecraft.version),
|
|
1378
|
+
isPath: true
|
|
1379
|
+
},
|
|
1380
|
+
MINECRAFT_VERSION: { value: input.minecraft.version, isPath: false },
|
|
1381
|
+
ROOT: { value: input.directory, isPath: true },
|
|
1382
|
+
INSTALLER: { value: input.installerPath, isPath: true },
|
|
1383
|
+
LIBRARY_DIR: { value: targetPaths.librariesDir(input.directory), isPath: true }
|
|
1384
|
+
};
|
|
1385
|
+
const tokens = {
|
|
1386
|
+
...builtIns,
|
|
1387
|
+
...input.dataResolved.tokens
|
|
1388
|
+
};
|
|
1389
|
+
const actions = [];
|
|
1390
|
+
let index = 0;
|
|
1391
|
+
for (const processor of input.profile.processors) {
|
|
1392
|
+
if (!processorAppliesToClient(processor)) {
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
if (!evaluateRules([], { system: input.system })) ;
|
|
1396
|
+
const action = buildProcessorAction({
|
|
1397
|
+
processor,
|
|
1398
|
+
directory: input.directory,
|
|
1399
|
+
tokens,
|
|
1400
|
+
index
|
|
1401
|
+
});
|
|
1402
|
+
actions.push(action);
|
|
1403
|
+
index++;
|
|
1404
|
+
}
|
|
1405
|
+
return actions;
|
|
1406
|
+
}
|
|
1407
|
+
function processorAppliesToClient(processor) {
|
|
1408
|
+
if (!processor.sides || processor.sides.length === 0) return true;
|
|
1409
|
+
return processor.sides.includes("client");
|
|
1410
|
+
}
|
|
1411
|
+
function buildProcessorAction(input) {
|
|
1412
|
+
const jarPath = path__default.default.join(
|
|
1413
|
+
targetPaths.librariesDir(input.directory),
|
|
1414
|
+
mavenRelativePathFor(input.processor.jar)
|
|
1415
|
+
);
|
|
1416
|
+
const classpath = [
|
|
1417
|
+
jarPath,
|
|
1418
|
+
...input.processor.classpath.map(
|
|
1419
|
+
(coord) => path__default.default.join(targetPaths.librariesDir(input.directory), mavenRelativePathFor(coord))
|
|
1420
|
+
)
|
|
1421
|
+
];
|
|
1422
|
+
const args = input.processor.args.map((arg) => substituteToken(arg, input.tokens));
|
|
1423
|
+
const outputs = {};
|
|
1424
|
+
if (input.processor.outputs) {
|
|
1425
|
+
for (const [key, value] of Object.entries(input.processor.outputs)) {
|
|
1426
|
+
outputs[substituteToken(key, input.tokens)] = stripLiteralPrefix(
|
|
1427
|
+
substituteToken(value, input.tokens)
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return {
|
|
1432
|
+
kind: InstallActionKinds.RUN_FORGE_PROCESSOR,
|
|
1433
|
+
index: input.index,
|
|
1434
|
+
classpath,
|
|
1435
|
+
args,
|
|
1436
|
+
outputs
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
function substituteToken(raw, tokens) {
|
|
1440
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
1441
|
+
return path__default.default.join(...mavenRelativePathFor(raw.slice(1, -1)).split("/"));
|
|
1442
|
+
}
|
|
1443
|
+
return raw.replaceAll(/\{([A-Z0-9_]+)\}/g, (match, key) => {
|
|
1444
|
+
const token = tokens[key];
|
|
1445
|
+
if (token === void 0) {
|
|
1446
|
+
throw new MinecraftKitError("FORGE_INSTALLER_INVALID", `Unknown processor token: ${match}`, {
|
|
1447
|
+
context: { token: key }
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
return token.value;
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
function stripLiteralPrefix(value) {
|
|
1454
|
+
return value.startsWith("'") ? value.slice(1) : value;
|
|
1455
|
+
}
|
|
1456
|
+
async function planRuntimeDownloads(input) {
|
|
1457
|
+
const manifest = await fetchJson(input.http, input.cache, {
|
|
1458
|
+
url: input.runtime.manifestUrl,
|
|
1459
|
+
cacheKey: `runtime-manifest:${input.runtime.component}:${input.runtime.platformKey}:${input.runtime.manifestSha1}`,
|
|
1460
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1461
|
+
});
|
|
1462
|
+
const actions = [];
|
|
1463
|
+
const runtimeRoot = targetPaths.runtimeRoot(
|
|
1464
|
+
input.directory,
|
|
1465
|
+
input.runtime.component,
|
|
1466
|
+
input.runtime.installRoot
|
|
1467
|
+
);
|
|
1468
|
+
for (const [relativePath, entry] of Object.entries(manifest.files)) {
|
|
1469
|
+
if (entry.type !== "file") continue;
|
|
1470
|
+
const target = path__default.default.join(runtimeRoot, relativePath);
|
|
1471
|
+
actions.push({
|
|
1472
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
1473
|
+
url: entry.downloads.raw.url,
|
|
1474
|
+
target,
|
|
1475
|
+
expectedSha1: entry.downloads.raw.sha1,
|
|
1476
|
+
expectedSize: entry.downloads.raw.size,
|
|
1477
|
+
category: "runtime-file"
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
return { actions, manifest };
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// src/install/planner.ts
|
|
1484
|
+
async function planInstall(input) {
|
|
1485
|
+
const { target } = input;
|
|
1486
|
+
const actions = [];
|
|
1487
|
+
actions.push({
|
|
1488
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
1489
|
+
url: target.minecraft.manifest.downloads.client.url,
|
|
1490
|
+
target: targetPaths.versionJar(target.directory, target.minecraft.version),
|
|
1491
|
+
expectedSha1: target.minecraft.manifest.downloads.client.sha1,
|
|
1492
|
+
expectedSize: target.minecraft.manifest.downloads.client.size,
|
|
1493
|
+
category: "client-jar"
|
|
1494
|
+
});
|
|
1495
|
+
actions.push({
|
|
1496
|
+
kind: InstallActionKinds.WRITE_VERSION_JSON,
|
|
1497
|
+
path: targetPaths.versionJson(target.directory, target.minecraft.version),
|
|
1498
|
+
content: `${JSON.stringify(target.minecraft.manifest, null, 2)}
|
|
1499
|
+
`
|
|
1500
|
+
});
|
|
1501
|
+
const vanillaLibraries = planLibraryDownloads({
|
|
1502
|
+
libraries: target.minecraft.manifest.libraries,
|
|
1503
|
+
directory: target.directory,
|
|
1504
|
+
system: target.runtime.system,
|
|
1505
|
+
versionId: target.minecraft.version,
|
|
1506
|
+
category: "library"
|
|
1507
|
+
});
|
|
1508
|
+
actions.push(...vanillaLibraries.downloads);
|
|
1509
|
+
actions.push(...vanillaLibraries.nativeExtractions);
|
|
1510
|
+
const assetPlan = await planAssetDownloads({
|
|
1511
|
+
directory: target.directory,
|
|
1512
|
+
assetIndex: target.minecraft.manifest.assetIndex,
|
|
1513
|
+
http: input.http,
|
|
1514
|
+
cache: input.cache,
|
|
1515
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1516
|
+
});
|
|
1517
|
+
actions.push(...assetPlan.actions);
|
|
1518
|
+
if (target.minecraft.manifest.logging?.client) {
|
|
1519
|
+
const logging = target.minecraft.manifest.logging.client;
|
|
1520
|
+
actions.push({
|
|
1521
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
1522
|
+
url: logging.file.url,
|
|
1523
|
+
target: targetPaths.loggingConfig(target.directory, logging.file.id),
|
|
1524
|
+
expectedSha1: logging.file.sha1,
|
|
1525
|
+
expectedSize: logging.file.size,
|
|
1526
|
+
category: "logging-config"
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
const runtimePlan = await planRuntimeDownloads({
|
|
1530
|
+
runtime: target.runtime,
|
|
1531
|
+
directory: target.directory,
|
|
1532
|
+
http: input.http,
|
|
1533
|
+
cache: input.cache,
|
|
1534
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1535
|
+
});
|
|
1536
|
+
actions.push(...runtimePlan.actions);
|
|
1537
|
+
if (target.loader.type === Loaders.FABRIC) {
|
|
1538
|
+
const fabricPlan = planFabricInstall({
|
|
1539
|
+
loader: target.loader,
|
|
1540
|
+
minecraft: target.minecraft,
|
|
1541
|
+
directory: target.directory,
|
|
1542
|
+
system: target.runtime.system
|
|
1543
|
+
});
|
|
1544
|
+
actions.push(fabricPlan.versionJson);
|
|
1545
|
+
actions.push(...fabricPlan.libraryDownloads);
|
|
1546
|
+
} else if (target.loader.type === Loaders.FORGE) {
|
|
1547
|
+
const forgePlan = await planForgeInstall({
|
|
1548
|
+
loader: target.loader,
|
|
1549
|
+
minecraft: target.minecraft,
|
|
1550
|
+
directory: target.directory,
|
|
1551
|
+
system: target.runtime.system,
|
|
1552
|
+
http: input.http,
|
|
1553
|
+
cache: input.cache,
|
|
1554
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
1555
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
1556
|
+
});
|
|
1557
|
+
actions.push(forgePlan.installerDownload);
|
|
1558
|
+
actions.push(...forgePlan.libraryDownloads);
|
|
1559
|
+
actions.push(forgePlan.versionJson);
|
|
1560
|
+
actions.push(...forgePlan.processorActions);
|
|
1561
|
+
}
|
|
1562
|
+
const totalBytes = actions.reduce((sum, action) => {
|
|
1563
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
1564
|
+
return sum + (action.expectedSize ?? 0);
|
|
1565
|
+
}
|
|
1566
|
+
return sum;
|
|
1567
|
+
}, 0);
|
|
1568
|
+
return {
|
|
1569
|
+
targetId: target.id,
|
|
1570
|
+
directory: target.directory,
|
|
1571
|
+
target,
|
|
1572
|
+
actions,
|
|
1573
|
+
totalActions: actions.length,
|
|
1574
|
+
totalBytes
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
async function materializeRuntimeExtras(input) {
|
|
1578
|
+
const root = targetPaths.runtimeRoot(
|
|
1579
|
+
input.directory,
|
|
1580
|
+
input.runtime.component,
|
|
1581
|
+
input.runtime.installRoot
|
|
1582
|
+
);
|
|
1583
|
+
for (const [relativePath, entry] of Object.entries(input.manifest.files)) {
|
|
1584
|
+
const fullPath = path__default.default.join(root, relativePath);
|
|
1585
|
+
if (entry.type === "directory") {
|
|
1586
|
+
await ensureDir(fullPath);
|
|
1587
|
+
} else if (entry.type === "link") {
|
|
1588
|
+
await ensureDir(path__default.default.dirname(fullPath));
|
|
1589
|
+
await unlinkIfPresent(fullPath);
|
|
1590
|
+
await createLinkOrCopy(root, relativePath, entry.target, fullPath);
|
|
1591
|
+
} else if (entry.executable && process.platform !== "win32") {
|
|
1592
|
+
await fs__default.default.chmod(fullPath, 493).catch(() => {
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
async function unlinkIfPresent(target) {
|
|
1598
|
+
try {
|
|
1599
|
+
await fs__default.default.unlink(target);
|
|
1600
|
+
} catch (cause) {
|
|
1601
|
+
if (isNotFound(cause)) return;
|
|
1602
|
+
throw new MinecraftKitError(
|
|
1603
|
+
"FILESYSTEM_WRITE_ERROR",
|
|
1604
|
+
`Failed to remove stale runtime entry: ${target}`,
|
|
1605
|
+
{ cause, context: { filePath: target } }
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
async function createLinkOrCopy(root, relativePath, linkTarget, destination) {
|
|
1610
|
+
try {
|
|
1611
|
+
await fs__default.default.symlink(linkTarget, destination);
|
|
1612
|
+
return;
|
|
1613
|
+
} catch (symlinkError) {
|
|
1614
|
+
const absoluteSource = path__default.default.resolve(path__default.default.dirname(path__default.default.join(root, relativePath)), linkTarget);
|
|
1615
|
+
try {
|
|
1616
|
+
await fs__default.default.copyFile(absoluteSource, destination);
|
|
1617
|
+
} catch (copyError) {
|
|
1618
|
+
throw new MinecraftKitError(
|
|
1619
|
+
"FILESYSTEM_WRITE_ERROR",
|
|
1620
|
+
`Failed to materialize runtime entry: ${destination}`,
|
|
1621
|
+
{
|
|
1622
|
+
cause: copyError,
|
|
1623
|
+
context: {
|
|
1624
|
+
filePath: destination,
|
|
1625
|
+
linkTarget,
|
|
1626
|
+
symlinkError: errorMessage(symlinkError)
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
function isNotFound(error) {
|
|
1634
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1635
|
+
}
|
|
1636
|
+
function errorMessage(error) {
|
|
1637
|
+
return error instanceof Error ? error.message : String(error);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// src/install/runner.ts
|
|
1641
|
+
async function runInstall(input) {
|
|
1642
|
+
const startedAt = Date.now();
|
|
1643
|
+
let bytesDownloaded = 0;
|
|
1644
|
+
let actionsCompleted = 0;
|
|
1645
|
+
let actionsSkipped = 0;
|
|
1646
|
+
const onEvent = input.onEvent;
|
|
1647
|
+
let currentPhase = null;
|
|
1648
|
+
const enterPhase = (phase) => {
|
|
1649
|
+
if (phase === currentPhase) return;
|
|
1650
|
+
onEvent?.({ type: "install:phase-changed", phase, previous: currentPhase });
|
|
1651
|
+
currentPhase = phase;
|
|
1652
|
+
};
|
|
1653
|
+
const downloads = input.plan.actions.filter(isDownload);
|
|
1654
|
+
const natives = input.plan.actions.filter(isNative);
|
|
1655
|
+
const writeActions = input.plan.actions.filter(isWrite);
|
|
1656
|
+
const processors = input.plan.actions.filter(isProcessor);
|
|
1657
|
+
enterPhase(InstallPhases.PLANNING);
|
|
1658
|
+
enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
|
|
1659
|
+
const limit = pLimit__default.default(input.concurrency ?? DOWNLOAD_CONCURRENCY);
|
|
1660
|
+
await Promise.all(
|
|
1661
|
+
downloads.map(
|
|
1662
|
+
(action) => limit(async () => {
|
|
1663
|
+
if (input.signal?.aborted) {
|
|
1664
|
+
throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
|
|
1665
|
+
}
|
|
1666
|
+
const result = await downloadFile(input.http, {
|
|
1667
|
+
url: action.url,
|
|
1668
|
+
target: action.target,
|
|
1669
|
+
...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
|
|
1670
|
+
...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
|
|
1671
|
+
...action.category !== void 0 ? { category: action.category } : {},
|
|
1672
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
1673
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
1674
|
+
});
|
|
1675
|
+
bytesDownloaded += result.bytesDownloaded;
|
|
1676
|
+
if (result.skipped) actionsSkipped++;
|
|
1677
|
+
actionsCompleted++;
|
|
1678
|
+
})
|
|
1679
|
+
)
|
|
1680
|
+
);
|
|
1681
|
+
if (writeActions.length > 0) {
|
|
1682
|
+
enterPhase(InstallPhases.WRITING_FILES);
|
|
1683
|
+
for (const action of writeActions) {
|
|
1684
|
+
if (input.signal?.aborted) {
|
|
1685
|
+
throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
|
|
1686
|
+
}
|
|
1687
|
+
await atomicWrite(action.path, action.content);
|
|
1688
|
+
actionsCompleted++;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
if (natives.length > 0) {
|
|
1692
|
+
enterPhase(InstallPhases.EXTRACTING_NATIVES);
|
|
1693
|
+
for (const action of natives) {
|
|
1694
|
+
if (input.signal?.aborted) {
|
|
1695
|
+
throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
|
|
1696
|
+
}
|
|
1697
|
+
const { fileCount } = await extractAllToDir(action.source, action.destination, {
|
|
1698
|
+
excludePrefixes: action.exclude
|
|
1699
|
+
});
|
|
1700
|
+
input.onEvent?.({
|
|
1701
|
+
type: "archive:extracted",
|
|
1702
|
+
archive: action.source,
|
|
1703
|
+
target: action.destination,
|
|
1704
|
+
fileCount
|
|
1705
|
+
});
|
|
1706
|
+
actionsCompleted++;
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
if (input.plan.target.runtime !== void 0) {
|
|
1710
|
+
enterPhase(InstallPhases.INSTALLING_RUNTIME);
|
|
1711
|
+
const runtimePlan = await planRuntimeDownloads({
|
|
1712
|
+
runtime: input.plan.target.runtime,
|
|
1713
|
+
directory: input.plan.directory,
|
|
1714
|
+
http: input.http,
|
|
1715
|
+
cache: input.cache,
|
|
1716
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1717
|
+
});
|
|
1718
|
+
await materializeRuntimeExtras({
|
|
1719
|
+
runtime: input.plan.target.runtime,
|
|
1720
|
+
directory: input.plan.directory,
|
|
1721
|
+
manifest: runtimePlan.manifest
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
if (processors.length > 0) {
|
|
1725
|
+
enterPhase(InstallPhases.RUNNING_FORGE_PROCESSORS);
|
|
1726
|
+
if (input.plan.target.loader.type !== Loaders.FORGE) {
|
|
1727
|
+
throw new MinecraftKitError(
|
|
1728
|
+
"FORGE_PROCESSOR_FAILED",
|
|
1729
|
+
"Forge processors planned for a non-Forge target"
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
const javaPath = targetPaths.runtimeJavaExecutable(
|
|
1733
|
+
input.plan.directory,
|
|
1734
|
+
input.plan.target.runtime.component,
|
|
1735
|
+
input.plan.target.runtime.system.os,
|
|
1736
|
+
input.plan.target.runtime.installRoot
|
|
1737
|
+
);
|
|
1738
|
+
for (const action of processors) {
|
|
1739
|
+
if (input.signal?.aborted) {
|
|
1740
|
+
throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
|
|
1741
|
+
}
|
|
1742
|
+
await runProcessor({
|
|
1743
|
+
action,
|
|
1744
|
+
javaPath,
|
|
1745
|
+
spawner: input.spawner,
|
|
1746
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
|
|
1747
|
+
total: processors.length
|
|
1748
|
+
});
|
|
1749
|
+
actionsCompleted++;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
enterPhase(InstallPhases.COMPLETED);
|
|
1753
|
+
return {
|
|
1754
|
+
targetId: input.plan.targetId,
|
|
1755
|
+
bytesDownloaded,
|
|
1756
|
+
actionsCompleted,
|
|
1757
|
+
actionsSkipped,
|
|
1758
|
+
durationMs: Date.now() - startedAt
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function isDownload(action) {
|
|
1762
|
+
return action.kind === InstallActionKinds.DOWNLOAD_FILE;
|
|
1763
|
+
}
|
|
1764
|
+
function isNative(action) {
|
|
1765
|
+
return action.kind === InstallActionKinds.EXTRACT_NATIVE;
|
|
1766
|
+
}
|
|
1767
|
+
function isProcessor(action) {
|
|
1768
|
+
return action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR;
|
|
1769
|
+
}
|
|
1770
|
+
function isWrite(action) {
|
|
1771
|
+
return action.kind === InstallActionKinds.WRITE_VERSION_JSON || action.kind === InstallActionKinds.WRITE_LOGGING_CONFIG;
|
|
1772
|
+
}
|
|
1773
|
+
async function runProcessor(input) {
|
|
1774
|
+
const startedAt = Date.now();
|
|
1775
|
+
const processorJar = input.action.classpath[0];
|
|
1776
|
+
if (processorJar === void 0) {
|
|
1777
|
+
throw new MinecraftKitError(
|
|
1778
|
+
"FORGE_INSTALLER_INVALID",
|
|
1779
|
+
"Forge processor has an empty classpath",
|
|
1780
|
+
{ context: { processorIndex: input.action.index } }
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
const mainClass = await readJarMainClass(processorJar);
|
|
1784
|
+
if (!mainClass) {
|
|
1785
|
+
throw new MinecraftKitError(
|
|
1786
|
+
"FORGE_INSTALLER_INVALID",
|
|
1787
|
+
`Forge processor jar has no Main-Class: ${processorJar}`,
|
|
1788
|
+
{ context: { filePath: processorJar } }
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
const classpathSeparator = process.platform === "win32" ? ";" : ":";
|
|
1792
|
+
const args = [
|
|
1793
|
+
"-cp",
|
|
1794
|
+
input.action.classpath.join(classpathSeparator),
|
|
1795
|
+
mainClass,
|
|
1796
|
+
...input.action.args
|
|
1797
|
+
];
|
|
1798
|
+
input.onEvent?.({
|
|
1799
|
+
type: "forge:processor-started",
|
|
1800
|
+
processor: { index: input.action.index, mainClass },
|
|
1801
|
+
total: input.total
|
|
1802
|
+
});
|
|
1803
|
+
const stderrTail = [];
|
|
1804
|
+
const child = input.spawner.spawn(input.javaPath, args, { cwd: process.cwd() });
|
|
1805
|
+
child.stdout.on("data", () => {
|
|
1806
|
+
});
|
|
1807
|
+
child.stderr.on("data", (line) => {
|
|
1808
|
+
if (stderrTail.length >= MAX_PROCESSOR_STDERR_LINES) stderrTail.shift();
|
|
1809
|
+
stderrTail.push(line);
|
|
1810
|
+
});
|
|
1811
|
+
const exit = await child.exited;
|
|
1812
|
+
if (exit.code !== 0) {
|
|
1813
|
+
throw new MinecraftKitError(
|
|
1814
|
+
"FORGE_PROCESSOR_FAILED",
|
|
1815
|
+
`Forge processor exited with code ${exit.code ?? "(signal)"}: ${mainClass}`,
|
|
1816
|
+
{
|
|
1817
|
+
context: {
|
|
1818
|
+
exitCode: exit.code ?? void 0,
|
|
1819
|
+
mainClass,
|
|
1820
|
+
stderr: stderrTail.join("\n")
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
input.onEvent?.({
|
|
1826
|
+
type: "forge:processor-completed",
|
|
1827
|
+
processor: { index: input.action.index, mainClass },
|
|
1828
|
+
exitCode: exit.code ?? 0,
|
|
1829
|
+
durationMs: Date.now() - startedAt
|
|
1830
|
+
});
|
|
1831
|
+
for (const [outputPath, expectedSha1] of Object.entries(input.action.outputs)) {
|
|
1832
|
+
const sha1 = await sha1OfFileStreaming(outputPath);
|
|
1833
|
+
if (sha1 !== expectedSha1) {
|
|
1834
|
+
throw new MinecraftKitError(
|
|
1835
|
+
"FORGE_PROCESSOR_FAILED",
|
|
1836
|
+
`Processor output hash mismatch: ${outputPath}`,
|
|
1837
|
+
{ context: { filePath: outputPath, expectedHash: expectedSha1, actualHash: sha1 } }
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
input.onEvent?.({
|
|
1841
|
+
type: "forge:processor-output-verified",
|
|
1842
|
+
processor: { index: input.action.index, mainClass },
|
|
1843
|
+
path: outputPath
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
async function sha1OfFileStreaming(filePath) {
|
|
1848
|
+
const hash = crypto2__default.default.createHash("sha1");
|
|
1849
|
+
await new Promise((resolve, reject) => {
|
|
1850
|
+
const stream = fs$1.createReadStream(filePath);
|
|
1851
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1852
|
+
stream.on("end", () => resolve());
|
|
1853
|
+
stream.on("error", reject);
|
|
1854
|
+
});
|
|
1855
|
+
return hash.digest("hex");
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// src/install/runtime-install.ts
|
|
1859
|
+
async function planRuntimeInstall(input) {
|
|
1860
|
+
const runtimePlan = await planRuntimeDownloads({
|
|
1861
|
+
runtime: input.target.runtime,
|
|
1862
|
+
directory: input.target.directory,
|
|
1863
|
+
http: input.http,
|
|
1864
|
+
cache: input.cache,
|
|
1865
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1866
|
+
});
|
|
1867
|
+
const actions = runtimePlan.actions;
|
|
1868
|
+
const totalBytes = runtimePlan.actions.reduce(
|
|
1869
|
+
(sum, action) => sum + (action.expectedSize ?? 0),
|
|
1870
|
+
0
|
|
1871
|
+
);
|
|
1872
|
+
return {
|
|
1873
|
+
targetId: input.target.id,
|
|
1874
|
+
directory: input.target.directory,
|
|
1875
|
+
target: input.target,
|
|
1876
|
+
actions,
|
|
1877
|
+
totalActions: actions.length,
|
|
1878
|
+
totalBytes
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
async function planStandaloneRuntimeInstall(input) {
|
|
1882
|
+
const runtimePlan = await planRuntimeDownloads({
|
|
1883
|
+
runtime: input.runtime,
|
|
1884
|
+
directory: input.directory,
|
|
1885
|
+
http: input.http,
|
|
1886
|
+
cache: input.cache,
|
|
1887
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1888
|
+
});
|
|
1889
|
+
const actions = runtimePlan.actions;
|
|
1890
|
+
const totalBytes = runtimePlan.actions.reduce(
|
|
1891
|
+
(sum, action) => sum + (action.expectedSize ?? 0),
|
|
1892
|
+
0
|
|
1893
|
+
);
|
|
1894
|
+
const target = {
|
|
1895
|
+
id: input.id,
|
|
1896
|
+
directory: input.directory,
|
|
1897
|
+
runtime: input.runtime,
|
|
1898
|
+
minecraft: void 0,
|
|
1899
|
+
loader: void 0
|
|
1900
|
+
};
|
|
1901
|
+
return {
|
|
1902
|
+
targetId: input.id,
|
|
1903
|
+
directory: input.directory,
|
|
1904
|
+
target,
|
|
1905
|
+
actions,
|
|
1906
|
+
totalActions: actions.length,
|
|
1907
|
+
totalBytes
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// src/constants/launch.ts
|
|
1912
|
+
var BASE_JVM_ARGS = [
|
|
1913
|
+
"-XX:+UnlockExperimentalVMOptions",
|
|
1914
|
+
"-XX:+UseG1GC",
|
|
1915
|
+
"-XX:G1NewSizePercent=20",
|
|
1916
|
+
"-XX:G1ReservePercent=20",
|
|
1917
|
+
"-XX:MaxGCPauseMillis=50",
|
|
1918
|
+
"-XX:G1HeapRegionSize=32M"
|
|
1919
|
+
];
|
|
1920
|
+
var LEGACY_JVM_ARGS = [
|
|
1921
|
+
"-Djava.library.path=${natives_directory}",
|
|
1922
|
+
"-Dminecraft.launcher.brand=${launcher_name}",
|
|
1923
|
+
"-Dminecraft.launcher.version=${launcher_version}",
|
|
1924
|
+
"-cp",
|
|
1925
|
+
"${classpath}"
|
|
1926
|
+
];
|
|
1927
|
+
var MACOS_JVM_ARGS = ["-Xdock:name=Minecraft"];
|
|
1928
|
+
|
|
1929
|
+
// src/launch/arguments.ts
|
|
1930
|
+
function flattenArguments(entries, context) {
|
|
1931
|
+
const result = [];
|
|
1932
|
+
for (const entry of entries) {
|
|
1933
|
+
if (typeof entry === "string") {
|
|
1934
|
+
result.push(entry);
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
if (!evaluateRules(entry.rules, context)) continue;
|
|
1938
|
+
if (typeof entry.value === "string") {
|
|
1939
|
+
result.push(entry.value);
|
|
1940
|
+
} else {
|
|
1941
|
+
result.push(...entry.value);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
return result;
|
|
1945
|
+
}
|
|
1946
|
+
function splitLegacyArguments(raw) {
|
|
1947
|
+
return raw.trim().length === 0 ? [] : raw.trim().split(/\s+/);
|
|
1948
|
+
}
|
|
1949
|
+
function pickArguments(args, context) {
|
|
1950
|
+
return {
|
|
1951
|
+
game: flattenArguments(args?.game ?? [], context),
|
|
1952
|
+
jvm: flattenArguments(args?.jvm ?? [], context)
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// src/launch/placeholders.ts
|
|
1957
|
+
function substituteArg(raw, values) {
|
|
1958
|
+
return raw.replaceAll(/\$\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
|
|
1959
|
+
const value = values[key];
|
|
1960
|
+
if (value === void 0) {
|
|
1961
|
+
throw new MinecraftKitError("INVALID_INPUT", `Unknown launch placeholder: ${match}`, {
|
|
1962
|
+
context: { placeholder: key }
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
return value;
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
function substituteArgs(args, values) {
|
|
1969
|
+
return args.map((arg) => substituteArg(arg, values));
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// src/launch/args-composition.ts
|
|
1973
|
+
function composeArgs(input) {
|
|
1974
|
+
const minMb = input.options.memory?.minMb ?? DEFAULT_MIN_MB;
|
|
1975
|
+
const maxMb = input.options.memory?.maxMb ?? DEFAULT_MAX_MB;
|
|
1976
|
+
const memoryArgs = [`-Xms${minMb}M`, `-Xmx${maxMb}M`];
|
|
1977
|
+
const ruleContext = { system: input.target.runtime.system, features: input.features };
|
|
1978
|
+
let rawJvm;
|
|
1979
|
+
let rawGame;
|
|
1980
|
+
if (input.merged.arguments) {
|
|
1981
|
+
const picked = pickArguments(input.merged.arguments, ruleContext);
|
|
1982
|
+
rawJvm = picked.jvm;
|
|
1983
|
+
rawGame = picked.game;
|
|
1984
|
+
} else if (input.merged.minecraftArguments) {
|
|
1985
|
+
rawJvm = LEGACY_JVM_ARGS;
|
|
1986
|
+
rawGame = splitLegacyArguments(input.merged.minecraftArguments);
|
|
1987
|
+
} else {
|
|
1988
|
+
rawJvm = [];
|
|
1989
|
+
rawGame = [];
|
|
1990
|
+
}
|
|
1991
|
+
const macosArgs = input.target.runtime.system.os === "osx" ? MACOS_JVM_ARGS : [];
|
|
1992
|
+
const baseJvm = [...memoryArgs, ...BASE_JVM_ARGS, ...macosArgs];
|
|
1993
|
+
const substitutedJvm = substituteArgs(rawJvm, input.placeholderValues);
|
|
1994
|
+
const substitutedGame = substituteArgs(rawGame, input.placeholderValues);
|
|
1995
|
+
const jvmArgs = [...baseJvm, ...substitutedJvm];
|
|
1996
|
+
if (input.merged.logging?.client?.argument) {
|
|
1997
|
+
const logging = input.merged.logging.client;
|
|
1998
|
+
const loggingArg = substituteArgs([logging.argument], {
|
|
1999
|
+
...input.placeholderValues,
|
|
2000
|
+
path: targetPaths.loggingConfig(input.target.directory, logging.file.id)
|
|
2001
|
+
})[0];
|
|
2002
|
+
if (loggingArg !== void 0) jvmArgs.push(loggingArg);
|
|
2003
|
+
}
|
|
2004
|
+
const extraJvm = input.options.extraJvmArgs ?? [];
|
|
2005
|
+
const extraGame = input.options.extraGameArgs ?? [];
|
|
2006
|
+
const gameArgs = [...substitutedGame, ...extraGame];
|
|
2007
|
+
if (input.options.fullscreen === true) gameArgs.push("--fullscreen");
|
|
2008
|
+
if (input.options.resolution !== void 0 && rawGame.every((a) => !a.includes("--width"))) {
|
|
2009
|
+
gameArgs.push(
|
|
2010
|
+
"--width",
|
|
2011
|
+
input.options.resolution.width.toString(),
|
|
2012
|
+
"--height",
|
|
2013
|
+
input.options.resolution.height.toString()
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
return { jvmArgs: [...jvmArgs, ...extraJvm], gameArgs };
|
|
2017
|
+
}
|
|
2018
|
+
function buildClasspath(input) {
|
|
2019
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2020
|
+
const entries = [];
|
|
2021
|
+
for (const library of input.merged.libraries) {
|
|
2022
|
+
if (library.natives) continue;
|
|
2023
|
+
if (!evaluateRules(library.rules, { system: input.system })) continue;
|
|
2024
|
+
const relative = relativeFor(library);
|
|
2025
|
+
if (!relative) continue;
|
|
2026
|
+
const absolute = path__default.default.join(targetPaths.librariesDir(input.directory), relative);
|
|
2027
|
+
if (seen.has(absolute)) continue;
|
|
2028
|
+
seen.add(absolute);
|
|
2029
|
+
entries.push(absolute);
|
|
2030
|
+
}
|
|
2031
|
+
const versionJar = targetPaths.versionJar(input.directory, input.versionId);
|
|
2032
|
+
if (!seen.has(versionJar)) entries.push(versionJar);
|
|
2033
|
+
return entries;
|
|
2034
|
+
}
|
|
2035
|
+
function relativeFor(library) {
|
|
2036
|
+
if (library.downloads?.artifact?.path) return library.downloads.artifact.path;
|
|
2037
|
+
if (library.name) {
|
|
2038
|
+
const coord = parseMavenCoordinate(library.name);
|
|
2039
|
+
return mavenRelativePath(coord);
|
|
2040
|
+
}
|
|
2041
|
+
return null;
|
|
2042
|
+
}
|
|
2043
|
+
function offlineUuidFor(username) {
|
|
2044
|
+
const md5 = crypto2__default.default.createHash("md5");
|
|
2045
|
+
md5.update(`OfflinePlayer:${username}`, "utf8");
|
|
2046
|
+
const bytes = md5.digest();
|
|
2047
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 48;
|
|
2048
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
2049
|
+
return formatUuid(bytes);
|
|
2050
|
+
}
|
|
2051
|
+
function formatUuid(bytes) {
|
|
2052
|
+
const hex = bytes.toString("hex");
|
|
2053
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
2054
|
+
}
|
|
2055
|
+
function stripUuidDashes(uuid) {
|
|
2056
|
+
return uuid.replaceAll("-", "");
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/types/auth.ts
|
|
2060
|
+
var AuthModes = {
|
|
2061
|
+
/** Offline-mode play with a chosen username and synthetic UUID. */
|
|
2062
|
+
OFFLINE: "offline",
|
|
2063
|
+
/** Pre-authenticated session — caller provides the access token and identity. */
|
|
2064
|
+
ONLINE: "online"
|
|
2065
|
+
};
|
|
2066
|
+
|
|
2067
|
+
// src/launch/placeholder-values.ts
|
|
2068
|
+
function buildPlaceholderValues(input) {
|
|
2069
|
+
const cpSeparator = process.platform === "win32" ? ";" : ":";
|
|
2070
|
+
const directory = input.target.directory;
|
|
2071
|
+
const username = input.auth.username;
|
|
2072
|
+
const uuid = input.auth.mode === AuthModes.OFFLINE ? input.auth.uuid ?? offlineUuidFor(username) : input.auth.uuid;
|
|
2073
|
+
const accessToken = input.auth.mode === AuthModes.OFFLINE ? "0" : input.auth.accessToken;
|
|
2074
|
+
const userType = input.auth.mode === AuthModes.OFFLINE ? "legacy" : input.auth.userType ?? "msa";
|
|
2075
|
+
const launcherName = input.options.launcherName ?? DEFAULT_LAUNCHER_NAME;
|
|
2076
|
+
const launcherVersion = input.options.launcherVersion ?? DEFAULT_LAUNCHER_VERSION;
|
|
2077
|
+
return {
|
|
2078
|
+
auth_player_name: username,
|
|
2079
|
+
version_name: input.versionId,
|
|
2080
|
+
game_directory: directory,
|
|
2081
|
+
assets_root: path__default.default.join(directory, ASSETS_DIR),
|
|
2082
|
+
assets_index_name: input.target.minecraft.manifest.assets,
|
|
2083
|
+
auth_uuid: stripUuidDashes(uuid),
|
|
2084
|
+
auth_access_token: accessToken,
|
|
2085
|
+
auth_session: `token:${accessToken}:${stripUuidDashes(uuid)}`,
|
|
2086
|
+
clientid: input.auth.mode === AuthModes.ONLINE ? input.auth.clientId ?? "" : "",
|
|
2087
|
+
auth_xuid: input.auth.mode === AuthModes.ONLINE ? input.auth.xuid ?? "" : "",
|
|
2088
|
+
user_type: userType,
|
|
2089
|
+
user_properties: "{}",
|
|
2090
|
+
version_type: input.target.minecraft.channel,
|
|
2091
|
+
game_assets: path__default.default.join(directory, ASSETS_LEGACY_DIR),
|
|
2092
|
+
natives_directory: targetPaths.nativesDir(directory, input.target.minecraft.version),
|
|
2093
|
+
classpath: input.classpath.join(cpSeparator),
|
|
2094
|
+
classpath_separator: cpSeparator,
|
|
2095
|
+
library_directory: path__default.default.join(directory, LIBRARIES_DIR),
|
|
2096
|
+
launcher_name: launcherName,
|
|
2097
|
+
launcher_version: launcherVersion,
|
|
2098
|
+
resolution_width: input.options.resolution?.width.toString() ?? "",
|
|
2099
|
+
resolution_height: input.options.resolution?.height.toString() ?? ""
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// src/core/manifest-merge.ts
|
|
2104
|
+
function mergeManifest(parent, child) {
|
|
2105
|
+
const merged = {
|
|
2106
|
+
id: child.id || parent.id,
|
|
2107
|
+
type: child.type ?? parent.type,
|
|
2108
|
+
mainClass: child.mainClass ?? parent.mainClass,
|
|
2109
|
+
assetIndex: child.assetIndex ?? parent.assetIndex,
|
|
2110
|
+
assets: child.assets ?? parent.assets,
|
|
2111
|
+
downloads: { ...parent.downloads, ...child.downloads },
|
|
2112
|
+
libraries: mergeLibraries(parent.libraries, child.libraries),
|
|
2113
|
+
arguments: mergeArguments(parent.arguments, child.arguments),
|
|
2114
|
+
minecraftArguments: child.minecraftArguments ?? parent.minecraftArguments,
|
|
2115
|
+
javaVersion: child.javaVersion ?? parent.javaVersion,
|
|
2116
|
+
logging: child.logging ?? parent.logging,
|
|
2117
|
+
inheritsFrom: child.inheritsFrom ?? parent.inheritsFrom,
|
|
2118
|
+
releaseTime: child.releaseTime ?? parent.releaseTime,
|
|
2119
|
+
time: child.time ?? parent.time,
|
|
2120
|
+
minimumLauncherVersion: child.minimumLauncherVersion ?? parent.minimumLauncherVersion,
|
|
2121
|
+
complianceLevel: child.complianceLevel ?? parent.complianceLevel
|
|
2122
|
+
};
|
|
2123
|
+
return merged;
|
|
2124
|
+
}
|
|
2125
|
+
function mergeLibraries(parent, child) {
|
|
2126
|
+
return [...parent, ...child];
|
|
2127
|
+
}
|
|
2128
|
+
function mergeArguments(parent, child) {
|
|
2129
|
+
if (!parent && !child) return void 0;
|
|
2130
|
+
const parentGame = parent?.game ?? [];
|
|
2131
|
+
const parentJvm = parent?.jvm ?? [];
|
|
2132
|
+
const childGame = child?.game ?? [];
|
|
2133
|
+
const childJvm = child?.jvm ?? [];
|
|
2134
|
+
return {
|
|
2135
|
+
game: [...parentGame, ...childGame],
|
|
2136
|
+
jvm: [...parentJvm, ...childJvm]
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// src/launch/version-resolution.ts
|
|
2141
|
+
async function resolveLaunchVersion(target) {
|
|
2142
|
+
if (target.loader.type === Loaders.VANILLA) {
|
|
2143
|
+
return {
|
|
2144
|
+
versionId: target.minecraft.version,
|
|
2145
|
+
merged: target.minecraft.manifest,
|
|
2146
|
+
chain: [target.minecraft.version]
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
const versionId = await pickInstalledVersionId(target);
|
|
2150
|
+
const merged = await loadAndMerge(target.directory, versionId, target.minecraft.manifest);
|
|
2151
|
+
return { versionId, merged, chain: [versionId, target.minecraft.version] };
|
|
2152
|
+
}
|
|
2153
|
+
async function pickClientJarVersionId(directory, chain) {
|
|
2154
|
+
for (const id of chain) {
|
|
2155
|
+
const jar = targetPaths.versionJar(directory, id);
|
|
2156
|
+
if (await fileExists(jar)) return id;
|
|
2157
|
+
}
|
|
2158
|
+
const fallback = chain.at(-1);
|
|
2159
|
+
if (fallback === void 0) {
|
|
2160
|
+
throw new MinecraftKitError(
|
|
2161
|
+
"MANIFEST_NOT_FOUND",
|
|
2162
|
+
"Cannot resolve a client jar version id from an empty inheritsFrom chain"
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
return fallback;
|
|
2166
|
+
}
|
|
2167
|
+
async function pickInstalledVersionId(target) {
|
|
2168
|
+
if (target.loader.type === Loaders.FABRIC) {
|
|
2169
|
+
const candidate = target.loader.profile.id;
|
|
2170
|
+
const versionJsonPath = targetPaths.versionJson(target.directory, candidate);
|
|
2171
|
+
if (await fileExists(versionJsonPath)) return candidate;
|
|
2172
|
+
}
|
|
2173
|
+
if (target.loader.type === Loaders.FORGE) {
|
|
2174
|
+
const directories = await listChildDirectories(targetPaths.versionsDir(target.directory));
|
|
2175
|
+
for (const id of directories) {
|
|
2176
|
+
const versionJsonPath = targetPaths.versionJson(target.directory, id);
|
|
2177
|
+
if (!await fileExists(versionJsonPath)) continue;
|
|
2178
|
+
const text = await readText(versionJsonPath);
|
|
2179
|
+
try {
|
|
2180
|
+
const parsed = JSON.parse(text);
|
|
2181
|
+
if (parsed.inheritsFrom === target.minecraft.version && (id.includes("forge") || (parsed.id ?? "").includes("forge"))) {
|
|
2182
|
+
return id;
|
|
2183
|
+
}
|
|
2184
|
+
} catch {
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
throw new MinecraftKitError(
|
|
2189
|
+
"MANIFEST_NOT_FOUND",
|
|
2190
|
+
`Could not find an installed version JSON for target ${target.id}`,
|
|
2191
|
+
{ context: { targetId: target.id, loaderType: target.loader.type } }
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
async function loadAndMerge(directory, versionId, parentManifest) {
|
|
2195
|
+
const versionJsonPath = targetPaths.versionJson(directory, versionId);
|
|
2196
|
+
const text = await readText(versionJsonPath);
|
|
2197
|
+
let child;
|
|
2198
|
+
try {
|
|
2199
|
+
child = JSON.parse(text);
|
|
2200
|
+
} catch (cause) {
|
|
2201
|
+
throw new MinecraftKitError(
|
|
2202
|
+
"MANIFEST_INVALID",
|
|
2203
|
+
`Version JSON is not valid JSON: ${versionJsonPath}`,
|
|
2204
|
+
{ cause, context: { filePath: versionJsonPath } }
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
if (child.inheritsFrom !== void 0 && child.inheritsFrom !== parentManifest.id) {
|
|
2208
|
+
return mergeManifest(parentManifest, child);
|
|
2209
|
+
}
|
|
2210
|
+
return mergeManifest(parentManifest, child);
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/launch/compose.ts
|
|
2214
|
+
async function composeLaunch(input) {
|
|
2215
|
+
const { target, options } = input;
|
|
2216
|
+
if (!options.auth.username || options.auth.username.length === 0) {
|
|
2217
|
+
throw new MinecraftKitError(
|
|
2218
|
+
"INVALID_INPUT",
|
|
2219
|
+
`Auth username must be non-empty (target ${target.id})`,
|
|
2220
|
+
{ context: { targetId: target.id } }
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
const resolved = await resolveLaunchVersion(target);
|
|
2224
|
+
const javaPath = targetPaths.runtimeJavaExecutable(
|
|
2225
|
+
target.directory,
|
|
2226
|
+
target.runtime.component,
|
|
2227
|
+
target.runtime.system.os,
|
|
2228
|
+
target.runtime.installRoot
|
|
2229
|
+
);
|
|
2230
|
+
const clientJarVersionId = await pickClientJarVersionId(target.directory, resolved.chain);
|
|
2231
|
+
const classpath = buildClasspath({
|
|
2232
|
+
directory: target.directory,
|
|
2233
|
+
versionId: clientJarVersionId,
|
|
2234
|
+
merged: resolved.merged,
|
|
2235
|
+
system: target.runtime.system
|
|
2236
|
+
});
|
|
2237
|
+
const features = buildFeatures(options);
|
|
2238
|
+
const placeholderValues = buildPlaceholderValues({
|
|
2239
|
+
target,
|
|
2240
|
+
versionId: resolved.versionId,
|
|
2241
|
+
auth: options.auth,
|
|
2242
|
+
classpath,
|
|
2243
|
+
options
|
|
2244
|
+
});
|
|
2245
|
+
const composed = composeArgs({
|
|
2246
|
+
target,
|
|
2247
|
+
merged: resolved.merged,
|
|
2248
|
+
options,
|
|
2249
|
+
placeholderValues,
|
|
2250
|
+
features
|
|
2251
|
+
});
|
|
2252
|
+
return {
|
|
2253
|
+
targetId: target.id,
|
|
2254
|
+
directory: target.directory,
|
|
2255
|
+
javaPath,
|
|
2256
|
+
mainClass: resolved.merged.mainClass,
|
|
2257
|
+
jvmArgs: composed.jvmArgs,
|
|
2258
|
+
gameArgs: composed.gameArgs,
|
|
2259
|
+
classpath,
|
|
2260
|
+
nativesDirectory: targetPaths.nativesDir(target.directory, target.minecraft.version),
|
|
2261
|
+
auth: options.auth,
|
|
2262
|
+
workingDirectory: target.directory
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
function buildFeatures(options) {
|
|
2266
|
+
const features = { ...options.features ?? {} };
|
|
2267
|
+
if (options.resolution !== void 0) {
|
|
2268
|
+
features.has_custom_resolution = true;
|
|
2269
|
+
}
|
|
2270
|
+
return features;
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// src/launch/runner.ts
|
|
2274
|
+
function runLaunch(input) {
|
|
2275
|
+
const composition = input.composition;
|
|
2276
|
+
const options = input.options ?? {};
|
|
2277
|
+
const args = [...composition.jvmArgs, composition.mainClass, ...composition.gameArgs];
|
|
2278
|
+
options.onEvent?.({
|
|
2279
|
+
type: "launch:starting",
|
|
2280
|
+
command: composition.javaPath,
|
|
2281
|
+
args,
|
|
2282
|
+
cwd: composition.workingDirectory
|
|
2283
|
+
});
|
|
2284
|
+
const child = input.spawner.spawn(composition.javaPath, args, {
|
|
2285
|
+
cwd: composition.workingDirectory,
|
|
2286
|
+
...composition.env !== void 0 ? { env: composition.env } : {}
|
|
2287
|
+
});
|
|
2288
|
+
options.onEvent?.({ type: "launch:started", pid: child.pid });
|
|
2289
|
+
child.stdout.on("data", (line) => {
|
|
2290
|
+
options.onEvent?.({ type: "launch:stdout", line });
|
|
2291
|
+
});
|
|
2292
|
+
child.stderr.on("data", (line) => {
|
|
2293
|
+
options.onEvent?.({ type: "launch:stderr", line });
|
|
2294
|
+
});
|
|
2295
|
+
const grace = options.killGracePeriodMs ?? DEFAULT_KILL_GRACE_MS;
|
|
2296
|
+
let aborted = false;
|
|
2297
|
+
const doAbort = (reason) => {
|
|
2298
|
+
if (aborted) return;
|
|
2299
|
+
aborted = true;
|
|
2300
|
+
options.onEvent?.({ type: "launch:aborted", reason });
|
|
2301
|
+
child.kill("SIGTERM");
|
|
2302
|
+
setTimeout(() => child.kill("SIGKILL"), grace).unref();
|
|
2303
|
+
};
|
|
2304
|
+
if (options.signal !== void 0) {
|
|
2305
|
+
if (options.signal.aborted) {
|
|
2306
|
+
doAbort(reasonFrom(options.signal.reason));
|
|
2307
|
+
} else {
|
|
2308
|
+
options.signal.addEventListener("abort", () => doAbort(reasonFrom(options.signal?.reason)), {
|
|
2309
|
+
once: true
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
const exited = (async () => {
|
|
2314
|
+
const { code, signal } = await child.exited;
|
|
2315
|
+
options.onEvent?.({ type: "launch:exited", code, signal });
|
|
2316
|
+
if (!aborted && code !== 0 && code !== null) {
|
|
2317
|
+
throw new MinecraftKitError(
|
|
2318
|
+
"LAUNCH_PROCESS_FAILED",
|
|
2319
|
+
`Minecraft process exited with code ${code}`,
|
|
2320
|
+
{ context: { exitCode: code } }
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2323
|
+
return { code, signal, aborted };
|
|
2324
|
+
})();
|
|
2325
|
+
return {
|
|
2326
|
+
pid: child.pid,
|
|
2327
|
+
exited,
|
|
2328
|
+
abort(reason) {
|
|
2329
|
+
doAbort(reason ?? "user");
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
}
|
|
2333
|
+
function reasonFrom(value) {
|
|
2334
|
+
if (value === void 0) return "aborted";
|
|
2335
|
+
if (typeof value === "string") return value;
|
|
2336
|
+
if (value instanceof Error) return value.message;
|
|
2337
|
+
return String(value);
|
|
2338
|
+
}
|
|
2339
|
+
var ChildProcessSpawner = class {
|
|
2340
|
+
spawn(command, args, options) {
|
|
2341
|
+
const child = child_process.spawn(command, [...args], {
|
|
2342
|
+
cwd: options.cwd,
|
|
2343
|
+
env: options.env === void 0 ? process.env : { ...process.env, ...options.env },
|
|
2344
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2345
|
+
});
|
|
2346
|
+
const stdout = streamFromBuffer(child.stdout);
|
|
2347
|
+
const stderr = streamFromBuffer(child.stderr);
|
|
2348
|
+
const exited = new Promise((resolve) => {
|
|
2349
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
2350
|
+
});
|
|
2351
|
+
return {
|
|
2352
|
+
pid: child.pid ?? -1,
|
|
2353
|
+
stdout,
|
|
2354
|
+
stderr,
|
|
2355
|
+
exited,
|
|
2356
|
+
kill(signal) {
|
|
2357
|
+
return child.kill(signal);
|
|
2358
|
+
}
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
};
|
|
2362
|
+
function streamFromBuffer(stream) {
|
|
2363
|
+
if (!stream) {
|
|
2364
|
+
return { on() {
|
|
2365
|
+
} };
|
|
2366
|
+
}
|
|
2367
|
+
let buffer$1 = "";
|
|
2368
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2369
|
+
const emit = (line) => {
|
|
2370
|
+
for (const listener of listeners) listener(line);
|
|
2371
|
+
};
|
|
2372
|
+
stream.on("data", (chunk) => {
|
|
2373
|
+
const text = buffer.Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
2374
|
+
buffer$1 += text;
|
|
2375
|
+
let index = buffer$1.indexOf("\n");
|
|
2376
|
+
while (index !== -1) {
|
|
2377
|
+
const line = buffer$1.slice(0, index).replace(/\r$/, "");
|
|
2378
|
+
buffer$1 = buffer$1.slice(index + 1);
|
|
2379
|
+
emitBounded(emit, line);
|
|
2380
|
+
index = buffer$1.indexOf("\n");
|
|
2381
|
+
}
|
|
2382
|
+
while (buffer$1.length > SPAWNER_MAX_LINE_BYTES) {
|
|
2383
|
+
emit(buffer$1.slice(0, SPAWNER_MAX_LINE_BYTES));
|
|
2384
|
+
buffer$1 = buffer$1.slice(SPAWNER_MAX_LINE_BYTES);
|
|
2385
|
+
}
|
|
2386
|
+
});
|
|
2387
|
+
stream.on("end", () => {
|
|
2388
|
+
if (buffer$1.length > 0) {
|
|
2389
|
+
emitBounded(emit, buffer$1);
|
|
2390
|
+
buffer$1 = "";
|
|
2391
|
+
}
|
|
2392
|
+
});
|
|
2393
|
+
return {
|
|
2394
|
+
on(_event, listener) {
|
|
2395
|
+
listeners.add(listener);
|
|
2396
|
+
}
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
2399
|
+
function emitBounded(emit, line) {
|
|
2400
|
+
if (line.length <= SPAWNER_MAX_LINE_BYTES) {
|
|
2401
|
+
emit(line);
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
for (let i = 0; i < line.length; i += SPAWNER_MAX_LINE_BYTES) {
|
|
2405
|
+
emit(line.slice(i, i + SPAWNER_MAX_LINE_BYTES));
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
// src/types/verify.ts
|
|
2410
|
+
var VerificationKinds = {
|
|
2411
|
+
MINECRAFT: "minecraft",
|
|
2412
|
+
FABRIC: "fabric",
|
|
2413
|
+
FORGE: "forge",
|
|
2414
|
+
RUNTIME: "runtime"
|
|
2415
|
+
};
|
|
2416
|
+
var VerifyFileStatuses = {
|
|
2417
|
+
OK: "ok",
|
|
2418
|
+
MISSING: "missing",
|
|
2419
|
+
CORRUPT: "corrupt",
|
|
2420
|
+
WRONG_SIZE: "wrong-size"
|
|
2421
|
+
};
|
|
2422
|
+
var VerifyFileCategories = {
|
|
2423
|
+
CLIENT_JAR: "client-jar",
|
|
2424
|
+
LIBRARY: "library",
|
|
2425
|
+
ASSET: "asset",
|
|
2426
|
+
ASSET_INDEX: "asset-index",
|
|
2427
|
+
NATIVE: "native",
|
|
2428
|
+
LOADER_LIBRARY: "loader-library",
|
|
2429
|
+
RUNTIME_FILE: "runtime-file",
|
|
2430
|
+
LOGGING_CONFIG: "logging-config"
|
|
2431
|
+
};
|
|
2432
|
+
|
|
2433
|
+
// src/repair/helpers.ts
|
|
2434
|
+
function asResultArray(from) {
|
|
2435
|
+
return Array.isArray(from) ? from : [from];
|
|
2436
|
+
}
|
|
2437
|
+
function buildIssueIndex(from) {
|
|
2438
|
+
const map = /* @__PURE__ */ new Map();
|
|
2439
|
+
for (const v of asResultArray(from)) {
|
|
2440
|
+
for (const issue of v.issues) {
|
|
2441
|
+
const set = map.get(issue.path);
|
|
2442
|
+
if (set) set.add(issue.category);
|
|
2443
|
+
else map.set(issue.path, /* @__PURE__ */ new Set([issue.category]));
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
return {
|
|
2447
|
+
has: (path13) => map.has(path13),
|
|
2448
|
+
hasNonNative: (path13) => {
|
|
2449
|
+
const cats = map.get(path13);
|
|
2450
|
+
if (!cats) return false;
|
|
2451
|
+
for (const c of cats) {
|
|
2452
|
+
if (c !== VerifyFileCategories.NATIVE) return true;
|
|
2453
|
+
}
|
|
2454
|
+
return false;
|
|
2455
|
+
},
|
|
2456
|
+
categoriesAt: (path13) => map.get(path13) ?? /* @__PURE__ */ new Set()
|
|
2457
|
+
};
|
|
2458
|
+
}
|
|
2459
|
+
function sumDownloadBytes(actions) {
|
|
2460
|
+
return actions.reduce((sum, action) => {
|
|
2461
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
2462
|
+
return sum + (action.expectedSize ?? 0);
|
|
2463
|
+
}
|
|
2464
|
+
return sum;
|
|
2465
|
+
}, 0);
|
|
2466
|
+
}
|
|
2467
|
+
function buildRepairPlan(target, actions) {
|
|
2468
|
+
return {
|
|
2469
|
+
targetId: target.id,
|
|
2470
|
+
directory: target.directory,
|
|
2471
|
+
target,
|
|
2472
|
+
actions,
|
|
2473
|
+
totalActions: actions.length,
|
|
2474
|
+
totalBytes: sumDownloadBytes(actions)
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
async function planAspectRepair(input, aspectFilter, postprocess) {
|
|
2478
|
+
const installPlan = await planInstall({
|
|
2479
|
+
target: input.target,
|
|
2480
|
+
http: input.http,
|
|
2481
|
+
cache: input.cache,
|
|
2482
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2483
|
+
});
|
|
2484
|
+
const issues = buildIssueIndex(input.from);
|
|
2485
|
+
const actions = selectRepairActions({
|
|
2486
|
+
target: input.target,
|
|
2487
|
+
installPlan,
|
|
2488
|
+
issues,
|
|
2489
|
+
aspectFilter
|
|
2490
|
+
});
|
|
2491
|
+
postprocess?.({ actions, installPlan, issues });
|
|
2492
|
+
return buildRepairPlan(input.target, actions);
|
|
2493
|
+
}
|
|
2494
|
+
function selectRepairActions(input) {
|
|
2495
|
+
const matching = [];
|
|
2496
|
+
for (const action of input.installPlan.actions) {
|
|
2497
|
+
if (!input.aspectFilter(action)) continue;
|
|
2498
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
2499
|
+
if (input.issues.hasNonNative(action.target)) {
|
|
2500
|
+
matching.push(action);
|
|
2501
|
+
}
|
|
2502
|
+
} else if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
|
|
2503
|
+
if (input.issues.has(action.path)) {
|
|
2504
|
+
matching.push(action);
|
|
2505
|
+
}
|
|
2506
|
+
} else if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
|
|
2507
|
+
if (input.issues.has(action.source)) {
|
|
2508
|
+
matching.push(action);
|
|
2509
|
+
}
|
|
2510
|
+
} else {
|
|
2511
|
+
matching.push(action);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return matching;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// src/repair/fabric.ts
|
|
2518
|
+
async function planFabricRepair(input) {
|
|
2519
|
+
if (input.target.loader.type !== Loaders.FABRIC) {
|
|
2520
|
+
throw new MinecraftKitError(
|
|
2521
|
+
"INVALID_INPUT",
|
|
2522
|
+
`repair.fabric requires a Fabric target (got ${input.target.loader.type})`
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
const fabricJsonPath = targetPaths.versionJson(
|
|
2526
|
+
input.target.directory,
|
|
2527
|
+
input.target.loader.profile.id
|
|
2528
|
+
);
|
|
2529
|
+
return planAspectRepair(input, (action) => {
|
|
2530
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
2531
|
+
return action.category === "fabric-library";
|
|
2532
|
+
}
|
|
2533
|
+
if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
|
|
2534
|
+
return action.path === fabricJsonPath;
|
|
2535
|
+
}
|
|
2536
|
+
return false;
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
// src/repair/forge.ts
|
|
2541
|
+
var FORGE_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
|
|
2542
|
+
"forge-library",
|
|
2543
|
+
"forge-installer"
|
|
2544
|
+
]);
|
|
2545
|
+
async function planForgeRepair(input) {
|
|
2546
|
+
if (input.target.loader.type !== Loaders.FORGE) {
|
|
2547
|
+
throw new MinecraftKitError(
|
|
2548
|
+
"INVALID_INPUT",
|
|
2549
|
+
`repair.forge requires a Forge target (got ${input.target.loader.type})`
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
const forgeJsonPath = targetPaths.versionJson(
|
|
2553
|
+
input.target.directory,
|
|
2554
|
+
input.target.loader.fullVersion
|
|
2555
|
+
);
|
|
2556
|
+
return planAspectRepair(
|
|
2557
|
+
input,
|
|
2558
|
+
(action) => {
|
|
2559
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
2560
|
+
return FORGE_DOWNLOAD_CATEGORIES.has(action.category);
|
|
2561
|
+
}
|
|
2562
|
+
if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
|
|
2563
|
+
return action.path === forgeJsonPath;
|
|
2564
|
+
}
|
|
2565
|
+
return false;
|
|
2566
|
+
},
|
|
2567
|
+
({ actions, installPlan, issues }) => {
|
|
2568
|
+
if (!issues.has(forgeJsonPath)) return;
|
|
2569
|
+
const alreadyIncluded = new Set(
|
|
2570
|
+
actions.filter((a) => a.kind === InstallActionKinds.DOWNLOAD_FILE).map((a) => a.target)
|
|
2571
|
+
);
|
|
2572
|
+
for (const action of installPlan.actions) {
|
|
2573
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "forge-library" && !alreadyIncluded.has(action.target)) {
|
|
2574
|
+
actions.push(action);
|
|
2575
|
+
} else if (action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR) {
|
|
2576
|
+
actions.push(action);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// src/repair/minecraft.ts
|
|
2584
|
+
var MINECRAFT_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
|
|
2585
|
+
"client-jar",
|
|
2586
|
+
"library",
|
|
2587
|
+
"asset-index",
|
|
2588
|
+
"asset",
|
|
2589
|
+
"logging-config"
|
|
2590
|
+
]);
|
|
2591
|
+
async function planMinecraftRepair(input) {
|
|
2592
|
+
const vanillaJsonPath = targetPaths.versionJson(
|
|
2593
|
+
input.target.directory,
|
|
2594
|
+
input.target.minecraft.version
|
|
2595
|
+
);
|
|
2596
|
+
return planAspectRepair(input, (action) => {
|
|
2597
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
2598
|
+
return MINECRAFT_DOWNLOAD_CATEGORIES.has(action.category);
|
|
2599
|
+
}
|
|
2600
|
+
if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
|
|
2601
|
+
return action.path === vanillaJsonPath;
|
|
2602
|
+
}
|
|
2603
|
+
if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
|
|
2604
|
+
return true;
|
|
2605
|
+
}
|
|
2606
|
+
return false;
|
|
2607
|
+
});
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
// src/repair/runner.ts
|
|
2611
|
+
async function runRepair(input) {
|
|
2612
|
+
const report = await runInstall({
|
|
2613
|
+
plan: {
|
|
2614
|
+
...input.plan,
|
|
2615
|
+
totalActions: input.plan.actions.length,
|
|
2616
|
+
totalBytes: input.plan.totalBytes
|
|
2617
|
+
},
|
|
2618
|
+
http: input.http,
|
|
2619
|
+
cache: input.cache,
|
|
2620
|
+
spawner: input.spawner,
|
|
2621
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
2622
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
2623
|
+
});
|
|
2624
|
+
return {
|
|
2625
|
+
targetId: report.targetId,
|
|
2626
|
+
bytesDownloaded: report.bytesDownloaded,
|
|
2627
|
+
actionsCompleted: report.actionsCompleted,
|
|
2628
|
+
durationMs: report.durationMs
|
|
2629
|
+
};
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// src/repair/runtime.ts
|
|
2633
|
+
async function planRuntimeRepair(input) {
|
|
2634
|
+
return planAspectRepair(
|
|
2635
|
+
input,
|
|
2636
|
+
(action) => action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "runtime-file"
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
// src/types/runtime.ts
|
|
2641
|
+
var RuntimeComponents = {
|
|
2642
|
+
JRE_LEGACY: "jre-legacy",
|
|
2643
|
+
JAVA_RUNTIME_ALPHA: "java-runtime-alpha",
|
|
2644
|
+
JAVA_RUNTIME_BETA: "java-runtime-beta",
|
|
2645
|
+
JAVA_RUNTIME_GAMMA: "java-runtime-gamma",
|
|
2646
|
+
JAVA_RUNTIME_GAMMA_SNAPSHOT: "java-runtime-gamma-snapshot",
|
|
2647
|
+
JAVA_RUNTIME_DELTA: "java-runtime-delta",
|
|
2648
|
+
JAVA_RUNTIME_EPSILON: "java-runtime-epsilon",
|
|
2649
|
+
MINECRAFT_JAVA_EXE: "minecraft-java-exe"
|
|
2650
|
+
};
|
|
2651
|
+
var RuntimePreference = {
|
|
2652
|
+
/** Component declared by the Minecraft manifest. */
|
|
2653
|
+
RECOMMENDED: "recommended",
|
|
2654
|
+
/** Newest component available for the platform. */
|
|
2655
|
+
LATEST: "latest"
|
|
2656
|
+
};
|
|
2657
|
+
|
|
2658
|
+
// src/targets/index.ts
|
|
2659
|
+
var TargetsApi = class {
|
|
2660
|
+
constructor(ctx) {
|
|
2661
|
+
this.ctx = ctx;
|
|
2662
|
+
}
|
|
2663
|
+
ctx;
|
|
2664
|
+
/** The detected host system used by `resolve()` when no `system` is supplied. */
|
|
2665
|
+
get system() {
|
|
2666
|
+
return this.ctx.system;
|
|
2667
|
+
}
|
|
2668
|
+
/** Build a {@link Target} from already-resolved components. */
|
|
2669
|
+
create(input) {
|
|
2670
|
+
if (!input.id) {
|
|
2671
|
+
throw new MinecraftKitError("INVALID_INPUT", "Target id must be non-empty");
|
|
2672
|
+
}
|
|
2673
|
+
if (!input.directory) {
|
|
2674
|
+
throw new MinecraftKitError("INVALID_INPUT", "Target directory must be non-empty");
|
|
2675
|
+
}
|
|
2676
|
+
if (input.loader.minecraftVersion !== input.minecraft.version) {
|
|
2677
|
+
throw new MinecraftKitError(
|
|
2678
|
+
"INVALID_INPUT",
|
|
2679
|
+
`Loader Minecraft version (${input.loader.minecraftVersion}) does not match resolved Minecraft (${input.minecraft.version})`,
|
|
2680
|
+
{
|
|
2681
|
+
context: {
|
|
2682
|
+
loaderMinecraft: input.loader.minecraftVersion,
|
|
2683
|
+
minecraftVersion: input.minecraft.version
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
return {
|
|
2689
|
+
id: input.id,
|
|
2690
|
+
directory: input.directory,
|
|
2691
|
+
minecraft: input.minecraft,
|
|
2692
|
+
loader: input.loader,
|
|
2693
|
+
runtime: input.runtime
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2696
|
+
/** Sugar API: resolve every component then assemble a target. */
|
|
2697
|
+
async resolve(input) {
|
|
2698
|
+
const minecraft = await this.ctx.minecraft.resolve({
|
|
2699
|
+
version: input.minecraft.version,
|
|
2700
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2701
|
+
});
|
|
2702
|
+
const system = input.system ?? this.ctx.system;
|
|
2703
|
+
const componentOverride = input.runtime?.component;
|
|
2704
|
+
const runtimeComponent = componentOverride ?? minecraft.manifest.javaVersion?.component;
|
|
2705
|
+
const resolvedRuntime = await this.ctx.runtime.resolve({
|
|
2706
|
+
system,
|
|
2707
|
+
...runtimeComponent !== void 0 ? { component: runtimeComponent } : {},
|
|
2708
|
+
preference: input.runtime?.preference ?? RuntimePreference.RECOMMENDED,
|
|
2709
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2710
|
+
});
|
|
2711
|
+
const runtime = input.runtime?.installRoot !== void 0 ? { ...resolvedRuntime, installRoot: input.runtime.installRoot } : resolvedRuntime;
|
|
2712
|
+
let loader;
|
|
2713
|
+
if (input.loader.type === Loaders.VANILLA) {
|
|
2714
|
+
loader = {
|
|
2715
|
+
type: Loaders.VANILLA,
|
|
2716
|
+
minecraftVersion: minecraft.version,
|
|
2717
|
+
minecraft
|
|
2718
|
+
};
|
|
2719
|
+
} else if (input.loader.type === Loaders.FABRIC) {
|
|
2720
|
+
loader = await this.ctx.fabric.resolve({
|
|
2721
|
+
minecraftVersion: minecraft.version,
|
|
2722
|
+
...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
|
|
2723
|
+
...input.loader.version !== void 0 ? { loaderVersion: input.loader.version } : {},
|
|
2724
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2725
|
+
});
|
|
2726
|
+
} else {
|
|
2727
|
+
loader = await this.ctx.forge.resolve({
|
|
2728
|
+
minecraftVersion: minecraft.version,
|
|
2729
|
+
...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
|
|
2730
|
+
...input.loader.version !== void 0 ? { forgeVersion: input.loader.version } : {},
|
|
2731
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
return this.create({
|
|
2735
|
+
id: input.id,
|
|
2736
|
+
directory: input.directory,
|
|
2737
|
+
minecraft,
|
|
2738
|
+
loader,
|
|
2739
|
+
runtime
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
/** Scan a root directory for Minecraft installations. Returns only what is on disk. */
|
|
2743
|
+
async list(input) {
|
|
2744
|
+
if (!await dirExists(input.rootDir)) return [];
|
|
2745
|
+
const subdirs = await listChildDirectories(input.rootDir);
|
|
2746
|
+
const results = [];
|
|
2747
|
+
for (const id of subdirs) {
|
|
2748
|
+
const directory = path__default.default.join(input.rootDir, id);
|
|
2749
|
+
const discovered = await discoverInstallation(id, directory);
|
|
2750
|
+
if (discovered) results.push(discovered);
|
|
2751
|
+
}
|
|
2752
|
+
return results;
|
|
2753
|
+
}
|
|
2754
|
+
};
|
|
2755
|
+
async function discoverInstallation(id, directory) {
|
|
2756
|
+
const versionsDir = path__default.default.join(directory, VERSIONS_DIR);
|
|
2757
|
+
const librariesDir = path__default.default.join(directory, LIBRARIES_DIR);
|
|
2758
|
+
const assetsDir = path__default.default.join(directory, ASSETS_DIR);
|
|
2759
|
+
const looksLikeInstall = await dirExists(versionsDir) && (await dirExists(librariesDir) || await dirExists(assetsDir));
|
|
2760
|
+
if (!looksLikeInstall) return null;
|
|
2761
|
+
const versionDirs = await listChildDirectories(versionsDir);
|
|
2762
|
+
const minecraftVersions = [];
|
|
2763
|
+
const loaders = [];
|
|
2764
|
+
for (const versionId of versionDirs) {
|
|
2765
|
+
const hint = inferLoaderFromVersionId(versionId);
|
|
2766
|
+
if (hint) {
|
|
2767
|
+
loaders.push(hint);
|
|
2768
|
+
if (hint.minecraftVersion && !minecraftVersions.includes(hint.minecraftVersion)) {
|
|
2769
|
+
minecraftVersions.push(hint.minecraftVersion);
|
|
2770
|
+
}
|
|
2771
|
+
} else {
|
|
2772
|
+
minecraftVersions.push(versionId);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
const runtime = await discoverRuntime(directory);
|
|
2776
|
+
return { id, directory, minecraftVersions, loaders, ...runtime ? { runtime } : {} };
|
|
2777
|
+
}
|
|
2778
|
+
async function discoverRuntime(directory) {
|
|
2779
|
+
const runtimeDir = path__default.default.join(directory, RUNTIMES_DIR);
|
|
2780
|
+
if (!await dirExists(runtimeDir)) return void 0;
|
|
2781
|
+
let components;
|
|
2782
|
+
try {
|
|
2783
|
+
components = await listChildDirectories(runtimeDir);
|
|
2784
|
+
} catch {
|
|
2785
|
+
return void 0;
|
|
2786
|
+
}
|
|
2787
|
+
for (const component of components) {
|
|
2788
|
+
const root = path__default.default.join(runtimeDir, component);
|
|
2789
|
+
const javaPath = process.platform === "win32" ? path__default.default.join(root, "bin", "javaw.exe") : process.platform === "darwin" ? path__default.default.join(root, "jre.bundle", "Contents", "Home", "bin", "java") : path__default.default.join(root, "bin", "java");
|
|
2790
|
+
if (await fileExists(javaPath)) {
|
|
2791
|
+
return { component, javaPath };
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
return void 0;
|
|
2795
|
+
}
|
|
2796
|
+
function inferLoaderFromVersionId(versionId) {
|
|
2797
|
+
const fabricMatch = /^fabric-loader-([^-]+)-(.+)$/.exec(versionId);
|
|
2798
|
+
if (fabricMatch?.[1] && fabricMatch[2]) {
|
|
2799
|
+
return { type: Loaders.FABRIC, version: fabricMatch[1], minecraftVersion: fabricMatch[2] };
|
|
2800
|
+
}
|
|
2801
|
+
const forgeMatch = /^([^-]+)-forge-(.+)$/.exec(versionId);
|
|
2802
|
+
if (forgeMatch?.[1] && forgeMatch[2]) {
|
|
2803
|
+
return { type: Loaders.FORGE, minecraftVersion: forgeMatch[1], version: forgeMatch[2] };
|
|
2804
|
+
}
|
|
2805
|
+
return null;
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// src/update/runner.ts
|
|
2809
|
+
async function planUpdate(input) {
|
|
2810
|
+
return planInstall({
|
|
2811
|
+
target: input.target,
|
|
2812
|
+
http: input.http,
|
|
2813
|
+
cache: input.cache,
|
|
2814
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
async function runUpdate(input) {
|
|
2818
|
+
const report = await runInstall({
|
|
2819
|
+
plan: input.plan,
|
|
2820
|
+
http: input.http,
|
|
2821
|
+
cache: input.cache,
|
|
2822
|
+
spawner: input.spawner,
|
|
2823
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
2824
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
2825
|
+
});
|
|
2826
|
+
return {
|
|
2827
|
+
targetId: report.targetId,
|
|
2828
|
+
bytesDownloaded: report.bytesDownloaded,
|
|
2829
|
+
actionsCompleted: report.actionsCompleted,
|
|
2830
|
+
actionsSkipped: report.actionsSkipped,
|
|
2831
|
+
durationMs: report.durationMs
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
async function sha1OfFile(filePath) {
|
|
2835
|
+
const hash = crypto2__default.default.createHash("sha1");
|
|
2836
|
+
await new Promise((resolve, reject) => {
|
|
2837
|
+
const stream = fs$1.createReadStream(filePath);
|
|
2838
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
2839
|
+
stream.on("end", resolve);
|
|
2840
|
+
stream.on("error", reject);
|
|
2841
|
+
});
|
|
2842
|
+
return hash.digest("hex");
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
// src/verify/helpers.ts
|
|
2846
|
+
async function runVerification(input, check) {
|
|
2847
|
+
const startedAt = Date.now();
|
|
2848
|
+
const results = [];
|
|
2849
|
+
const record = (result) => {
|
|
2850
|
+
results.push(result);
|
|
2851
|
+
input.onEvent?.({ type: "verify:file-checked", file: result });
|
|
2852
|
+
};
|
|
2853
|
+
await check(record);
|
|
2854
|
+
return {
|
|
2855
|
+
targetId: input.targetId,
|
|
2856
|
+
kind: input.kind,
|
|
2857
|
+
isValid: results.every((r) => r.status === VerifyFileStatuses.OK),
|
|
2858
|
+
issues: results.filter((r) => r.status !== VerifyFileStatuses.OK),
|
|
2859
|
+
checkedFiles: results.length,
|
|
2860
|
+
durationMs: Date.now() - startedAt
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
async function verifyHashedFile(input) {
|
|
2864
|
+
if (!await fileExists(input.path)) {
|
|
2865
|
+
return {
|
|
2866
|
+
path: input.path,
|
|
2867
|
+
category: input.category,
|
|
2868
|
+
status: VerifyFileStatuses.MISSING,
|
|
2869
|
+
...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
|
|
2870
|
+
...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
|
|
2871
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2872
|
+
};
|
|
2873
|
+
}
|
|
2874
|
+
if (input.expectedSize !== void 0) {
|
|
2875
|
+
const size = await fileSize(input.path);
|
|
2876
|
+
if (size !== input.expectedSize) {
|
|
2877
|
+
return {
|
|
2878
|
+
path: input.path,
|
|
2879
|
+
category: input.category,
|
|
2880
|
+
status: VerifyFileStatuses.WRONG_SIZE,
|
|
2881
|
+
expectedSize: input.expectedSize,
|
|
2882
|
+
actualSize: size,
|
|
2883
|
+
...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
|
|
2884
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
if (input.expectedSha1 !== void 0) {
|
|
2889
|
+
const actualSha1 = await sha1OfFile(input.path);
|
|
2890
|
+
if (actualSha1 !== input.expectedSha1) {
|
|
2891
|
+
return {
|
|
2892
|
+
path: input.path,
|
|
2893
|
+
category: input.category,
|
|
2894
|
+
status: VerifyFileStatuses.CORRUPT,
|
|
2895
|
+
expectedSha1: input.expectedSha1,
|
|
2896
|
+
actualSha1,
|
|
2897
|
+
...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
|
|
2898
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
return {
|
|
2903
|
+
path: input.path,
|
|
2904
|
+
category: input.category,
|
|
2905
|
+
status: VerifyFileStatuses.OK,
|
|
2906
|
+
...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
|
|
2907
|
+
...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
|
|
2908
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
async function verifyExistence(input) {
|
|
2912
|
+
if (await fileExists(input.path)) {
|
|
2913
|
+
return {
|
|
2914
|
+
path: input.path,
|
|
2915
|
+
category: input.category,
|
|
2916
|
+
status: VerifyFileStatuses.OK,
|
|
2917
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
return {
|
|
2921
|
+
path: input.path,
|
|
2922
|
+
category: input.category,
|
|
2923
|
+
status: VerifyFileStatuses.MISSING,
|
|
2924
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
async function findForgeVersionJsonPath(directory, minecraftVersion) {
|
|
2928
|
+
const versionsDir = targetPaths.versionsDir(directory);
|
|
2929
|
+
const dirs = await listChildDirectories(versionsDir);
|
|
2930
|
+
for (const id of dirs) {
|
|
2931
|
+
if (!id.startsWith(`${minecraftVersion}-forge-`)) continue;
|
|
2932
|
+
const jsonPath = targetPaths.versionJson(directory, id);
|
|
2933
|
+
if (!await fileExists(jsonPath)) {
|
|
2934
|
+
return jsonPath;
|
|
2935
|
+
}
|
|
2936
|
+
const parsed = await tryParseInheritsFrom(jsonPath);
|
|
2937
|
+
if (parsed === minecraftVersion) return jsonPath;
|
|
2938
|
+
}
|
|
2939
|
+
return null;
|
|
2940
|
+
}
|
|
2941
|
+
async function tryParseInheritsFrom(jsonPath) {
|
|
2942
|
+
try {
|
|
2943
|
+
const parsed = JSON.parse(await readText(jsonPath));
|
|
2944
|
+
return parsed.inheritsFrom;
|
|
2945
|
+
} catch {
|
|
2946
|
+
return void 0;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
// src/verify/fabric.ts
|
|
2951
|
+
async function verifyFabric(input) {
|
|
2952
|
+
if (input.target.loader.type !== Loaders.FABRIC) {
|
|
2953
|
+
throw new MinecraftKitError(
|
|
2954
|
+
"INVALID_INPUT",
|
|
2955
|
+
`verify.fabric requires a Fabric target (got ${input.target.loader.type})`
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
const loader = input.target.loader;
|
|
2959
|
+
return runVerification(
|
|
2960
|
+
{
|
|
2961
|
+
targetId: input.target.id,
|
|
2962
|
+
kind: VerificationKinds.FABRIC,
|
|
2963
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
2964
|
+
},
|
|
2965
|
+
async (record) => {
|
|
2966
|
+
record(
|
|
2967
|
+
await verifyExistence({
|
|
2968
|
+
path: targetPaths.versionJson(input.target.directory, loader.profile.id),
|
|
2969
|
+
category: VerifyFileCategories.LOADER_LIBRARY
|
|
2970
|
+
})
|
|
2971
|
+
);
|
|
2972
|
+
const fabricLibraries = planLibraryDownloads({
|
|
2973
|
+
libraries: loader.profile.libraries,
|
|
2974
|
+
directory: input.target.directory,
|
|
2975
|
+
system: input.target.runtime.system,
|
|
2976
|
+
versionId: input.target.minecraft.version,
|
|
2977
|
+
category: "fabric-library"
|
|
2978
|
+
});
|
|
2979
|
+
for (const action of fabricLibraries.downloads) {
|
|
2980
|
+
record(
|
|
2981
|
+
await verifyHashedFile({
|
|
2982
|
+
path: action.target,
|
|
2983
|
+
expectedSha1: action.expectedSha1,
|
|
2984
|
+
expectedSize: action.expectedSize,
|
|
2985
|
+
...action.url ? { url: action.url } : {},
|
|
2986
|
+
category: VerifyFileCategories.LOADER_LIBRARY
|
|
2987
|
+
})
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
// src/verify/forge.ts
|
|
2995
|
+
async function verifyForge(input) {
|
|
2996
|
+
if (input.target.loader.type !== Loaders.FORGE) {
|
|
2997
|
+
throw new MinecraftKitError(
|
|
2998
|
+
"INVALID_INPUT",
|
|
2999
|
+
`verify.forge requires a Forge target (got ${input.target.loader.type})`
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
return runVerification(
|
|
3003
|
+
{
|
|
3004
|
+
targetId: input.target.id,
|
|
3005
|
+
kind: VerificationKinds.FORGE,
|
|
3006
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
3007
|
+
},
|
|
3008
|
+
async (record) => {
|
|
3009
|
+
const forgeVersionJsonPath = await findForgeVersionJsonPath(
|
|
3010
|
+
input.target.directory,
|
|
3011
|
+
input.target.minecraft.version
|
|
3012
|
+
);
|
|
3013
|
+
if (forgeVersionJsonPath === null) return;
|
|
3014
|
+
record(
|
|
3015
|
+
await verifyExistence({
|
|
3016
|
+
path: forgeVersionJsonPath,
|
|
3017
|
+
category: VerifyFileCategories.LOADER_LIBRARY
|
|
3018
|
+
})
|
|
3019
|
+
);
|
|
3020
|
+
if (!await fileExists(forgeVersionJsonPath)) return;
|
|
3021
|
+
let parsed;
|
|
3022
|
+
try {
|
|
3023
|
+
parsed = JSON.parse(await readText(forgeVersionJsonPath));
|
|
3024
|
+
} catch {
|
|
3025
|
+
record({
|
|
3026
|
+
path: forgeVersionJsonPath,
|
|
3027
|
+
category: VerifyFileCategories.LOADER_LIBRARY,
|
|
3028
|
+
status: VerifyFileStatuses.CORRUPT
|
|
3029
|
+
});
|
|
3030
|
+
return;
|
|
3031
|
+
}
|
|
3032
|
+
const forgeLibraries = planLibraryDownloads({
|
|
3033
|
+
libraries: parsed.libraries,
|
|
3034
|
+
directory: input.target.directory,
|
|
3035
|
+
system: input.target.runtime.system,
|
|
3036
|
+
versionId: input.target.minecraft.version,
|
|
3037
|
+
category: "forge-library"
|
|
3038
|
+
});
|
|
3039
|
+
for (const action of forgeLibraries.downloads) {
|
|
3040
|
+
record(
|
|
3041
|
+
await verifyHashedFile({
|
|
3042
|
+
path: action.target,
|
|
3043
|
+
expectedSha1: action.expectedSha1,
|
|
3044
|
+
expectedSize: action.expectedSize,
|
|
3045
|
+
...action.url ? { url: action.url } : {},
|
|
3046
|
+
category: VerifyFileCategories.LOADER_LIBRARY
|
|
3047
|
+
})
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// src/verify/minecraft.ts
|
|
3055
|
+
async function verifyMinecraft(input) {
|
|
3056
|
+
return runVerification(
|
|
3057
|
+
{
|
|
3058
|
+
targetId: input.target.id,
|
|
3059
|
+
kind: VerificationKinds.MINECRAFT,
|
|
3060
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
3061
|
+
},
|
|
3062
|
+
async (record) => {
|
|
3063
|
+
const { directory, minecraft, runtime } = input.target;
|
|
3064
|
+
record(
|
|
3065
|
+
await verifyHashedFile({
|
|
3066
|
+
path: targetPaths.versionJar(directory, minecraft.version),
|
|
3067
|
+
expectedSha1: minecraft.manifest.downloads.client.sha1,
|
|
3068
|
+
expectedSize: minecraft.manifest.downloads.client.size,
|
|
3069
|
+
url: minecraft.manifest.downloads.client.url,
|
|
3070
|
+
category: VerifyFileCategories.CLIENT_JAR
|
|
3071
|
+
})
|
|
3072
|
+
);
|
|
3073
|
+
record(
|
|
3074
|
+
await verifyExistence({
|
|
3075
|
+
path: targetPaths.versionJson(directory, minecraft.version),
|
|
3076
|
+
category: VerifyFileCategories.CLIENT_JAR
|
|
3077
|
+
})
|
|
3078
|
+
);
|
|
3079
|
+
if (minecraft.manifest.logging?.client) {
|
|
3080
|
+
const logging = minecraft.manifest.logging.client;
|
|
3081
|
+
record(
|
|
3082
|
+
await verifyHashedFile({
|
|
3083
|
+
path: targetPaths.loggingConfig(directory, logging.file.id),
|
|
3084
|
+
expectedSha1: logging.file.sha1,
|
|
3085
|
+
expectedSize: logging.file.size,
|
|
3086
|
+
url: logging.file.url,
|
|
3087
|
+
category: VerifyFileCategories.LOGGING_CONFIG
|
|
3088
|
+
})
|
|
3089
|
+
);
|
|
3090
|
+
}
|
|
3091
|
+
const libraryPlan = planLibraryDownloads({
|
|
3092
|
+
libraries: minecraft.manifest.libraries,
|
|
3093
|
+
directory,
|
|
3094
|
+
system: runtime.system,
|
|
3095
|
+
versionId: minecraft.version,
|
|
3096
|
+
category: "library"
|
|
3097
|
+
});
|
|
3098
|
+
for (const action of libraryPlan.downloads) {
|
|
3099
|
+
record(
|
|
3100
|
+
await verifyHashedFile({
|
|
3101
|
+
path: action.target,
|
|
3102
|
+
expectedSha1: action.expectedSha1,
|
|
3103
|
+
expectedSize: action.expectedSize,
|
|
3104
|
+
url: action.url,
|
|
3105
|
+
category: VerifyFileCategories.LIBRARY
|
|
3106
|
+
})
|
|
3107
|
+
);
|
|
3108
|
+
}
|
|
3109
|
+
const indexUrl = minecraft.manifest.assetIndex.url;
|
|
3110
|
+
const indexPath = targetPaths.assetIndex(directory, minecraft.manifest.assetIndex.id);
|
|
3111
|
+
record(
|
|
3112
|
+
await verifyHashedFile({
|
|
3113
|
+
path: indexPath,
|
|
3114
|
+
expectedSha1: minecraft.manifest.assetIndex.sha1,
|
|
3115
|
+
expectedSize: minecraft.manifest.assetIndex.size,
|
|
3116
|
+
url: indexUrl,
|
|
3117
|
+
category: VerifyFileCategories.ASSET_INDEX
|
|
3118
|
+
})
|
|
3119
|
+
);
|
|
3120
|
+
const indexDocument = await fetchJson(input.http, input.cache, {
|
|
3121
|
+
url: indexUrl,
|
|
3122
|
+
cacheKey: `asset-index:${minecraft.manifest.assetIndex.id}:${minecraft.manifest.assetIndex.sha1}`,
|
|
3123
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3124
|
+
});
|
|
3125
|
+
const seenAssetHashes = /* @__PURE__ */ new Set();
|
|
3126
|
+
for (const entry of Object.values(indexDocument.objects)) {
|
|
3127
|
+
if (seenAssetHashes.has(entry.hash)) continue;
|
|
3128
|
+
seenAssetHashes.add(entry.hash);
|
|
3129
|
+
record(
|
|
3130
|
+
await verifyHashedFile({
|
|
3131
|
+
path: targetPaths.assetObject(directory, entry.hash),
|
|
3132
|
+
expectedSha1: entry.hash,
|
|
3133
|
+
expectedSize: entry.size,
|
|
3134
|
+
category: VerifyFileCategories.ASSET
|
|
3135
|
+
})
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
const nativesDir = targetPaths.nativesDir(directory, minecraft.version);
|
|
3139
|
+
if (!await fileExists(nativesDir)) {
|
|
3140
|
+
for (const extraction of libraryPlan.nativeExtractions) {
|
|
3141
|
+
record({
|
|
3142
|
+
path: extraction.source,
|
|
3143
|
+
category: VerifyFileCategories.NATIVE,
|
|
3144
|
+
status: VerifyFileStatuses.MISSING
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
);
|
|
3150
|
+
}
|
|
3151
|
+
async function verifyRuntime(input) {
|
|
3152
|
+
return runVerification(
|
|
3153
|
+
{
|
|
3154
|
+
targetId: input.target.id,
|
|
3155
|
+
kind: VerificationKinds.RUNTIME,
|
|
3156
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
3157
|
+
},
|
|
3158
|
+
async (record) => {
|
|
3159
|
+
let manifest;
|
|
3160
|
+
try {
|
|
3161
|
+
manifest = await fetchJson(input.http, input.cache, {
|
|
3162
|
+
url: input.target.runtime.manifestUrl,
|
|
3163
|
+
cacheKey: `runtime-manifest:${input.target.runtime.component}:${input.target.runtime.platformKey}:${input.target.runtime.manifestSha1}`,
|
|
3164
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3165
|
+
});
|
|
3166
|
+
} catch {
|
|
3167
|
+
record({
|
|
3168
|
+
path: input.target.runtime.manifestUrl,
|
|
3169
|
+
category: VerifyFileCategories.RUNTIME_FILE,
|
|
3170
|
+
status: VerifyFileStatuses.MISSING
|
|
3171
|
+
});
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
const runtimeRoot = targetPaths.runtimeRoot(
|
|
3175
|
+
input.target.directory,
|
|
3176
|
+
input.target.runtime.component,
|
|
3177
|
+
input.target.runtime.installRoot
|
|
3178
|
+
);
|
|
3179
|
+
for (const [relative, entry] of Object.entries(manifest.files)) {
|
|
3180
|
+
if (entry.type !== "file") continue;
|
|
3181
|
+
record(
|
|
3182
|
+
await verifyHashedFile({
|
|
3183
|
+
path: path__default.default.join(runtimeRoot, relative),
|
|
3184
|
+
expectedSha1: entry.downloads.raw.sha1,
|
|
3185
|
+
expectedSize: entry.downloads.raw.size,
|
|
3186
|
+
url: entry.downloads.raw.url,
|
|
3187
|
+
category: VerifyFileCategories.RUNTIME_FILE
|
|
3188
|
+
})
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3192
|
+
);
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
// src/versions/fabric.ts
|
|
3196
|
+
var FabricVersionsApi = class {
|
|
3197
|
+
constructor(ctx) {
|
|
3198
|
+
this.ctx = ctx;
|
|
3199
|
+
}
|
|
3200
|
+
ctx;
|
|
3201
|
+
/** List Fabric loader versions, optionally constrained to a Minecraft version. */
|
|
3202
|
+
async list(input = {}) {
|
|
3203
|
+
if (input.minecraftVersion === void 0) {
|
|
3204
|
+
return fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3205
|
+
url: ApiEndpoints.fabric.loaderVersions(),
|
|
3206
|
+
cacheKey: "fabric-loader-all",
|
|
3207
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
const compat = await fetchJson(
|
|
3211
|
+
this.ctx.http,
|
|
3212
|
+
this.ctx.cache,
|
|
3213
|
+
{
|
|
3214
|
+
url: ApiEndpoints.fabric.loaderForGame(input.minecraftVersion),
|
|
3215
|
+
cacheKey: `fabric-loader-mc:${input.minecraftVersion}`,
|
|
3216
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3217
|
+
}
|
|
3218
|
+
);
|
|
3219
|
+
return compat.map((c) => c.loader);
|
|
3220
|
+
}
|
|
3221
|
+
/** Resolve a Fabric loader version against a Minecraft version. */
|
|
3222
|
+
async resolve(input) {
|
|
3223
|
+
const loaders = await this.list({
|
|
3224
|
+
minecraftVersion: input.minecraftVersion,
|
|
3225
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3226
|
+
});
|
|
3227
|
+
if (loaders.length === 0) {
|
|
3228
|
+
throw new MinecraftKitError(
|
|
3229
|
+
"MANIFEST_NOT_FOUND",
|
|
3230
|
+
`No Fabric loader available for Minecraft ${input.minecraftVersion}`,
|
|
3231
|
+
{ context: { version: input.minecraftVersion } }
|
|
3232
|
+
);
|
|
3233
|
+
}
|
|
3234
|
+
const chosen = pickFabricLoader(loaders, input);
|
|
3235
|
+
if (!chosen) {
|
|
3236
|
+
throw new MinecraftKitError(
|
|
3237
|
+
"MANIFEST_NOT_FOUND",
|
|
3238
|
+
`Fabric loader version not found: ${input.loaderVersion ?? "(none matched)"}`,
|
|
3239
|
+
{ context: { version: input.loaderVersion } }
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3242
|
+
const profile = await fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3243
|
+
url: ApiEndpoints.fabric.profile(input.minecraftVersion, chosen.version),
|
|
3244
|
+
cacheKey: `fabric-profile:${input.minecraftVersion}:${chosen.version}`,
|
|
3245
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3246
|
+
});
|
|
3247
|
+
return {
|
|
3248
|
+
type: Loaders.FABRIC,
|
|
3249
|
+
minecraftVersion: input.minecraftVersion,
|
|
3250
|
+
loaderVersion: chosen.version,
|
|
3251
|
+
profile
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
};
|
|
3255
|
+
function pickFabricLoader(loaders, input) {
|
|
3256
|
+
if (input.loaderVersion !== void 0) {
|
|
3257
|
+
return loaders.find((l) => l.version === input.loaderVersion);
|
|
3258
|
+
}
|
|
3259
|
+
const preference = input.preference ?? VersionPreference.LATEST;
|
|
3260
|
+
if (preference === VersionPreference.RECOMMENDED) {
|
|
3261
|
+
const stable = loaders.find((l) => l.stable);
|
|
3262
|
+
if (stable) return stable;
|
|
3263
|
+
}
|
|
3264
|
+
return loaders[0];
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
// src/core/xml.ts
|
|
3268
|
+
function parseMavenMetadataVersions(xml) {
|
|
3269
|
+
const versions = [];
|
|
3270
|
+
const regex = /<version>\s*([^<]+?)\s*<\/version>/g;
|
|
3271
|
+
for (const match of xml.matchAll(regex)) {
|
|
3272
|
+
if (match[1]) versions.push(match[1]);
|
|
3273
|
+
}
|
|
3274
|
+
return versions;
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
// src/versions/forge.ts
|
|
3278
|
+
var ForgeVersionsApi = class {
|
|
3279
|
+
constructor(ctx) {
|
|
3280
|
+
this.ctx = ctx;
|
|
3281
|
+
}
|
|
3282
|
+
ctx;
|
|
3283
|
+
/** List Forge builds (across all Minecraft versions, or filtered to one). */
|
|
3284
|
+
async list(input = {}) {
|
|
3285
|
+
const xml = await fetchText(this.ctx.http, this.ctx.cache, {
|
|
3286
|
+
url: ApiEndpoints.forge.mavenMetadata(),
|
|
3287
|
+
cacheKey: "forge-maven-metadata",
|
|
3288
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3289
|
+
});
|
|
3290
|
+
const allVersions = parseMavenMetadataVersions(xml);
|
|
3291
|
+
const promotions = await fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3292
|
+
url: ApiEndpoints.forge.promotions(),
|
|
3293
|
+
cacheKey: "forge-promotions",
|
|
3294
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3295
|
+
});
|
|
3296
|
+
const summaries = allVersions.map((fullVersion) => buildSummary(fullVersion, promotions)).filter((s) => s !== null);
|
|
3297
|
+
if (input.minecraftVersion === void 0) return summaries;
|
|
3298
|
+
return summaries.filter((s) => s.minecraftVersion === input.minecraftVersion);
|
|
3299
|
+
}
|
|
3300
|
+
/** Resolve a Forge build for a Minecraft version. */
|
|
3301
|
+
async resolve(input) {
|
|
3302
|
+
const builds = await this.list({
|
|
3303
|
+
minecraftVersion: input.minecraftVersion,
|
|
3304
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3305
|
+
});
|
|
3306
|
+
if (builds.length === 0) {
|
|
3307
|
+
throw new MinecraftKitError(
|
|
3308
|
+
"MANIFEST_NOT_FOUND",
|
|
3309
|
+
`No Forge build available for Minecraft ${input.minecraftVersion}`,
|
|
3310
|
+
{ context: { version: input.minecraftVersion } }
|
|
3311
|
+
);
|
|
3312
|
+
}
|
|
3313
|
+
const chosen = pickForge(builds, input);
|
|
3314
|
+
if (!chosen) {
|
|
3315
|
+
throw new MinecraftKitError(
|
|
3316
|
+
"MANIFEST_NOT_FOUND",
|
|
3317
|
+
`Forge build not found for ${input.minecraftVersion}: ${input.forgeVersion ?? "(none matched)"}`,
|
|
3318
|
+
{ context: { version: input.forgeVersion } }
|
|
3319
|
+
);
|
|
3320
|
+
}
|
|
3321
|
+
return {
|
|
3322
|
+
type: Loaders.FORGE,
|
|
3323
|
+
minecraftVersion: chosen.minecraftVersion,
|
|
3324
|
+
forgeVersion: chosen.forgeVersion,
|
|
3325
|
+
fullVersion: chosen.fullVersion,
|
|
3326
|
+
installerUrl: ApiEndpoints.forge.installer(chosen.fullVersion)
|
|
3327
|
+
};
|
|
3328
|
+
}
|
|
3329
|
+
};
|
|
3330
|
+
function buildSummary(fullVersion, promotions) {
|
|
3331
|
+
const dashIndex = fullVersion.indexOf("-");
|
|
3332
|
+
if (dashIndex <= 0 || dashIndex === fullVersion.length - 1) return null;
|
|
3333
|
+
const minecraftVersion = fullVersion.slice(0, dashIndex);
|
|
3334
|
+
const forgeVersion = fullVersion.slice(dashIndex + 1);
|
|
3335
|
+
const promos = promotions.promos;
|
|
3336
|
+
const recommended = promos[`${minecraftVersion}-recommended`];
|
|
3337
|
+
const latest = promos[`${minecraftVersion}-latest`];
|
|
3338
|
+
return {
|
|
3339
|
+
fullVersion,
|
|
3340
|
+
minecraftVersion,
|
|
3341
|
+
forgeVersion,
|
|
3342
|
+
isRecommended: recommended === forgeVersion,
|
|
3343
|
+
isLatest: latest === forgeVersion
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
function pickForge(builds, input) {
|
|
3347
|
+
if (input.forgeVersion !== void 0) {
|
|
3348
|
+
return builds.find(
|
|
3349
|
+
(b) => b.forgeVersion === input.forgeVersion || b.fullVersion === input.forgeVersion
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3352
|
+
const preference = input.preference ?? VersionPreference.RECOMMENDED;
|
|
3353
|
+
if (preference === VersionPreference.RECOMMENDED) {
|
|
3354
|
+
const recommended = builds.find((b) => b.isRecommended);
|
|
3355
|
+
if (recommended) return recommended;
|
|
3356
|
+
}
|
|
3357
|
+
const latest = builds.find((b) => b.isLatest);
|
|
3358
|
+
if (latest) return latest;
|
|
3359
|
+
return builds[builds.length - 1];
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
// src/versions/minecraft.ts
|
|
3363
|
+
var MinecraftVersionsApi = class {
|
|
3364
|
+
constructor(ctx) {
|
|
3365
|
+
this.ctx = ctx;
|
|
3366
|
+
}
|
|
3367
|
+
ctx;
|
|
3368
|
+
/** List all Minecraft versions, optionally filtered by channel. */
|
|
3369
|
+
async list(input = {}) {
|
|
3370
|
+
const root = await this.fetchManifestRoot(input.signal);
|
|
3371
|
+
if (input.channel === void 0) return root.versions;
|
|
3372
|
+
return root.versions.filter((v) => v.type === input.channel);
|
|
3373
|
+
}
|
|
3374
|
+
/** Return the latest version on the given channel (defaults to RELEASE). */
|
|
3375
|
+
async latest(input = {}) {
|
|
3376
|
+
const root = await this.fetchManifestRoot(input.signal);
|
|
3377
|
+
const targetId = input.channel === "snapshot" ? root.latest.snapshot : root.latest.release;
|
|
3378
|
+
const summary = root.versions.find((v) => v.id === targetId);
|
|
3379
|
+
if (!summary) {
|
|
3380
|
+
throw new MinecraftKitError(
|
|
3381
|
+
"MANIFEST_NOT_FOUND",
|
|
3382
|
+
`Latest version ${targetId} not found in manifest`
|
|
3383
|
+
);
|
|
3384
|
+
}
|
|
3385
|
+
return summary;
|
|
3386
|
+
}
|
|
3387
|
+
/** Return a single version summary or throw `MANIFEST_NOT_FOUND`. */
|
|
3388
|
+
async get(input) {
|
|
3389
|
+
const root = await this.fetchManifestRoot(input.signal);
|
|
3390
|
+
const summary = root.versions.find((v) => v.id === input.version);
|
|
3391
|
+
if (!summary) {
|
|
3392
|
+
throw new MinecraftKitError(
|
|
3393
|
+
"MANIFEST_NOT_FOUND",
|
|
3394
|
+
`Minecraft version not found: ${input.version}`,
|
|
3395
|
+
{ context: { version: input.version } }
|
|
3396
|
+
);
|
|
3397
|
+
}
|
|
3398
|
+
return summary;
|
|
3399
|
+
}
|
|
3400
|
+
/** Fetch and parse the per-version manifest in addition to the summary. */
|
|
3401
|
+
async resolve(input) {
|
|
3402
|
+
const summary = await this.get(input);
|
|
3403
|
+
const manifest = await fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3404
|
+
url: summary.url,
|
|
3405
|
+
cacheKey: `minecraft-manifest:${summary.id}:${summary.sha1}`,
|
|
3406
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3407
|
+
});
|
|
3408
|
+
if (!manifest.id || !manifest.mainClass) {
|
|
3409
|
+
throw new MinecraftKitError(
|
|
3410
|
+
"MANIFEST_INVALID",
|
|
3411
|
+
`Per-version manifest is missing required fields: ${summary.id}`,
|
|
3412
|
+
{ context: { version: summary.id, url: summary.url } }
|
|
3413
|
+
);
|
|
3414
|
+
}
|
|
3415
|
+
return {
|
|
3416
|
+
version: summary.id,
|
|
3417
|
+
channel: summary.type,
|
|
3418
|
+
manifest,
|
|
3419
|
+
summary
|
|
3420
|
+
};
|
|
3421
|
+
}
|
|
3422
|
+
async fetchManifestRoot(signal) {
|
|
3423
|
+
return fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3424
|
+
url: ApiEndpoints.mojang.versionManifest(),
|
|
3425
|
+
cacheKey: "minecraft-version-manifest-v2",
|
|
3426
|
+
...signal !== void 0 ? { signal } : {}
|
|
3427
|
+
});
|
|
3428
|
+
}
|
|
3429
|
+
};
|
|
3430
|
+
|
|
3431
|
+
// src/constants/runtime.ts
|
|
3432
|
+
var FALLBACK_COMPONENT = RuntimeComponents.JRE_LEGACY;
|
|
3433
|
+
|
|
3434
|
+
// src/versions/runtime.ts
|
|
3435
|
+
var RuntimeVersionsApi = class {
|
|
3436
|
+
constructor(ctx) {
|
|
3437
|
+
this.ctx = ctx;
|
|
3438
|
+
}
|
|
3439
|
+
ctx;
|
|
3440
|
+
/** List available runtime entries for the host platform. */
|
|
3441
|
+
async list(input) {
|
|
3442
|
+
const platformKey = pickPlatformKey(input.system);
|
|
3443
|
+
const index = await this.fetchIndex(input.signal);
|
|
3444
|
+
const platform = index[platformKey];
|
|
3445
|
+
if (!platform) return [];
|
|
3446
|
+
const entries = [];
|
|
3447
|
+
for (const [component, items] of Object.entries(platform)) {
|
|
3448
|
+
for (const item of items) {
|
|
3449
|
+
entries.push({
|
|
3450
|
+
component,
|
|
3451
|
+
platformKey,
|
|
3452
|
+
versionName: item.version.name,
|
|
3453
|
+
released: item.version.released,
|
|
3454
|
+
manifestUrl: item.manifest.url
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
return entries;
|
|
3459
|
+
}
|
|
3460
|
+
/** Resolve a single runtime for the host platform and Minecraft version. */
|
|
3461
|
+
async resolve(input) {
|
|
3462
|
+
const platformKey = pickPlatformKey(input.system);
|
|
3463
|
+
const index = await this.fetchIndex(input.signal);
|
|
3464
|
+
const platform = index[platformKey];
|
|
3465
|
+
if (!platform) {
|
|
3466
|
+
throw new MinecraftKitError(
|
|
3467
|
+
"RUNTIME_UNSUPPORTED_PLATFORM",
|
|
3468
|
+
`No runtimes published for platform: ${platformKey}`,
|
|
3469
|
+
{ context: { platform: platformKey } }
|
|
3470
|
+
);
|
|
3471
|
+
}
|
|
3472
|
+
const component = input.component ?? FALLBACK_COMPONENT;
|
|
3473
|
+
const candidates = platform[component] ?? [];
|
|
3474
|
+
if (candidates.length === 0) {
|
|
3475
|
+
const all = Object.entries(platform);
|
|
3476
|
+
const preference = input.preference ?? RuntimePreference.RECOMMENDED;
|
|
3477
|
+
if (preference === RuntimePreference.LATEST) {
|
|
3478
|
+
const fallback = pickLatestAcrossComponents(all);
|
|
3479
|
+
if (fallback) {
|
|
3480
|
+
return toResolved(fallback.component, platformKey, fallback.entry, input.system);
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
throw new MinecraftKitError(
|
|
3484
|
+
"RUNTIME_NOT_FOUND",
|
|
3485
|
+
`Runtime component ${component} not available on ${platformKey}`,
|
|
3486
|
+
{ context: { platform: platformKey, version: component } }
|
|
3487
|
+
);
|
|
3488
|
+
}
|
|
3489
|
+
const entry = candidates[0];
|
|
3490
|
+
if (!entry) {
|
|
3491
|
+
throw new MinecraftKitError(
|
|
3492
|
+
"RUNTIME_NOT_FOUND",
|
|
3493
|
+
`Runtime component ${component} list is empty for ${platformKey}`,
|
|
3494
|
+
{ context: { platform: platformKey, version: component } }
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
return toResolved(component, platformKey, entry, input.system);
|
|
3498
|
+
}
|
|
3499
|
+
async fetchIndex(signal) {
|
|
3500
|
+
return fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3501
|
+
url: ApiEndpoints.mojang.runtimeIndex(),
|
|
3502
|
+
cacheKey: "mojang-runtime-index",
|
|
3503
|
+
...signal !== void 0 ? { signal } : {}
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
};
|
|
3507
|
+
function pickPlatformKey(system) {
|
|
3508
|
+
const archMap = RUNTIME_PLATFORM_KEYS[system.os];
|
|
3509
|
+
return archMap[system.arch];
|
|
3510
|
+
}
|
|
3511
|
+
function pickLatestAcrossComponents(entries) {
|
|
3512
|
+
let bestComponent = null;
|
|
3513
|
+
let bestEntry = null;
|
|
3514
|
+
for (const [component, list] of entries) {
|
|
3515
|
+
for (const entry of list) {
|
|
3516
|
+
if (!bestEntry || entry.version.released > bestEntry.version.released) {
|
|
3517
|
+
bestComponent = component;
|
|
3518
|
+
bestEntry = entry;
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
if (!bestComponent || !bestEntry) return null;
|
|
3523
|
+
return { component: bestComponent, entry: bestEntry };
|
|
3524
|
+
}
|
|
3525
|
+
function toResolved(component, platformKey, entry, system) {
|
|
3526
|
+
return {
|
|
3527
|
+
component,
|
|
3528
|
+
platformKey,
|
|
3529
|
+
versionName: entry.version.name,
|
|
3530
|
+
system,
|
|
3531
|
+
manifestUrl: entry.manifest.url,
|
|
3532
|
+
manifestSha1: entry.manifest.sha1
|
|
3533
|
+
};
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
// src/kit.ts
|
|
3537
|
+
var MinecraftKit = class {
|
|
3538
|
+
versions;
|
|
3539
|
+
targets;
|
|
3540
|
+
install;
|
|
3541
|
+
update;
|
|
3542
|
+
verify;
|
|
3543
|
+
repair;
|
|
3544
|
+
launch;
|
|
3545
|
+
/** Cache surface useful for advanced consumers (e.g. clearing between operations). */
|
|
3546
|
+
cache;
|
|
3547
|
+
constructor(options = {}) {
|
|
3548
|
+
const http = options.httpClient ?? new FetchHttpClient();
|
|
3549
|
+
const cache = options.cache ?? createMemoryCache();
|
|
3550
|
+
const logger = options.logger ?? silentLogger;
|
|
3551
|
+
const system = options.system ?? detectSystem();
|
|
3552
|
+
const spawner = options.spawner ?? new ChildProcessSpawner();
|
|
3553
|
+
const ctx = { http, cache, logger };
|
|
3554
|
+
const minecraft = new MinecraftVersionsApi(ctx);
|
|
3555
|
+
const fabric = new FabricVersionsApi(ctx);
|
|
3556
|
+
const forge = new ForgeVersionsApi(ctx);
|
|
3557
|
+
const runtime = new RuntimeVersionsApi(ctx);
|
|
3558
|
+
this.versions = { minecraft, fabric, forge, runtime };
|
|
3559
|
+
this.targets = new TargetsApi({ minecraft, fabric, forge, runtime, system });
|
|
3560
|
+
this.cache = cache;
|
|
3561
|
+
const carry = (opts) => ({
|
|
3562
|
+
...opts?.signal !== void 0 ? { signal: opts.signal } : {},
|
|
3563
|
+
...opts?.onEvent !== void 0 ? { onEvent: opts.onEvent } : {}
|
|
3564
|
+
});
|
|
3565
|
+
const runInstallPlan = (plan, opts) => runInstall({ plan, http, cache, spawner, ...carry(opts) });
|
|
3566
|
+
this.install = {
|
|
3567
|
+
plan: (target, opts) => planInstall({ target, http, cache, ...carry(opts) }),
|
|
3568
|
+
run: runInstallPlan,
|
|
3569
|
+
runtime: {
|
|
3570
|
+
plan: (target, opts) => planRuntimeInstall({ target, http, cache, ...carry(opts) }),
|
|
3571
|
+
run: runInstallPlan,
|
|
3572
|
+
standalonePlan: (input) => planStandaloneRuntimeInstall({ ...input, http, cache })
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
this.update = {
|
|
3576
|
+
plan: (target, opts) => planUpdate({ target, http, cache, ...carry(opts) }),
|
|
3577
|
+
run: (plan, opts) => runUpdate({ plan, http, cache, spawner, ...carry(opts) })
|
|
3578
|
+
};
|
|
3579
|
+
const verifyArgs = (target, opts) => ({
|
|
3580
|
+
target,
|
|
3581
|
+
http,
|
|
3582
|
+
cache,
|
|
3583
|
+
...carry(opts)
|
|
3584
|
+
});
|
|
3585
|
+
this.verify = {
|
|
3586
|
+
minecraft: { run: (target, opts) => verifyMinecraft(verifyArgs(target, opts)) },
|
|
3587
|
+
fabric: { run: (target, opts) => verifyFabric(verifyArgs(target, opts)) },
|
|
3588
|
+
forge: { run: (target, opts) => verifyForge(verifyArgs(target, opts)) },
|
|
3589
|
+
runtime: { run: (target, opts) => verifyRuntime(verifyArgs(target, opts)) }
|
|
3590
|
+
};
|
|
3591
|
+
const repairArgs = (target, opts) => ({
|
|
3592
|
+
target,
|
|
3593
|
+
from: opts.from,
|
|
3594
|
+
http,
|
|
3595
|
+
cache,
|
|
3596
|
+
...carry({ ...opts.signal !== void 0 ? { signal: opts.signal } : {} })
|
|
3597
|
+
});
|
|
3598
|
+
const runRepairPlan = (plan, opts) => runRepair({ plan, http, cache, spawner, ...carry(opts) });
|
|
3599
|
+
this.repair = {
|
|
3600
|
+
minecraft: {
|
|
3601
|
+
plan: (target, opts) => planMinecraftRepair(repairArgs(target, opts)),
|
|
3602
|
+
run: runRepairPlan
|
|
3603
|
+
},
|
|
3604
|
+
fabric: {
|
|
3605
|
+
plan: (target, opts) => planFabricRepair(repairArgs(target, opts)),
|
|
3606
|
+
run: runRepairPlan
|
|
3607
|
+
},
|
|
3608
|
+
forge: {
|
|
3609
|
+
plan: (target, opts) => planForgeRepair(repairArgs(target, opts)),
|
|
3610
|
+
run: runRepairPlan
|
|
3611
|
+
},
|
|
3612
|
+
runtime: {
|
|
3613
|
+
plan: (target, opts) => planRuntimeRepair(repairArgs(target, opts)),
|
|
3614
|
+
run: runRepairPlan
|
|
3615
|
+
}
|
|
3616
|
+
};
|
|
3617
|
+
this.launch = {
|
|
3618
|
+
compose: (target, opts) => composeLaunch({ target, options: opts }),
|
|
3619
|
+
run: (composition, opts) => runLaunch({
|
|
3620
|
+
composition,
|
|
3621
|
+
...opts !== void 0 ? { options: opts } : {},
|
|
3622
|
+
spawner
|
|
3623
|
+
})
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
};
|
|
3627
|
+
|
|
3628
|
+
// src/types/events.ts
|
|
3629
|
+
var EventTypes = {
|
|
3630
|
+
INSTALL_PHASE_CHANGED: "install:phase-changed",
|
|
3631
|
+
DOWNLOAD_STARTED: "download:started",
|
|
3632
|
+
DOWNLOAD_PROGRESS: "download:progress",
|
|
3633
|
+
DOWNLOAD_SKIPPED: "download:skipped",
|
|
3634
|
+
DOWNLOAD_COMPLETED: "download:completed",
|
|
3635
|
+
DOWNLOAD_FAILED: "download:failed",
|
|
3636
|
+
INTEGRITY_VERIFIED: "integrity:verified",
|
|
3637
|
+
INTEGRITY_MISMATCH: "integrity:mismatch",
|
|
3638
|
+
ARCHIVE_EXTRACTED: "archive:extracted",
|
|
3639
|
+
FORGE_PROCESSOR_STARTED: "forge:processor-started",
|
|
3640
|
+
FORGE_PROCESSOR_COMPLETED: "forge:processor-completed",
|
|
3641
|
+
FORGE_PROCESSOR_OUTPUT_VERIFIED: "forge:processor-output-verified",
|
|
3642
|
+
VERIFY_FILE_CHECKED: "verify:file-checked",
|
|
3643
|
+
VERIFY_COMPLETED: "verify:completed",
|
|
3644
|
+
REPAIR_PHASE_CHANGED: "repair:phase-changed",
|
|
3645
|
+
LAUNCH_STARTING: "launch:starting",
|
|
3646
|
+
LAUNCH_STARTED: "launch:started",
|
|
3647
|
+
LAUNCH_STDOUT: "launch:stdout",
|
|
3648
|
+
LAUNCH_STDERR: "launch:stderr",
|
|
3649
|
+
LAUNCH_EXITED: "launch:exited",
|
|
3650
|
+
LAUNCH_ABORTED: "launch:aborted"
|
|
3651
|
+
};
|
|
3652
|
+
|
|
3653
|
+
// src/types/minecraft.ts
|
|
3654
|
+
var MinecraftChannels = {
|
|
3655
|
+
RELEASE: "release",
|
|
3656
|
+
SNAPSHOT: "snapshot",
|
|
3657
|
+
OLD_BETA: "old_beta",
|
|
3658
|
+
OLD_ALPHA: "old_alpha"
|
|
3659
|
+
};
|
|
3660
|
+
|
|
3661
|
+
// src/types/repair.ts
|
|
3662
|
+
var RepairPhases = {
|
|
3663
|
+
PLANNING: "planning",
|
|
3664
|
+
REPAIRING_CLIENT_JAR: "repairing-client-jar",
|
|
3665
|
+
REPAIRING_LIBRARIES: "repairing-libraries",
|
|
3666
|
+
REPAIRING_ASSETS: "repairing-assets",
|
|
3667
|
+
REPAIRING_NATIVES: "repairing-natives",
|
|
3668
|
+
REPAIRING_RUNTIME: "repairing-runtime",
|
|
3669
|
+
REPAIRING_LOADER: "repairing-loader",
|
|
3670
|
+
COMPLETED: "completed"
|
|
3671
|
+
};
|
|
3672
|
+
|
|
3673
|
+
// src/types/system.ts
|
|
3674
|
+
var OperatingSystems = {
|
|
3675
|
+
WINDOWS: "windows",
|
|
3676
|
+
OSX: "osx",
|
|
3677
|
+
LINUX: "linux"
|
|
3678
|
+
};
|
|
3679
|
+
var Architectures = {
|
|
3680
|
+
X86: "x86",
|
|
3681
|
+
X64: "x64",
|
|
3682
|
+
ARM64: "arm64"
|
|
3683
|
+
};
|
|
3684
|
+
|
|
3685
|
+
// src/constants/placeholders.ts
|
|
3686
|
+
var LAUNCH_PLACEHOLDERS = {
|
|
3687
|
+
"${auth_player_name}": "Player display name.",
|
|
3688
|
+
"${version_name}": "Resolved Minecraft version id.",
|
|
3689
|
+
"${game_directory}": "Per-target directory.",
|
|
3690
|
+
"${assets_root}": "Assets root (`<directory>/assets`).",
|
|
3691
|
+
"${assets_index_name}": "Asset index id from the manifest.",
|
|
3692
|
+
"${auth_uuid}": "Player UUID (no dashes).",
|
|
3693
|
+
"${auth_access_token}": "Yggdrasil/MSA access token.",
|
|
3694
|
+
"${auth_session}": "Legacy session token (`token:<token>:<uuid>`).",
|
|
3695
|
+
"${clientid}": "MSA client id.",
|
|
3696
|
+
"${auth_xuid}": "Xbox user id.",
|
|
3697
|
+
"${user_type}": "`msa` | `mojang` | `legacy`.",
|
|
3698
|
+
"${user_properties}": "User properties JSON (often `{}`).",
|
|
3699
|
+
"${version_type}": "Channel string, e.g. `release`.",
|
|
3700
|
+
"${game_assets}": "Legacy virtual assets directory.",
|
|
3701
|
+
"${resolution_width}": "Window width (feature-gated).",
|
|
3702
|
+
"${resolution_height}": "Window height (feature-gated).",
|
|
3703
|
+
"${natives_directory}": "Extracted natives directory.",
|
|
3704
|
+
"${classpath}": "Joined classpath of libraries + version jar.",
|
|
3705
|
+
"${classpath_separator}": "OS-specific classpath separator (`:` / `;`).",
|
|
3706
|
+
"${library_directory}": "Per-target libraries directory.",
|
|
3707
|
+
"${launcher_name}": "Launcher brand string.",
|
|
3708
|
+
"${launcher_version}": "Launcher version string.",
|
|
3709
|
+
"${path}": "Path to the log4j config file (logging.client.argument only)."
|
|
3710
|
+
};
|
|
3711
|
+
|
|
3712
|
+
exports.ASSETS_DIR = ASSETS_DIR;
|
|
3713
|
+
exports.ASSETS_INDEXES_DIR = ASSETS_INDEXES_DIR;
|
|
3714
|
+
exports.ASSETS_LEGACY_DIR = ASSETS_LEGACY_DIR;
|
|
3715
|
+
exports.ASSETS_LOG_CONFIGS_DIR = ASSETS_LOG_CONFIGS_DIR;
|
|
3716
|
+
exports.ASSETS_OBJECTS_DIR = ASSETS_OBJECTS_DIR;
|
|
3717
|
+
exports.ASSETS_RESOURCES_DIR = ASSETS_RESOURCES_DIR;
|
|
3718
|
+
exports.ASSETS_VIRTUAL_DIR = ASSETS_VIRTUAL_DIR;
|
|
3719
|
+
exports.ApiEndpoints = ApiEndpoints;
|
|
3720
|
+
exports.Architectures = Architectures;
|
|
3721
|
+
exports.AuthModes = AuthModes;
|
|
3722
|
+
exports.BASE_JVM_ARGS = BASE_JVM_ARGS;
|
|
3723
|
+
exports.CACHE_MAX_ENTRIES = CACHE_MAX_ENTRIES;
|
|
3724
|
+
exports.CACHE_TTL_MS = CACHE_TTL_MS;
|
|
3725
|
+
exports.ChildProcessSpawner = ChildProcessSpawner;
|
|
3726
|
+
exports.DEFAULT_KILL_GRACE_MS = DEFAULT_KILL_GRACE_MS;
|
|
3727
|
+
exports.DEFAULT_LAUNCHER_NAME = DEFAULT_LAUNCHER_NAME;
|
|
3728
|
+
exports.DEFAULT_LAUNCHER_VERSION = DEFAULT_LAUNCHER_VERSION;
|
|
3729
|
+
exports.DEFAULT_LIBRARY_REPOSITORY = DEFAULT_LIBRARY_REPOSITORY;
|
|
3730
|
+
exports.DEFAULT_MAX_MB = DEFAULT_MAX_MB;
|
|
3731
|
+
exports.DEFAULT_MIN_MB = DEFAULT_MIN_MB;
|
|
3732
|
+
exports.DOWNLOAD_CONCURRENCY = DOWNLOAD_CONCURRENCY;
|
|
3733
|
+
exports.EXTRACTION_MAX_COMPRESSION_RATIO = EXTRACTION_MAX_COMPRESSION_RATIO;
|
|
3734
|
+
exports.EXTRACTION_MAX_ENTRY_COUNT = EXTRACTION_MAX_ENTRY_COUNT;
|
|
3735
|
+
exports.EXTRACTION_MAX_FILE_SIZE = EXTRACTION_MAX_FILE_SIZE;
|
|
3736
|
+
exports.EXTRACTION_MAX_TOTAL_SIZE = EXTRACTION_MAX_TOTAL_SIZE;
|
|
3737
|
+
exports.EventTypes = EventTypes;
|
|
3738
|
+
exports.FABRIC_MAVEN_BASE = FABRIC_MAVEN_BASE;
|
|
3739
|
+
exports.FALLBACK_COMPONENT = FALLBACK_COMPONENT;
|
|
3740
|
+
exports.FORGE_INSTALLERS_DIR = FORGE_INSTALLERS_DIR;
|
|
3741
|
+
exports.FORGE_INSTALLER_MAX_SIZE = FORGE_INSTALLER_MAX_SIZE;
|
|
3742
|
+
exports.FORGE_MAVEN_BASE = FORGE_MAVEN_BASE;
|
|
3743
|
+
exports.FabricVersionsApi = FabricVersionsApi;
|
|
3744
|
+
exports.FetchHttpClient = FetchHttpClient;
|
|
3745
|
+
exports.ForgeVersionsApi = ForgeVersionsApi;
|
|
3746
|
+
exports.HTTP_RETRY_BACKOFF_BASE_MS = HTTP_RETRY_BACKOFF_BASE_MS;
|
|
3747
|
+
exports.HTTP_RETRY_BACKOFF_CAP_MS = HTTP_RETRY_BACKOFF_CAP_MS;
|
|
3748
|
+
exports.HTTP_RETRY_MAX = HTTP_RETRY_MAX;
|
|
3749
|
+
exports.HTTP_TIMEOUT_MS = HTTP_TIMEOUT_MS;
|
|
3750
|
+
exports.InstallActionKinds = InstallActionKinds;
|
|
3751
|
+
exports.InstallPhases = InstallPhases;
|
|
3752
|
+
exports.JAVA_EXECUTABLE = JAVA_EXECUTABLE;
|
|
3753
|
+
exports.LAUNCH_PLACEHOLDERS = LAUNCH_PLACEHOLDERS;
|
|
3754
|
+
exports.LEGACY_JVM_ARGS = LEGACY_JVM_ARGS;
|
|
3755
|
+
exports.LIBRARIES_DIR = LIBRARIES_DIR;
|
|
3756
|
+
exports.Loaders = Loaders;
|
|
3757
|
+
exports.LogLevels = LogLevels;
|
|
3758
|
+
exports.MACOS_JVM_ARGS = MACOS_JVM_ARGS;
|
|
3759
|
+
exports.MAC_RUNTIME_PREFIX = MAC_RUNTIME_PREFIX;
|
|
3760
|
+
exports.MAX_PROCESSOR_STDERR_LINES = MAX_PROCESSOR_STDERR_LINES;
|
|
3761
|
+
exports.MinecraftChannels = MinecraftChannels;
|
|
3762
|
+
exports.MinecraftKit = MinecraftKit;
|
|
3763
|
+
exports.MinecraftKitError = MinecraftKitError;
|
|
3764
|
+
exports.MinecraftVersionsApi = MinecraftVersionsApi;
|
|
3765
|
+
exports.NATIVES_DIR_NAME = NATIVES_DIR_NAME;
|
|
3766
|
+
exports.NODE_ARCH_TO_MOJANG_ARCH = NODE_ARCH_TO_MOJANG_ARCH;
|
|
3767
|
+
exports.NODE_PLATFORM_TO_MOJANG_OS = NODE_PLATFORM_TO_MOJANG_OS;
|
|
3768
|
+
exports.OperatingSystems = OperatingSystems;
|
|
3769
|
+
exports.PROGRESS_EVENT_INTERVAL_MS = PROGRESS_EVENT_INTERVAL_MS;
|
|
3770
|
+
exports.RUNTIMES_DIR = RUNTIMES_DIR;
|
|
3771
|
+
exports.RUNTIME_PLATFORM_KEYS = RUNTIME_PLATFORM_KEYS;
|
|
3772
|
+
exports.RepairPhases = RepairPhases;
|
|
3773
|
+
exports.RuntimeComponents = RuntimeComponents;
|
|
3774
|
+
exports.RuntimePreference = RuntimePreference;
|
|
3775
|
+
exports.RuntimeVersionsApi = RuntimeVersionsApi;
|
|
3776
|
+
exports.SPAWNER_MAX_LINE_BYTES = SPAWNER_MAX_LINE_BYTES;
|
|
3777
|
+
exports.TargetsApi = TargetsApi;
|
|
3778
|
+
exports.USER_AGENT = USER_AGENT;
|
|
3779
|
+
exports.VERSIONS_DIR = VERSIONS_DIR;
|
|
3780
|
+
exports.VerificationKinds = VerificationKinds;
|
|
3781
|
+
exports.VerifyFileCategories = VerifyFileCategories;
|
|
3782
|
+
exports.VerifyFileStatuses = VerifyFileStatuses;
|
|
3783
|
+
exports.VersionPreference = VersionPreference;
|
|
3784
|
+
exports.consoleLogger = consoleLogger;
|
|
3785
|
+
exports.createMemoryCache = createMemoryCache;
|
|
3786
|
+
exports.detectSystem = detectSystem;
|
|
3787
|
+
exports.isErrorCode = isErrorCode;
|
|
3788
|
+
exports.isMinecraftKitError = isMinecraftKitError;
|
|
3789
|
+
exports.offlineUuidFor = offlineUuidFor;
|
|
3790
|
+
exports.planFabricRepair = planFabricRepair;
|
|
3791
|
+
exports.planForgeRepair = planForgeRepair;
|
|
3792
|
+
exports.planMinecraftRepair = planMinecraftRepair;
|
|
3793
|
+
exports.planRuntimeInstall = planRuntimeInstall;
|
|
3794
|
+
exports.planRuntimeRepair = planRuntimeRepair;
|
|
3795
|
+
exports.planStandaloneRuntimeInstall = planStandaloneRuntimeInstall;
|
|
3796
|
+
exports.runRepair = runRepair;
|
|
3797
|
+
exports.silentLogger = silentLogger;
|
|
3798
|
+
exports.stripUuidDashes = stripUuidDashes;
|
|
3799
|
+
exports.verifyFabric = verifyFabric;
|
|
3800
|
+
exports.verifyForge = verifyForge;
|
|
3801
|
+
exports.verifyMinecraft = verifyMinecraft;
|
|
3802
|
+
exports.verifyRuntime = verifyRuntime;
|
|
3803
|
+
//# sourceMappingURL=index.cjs.map
|
|
3804
|
+
//# sourceMappingURL=index.cjs.map
|