@loontail/minecraft-kit 0.2.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/LICENSE +21 -0
- package/LICENSE.md +22 -0
- package/README.md +54 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +5010 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +1709 -0
- package/dist/index.js +3693 -0
- package/dist/index.js.map +1 -0
- package/package.json +88 -0
|
@@ -0,0 +1,5010 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process2 from 'process';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { LRUCache } from 'lru-cache';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { createWriteStream, createReadStream } from 'fs';
|
|
7
|
+
import { pipeline } from 'stream/promises';
|
|
8
|
+
import yauzl from 'yauzl';
|
|
9
|
+
import crypto2 from 'crypto';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import { Readable } from 'stream';
|
|
12
|
+
import pLimit from 'p-limit';
|
|
13
|
+
import { Buffer as Buffer$1 } from 'buffer';
|
|
14
|
+
import { spawn } from 'child_process';
|
|
15
|
+
|
|
16
|
+
// src/core/logger.ts
|
|
17
|
+
var silentLogger = {
|
|
18
|
+
log() {
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/constants/platform.ts
|
|
23
|
+
var NODE_PLATFORM_TO_MOJANG_OS = {
|
|
24
|
+
win32: "windows",
|
|
25
|
+
darwin: "osx",
|
|
26
|
+
linux: "linux"
|
|
27
|
+
};
|
|
28
|
+
var NODE_ARCH_TO_MOJANG_ARCH = {
|
|
29
|
+
x64: "x64",
|
|
30
|
+
ia32: "x86",
|
|
31
|
+
arm64: "arm64"
|
|
32
|
+
};
|
|
33
|
+
var RUNTIME_PLATFORM_KEYS = {
|
|
34
|
+
windows: { x64: "windows-x64", x86: "windows-x86", arm64: "windows-arm64" },
|
|
35
|
+
osx: { x64: "mac-os", arm64: "mac-os-arm64", x86: "mac-os" },
|
|
36
|
+
linux: { x64: "linux", x86: "linux-i386", arm64: "linux" }
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// src/core/errors.ts
|
|
40
|
+
var MinecraftKitError = class extends Error {
|
|
41
|
+
name = "MinecraftKitError";
|
|
42
|
+
/** Stable discriminator. */
|
|
43
|
+
code;
|
|
44
|
+
/** Structured context; safe to serialize. */
|
|
45
|
+
context;
|
|
46
|
+
constructor(code, message, options = {}) {
|
|
47
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
48
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
49
|
+
this.code = code;
|
|
50
|
+
this.context = Object.freeze({ ...options.context ?? {} });
|
|
51
|
+
}
|
|
52
|
+
/** JSON-friendly representation. */
|
|
53
|
+
toJSON() {
|
|
54
|
+
return {
|
|
55
|
+
name: this.name,
|
|
56
|
+
code: this.code,
|
|
57
|
+
message: this.message,
|
|
58
|
+
context: this.context,
|
|
59
|
+
cause: this.cause instanceof Error ? { name: this.cause.name, message: this.cause.message } : this.cause
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
function isMinecraftKitError(e) {
|
|
64
|
+
return e instanceof MinecraftKitError;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/core/system.ts
|
|
68
|
+
function detectSystem(input = {}) {
|
|
69
|
+
const platform = input.platform ?? process.platform;
|
|
70
|
+
const arch = input.arch ?? process.arch;
|
|
71
|
+
const osVersion = input.osVersion ?? os.release();
|
|
72
|
+
const mojangOs = NODE_PLATFORM_TO_MOJANG_OS[platform];
|
|
73
|
+
const mojangArch = NODE_ARCH_TO_MOJANG_ARCH[arch];
|
|
74
|
+
if (mojangOs === void 0 || mojangArch === void 0) {
|
|
75
|
+
throw new MinecraftKitError(
|
|
76
|
+
"RUNTIME_UNSUPPORTED_PLATFORM",
|
|
77
|
+
`Unsupported platform/arch combination: ${platform}/${arch}`,
|
|
78
|
+
{ context: { platform, arch: String(arch) } }
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return { os: mojangOs, arch: mojangArch, osVersion };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/constants/defaults.ts
|
|
85
|
+
var HTTP_TIMEOUT_MS = 3e4;
|
|
86
|
+
var HTTP_RETRY_MAX = 4;
|
|
87
|
+
var HTTP_RETRY_BACKOFF_BASE_MS = 500;
|
|
88
|
+
var HTTP_RETRY_BACKOFF_CAP_MS = 3e4;
|
|
89
|
+
var DOWNLOAD_CONCURRENCY = 32;
|
|
90
|
+
var CACHE_TTL_MS = 5 * 6e4;
|
|
91
|
+
var CACHE_MAX_ENTRIES = 256;
|
|
92
|
+
var USER_AGENT = "minecraft-kit/0.1";
|
|
93
|
+
var DEFAULT_LAUNCHER_NAME = "minecraft-kit";
|
|
94
|
+
var DEFAULT_LAUNCHER_VERSION = "0.1.0";
|
|
95
|
+
var DEFAULT_MIN_MB = 1024;
|
|
96
|
+
var DEFAULT_MAX_MB = 4096;
|
|
97
|
+
var DEFAULT_KILL_GRACE_MS = 5e3;
|
|
98
|
+
var MAX_PROCESSOR_STDERR_LINES = 20;
|
|
99
|
+
var SPAWNER_MAX_LINE_BYTES = 64 * 1024;
|
|
100
|
+
|
|
101
|
+
// src/http/cache.ts
|
|
102
|
+
function createMemoryCache(options = {}) {
|
|
103
|
+
const cache = new LRUCache({
|
|
104
|
+
max: options.maxEntries ?? CACHE_MAX_ENTRIES,
|
|
105
|
+
ttl: options.ttlMs ?? CACHE_TTL_MS
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
get(key) {
|
|
109
|
+
const wrapped = cache.get(key);
|
|
110
|
+
return wrapped === void 0 ? void 0 : wrapped.value;
|
|
111
|
+
},
|
|
112
|
+
set(key, value, ttlMs) {
|
|
113
|
+
const wrapped = { value };
|
|
114
|
+
if (ttlMs === void 0) {
|
|
115
|
+
cache.set(key, wrapped);
|
|
116
|
+
} else {
|
|
117
|
+
cache.set(key, wrapped, { ttl: ttlMs });
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
delete(key) {
|
|
121
|
+
cache.delete(key);
|
|
122
|
+
},
|
|
123
|
+
clear() {
|
|
124
|
+
cache.clear();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/http/client.ts
|
|
130
|
+
var TIMEOUT_REASON = /* @__PURE__ */ Symbol("http-timeout");
|
|
131
|
+
var FetchHttpClient = class {
|
|
132
|
+
async request(url, options = {}) {
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
const timeoutMs = options.timeoutMs ?? HTTP_TIMEOUT_MS;
|
|
135
|
+
const onParentAbort = () => controller.abort(options.signal?.reason);
|
|
136
|
+
if (options.signal) {
|
|
137
|
+
if (options.signal.aborted) {
|
|
138
|
+
controller.abort(options.signal.reason);
|
|
139
|
+
} else {
|
|
140
|
+
options.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const timer = setTimeout(() => controller.abort(TIMEOUT_REASON), timeoutMs);
|
|
144
|
+
let response;
|
|
145
|
+
try {
|
|
146
|
+
response = await fetch(url, {
|
|
147
|
+
method: "GET",
|
|
148
|
+
headers: { "user-agent": USER_AGENT, ...options.headers ?? {} },
|
|
149
|
+
signal: controller.signal,
|
|
150
|
+
redirect: "follow"
|
|
151
|
+
});
|
|
152
|
+
} catch (cause) {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
options.signal?.removeEventListener("abort", onParentAbort);
|
|
155
|
+
if (controller.signal.reason === TIMEOUT_REASON) {
|
|
156
|
+
throw new MinecraftKitError("NETWORK_TIMEOUT", `Request timed out: ${url}`, {
|
|
157
|
+
cause,
|
|
158
|
+
context: { url, timeoutMs }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (options.signal?.aborted) {
|
|
162
|
+
throw new MinecraftKitError("NETWORK_ABORTED", `Request aborted: ${url}`, {
|
|
163
|
+
cause,
|
|
164
|
+
context: { url }
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
throw new MinecraftKitError("NETWORK_HTTP_ERROR", `Network request failed: ${url}`, {
|
|
168
|
+
cause,
|
|
169
|
+
context: { url }
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
options.signal?.removeEventListener("abort", onParentAbort);
|
|
174
|
+
if (!response.ok) {
|
|
175
|
+
throw new MinecraftKitError("NETWORK_HTTP_ERROR", `HTTP ${response.status} for ${url}`, {
|
|
176
|
+
context: { url, httpStatus: response.status }
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return new FetchHttpResponse(response, url);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
var FetchHttpResponse = class {
|
|
183
|
+
constructor(response, url) {
|
|
184
|
+
this.response = response;
|
|
185
|
+
this.status = response.status;
|
|
186
|
+
this.url = url;
|
|
187
|
+
const headers = {};
|
|
188
|
+
response.headers.forEach((value, key) => {
|
|
189
|
+
headers[key.toLowerCase()] = value;
|
|
190
|
+
});
|
|
191
|
+
this.headers = headers;
|
|
192
|
+
}
|
|
193
|
+
response;
|
|
194
|
+
status;
|
|
195
|
+
headers;
|
|
196
|
+
url;
|
|
197
|
+
async text() {
|
|
198
|
+
return this.response.text();
|
|
199
|
+
}
|
|
200
|
+
async json() {
|
|
201
|
+
return await this.response.json();
|
|
202
|
+
}
|
|
203
|
+
async bytes() {
|
|
204
|
+
const buf = await this.response.arrayBuffer();
|
|
205
|
+
return new Uint8Array(buf);
|
|
206
|
+
}
|
|
207
|
+
async *stream() {
|
|
208
|
+
const body = this.response.body;
|
|
209
|
+
if (!body) {
|
|
210
|
+
const buf = await this.bytes();
|
|
211
|
+
yield buf;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const reader = body.getReader();
|
|
215
|
+
try {
|
|
216
|
+
while (true) {
|
|
217
|
+
const { value, done } = await reader.read();
|
|
218
|
+
if (done) return;
|
|
219
|
+
if (value) yield value;
|
|
220
|
+
}
|
|
221
|
+
} finally {
|
|
222
|
+
reader.releaseLock();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/constants/files.ts
|
|
228
|
+
var VERSIONS_DIR = "versions";
|
|
229
|
+
var LIBRARIES_DIR = "libraries";
|
|
230
|
+
var ASSETS_DIR = "assets";
|
|
231
|
+
var ASSETS_OBJECTS_DIR = "assets/objects";
|
|
232
|
+
var ASSETS_INDEXES_DIR = "assets/indexes";
|
|
233
|
+
var ASSETS_VIRTUAL_DIR = "assets/virtual";
|
|
234
|
+
var ASSETS_LEGACY_DIR = "assets/virtual/legacy";
|
|
235
|
+
var ASSETS_RESOURCES_DIR = "resources";
|
|
236
|
+
var ASSETS_LOG_CONFIGS_DIR = "assets/log_configs";
|
|
237
|
+
var RUNTIMES_DIR = "runtime";
|
|
238
|
+
var NATIVES_DIR_NAME = "natives";
|
|
239
|
+
var FORGE_INSTALLERS_DIR = "forge-installers";
|
|
240
|
+
var JAVA_EXECUTABLE = {
|
|
241
|
+
windows: "bin/javaw.exe",
|
|
242
|
+
linux: "bin/java",
|
|
243
|
+
/** Note: macOS uses an extra `jre.bundle/Contents/Home/` prefix above this. */
|
|
244
|
+
osx: "bin/java"
|
|
245
|
+
};
|
|
246
|
+
var MAC_RUNTIME_PREFIX = "jre.bundle/Contents/Home";
|
|
247
|
+
|
|
248
|
+
// src/core/paths.ts
|
|
249
|
+
var targetPaths = {
|
|
250
|
+
versionsDir: (root) => path.join(root, VERSIONS_DIR),
|
|
251
|
+
versionDir: (root, versionId) => path.join(root, VERSIONS_DIR, versionId),
|
|
252
|
+
versionJar: (root, versionId) => path.join(root, VERSIONS_DIR, versionId, `${versionId}.jar`),
|
|
253
|
+
versionJson: (root, versionId) => path.join(root, VERSIONS_DIR, versionId, `${versionId}.json`),
|
|
254
|
+
librariesDir: (root) => path.join(root, LIBRARIES_DIR),
|
|
255
|
+
libraryFile: (root, libraryPath) => path.join(root, LIBRARIES_DIR, libraryPath),
|
|
256
|
+
assetIndex: (root, indexId) => path.join(root, ASSETS_INDEXES_DIR, `${indexId}.json`),
|
|
257
|
+
assetObject: (root, hash) => path.join(root, ASSETS_OBJECTS_DIR, hash.slice(0, 2), hash),
|
|
258
|
+
assetVirtual: (root, virtualPath) => path.join(root, ASSETS_VIRTUAL_DIR, virtualPath),
|
|
259
|
+
assetLegacy: (root, virtualPath) => path.join(root, ASSETS_LEGACY_DIR, virtualPath),
|
|
260
|
+
assetResource: (root, virtualPath) => path.join(root, ASSETS_RESOURCES_DIR, virtualPath),
|
|
261
|
+
loggingConfig: (root, id) => path.join(root, ASSETS_LOG_CONFIGS_DIR, id),
|
|
262
|
+
nativesDir: (root, versionId) => path.join(root, VERSIONS_DIR, versionId, NATIVES_DIR_NAME),
|
|
263
|
+
/**
|
|
264
|
+
* Path to a runtime component's root directory. Honours `installRoot` (custom global
|
|
265
|
+
* runtime location) when present; otherwise falls back to `<directory>/runtime/<component>`.
|
|
266
|
+
*/
|
|
267
|
+
runtimeRoot: (directory, component, installRoot) => installRoot !== void 0 ? path.join(installRoot, component) : path.join(directory, RUNTIMES_DIR, component),
|
|
268
|
+
runtimeJavaExecutable: (directory, component, os2, installRoot) => {
|
|
269
|
+
const runtime = targetPaths.runtimeRoot(directory, component, installRoot);
|
|
270
|
+
if (os2 === "windows") return path.join(runtime, JAVA_EXECUTABLE.windows);
|
|
271
|
+
if (os2 === "osx") return path.join(runtime, MAC_RUNTIME_PREFIX, JAVA_EXECUTABLE.osx);
|
|
272
|
+
return path.join(runtime, JAVA_EXECUTABLE.linux);
|
|
273
|
+
},
|
|
274
|
+
forgeInstaller: (root, mavenVersion) => path.join(root, FORGE_INSTALLERS_DIR, `forge-${mavenVersion}-installer.jar`)
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/types/install.ts
|
|
278
|
+
var InstallPhases = {
|
|
279
|
+
PLANNING: "planning",
|
|
280
|
+
DOWNLOADING_LIBRARIES: "downloading-libraries",
|
|
281
|
+
EXTRACTING_NATIVES: "extracting-natives",
|
|
282
|
+
INSTALLING_RUNTIME: "installing-runtime",
|
|
283
|
+
RUNNING_FORGE_PROCESSORS: "running-forge-processors",
|
|
284
|
+
WRITING_FILES: "writing-files",
|
|
285
|
+
COMPLETED: "completed"
|
|
286
|
+
};
|
|
287
|
+
var InstallActionKinds = {
|
|
288
|
+
DOWNLOAD_FILE: "download-file",
|
|
289
|
+
EXTRACT_NATIVE: "extract-native",
|
|
290
|
+
RUN_FORGE_PROCESSOR: "run-forge-processor",
|
|
291
|
+
WRITE_VERSION_JSON: "write-version-json",
|
|
292
|
+
WRITE_LOGGING_CONFIG: "write-logging-config"
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/types/loader.ts
|
|
296
|
+
var Loaders = {
|
|
297
|
+
/** Plain vanilla Minecraft, no mod loader. */
|
|
298
|
+
VANILLA: "vanilla",
|
|
299
|
+
/** Fabric mod loader. */
|
|
300
|
+
FABRIC: "fabric",
|
|
301
|
+
/** Modern (1.13+) Forge mod loader. */
|
|
302
|
+
FORGE: "forge"
|
|
303
|
+
};
|
|
304
|
+
var VersionPreference = {
|
|
305
|
+
LATEST: "latest",
|
|
306
|
+
RECOMMENDED: "recommended"
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// src/constants/api.ts
|
|
310
|
+
var PISTON_META = "https://piston-meta.mojang.com";
|
|
311
|
+
var RESOURCES = "https://resources.download.minecraft.net";
|
|
312
|
+
var FABRIC_META = "https://meta.fabricmc.net";
|
|
313
|
+
var FORGE_MAVEN = "https://maven.minecraftforge.net";
|
|
314
|
+
var FORGE_FILES = "https://files.minecraftforge.net";
|
|
315
|
+
var RUNTIME_INDEX_DIGEST = "2ec0cc96c44e5a76b9c8b7c39df7210883d12871";
|
|
316
|
+
var ApiEndpoints = {
|
|
317
|
+
mojang: {
|
|
318
|
+
/** Top-level Minecraft version manifest (v2). */
|
|
319
|
+
versionManifest: () => `${PISTON_META}/mc/game/version_manifest_v2.json`,
|
|
320
|
+
/** Mojang Java-runtime index. */
|
|
321
|
+
runtimeIndex: () => `${PISTON_META}/v1/products/java-runtime/${RUNTIME_INDEX_DIGEST}/all.json`
|
|
322
|
+
},
|
|
323
|
+
resources: {
|
|
324
|
+
/** Hash-addressed Minecraft asset object. */
|
|
325
|
+
asset: (hash) => `${RESOURCES}/${hash.slice(0, 2)}/${hash}`
|
|
326
|
+
},
|
|
327
|
+
fabric: {
|
|
328
|
+
gameVersions: () => `${FABRIC_META}/v2/versions/game`,
|
|
329
|
+
loaderVersions: () => `${FABRIC_META}/v2/versions/loader`,
|
|
330
|
+
loaderForGame: (minecraftVersion) => `${FABRIC_META}/v2/versions/loader/${encodeURIComponent(minecraftVersion)}`,
|
|
331
|
+
profile: (minecraftVersion, loaderVersion) => `${FABRIC_META}/v2/versions/loader/${encodeURIComponent(minecraftVersion)}/${encodeURIComponent(loaderVersion)}/profile/json`
|
|
332
|
+
},
|
|
333
|
+
forge: {
|
|
334
|
+
/** Forge Maven listing of all builds across all MC versions. */
|
|
335
|
+
mavenMetadata: () => `${FORGE_MAVEN}/net/minecraftforge/forge/maven-metadata.xml`,
|
|
336
|
+
/** Slim "recommended" / "latest" promotion mapping. */
|
|
337
|
+
promotions: () => `${FORGE_FILES}/net/minecraftforge/forge/promotions_slim.json`,
|
|
338
|
+
/** URL of the modern installer JAR for a Maven version (e.g. `1.20.1-47.2.0`). */
|
|
339
|
+
installer: (mavenVersion) => {
|
|
340
|
+
const filename = `forge-${mavenVersion}-installer.jar`;
|
|
341
|
+
return `${FORGE_MAVEN}/net/minecraftforge/forge/${mavenVersion}/${filename}`;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// src/http/metadata.ts
|
|
347
|
+
async function fetchJson(http, cache, input) {
|
|
348
|
+
const key = input.cacheKey ?? `json:${input.url}`;
|
|
349
|
+
const cached = cache.get(key);
|
|
350
|
+
if (cached !== void 0) {
|
|
351
|
+
return cached;
|
|
352
|
+
}
|
|
353
|
+
const requestOptions = {};
|
|
354
|
+
if (input.signal !== void 0) requestOptions.signal = input.signal;
|
|
355
|
+
const response = await http.request(input.url, requestOptions);
|
|
356
|
+
const value = await response.json();
|
|
357
|
+
cache.set(key, value, input.ttlMs ?? CACHE_TTL_MS);
|
|
358
|
+
return value;
|
|
359
|
+
}
|
|
360
|
+
async function fetchText(http, cache, input) {
|
|
361
|
+
const key = input.cacheKey ?? `text:${input.url}`;
|
|
362
|
+
const cached = cache.get(key);
|
|
363
|
+
if (cached !== void 0) {
|
|
364
|
+
return cached;
|
|
365
|
+
}
|
|
366
|
+
const requestOptions = {};
|
|
367
|
+
if (input.signal !== void 0) requestOptions.signal = input.signal;
|
|
368
|
+
const response = await http.request(input.url, requestOptions);
|
|
369
|
+
const text = await response.text();
|
|
370
|
+
cache.set(key, text, input.ttlMs ?? CACHE_TTL_MS);
|
|
371
|
+
return text;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/install/assets.ts
|
|
375
|
+
async function planAssetDownloads(input) {
|
|
376
|
+
const indexUrl = input.assetIndex.url;
|
|
377
|
+
const indexPath = targetPaths.assetIndex(input.directory, input.assetIndex.id);
|
|
378
|
+
const indexDocument = await fetchJson(input.http, input.cache, {
|
|
379
|
+
url: indexUrl,
|
|
380
|
+
cacheKey: `asset-index:${input.assetIndex.id}:${input.assetIndex.sha1}`,
|
|
381
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
382
|
+
});
|
|
383
|
+
const actions = [
|
|
384
|
+
{
|
|
385
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
386
|
+
url: indexUrl,
|
|
387
|
+
target: indexPath,
|
|
388
|
+
expectedSha1: input.assetIndex.sha1,
|
|
389
|
+
expectedSize: input.assetIndex.size,
|
|
390
|
+
category: "asset-index"
|
|
391
|
+
}
|
|
392
|
+
];
|
|
393
|
+
const seen = /* @__PURE__ */ new Set();
|
|
394
|
+
for (const entry of Object.values(indexDocument.objects)) {
|
|
395
|
+
if (seen.has(entry.hash)) continue;
|
|
396
|
+
seen.add(entry.hash);
|
|
397
|
+
actions.push({
|
|
398
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
399
|
+
url: ApiEndpoints.resources.asset(entry.hash),
|
|
400
|
+
target: targetPaths.assetObject(input.directory, entry.hash),
|
|
401
|
+
expectedSha1: entry.hash,
|
|
402
|
+
expectedSize: entry.size,
|
|
403
|
+
category: "asset"
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return { actions, indexDocument };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/constants/maven.ts
|
|
410
|
+
var DEFAULT_LIBRARY_REPOSITORY = "https://libraries.minecraft.net/";
|
|
411
|
+
|
|
412
|
+
// src/core/maven.ts
|
|
413
|
+
function parseMavenCoordinate(input) {
|
|
414
|
+
const trimmed = input.startsWith("[") && input.endsWith("]") ? input.slice(1, -1) : input;
|
|
415
|
+
const atIndex = trimmed.indexOf("@");
|
|
416
|
+
const extension = atIndex === -1 ? "jar" : trimmed.slice(atIndex + 1);
|
|
417
|
+
const body = atIndex === -1 ? trimmed : trimmed.slice(0, atIndex);
|
|
418
|
+
const parts = body.split(":");
|
|
419
|
+
if (parts.length < 3 || parts.length > 4) {
|
|
420
|
+
throw new MinecraftKitError("INVALID_INPUT", `Invalid Maven coordinate: ${input}`, {
|
|
421
|
+
context: { input }
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
const [group, artifact, version, classifier] = parts;
|
|
425
|
+
if (!group || !artifact || !version) {
|
|
426
|
+
throw new MinecraftKitError(
|
|
427
|
+
"INVALID_INPUT",
|
|
428
|
+
`Invalid Maven coordinate (missing component): ${input}`,
|
|
429
|
+
{ context: { input } }
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
if (classifier === void 0) {
|
|
433
|
+
return { group, artifact, version, extension };
|
|
434
|
+
}
|
|
435
|
+
return { group, artifact, version, classifier, extension };
|
|
436
|
+
}
|
|
437
|
+
function mavenRelativePath(coord) {
|
|
438
|
+
const groupPath = coord.group.replaceAll(".", "/");
|
|
439
|
+
const classifierSegment = coord.classifier === void 0 ? "" : `-${coord.classifier}`;
|
|
440
|
+
const filename = `${coord.artifact}-${coord.version}${classifierSegment}.${coord.extension}`;
|
|
441
|
+
return `${groupPath}/${coord.artifact}/${coord.version}/${filename}`;
|
|
442
|
+
}
|
|
443
|
+
function mavenRelativePathFor(input) {
|
|
444
|
+
return mavenRelativePath(parseMavenCoordinate(input));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/core/rules.ts
|
|
448
|
+
function evaluateRules(rules, context) {
|
|
449
|
+
if (!rules || rules.length === 0) {
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
let allowed = false;
|
|
453
|
+
for (const rule of rules) {
|
|
454
|
+
if (matchesRule(rule, context)) {
|
|
455
|
+
allowed = rule.action === "allow";
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return allowed;
|
|
459
|
+
}
|
|
460
|
+
function matchesRule(rule, context) {
|
|
461
|
+
if (rule.os !== void 0) {
|
|
462
|
+
if (rule.os.name !== void 0 && rule.os.name !== context.system.os) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
if (rule.os.arch !== void 0 && normalizeArch(rule.os.arch) !== context.system.arch) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
if (rule.os.version !== void 0) {
|
|
469
|
+
try {
|
|
470
|
+
if (!new RegExp(rule.os.version).test(context.system.osVersion)) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (rule.features !== void 0) {
|
|
479
|
+
const features = context.features ?? {};
|
|
480
|
+
for (const [key, expected] of Object.entries(rule.features)) {
|
|
481
|
+
const actual = features[key] === true;
|
|
482
|
+
if (expected !== actual) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
function normalizeArch(arch) {
|
|
490
|
+
return arch === "ia32" ? "x86" : arch;
|
|
491
|
+
}
|
|
492
|
+
function resolveArchPlaceholder(template, archDigit2) {
|
|
493
|
+
return template.replaceAll("${arch}", archDigit2);
|
|
494
|
+
}
|
|
495
|
+
function archDigit(arch) {
|
|
496
|
+
if (arch === "x86") return "32";
|
|
497
|
+
if (arch === "x64") return "64";
|
|
498
|
+
return "64";
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/install/libraries.ts
|
|
502
|
+
function planLibraryDownloads(input) {
|
|
503
|
+
const downloads = [];
|
|
504
|
+
const nativeExtractions = [];
|
|
505
|
+
const classpathFiles = [];
|
|
506
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
507
|
+
const nativesDir = targetPaths.nativesDir(input.directory, input.versionId);
|
|
508
|
+
for (const library of input.libraries) {
|
|
509
|
+
if (!evaluateRules(library.rules, { system: input.system })) continue;
|
|
510
|
+
const artifact = pickPrimaryArtifact(library);
|
|
511
|
+
if (artifact) {
|
|
512
|
+
const targetPath = path.join(
|
|
513
|
+
targetPaths.librariesDir(input.directory),
|
|
514
|
+
artifact.relativePath
|
|
515
|
+
);
|
|
516
|
+
if (!seenPaths.has(targetPath)) {
|
|
517
|
+
seenPaths.add(targetPath);
|
|
518
|
+
downloads.push({
|
|
519
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
520
|
+
url: artifact.url,
|
|
521
|
+
target: targetPath,
|
|
522
|
+
...artifact.sha1 !== void 0 ? { expectedSha1: artifact.sha1 } : {},
|
|
523
|
+
...artifact.size !== void 0 ? { expectedSize: artifact.size } : {},
|
|
524
|
+
category: input.category
|
|
525
|
+
});
|
|
526
|
+
classpathFiles.push(targetPath);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const native = pickNative(library, input.system);
|
|
530
|
+
if (native) {
|
|
531
|
+
const targetPath = path.join(targetPaths.librariesDir(input.directory), native.relativePath);
|
|
532
|
+
if (!seenPaths.has(targetPath)) {
|
|
533
|
+
seenPaths.add(targetPath);
|
|
534
|
+
downloads.push({
|
|
535
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
536
|
+
url: native.url,
|
|
537
|
+
target: targetPath,
|
|
538
|
+
...native.sha1 !== void 0 ? { expectedSha1: native.sha1 } : {},
|
|
539
|
+
...native.size !== void 0 ? { expectedSize: native.size } : {},
|
|
540
|
+
category: input.category
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
nativeExtractions.push({
|
|
544
|
+
kind: InstallActionKinds.EXTRACT_NATIVE,
|
|
545
|
+
source: targetPath,
|
|
546
|
+
destination: nativesDir,
|
|
547
|
+
exclude: library.extract?.exclude ?? ["META-INF/"]
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return { downloads, nativeExtractions, classpathFiles };
|
|
552
|
+
}
|
|
553
|
+
function pickPrimaryArtifact(library) {
|
|
554
|
+
if (library.downloads?.artifact) {
|
|
555
|
+
return artifactFromDownload(library.downloads.artifact);
|
|
556
|
+
}
|
|
557
|
+
if (library.url) {
|
|
558
|
+
return mavenArtifactFromCoord(library.name, library.url);
|
|
559
|
+
}
|
|
560
|
+
if (library.name && !library.natives) {
|
|
561
|
+
return mavenArtifactFromCoord(library.name, DEFAULT_LIBRARY_REPOSITORY);
|
|
562
|
+
}
|
|
563
|
+
return null;
|
|
564
|
+
}
|
|
565
|
+
function pickNative(library, system) {
|
|
566
|
+
if (!library.natives) return null;
|
|
567
|
+
const classifierTemplate = library.natives[system.os];
|
|
568
|
+
if (!classifierTemplate) return null;
|
|
569
|
+
const classifier = resolveArchPlaceholder(classifierTemplate, archDigit(system.arch));
|
|
570
|
+
const classifierArtifact = library.downloads?.classifiers?.[classifier];
|
|
571
|
+
if (classifierArtifact) {
|
|
572
|
+
return artifactFromDownload(classifierArtifact);
|
|
573
|
+
}
|
|
574
|
+
if (library.url || library.name) {
|
|
575
|
+
const coord = parseMavenCoordinate(library.name);
|
|
576
|
+
const withClassifier = `${coord.group}:${coord.artifact}:${coord.version}:${classifier}`;
|
|
577
|
+
return mavenArtifactFromCoord(withClassifier, library.url ?? DEFAULT_LIBRARY_REPOSITORY);
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
function artifactFromDownload(artifact) {
|
|
582
|
+
return {
|
|
583
|
+
relativePath: artifact.path,
|
|
584
|
+
url: artifact.url,
|
|
585
|
+
sha1: artifact.sha1,
|
|
586
|
+
size: artifact.size
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function mavenArtifactFromCoord(coord, baseUrl) {
|
|
590
|
+
const relativePath = mavenRelativePathFor(coord);
|
|
591
|
+
const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
592
|
+
if (!relativePath) {
|
|
593
|
+
throw new MinecraftKitError("MANIFEST_INVALID", `Invalid library coordinate: ${coord}`, {
|
|
594
|
+
context: { input: coord }
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
relativePath,
|
|
599
|
+
url: `${normalizedBase}${relativePath}`,
|
|
600
|
+
sha1: void 0,
|
|
601
|
+
size: void 0
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// src/install/fabric-install.ts
|
|
606
|
+
function planFabricInstall(input) {
|
|
607
|
+
const versionId = input.loader.profile.id;
|
|
608
|
+
const versionJsonPath = targetPaths.versionJson(input.directory, versionId);
|
|
609
|
+
const versionJson = {
|
|
610
|
+
kind: InstallActionKinds.WRITE_VERSION_JSON,
|
|
611
|
+
path: versionJsonPath,
|
|
612
|
+
content: `${JSON.stringify(input.loader.profile, null, 2)}
|
|
613
|
+
`
|
|
614
|
+
};
|
|
615
|
+
const plan = planLibraryDownloads({
|
|
616
|
+
libraries: input.loader.profile.libraries,
|
|
617
|
+
directory: input.directory,
|
|
618
|
+
system: input.system,
|
|
619
|
+
versionId: input.minecraft.version,
|
|
620
|
+
category: "fabric-library"
|
|
621
|
+
});
|
|
622
|
+
return {
|
|
623
|
+
versionJson,
|
|
624
|
+
libraryDownloads: plan.downloads,
|
|
625
|
+
classpathFiles: plan.classpathFiles,
|
|
626
|
+
versionId
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/constants/limits.ts
|
|
631
|
+
var EXTRACTION_MAX_FILE_SIZE = 256 * 1024 * 1024;
|
|
632
|
+
var EXTRACTION_MAX_TOTAL_SIZE = 2 * 1024 * 1024 * 1024;
|
|
633
|
+
var EXTRACTION_MAX_COMPRESSION_RATIO = 200;
|
|
634
|
+
var EXTRACTION_MAX_ENTRY_COUNT = 1e5;
|
|
635
|
+
async function ensureDir(directory) {
|
|
636
|
+
try {
|
|
637
|
+
await fs.mkdir(directory, { recursive: true });
|
|
638
|
+
} catch (cause) {
|
|
639
|
+
throw new MinecraftKitError(
|
|
640
|
+
"FILESYSTEM_WRITE_ERROR",
|
|
641
|
+
`Failed to create directory: ${directory}`,
|
|
642
|
+
{ cause, context: { filePath: directory } }
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function fileExists(filePath) {
|
|
647
|
+
try {
|
|
648
|
+
const stat = await fs.stat(filePath);
|
|
649
|
+
return stat.isFile();
|
|
650
|
+
} catch {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function dirExists(filePath) {
|
|
655
|
+
try {
|
|
656
|
+
const stat = await fs.stat(filePath);
|
|
657
|
+
return stat.isDirectory();
|
|
658
|
+
} catch {
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async function fileSize(filePath) {
|
|
663
|
+
try {
|
|
664
|
+
const stat = await fs.stat(filePath);
|
|
665
|
+
return stat.size;
|
|
666
|
+
} catch {
|
|
667
|
+
return -1;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
async function atomicWrite(target, data) {
|
|
671
|
+
await ensureDir(path.dirname(target));
|
|
672
|
+
const tmp = `${target}.${crypto2.randomBytes(4).toString("hex")}.tmp`;
|
|
673
|
+
try {
|
|
674
|
+
if (typeof data === "string") {
|
|
675
|
+
await fs.writeFile(tmp, data, "utf8");
|
|
676
|
+
} else {
|
|
677
|
+
await fs.writeFile(tmp, data);
|
|
678
|
+
}
|
|
679
|
+
await fs.rename(tmp, target);
|
|
680
|
+
} catch (cause) {
|
|
681
|
+
try {
|
|
682
|
+
await fs.unlink(tmp);
|
|
683
|
+
} catch {
|
|
684
|
+
}
|
|
685
|
+
throw new MinecraftKitError("FILESYSTEM_WRITE_ERROR", `Failed to write file: ${target}`, {
|
|
686
|
+
cause,
|
|
687
|
+
context: { filePath: target }
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async function readText(filePath) {
|
|
692
|
+
try {
|
|
693
|
+
return await fs.readFile(filePath, "utf8");
|
|
694
|
+
} catch (cause) {
|
|
695
|
+
throw new MinecraftKitError("FILESYSTEM_READ_ERROR", `Failed to read file: ${filePath}`, {
|
|
696
|
+
cause,
|
|
697
|
+
context: { filePath }
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async function listChildDirectories(directory) {
|
|
702
|
+
try {
|
|
703
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
704
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
705
|
+
} catch {
|
|
706
|
+
return [];
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
async function chmodExecutable(filePath) {
|
|
710
|
+
if (process.platform === "win32") {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
try {
|
|
714
|
+
await fs.chmod(filePath, 493);
|
|
715
|
+
} catch {
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function assertWithinRoot(root, child) {
|
|
719
|
+
const normalizedRoot = path.resolve(root);
|
|
720
|
+
const normalizedChild = path.resolve(root, child);
|
|
721
|
+
const sep = path.sep;
|
|
722
|
+
if (normalizedChild !== normalizedRoot && !normalizedChild.startsWith(normalizedRoot + sep)) {
|
|
723
|
+
throw new MinecraftKitError("FILESYSTEM_PATH_TRAVERSAL", `Path escapes root: ${child}`, {
|
|
724
|
+
context: { filePath: child, rootDirectory: root }
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/core/archive.ts
|
|
730
|
+
function openZip(filePath) {
|
|
731
|
+
return new Promise((resolve, reject) => {
|
|
732
|
+
yauzl.open(filePath, { lazyEntries: true, autoClose: false }, (err, zipFile) => {
|
|
733
|
+
if (err || !zipFile) {
|
|
734
|
+
reject(
|
|
735
|
+
new MinecraftKitError("ARCHIVE_INVALID", `Failed to open archive: ${filePath}`, {
|
|
736
|
+
cause: err,
|
|
737
|
+
context: { filePath }
|
|
738
|
+
})
|
|
739
|
+
);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
resolve(new ZipReader(zipFile, filePath));
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
var ZipReader = class {
|
|
747
|
+
constructor(file, filePath) {
|
|
748
|
+
this.file = file;
|
|
749
|
+
this.filePath = filePath;
|
|
750
|
+
}
|
|
751
|
+
file;
|
|
752
|
+
filePath;
|
|
753
|
+
/** Iterate every entry. Caller may break out of the loop early. */
|
|
754
|
+
async *entries() {
|
|
755
|
+
const file = this.file;
|
|
756
|
+
let count = 0;
|
|
757
|
+
while (true) {
|
|
758
|
+
const entry = await this.readNext();
|
|
759
|
+
if (entry === null) return;
|
|
760
|
+
count++;
|
|
761
|
+
if (count > EXTRACTION_MAX_ENTRY_COUNT) {
|
|
762
|
+
throw new MinecraftKitError(
|
|
763
|
+
"ARCHIVE_TOO_LARGE",
|
|
764
|
+
`Archive contains too many entries: ${this.filePath}`,
|
|
765
|
+
{ context: { filePath: this.filePath } }
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
yield this.toZipEntry(entry, file);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/** Find a single entry by name. Returns undefined if absent. */
|
|
772
|
+
async findEntry(name) {
|
|
773
|
+
for await (const entry of this.entries()) {
|
|
774
|
+
if (entry.name === name) return entry;
|
|
775
|
+
}
|
|
776
|
+
return void 0;
|
|
777
|
+
}
|
|
778
|
+
/** Close the reader. */
|
|
779
|
+
close() {
|
|
780
|
+
this.file.close();
|
|
781
|
+
}
|
|
782
|
+
readNext() {
|
|
783
|
+
return new Promise((resolve, reject) => {
|
|
784
|
+
const onEntry = (entry) => {
|
|
785
|
+
cleanup();
|
|
786
|
+
resolve(entry);
|
|
787
|
+
};
|
|
788
|
+
const onEnd = () => {
|
|
789
|
+
cleanup();
|
|
790
|
+
resolve(null);
|
|
791
|
+
};
|
|
792
|
+
const onError = (err) => {
|
|
793
|
+
cleanup();
|
|
794
|
+
reject(
|
|
795
|
+
new MinecraftKitError("ARCHIVE_INVALID", "Failed to read archive entry", {
|
|
796
|
+
cause: err,
|
|
797
|
+
context: { filePath: this.filePath }
|
|
798
|
+
})
|
|
799
|
+
);
|
|
800
|
+
};
|
|
801
|
+
const cleanup = () => {
|
|
802
|
+
this.file.removeListener("entry", onEntry);
|
|
803
|
+
this.file.removeListener("end", onEnd);
|
|
804
|
+
this.file.removeListener("error", onError);
|
|
805
|
+
};
|
|
806
|
+
this.file.once("entry", onEntry);
|
|
807
|
+
this.file.once("end", onEnd);
|
|
808
|
+
this.file.once("error", onError);
|
|
809
|
+
this.file.readEntry();
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
toZipEntry(entry, file) {
|
|
813
|
+
const name = entry.fileName;
|
|
814
|
+
const isDirectory = name.endsWith("/");
|
|
815
|
+
return {
|
|
816
|
+
name,
|
|
817
|
+
compressedSize: entry.compressedSize,
|
|
818
|
+
uncompressedSize: entry.uncompressedSize,
|
|
819
|
+
isDirectory,
|
|
820
|
+
readBuffer: async () => {
|
|
821
|
+
if (entry.uncompressedSize > EXTRACTION_MAX_FILE_SIZE) {
|
|
822
|
+
throw new MinecraftKitError(
|
|
823
|
+
"ARCHIVE_TOO_LARGE",
|
|
824
|
+
`Archive entry exceeds size cap: ${name}`,
|
|
825
|
+
{ context: { filePath: this.filePath, entryName: name, size: entry.uncompressedSize } }
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
if (entry.compressedSize > 0 && entry.uncompressedSize / entry.compressedSize > EXTRACTION_MAX_COMPRESSION_RATIO) {
|
|
829
|
+
throw new MinecraftKitError(
|
|
830
|
+
"ARCHIVE_TOO_LARGE",
|
|
831
|
+
`Archive entry exceeds compression-ratio cap: ${name}`,
|
|
832
|
+
{ context: { filePath: this.filePath, entryName: name } }
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
const stream = await openStream(file, entry, this.filePath);
|
|
836
|
+
const chunks = [];
|
|
837
|
+
for await (const chunk of stream) {
|
|
838
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
839
|
+
}
|
|
840
|
+
return Buffer.concat(chunks);
|
|
841
|
+
},
|
|
842
|
+
openReadStream: () => openStream(file, entry, this.filePath)
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
function openStream(file, entry, archivePath) {
|
|
847
|
+
return new Promise((resolve, reject) => {
|
|
848
|
+
file.openReadStream(entry, (err, stream) => {
|
|
849
|
+
if (err || !stream) {
|
|
850
|
+
reject(
|
|
851
|
+
new MinecraftKitError(
|
|
852
|
+
"ARCHIVE_INVALID",
|
|
853
|
+
`Failed to open archive entry: ${entry.fileName}`,
|
|
854
|
+
{ cause: err, context: { filePath: archivePath, entryName: entry.fileName } }
|
|
855
|
+
)
|
|
856
|
+
);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
resolve(stream);
|
|
860
|
+
});
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
async function extractAllToDir(zipPath, targetDir, options = {}) {
|
|
864
|
+
const exclude = options.excludePrefixes ?? ["META-INF/"];
|
|
865
|
+
let fileCount = 0;
|
|
866
|
+
let totalSize = 0;
|
|
867
|
+
await ensureDir(targetDir);
|
|
868
|
+
const reader = await openZip(zipPath);
|
|
869
|
+
try {
|
|
870
|
+
for await (const entry of reader.entries()) {
|
|
871
|
+
if (entry.isDirectory) continue;
|
|
872
|
+
if (exclude.some((prefix) => entry.name.startsWith(prefix))) continue;
|
|
873
|
+
assertSafeEntryName(entry.name);
|
|
874
|
+
const destination = path.join(targetDir, entry.name);
|
|
875
|
+
assertWithinRoot(targetDir, entry.name);
|
|
876
|
+
totalSize += entry.uncompressedSize;
|
|
877
|
+
if (totalSize > EXTRACTION_MAX_TOTAL_SIZE) {
|
|
878
|
+
throw new MinecraftKitError(
|
|
879
|
+
"ARCHIVE_TOO_LARGE",
|
|
880
|
+
`Archive total size cap exceeded: ${zipPath}`,
|
|
881
|
+
{ context: { filePath: zipPath } }
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
if (entry.uncompressedSize > EXTRACTION_MAX_FILE_SIZE) {
|
|
885
|
+
throw new MinecraftKitError(
|
|
886
|
+
"ARCHIVE_TOO_LARGE",
|
|
887
|
+
`Archive entry exceeds size cap: ${entry.name}`,
|
|
888
|
+
{ context: { filePath: zipPath, entryName: entry.name } }
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
await ensureDir(path.dirname(destination));
|
|
892
|
+
const stream = await entry.openReadStream();
|
|
893
|
+
await pipeline(stream, createWriteStream(destination));
|
|
894
|
+
if (entry.name.endsWith(".so") || entry.name.endsWith(".dylib") || entry.name.endsWith(".jnilib")) {
|
|
895
|
+
await chmodExecutable(destination);
|
|
896
|
+
}
|
|
897
|
+
fileCount++;
|
|
898
|
+
}
|
|
899
|
+
} finally {
|
|
900
|
+
reader.close();
|
|
901
|
+
}
|
|
902
|
+
return { fileCount };
|
|
903
|
+
}
|
|
904
|
+
var RESERVED_NAME = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$/i;
|
|
905
|
+
function assertSafeEntryName(name) {
|
|
906
|
+
if (!name) {
|
|
907
|
+
throw rejectEntry(name, "empty entry name");
|
|
908
|
+
}
|
|
909
|
+
if (name.includes(String.fromCharCode(0))) {
|
|
910
|
+
throw rejectEntry(name, "null byte");
|
|
911
|
+
}
|
|
912
|
+
if (path.posix.isAbsolute(name) || /^[a-zA-Z]:/.test(name) || name.startsWith("\\")) {
|
|
913
|
+
throw rejectEntry(name, "absolute path");
|
|
914
|
+
}
|
|
915
|
+
const segments = name.split("/");
|
|
916
|
+
for (const segment of segments) {
|
|
917
|
+
if (segment === "..") {
|
|
918
|
+
throw rejectEntry(name, "parent traversal");
|
|
919
|
+
}
|
|
920
|
+
if (RESERVED_NAME.test(segment)) {
|
|
921
|
+
throw rejectEntry(name, "reserved Windows name");
|
|
922
|
+
}
|
|
923
|
+
if (/[\s.]$/.test(segment)) {
|
|
924
|
+
throw rejectEntry(name, "trailing dot or whitespace");
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
function rejectEntry(name, reason) {
|
|
929
|
+
return new MinecraftKitError(
|
|
930
|
+
"ARCHIVE_ENTRY_REJECTED",
|
|
931
|
+
`Archive entry rejected (${reason}): ${name}`,
|
|
932
|
+
{ context: { entryName: name, reason } }
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
async function readEntryBuffer(zipPath, entryName) {
|
|
936
|
+
const reader = await openZip(zipPath);
|
|
937
|
+
try {
|
|
938
|
+
const entry = await reader.findEntry(entryName);
|
|
939
|
+
if (!entry) return void 0;
|
|
940
|
+
return await entry.readBuffer();
|
|
941
|
+
} finally {
|
|
942
|
+
reader.close();
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async function extractSingleEntry(zipPath, entryName, destination) {
|
|
946
|
+
const buffer = await readEntryBuffer(zipPath, entryName);
|
|
947
|
+
if (!buffer) {
|
|
948
|
+
throw new MinecraftKitError("ARCHIVE_INVALID", `Archive entry not found: ${entryName}`, {
|
|
949
|
+
context: { filePath: zipPath, entryName }
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
await atomicWrite(destination, buffer);
|
|
953
|
+
}
|
|
954
|
+
var MANIFEST_LINE_CONTINUATION = /\r?\n[ \t]/g;
|
|
955
|
+
var MANIFEST_MAIN_CLASS = /^Main-Class:\s*(.+)$/i;
|
|
956
|
+
async function readJarMainClass(zipPath) {
|
|
957
|
+
const buf = await readEntryBuffer(zipPath, "META-INF/MANIFEST.MF");
|
|
958
|
+
if (!buf) return void 0;
|
|
959
|
+
const text = buf.toString("utf8").replaceAll(MANIFEST_LINE_CONTINUATION, "");
|
|
960
|
+
for (const line of text.split(/\r?\n/)) {
|
|
961
|
+
const match = MANIFEST_MAIN_CLASS.exec(line);
|
|
962
|
+
if (match?.[1]) return match[1].trim();
|
|
963
|
+
}
|
|
964
|
+
return void 0;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/core/collections.ts
|
|
968
|
+
function dedupeBy(values, key) {
|
|
969
|
+
const seen = /* @__PURE__ */ new Set();
|
|
970
|
+
const result = [];
|
|
971
|
+
for (const value of values) {
|
|
972
|
+
const k = key(value);
|
|
973
|
+
if (seen.has(k)) continue;
|
|
974
|
+
seen.add(k);
|
|
975
|
+
result.push(value);
|
|
976
|
+
}
|
|
977
|
+
return result;
|
|
978
|
+
}
|
|
979
|
+
function dedupe(values) {
|
|
980
|
+
return dedupeBy(values, (v) => v);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/core/retry.ts
|
|
984
|
+
function abortableSleep(ms, signal) {
|
|
985
|
+
return new Promise((resolve, reject) => {
|
|
986
|
+
if (signal?.aborted) {
|
|
987
|
+
reject(toAbortError(signal));
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
const timer = setTimeout(() => {
|
|
991
|
+
signal?.removeEventListener("abort", onAbort);
|
|
992
|
+
resolve();
|
|
993
|
+
}, ms);
|
|
994
|
+
const onAbort = () => {
|
|
995
|
+
clearTimeout(timer);
|
|
996
|
+
reject(toAbortError(signal));
|
|
997
|
+
};
|
|
998
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
function toAbortError(signal) {
|
|
1002
|
+
return new MinecraftKitError("NETWORK_ABORTED", "Operation aborted", {
|
|
1003
|
+
context: { reason: signal?.reason }
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
async function withRetry(op, isRetryable, options = {}) {
|
|
1007
|
+
const max = options.maxAttempts ?? HTTP_RETRY_MAX;
|
|
1008
|
+
const base = options.baseMs ?? HTTP_RETRY_BACKOFF_BASE_MS;
|
|
1009
|
+
const cap = options.capMs ?? HTTP_RETRY_BACKOFF_CAP_MS;
|
|
1010
|
+
const sleep = options.sleep ?? abortableSleep;
|
|
1011
|
+
const random = options.random ?? Math.random;
|
|
1012
|
+
let lastError;
|
|
1013
|
+
for (let attempt = 0; attempt < max; attempt++) {
|
|
1014
|
+
if (options.signal?.aborted) {
|
|
1015
|
+
throw toAbortError(options.signal);
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
return await op(attempt);
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
lastError = error;
|
|
1021
|
+
options.onAttemptFailed?.(error, attempt);
|
|
1022
|
+
if (!isRetryable(error) || attempt === max - 1) {
|
|
1023
|
+
throw error;
|
|
1024
|
+
}
|
|
1025
|
+
const delayCap = Math.min(cap, base * 2 ** attempt);
|
|
1026
|
+
const delay = Math.floor(random() * delayCap);
|
|
1027
|
+
await sleep(delay, options.signal);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
throw lastError ?? new Error("withRetry exhausted attempts");
|
|
1031
|
+
}
|
|
1032
|
+
function isHttpRetryable(error) {
|
|
1033
|
+
if (!isMinecraftKitError(error)) {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
if (error.code === "NETWORK_ABORTED") return false;
|
|
1037
|
+
if (error.code === "NETWORK_TIMEOUT") return true;
|
|
1038
|
+
if (error.code === "NETWORK_HTTP_ERROR") {
|
|
1039
|
+
const status = typeof error.context.httpStatus === "number" ? error.context.httpStatus : 0;
|
|
1040
|
+
if (status === 408 || status === 425 || status === 429) return true;
|
|
1041
|
+
if (status >= 500 && status < 600) return true;
|
|
1042
|
+
return status === 0;
|
|
1043
|
+
}
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/http/download.ts
|
|
1048
|
+
async function downloadFile(http, input) {
|
|
1049
|
+
const fileRef = { url: input.url, target: input.target, category: input.category };
|
|
1050
|
+
if (input.expectedSha1 !== void 0) {
|
|
1051
|
+
const existing = await checkExistingFile(input.target, input.expectedSha1, input.expectedSize);
|
|
1052
|
+
if (existing.matches) {
|
|
1053
|
+
input.onEvent?.({ type: "download:skipped", file: fileRef });
|
|
1054
|
+
return { bytesDownloaded: 0, sha1: existing.sha1, skipped: true };
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
await ensureDir(path.dirname(input.target));
|
|
1058
|
+
const tmp = `${input.target}.${crypto2.randomBytes(4).toString("hex")}.download`;
|
|
1059
|
+
return withRetry(
|
|
1060
|
+
async () => {
|
|
1061
|
+
input.onEvent?.({
|
|
1062
|
+
type: "download:started",
|
|
1063
|
+
file: fileRef,
|
|
1064
|
+
expectedSize: input.expectedSize ?? 0
|
|
1065
|
+
});
|
|
1066
|
+
const startedAt = Date.now();
|
|
1067
|
+
let bytesDownloaded = 0;
|
|
1068
|
+
const hash = crypto2.createHash("sha1");
|
|
1069
|
+
const response = await http.request(input.url, { signal: input.signal });
|
|
1070
|
+
const contentLength = Number(response.headers["content-length"] ?? "0");
|
|
1071
|
+
const total = input.expectedSize ?? (Number.isFinite(contentLength) ? contentLength : 0);
|
|
1072
|
+
const sourceIterable = response.stream();
|
|
1073
|
+
const counting = (async function* () {
|
|
1074
|
+
for await (const chunk of sourceIterable) {
|
|
1075
|
+
bytesDownloaded += chunk.byteLength;
|
|
1076
|
+
hash.update(chunk);
|
|
1077
|
+
input.onEvent?.({
|
|
1078
|
+
type: "download:progress",
|
|
1079
|
+
file: fileRef,
|
|
1080
|
+
bytesDownloaded,
|
|
1081
|
+
totalBytes: total
|
|
1082
|
+
});
|
|
1083
|
+
yield chunk;
|
|
1084
|
+
}
|
|
1085
|
+
})();
|
|
1086
|
+
try {
|
|
1087
|
+
await pipeline(Readable.from(counting), createWriteStream(tmp));
|
|
1088
|
+
} catch (cause) {
|
|
1089
|
+
await safeUnlink(tmp);
|
|
1090
|
+
throw new MinecraftKitError(
|
|
1091
|
+
"FILESYSTEM_WRITE_ERROR",
|
|
1092
|
+
`Failed to write download: ${input.target}`,
|
|
1093
|
+
{ cause, context: { filePath: input.target, url: input.url } }
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
const computedSha1 = hash.digest("hex");
|
|
1097
|
+
if (input.expectedSize !== void 0 && bytesDownloaded !== input.expectedSize) {
|
|
1098
|
+
await safeUnlink(tmp);
|
|
1099
|
+
throw new MinecraftKitError("INTEGRITY_SIZE_MISMATCH", `Size mismatch for ${input.url}`, {
|
|
1100
|
+
context: {
|
|
1101
|
+
url: input.url,
|
|
1102
|
+
expectedSize: input.expectedSize,
|
|
1103
|
+
actualSize: bytesDownloaded
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
if (input.expectedSha1 !== void 0 && computedSha1 !== input.expectedSha1) {
|
|
1108
|
+
await safeUnlink(tmp);
|
|
1109
|
+
input.onEvent?.({
|
|
1110
|
+
type: "integrity:mismatch",
|
|
1111
|
+
file: fileRef,
|
|
1112
|
+
algorithm: "sha1",
|
|
1113
|
+
expected: input.expectedSha1,
|
|
1114
|
+
actual: computedSha1
|
|
1115
|
+
});
|
|
1116
|
+
throw new MinecraftKitError("INTEGRITY_HASH_MISMATCH", `SHA-1 mismatch for ${input.url}`, {
|
|
1117
|
+
context: {
|
|
1118
|
+
url: input.url,
|
|
1119
|
+
expectedHash: input.expectedSha1,
|
|
1120
|
+
actualHash: computedSha1
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
await fs.rename(tmp, input.target);
|
|
1126
|
+
} catch (cause) {
|
|
1127
|
+
await safeUnlink(tmp);
|
|
1128
|
+
throw new MinecraftKitError(
|
|
1129
|
+
"FILESYSTEM_WRITE_ERROR",
|
|
1130
|
+
`Failed to finalize download: ${input.target}`,
|
|
1131
|
+
{ cause, context: { filePath: input.target } }
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
input.onEvent?.({
|
|
1135
|
+
type: "download:completed",
|
|
1136
|
+
file: fileRef,
|
|
1137
|
+
durationMs: Date.now() - startedAt,
|
|
1138
|
+
bytes: bytesDownloaded
|
|
1139
|
+
});
|
|
1140
|
+
if (input.expectedSha1 !== void 0) {
|
|
1141
|
+
input.onEvent?.({
|
|
1142
|
+
type: "integrity:verified",
|
|
1143
|
+
file: fileRef,
|
|
1144
|
+
algorithm: "sha1",
|
|
1145
|
+
hash: computedSha1
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
return { bytesDownloaded, sha1: computedSha1, skipped: false };
|
|
1149
|
+
},
|
|
1150
|
+
isHttpRetryable,
|
|
1151
|
+
{
|
|
1152
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
1153
|
+
onAttemptFailed: (error, attempt) => {
|
|
1154
|
+
input.onEvent?.({
|
|
1155
|
+
type: "download:failed",
|
|
1156
|
+
file: fileRef,
|
|
1157
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1158
|
+
willRetry: isHttpRetryable(error) && attempt < HTTP_RETRY_MAX - 1
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
async function checkExistingFile(target, expectedSha1, expectedSize) {
|
|
1165
|
+
let stat;
|
|
1166
|
+
try {
|
|
1167
|
+
stat = await fs.stat(target);
|
|
1168
|
+
} catch {
|
|
1169
|
+
return { matches: false, sha1: "" };
|
|
1170
|
+
}
|
|
1171
|
+
if (!stat.isFile()) {
|
|
1172
|
+
return { matches: false, sha1: "" };
|
|
1173
|
+
}
|
|
1174
|
+
if (expectedSize !== void 0 && stat.size !== expectedSize) {
|
|
1175
|
+
return { matches: false, sha1: "" };
|
|
1176
|
+
}
|
|
1177
|
+
const buf = await fs.readFile(target);
|
|
1178
|
+
const sha1 = crypto2.createHash("sha1").update(buf).digest("hex");
|
|
1179
|
+
return { matches: sha1 === expectedSha1, sha1 };
|
|
1180
|
+
}
|
|
1181
|
+
async function safeUnlink(filePath) {
|
|
1182
|
+
try {
|
|
1183
|
+
await fs.unlink(filePath);
|
|
1184
|
+
} catch {
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// src/install/forge-install.ts
|
|
1189
|
+
async function planForgeInstall(input) {
|
|
1190
|
+
const installerPath = targetPaths.forgeInstaller(input.directory, input.loader.fullVersion);
|
|
1191
|
+
await downloadFile(input.http, {
|
|
1192
|
+
url: input.loader.installerUrl,
|
|
1193
|
+
target: installerPath,
|
|
1194
|
+
category: "forge-installer",
|
|
1195
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
1196
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
1197
|
+
});
|
|
1198
|
+
const installerDownload = {
|
|
1199
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
1200
|
+
url: input.loader.installerUrl,
|
|
1201
|
+
target: installerPath,
|
|
1202
|
+
category: "forge-installer"
|
|
1203
|
+
};
|
|
1204
|
+
const profile = await readJsonEntry(installerPath, "install_profile.json");
|
|
1205
|
+
const versionRelative = profile.json.startsWith("/") ? profile.json.slice(1) : profile.json;
|
|
1206
|
+
const version = await readJsonEntry(installerPath, versionRelative);
|
|
1207
|
+
await extractInstallerMavenEntries(installerPath, input.directory);
|
|
1208
|
+
const dataResolved = await resolveProfileData({
|
|
1209
|
+
profile,
|
|
1210
|
+
installerPath,
|
|
1211
|
+
directory: input.directory
|
|
1212
|
+
});
|
|
1213
|
+
const installerLibraries = planLibraryDownloads({
|
|
1214
|
+
libraries: profile.libraries,
|
|
1215
|
+
directory: input.directory,
|
|
1216
|
+
system: input.system,
|
|
1217
|
+
versionId: input.minecraft.version,
|
|
1218
|
+
category: "forge-library"
|
|
1219
|
+
});
|
|
1220
|
+
const versionLibraries = planLibraryDownloads({
|
|
1221
|
+
libraries: version.libraries,
|
|
1222
|
+
directory: input.directory,
|
|
1223
|
+
system: input.system,
|
|
1224
|
+
versionId: version.id,
|
|
1225
|
+
category: "forge-library"
|
|
1226
|
+
});
|
|
1227
|
+
const dedupedDownloads = dedupeBy(
|
|
1228
|
+
[...installerLibraries.downloads, ...versionLibraries.downloads],
|
|
1229
|
+
(action) => action.target
|
|
1230
|
+
);
|
|
1231
|
+
const classpathFiles = dedupe([
|
|
1232
|
+
...installerLibraries.classpathFiles,
|
|
1233
|
+
...versionLibraries.classpathFiles
|
|
1234
|
+
]);
|
|
1235
|
+
const processorActions = await buildProcessorActions({
|
|
1236
|
+
profile,
|
|
1237
|
+
minecraft: input.minecraft,
|
|
1238
|
+
installerPath,
|
|
1239
|
+
directory: input.directory,
|
|
1240
|
+
system: input.system,
|
|
1241
|
+
dataResolved
|
|
1242
|
+
});
|
|
1243
|
+
const versionJsonPath = targetPaths.versionJson(input.directory, version.id);
|
|
1244
|
+
const versionJson = {
|
|
1245
|
+
kind: InstallActionKinds.WRITE_VERSION_JSON,
|
|
1246
|
+
path: versionJsonPath,
|
|
1247
|
+
content: `${JSON.stringify(version, null, 2)}
|
|
1248
|
+
`
|
|
1249
|
+
};
|
|
1250
|
+
return {
|
|
1251
|
+
installerDownload,
|
|
1252
|
+
libraryDownloads: dedupedDownloads,
|
|
1253
|
+
classpathFiles,
|
|
1254
|
+
processorActions,
|
|
1255
|
+
versionJson,
|
|
1256
|
+
versionId: version.id,
|
|
1257
|
+
profile,
|
|
1258
|
+
version
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
async function readJsonEntry(zipPath, entryName) {
|
|
1262
|
+
const buffer = await readEntryBuffer(zipPath, entryName);
|
|
1263
|
+
if (!buffer) {
|
|
1264
|
+
throw new MinecraftKitError(
|
|
1265
|
+
"FORGE_INSTALLER_INVALID",
|
|
1266
|
+
`Forge installer is missing required entry: ${entryName}`,
|
|
1267
|
+
{ context: { filePath: zipPath, entryName } }
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
try {
|
|
1271
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
1272
|
+
} catch (cause) {
|
|
1273
|
+
throw new MinecraftKitError(
|
|
1274
|
+
"FORGE_INSTALLER_INVALID",
|
|
1275
|
+
`Forge installer entry is not valid JSON: ${entryName}`,
|
|
1276
|
+
{ cause, context: { filePath: zipPath, entryName } }
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async function extractInstallerMavenEntries(installerPath, directory) {
|
|
1281
|
+
const reader = await openZip(installerPath);
|
|
1282
|
+
try {
|
|
1283
|
+
for await (const entry of reader.entries()) {
|
|
1284
|
+
if (!entry.name.startsWith("maven/") || entry.isDirectory) continue;
|
|
1285
|
+
const relativeWithinLibraries = entry.name.slice("maven/".length);
|
|
1286
|
+
const destination = path.join(targetPaths.librariesDir(directory), relativeWithinLibraries);
|
|
1287
|
+
const buffer = await entry.readBuffer();
|
|
1288
|
+
await atomicWrite(destination, buffer);
|
|
1289
|
+
}
|
|
1290
|
+
} finally {
|
|
1291
|
+
reader.close();
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
async function resolveProfileData(input) {
|
|
1295
|
+
const tokens = {};
|
|
1296
|
+
for (const [key, sided] of Object.entries(input.profile.data)) {
|
|
1297
|
+
const raw = sided.client;
|
|
1298
|
+
tokens[key] = await resolveDataValue(raw, input.installerPath, input.directory);
|
|
1299
|
+
}
|
|
1300
|
+
return { tokens };
|
|
1301
|
+
}
|
|
1302
|
+
async function resolveDataValue(raw, installerPath, directory) {
|
|
1303
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
1304
|
+
const coord = raw.slice(1, -1);
|
|
1305
|
+
const relativePath = mavenRelativePathFor(coord);
|
|
1306
|
+
return {
|
|
1307
|
+
value: path.join(targetPaths.librariesDir(directory), relativePath),
|
|
1308
|
+
isPath: true
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
if (raw.startsWith("'")) {
|
|
1312
|
+
return { value: raw.slice(1), isPath: false };
|
|
1313
|
+
}
|
|
1314
|
+
if (raw.startsWith("/")) {
|
|
1315
|
+
const entryName = raw.slice(1);
|
|
1316
|
+
const destination = path.join(targetPaths.librariesDir(directory), "forge-data", entryName);
|
|
1317
|
+
await extractSingleEntry(installerPath, entryName, destination);
|
|
1318
|
+
return { value: destination, isPath: true };
|
|
1319
|
+
}
|
|
1320
|
+
return { value: raw, isPath: false };
|
|
1321
|
+
}
|
|
1322
|
+
async function buildProcessorActions(input) {
|
|
1323
|
+
const builtIns = {
|
|
1324
|
+
SIDE: { value: "client", isPath: false },
|
|
1325
|
+
MINECRAFT_JAR: {
|
|
1326
|
+
value: targetPaths.versionJar(input.directory, input.minecraft.version),
|
|
1327
|
+
isPath: true
|
|
1328
|
+
},
|
|
1329
|
+
MINECRAFT_VERSION: { value: input.minecraft.version, isPath: false },
|
|
1330
|
+
ROOT: { value: input.directory, isPath: true },
|
|
1331
|
+
INSTALLER: { value: input.installerPath, isPath: true },
|
|
1332
|
+
LIBRARY_DIR: { value: targetPaths.librariesDir(input.directory), isPath: true }
|
|
1333
|
+
};
|
|
1334
|
+
const tokens = {
|
|
1335
|
+
...builtIns,
|
|
1336
|
+
...input.dataResolved.tokens
|
|
1337
|
+
};
|
|
1338
|
+
const actions = [];
|
|
1339
|
+
let index = 0;
|
|
1340
|
+
for (const processor of input.profile.processors) {
|
|
1341
|
+
if (!processorAppliesToClient(processor)) {
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
if (!evaluateRules([], { system: input.system })) ;
|
|
1345
|
+
const action = await buildProcessorAction({
|
|
1346
|
+
processor,
|
|
1347
|
+
directory: input.directory,
|
|
1348
|
+
tokens,
|
|
1349
|
+
index
|
|
1350
|
+
});
|
|
1351
|
+
actions.push(action);
|
|
1352
|
+
index++;
|
|
1353
|
+
}
|
|
1354
|
+
return actions;
|
|
1355
|
+
}
|
|
1356
|
+
function processorAppliesToClient(processor) {
|
|
1357
|
+
if (!processor.sides || processor.sides.length === 0) return true;
|
|
1358
|
+
return processor.sides.includes("client");
|
|
1359
|
+
}
|
|
1360
|
+
async function buildProcessorAction(input) {
|
|
1361
|
+
const jarPath = path.join(
|
|
1362
|
+
targetPaths.librariesDir(input.directory),
|
|
1363
|
+
mavenRelativePathFor(input.processor.jar)
|
|
1364
|
+
);
|
|
1365
|
+
const mainClass = await readJarMainClass(jarPath);
|
|
1366
|
+
if (!mainClass) {
|
|
1367
|
+
throw new MinecraftKitError(
|
|
1368
|
+
"FORGE_INSTALLER_INVALID",
|
|
1369
|
+
`Processor jar has no Main-Class: ${input.processor.jar}`,
|
|
1370
|
+
{ context: { filePath: jarPath } }
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
const classpath = [
|
|
1374
|
+
jarPath,
|
|
1375
|
+
...input.processor.classpath.map(
|
|
1376
|
+
(coord) => path.join(targetPaths.librariesDir(input.directory), mavenRelativePathFor(coord))
|
|
1377
|
+
)
|
|
1378
|
+
];
|
|
1379
|
+
const args = input.processor.args.map((arg) => substituteToken(arg, input.tokens));
|
|
1380
|
+
const outputs = {};
|
|
1381
|
+
if (input.processor.outputs) {
|
|
1382
|
+
for (const [key, value] of Object.entries(input.processor.outputs)) {
|
|
1383
|
+
outputs[substituteToken(key, input.tokens)] = stripLiteralPrefix(
|
|
1384
|
+
substituteToken(value, input.tokens)
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return {
|
|
1389
|
+
kind: InstallActionKinds.RUN_FORGE_PROCESSOR,
|
|
1390
|
+
index: input.index,
|
|
1391
|
+
mainClass,
|
|
1392
|
+
classpath,
|
|
1393
|
+
args,
|
|
1394
|
+
outputs
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function substituteToken(raw, tokens) {
|
|
1398
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
1399
|
+
return path.join(...mavenRelativePathFor(raw.slice(1, -1)).split("/"));
|
|
1400
|
+
}
|
|
1401
|
+
return raw.replaceAll(/\{([A-Z0-9_]+)\}/g, (match, key) => {
|
|
1402
|
+
const token = tokens[key];
|
|
1403
|
+
if (token === void 0) {
|
|
1404
|
+
throw new MinecraftKitError("FORGE_INSTALLER_INVALID", `Unknown processor token: ${match}`, {
|
|
1405
|
+
context: { token: key }
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
return token.value;
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
function stripLiteralPrefix(value) {
|
|
1412
|
+
return value.startsWith("'") ? value.slice(1) : value;
|
|
1413
|
+
}
|
|
1414
|
+
async function planRuntimeDownloads(input) {
|
|
1415
|
+
const manifest = await fetchJson(input.http, input.cache, {
|
|
1416
|
+
url: input.runtime.manifestUrl,
|
|
1417
|
+
cacheKey: `runtime-manifest:${input.runtime.component}:${input.runtime.platformKey}:${input.runtime.manifestSha1}`,
|
|
1418
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1419
|
+
});
|
|
1420
|
+
const actions = [];
|
|
1421
|
+
const runtimeRoot = targetPaths.runtimeRoot(
|
|
1422
|
+
input.directory,
|
|
1423
|
+
input.runtime.component,
|
|
1424
|
+
input.runtime.installRoot
|
|
1425
|
+
);
|
|
1426
|
+
for (const [relativePath, entry] of Object.entries(manifest.files)) {
|
|
1427
|
+
if (entry.type !== "file") continue;
|
|
1428
|
+
const target = path.join(runtimeRoot, relativePath);
|
|
1429
|
+
actions.push({
|
|
1430
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
1431
|
+
url: entry.downloads.raw.url,
|
|
1432
|
+
target,
|
|
1433
|
+
expectedSha1: entry.downloads.raw.sha1,
|
|
1434
|
+
expectedSize: entry.downloads.raw.size,
|
|
1435
|
+
category: "runtime-file"
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
return { actions, manifest };
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// src/install/planner.ts
|
|
1442
|
+
async function planInstall(input) {
|
|
1443
|
+
const { target } = input;
|
|
1444
|
+
const actions = [];
|
|
1445
|
+
actions.push({
|
|
1446
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
1447
|
+
url: target.minecraft.manifest.downloads.client.url,
|
|
1448
|
+
target: targetPaths.versionJar(target.directory, target.minecraft.version),
|
|
1449
|
+
expectedSha1: target.minecraft.manifest.downloads.client.sha1,
|
|
1450
|
+
expectedSize: target.minecraft.manifest.downloads.client.size,
|
|
1451
|
+
category: "client-jar"
|
|
1452
|
+
});
|
|
1453
|
+
actions.push({
|
|
1454
|
+
kind: InstallActionKinds.WRITE_VERSION_JSON,
|
|
1455
|
+
path: targetPaths.versionJson(target.directory, target.minecraft.version),
|
|
1456
|
+
content: `${JSON.stringify(target.minecraft.manifest, null, 2)}
|
|
1457
|
+
`
|
|
1458
|
+
});
|
|
1459
|
+
const vanillaLibraries = planLibraryDownloads({
|
|
1460
|
+
libraries: target.minecraft.manifest.libraries,
|
|
1461
|
+
directory: target.directory,
|
|
1462
|
+
system: target.runtime.system,
|
|
1463
|
+
versionId: target.minecraft.version,
|
|
1464
|
+
category: "library"
|
|
1465
|
+
});
|
|
1466
|
+
actions.push(...vanillaLibraries.downloads);
|
|
1467
|
+
actions.push(...vanillaLibraries.nativeExtractions);
|
|
1468
|
+
const assetPlan = await planAssetDownloads({
|
|
1469
|
+
directory: target.directory,
|
|
1470
|
+
assetIndex: target.minecraft.manifest.assetIndex,
|
|
1471
|
+
http: input.http,
|
|
1472
|
+
cache: input.cache,
|
|
1473
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1474
|
+
});
|
|
1475
|
+
actions.push(...assetPlan.actions);
|
|
1476
|
+
if (target.minecraft.manifest.logging?.client) {
|
|
1477
|
+
const logging = target.minecraft.manifest.logging.client;
|
|
1478
|
+
actions.push({
|
|
1479
|
+
kind: InstallActionKinds.DOWNLOAD_FILE,
|
|
1480
|
+
url: logging.file.url,
|
|
1481
|
+
target: targetPaths.loggingConfig(target.directory, logging.file.id),
|
|
1482
|
+
expectedSha1: logging.file.sha1,
|
|
1483
|
+
expectedSize: logging.file.size,
|
|
1484
|
+
category: "logging-config"
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
const runtimePlan = await planRuntimeDownloads({
|
|
1488
|
+
runtime: target.runtime,
|
|
1489
|
+
directory: target.directory,
|
|
1490
|
+
http: input.http,
|
|
1491
|
+
cache: input.cache,
|
|
1492
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1493
|
+
});
|
|
1494
|
+
actions.push(...runtimePlan.actions);
|
|
1495
|
+
if (target.loader.type === Loaders.FABRIC) {
|
|
1496
|
+
const fabricPlan = planFabricInstall({
|
|
1497
|
+
loader: target.loader,
|
|
1498
|
+
minecraft: target.minecraft,
|
|
1499
|
+
directory: target.directory,
|
|
1500
|
+
system: target.runtime.system
|
|
1501
|
+
});
|
|
1502
|
+
actions.push(fabricPlan.versionJson);
|
|
1503
|
+
actions.push(...fabricPlan.libraryDownloads);
|
|
1504
|
+
} else if (target.loader.type === Loaders.FORGE) {
|
|
1505
|
+
const forgePlan = await planForgeInstall({
|
|
1506
|
+
loader: target.loader,
|
|
1507
|
+
minecraft: target.minecraft,
|
|
1508
|
+
directory: target.directory,
|
|
1509
|
+
system: target.runtime.system,
|
|
1510
|
+
http: input.http,
|
|
1511
|
+
cache: input.cache,
|
|
1512
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
1513
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
1514
|
+
});
|
|
1515
|
+
actions.push(forgePlan.installerDownload);
|
|
1516
|
+
actions.push(...forgePlan.libraryDownloads);
|
|
1517
|
+
actions.push(forgePlan.versionJson);
|
|
1518
|
+
actions.push(...forgePlan.processorActions);
|
|
1519
|
+
}
|
|
1520
|
+
const totalBytes = actions.reduce((sum, action) => {
|
|
1521
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
1522
|
+
return sum + (action.expectedSize ?? 0);
|
|
1523
|
+
}
|
|
1524
|
+
return sum;
|
|
1525
|
+
}, 0);
|
|
1526
|
+
return {
|
|
1527
|
+
targetId: target.id,
|
|
1528
|
+
directory: target.directory,
|
|
1529
|
+
target,
|
|
1530
|
+
actions,
|
|
1531
|
+
totalActions: actions.length,
|
|
1532
|
+
totalBytes
|
|
1533
|
+
};
|
|
1534
|
+
}
|
|
1535
|
+
async function materializeRuntimeExtras(input) {
|
|
1536
|
+
const root = targetPaths.runtimeRoot(
|
|
1537
|
+
input.directory,
|
|
1538
|
+
input.runtime.component,
|
|
1539
|
+
input.runtime.installRoot
|
|
1540
|
+
);
|
|
1541
|
+
for (const [relativePath, entry] of Object.entries(input.manifest.files)) {
|
|
1542
|
+
const fullPath = path.join(root, relativePath);
|
|
1543
|
+
if (entry.type === "directory") {
|
|
1544
|
+
await ensureDir(fullPath);
|
|
1545
|
+
} else if (entry.type === "link") {
|
|
1546
|
+
await ensureDir(path.dirname(fullPath));
|
|
1547
|
+
await unlinkIfPresent(fullPath);
|
|
1548
|
+
await createLinkOrCopy(root, relativePath, entry.target, fullPath);
|
|
1549
|
+
} else if (entry.executable && process.platform !== "win32") {
|
|
1550
|
+
await fs.chmod(fullPath, 493).catch(() => {
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
async function unlinkIfPresent(target) {
|
|
1556
|
+
try {
|
|
1557
|
+
await fs.unlink(target);
|
|
1558
|
+
} catch (cause) {
|
|
1559
|
+
if (isNotFound(cause)) return;
|
|
1560
|
+
throw new MinecraftKitError(
|
|
1561
|
+
"FILESYSTEM_WRITE_ERROR",
|
|
1562
|
+
`Failed to remove stale runtime entry: ${target}`,
|
|
1563
|
+
{ cause, context: { filePath: target } }
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
async function createLinkOrCopy(root, relativePath, linkTarget, destination) {
|
|
1568
|
+
try {
|
|
1569
|
+
await fs.symlink(linkTarget, destination);
|
|
1570
|
+
return;
|
|
1571
|
+
} catch (symlinkError) {
|
|
1572
|
+
const absoluteSource = path.resolve(path.dirname(path.join(root, relativePath)), linkTarget);
|
|
1573
|
+
try {
|
|
1574
|
+
await fs.copyFile(absoluteSource, destination);
|
|
1575
|
+
} catch (copyError) {
|
|
1576
|
+
throw new MinecraftKitError(
|
|
1577
|
+
"FILESYSTEM_WRITE_ERROR",
|
|
1578
|
+
`Failed to materialize runtime entry: ${destination}`,
|
|
1579
|
+
{
|
|
1580
|
+
cause: copyError,
|
|
1581
|
+
context: {
|
|
1582
|
+
filePath: destination,
|
|
1583
|
+
linkTarget,
|
|
1584
|
+
symlinkError: errorMessage(symlinkError)
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
function isNotFound(error) {
|
|
1592
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1593
|
+
}
|
|
1594
|
+
function errorMessage(error) {
|
|
1595
|
+
return error instanceof Error ? error.message : String(error);
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// src/install/runner.ts
|
|
1599
|
+
async function runInstall(input) {
|
|
1600
|
+
const startedAt = Date.now();
|
|
1601
|
+
let bytesDownloaded = 0;
|
|
1602
|
+
let actionsCompleted = 0;
|
|
1603
|
+
let actionsSkipped = 0;
|
|
1604
|
+
const onEvent = input.onEvent;
|
|
1605
|
+
let currentPhase = null;
|
|
1606
|
+
const enterPhase = (phase) => {
|
|
1607
|
+
if (phase === currentPhase) return;
|
|
1608
|
+
onEvent?.({ type: "install:phase-changed", phase, previous: currentPhase });
|
|
1609
|
+
currentPhase = phase;
|
|
1610
|
+
};
|
|
1611
|
+
const downloads = input.plan.actions.filter(isDownload);
|
|
1612
|
+
const natives = input.plan.actions.filter(isNative);
|
|
1613
|
+
const writeActions = input.plan.actions.filter(isWrite);
|
|
1614
|
+
const processors = input.plan.actions.filter(isProcessor);
|
|
1615
|
+
enterPhase(InstallPhases.PLANNING);
|
|
1616
|
+
enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
|
|
1617
|
+
const limit = pLimit(input.concurrency ?? DOWNLOAD_CONCURRENCY);
|
|
1618
|
+
await Promise.all(
|
|
1619
|
+
downloads.map(
|
|
1620
|
+
(action) => limit(async () => {
|
|
1621
|
+
if (input.signal?.aborted) {
|
|
1622
|
+
throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
|
|
1623
|
+
}
|
|
1624
|
+
const result = await downloadFile(input.http, {
|
|
1625
|
+
url: action.url,
|
|
1626
|
+
target: action.target,
|
|
1627
|
+
...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
|
|
1628
|
+
...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
|
|
1629
|
+
...action.category !== void 0 ? { category: action.category } : {},
|
|
1630
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
1631
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
1632
|
+
});
|
|
1633
|
+
bytesDownloaded += result.bytesDownloaded;
|
|
1634
|
+
if (result.skipped) actionsSkipped++;
|
|
1635
|
+
actionsCompleted++;
|
|
1636
|
+
})
|
|
1637
|
+
)
|
|
1638
|
+
);
|
|
1639
|
+
if (writeActions.length > 0) {
|
|
1640
|
+
enterPhase(InstallPhases.WRITING_FILES);
|
|
1641
|
+
for (const action of writeActions) {
|
|
1642
|
+
if (input.signal?.aborted) {
|
|
1643
|
+
throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
|
|
1644
|
+
}
|
|
1645
|
+
await atomicWrite(action.path, action.content);
|
|
1646
|
+
actionsCompleted++;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
if (natives.length > 0) {
|
|
1650
|
+
enterPhase(InstallPhases.EXTRACTING_NATIVES);
|
|
1651
|
+
for (const action of natives) {
|
|
1652
|
+
if (input.signal?.aborted) {
|
|
1653
|
+
throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
|
|
1654
|
+
}
|
|
1655
|
+
const { fileCount } = await extractAllToDir(action.source, action.destination, {
|
|
1656
|
+
excludePrefixes: action.exclude
|
|
1657
|
+
});
|
|
1658
|
+
input.onEvent?.({
|
|
1659
|
+
type: "archive:extracted",
|
|
1660
|
+
archive: action.source,
|
|
1661
|
+
target: action.destination,
|
|
1662
|
+
fileCount
|
|
1663
|
+
});
|
|
1664
|
+
actionsCompleted++;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
if (input.plan.target.runtime !== void 0) {
|
|
1668
|
+
enterPhase(InstallPhases.INSTALLING_RUNTIME);
|
|
1669
|
+
const runtimePlan = await planRuntimeDownloads({
|
|
1670
|
+
runtime: input.plan.target.runtime,
|
|
1671
|
+
directory: input.plan.directory,
|
|
1672
|
+
http: input.http,
|
|
1673
|
+
cache: input.cache,
|
|
1674
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1675
|
+
});
|
|
1676
|
+
await materializeRuntimeExtras({
|
|
1677
|
+
runtime: input.plan.target.runtime,
|
|
1678
|
+
directory: input.plan.directory,
|
|
1679
|
+
manifest: runtimePlan.manifest
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
if (processors.length > 0) {
|
|
1683
|
+
enterPhase(InstallPhases.RUNNING_FORGE_PROCESSORS);
|
|
1684
|
+
if (input.plan.target.loader.type !== Loaders.FORGE) {
|
|
1685
|
+
throw new MinecraftKitError(
|
|
1686
|
+
"FORGE_PROCESSOR_FAILED",
|
|
1687
|
+
"Forge processors planned for a non-Forge target"
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
const javaPath = targetPaths.runtimeJavaExecutable(
|
|
1691
|
+
input.plan.directory,
|
|
1692
|
+
input.plan.target.runtime.component,
|
|
1693
|
+
input.plan.target.runtime.system.os,
|
|
1694
|
+
input.plan.target.runtime.installRoot
|
|
1695
|
+
);
|
|
1696
|
+
for (const action of processors) {
|
|
1697
|
+
if (input.signal?.aborted) {
|
|
1698
|
+
throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
|
|
1699
|
+
}
|
|
1700
|
+
await runProcessor({
|
|
1701
|
+
action,
|
|
1702
|
+
javaPath,
|
|
1703
|
+
spawner: input.spawner,
|
|
1704
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
|
|
1705
|
+
total: processors.length
|
|
1706
|
+
});
|
|
1707
|
+
actionsCompleted++;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
enterPhase(InstallPhases.COMPLETED);
|
|
1711
|
+
return {
|
|
1712
|
+
targetId: input.plan.targetId,
|
|
1713
|
+
bytesDownloaded,
|
|
1714
|
+
actionsCompleted,
|
|
1715
|
+
actionsSkipped,
|
|
1716
|
+
durationMs: Date.now() - startedAt
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
function isDownload(action) {
|
|
1720
|
+
return action.kind === InstallActionKinds.DOWNLOAD_FILE;
|
|
1721
|
+
}
|
|
1722
|
+
function isNative(action) {
|
|
1723
|
+
return action.kind === InstallActionKinds.EXTRACT_NATIVE;
|
|
1724
|
+
}
|
|
1725
|
+
function isProcessor(action) {
|
|
1726
|
+
return action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR;
|
|
1727
|
+
}
|
|
1728
|
+
function isWrite(action) {
|
|
1729
|
+
return action.kind === InstallActionKinds.WRITE_VERSION_JSON || action.kind === InstallActionKinds.WRITE_LOGGING_CONFIG;
|
|
1730
|
+
}
|
|
1731
|
+
async function runProcessor(input) {
|
|
1732
|
+
const startedAt = Date.now();
|
|
1733
|
+
const classpathSeparator = process.platform === "win32" ? ";" : ":";
|
|
1734
|
+
const args = [
|
|
1735
|
+
"-cp",
|
|
1736
|
+
input.action.classpath.join(classpathSeparator),
|
|
1737
|
+
input.action.mainClass,
|
|
1738
|
+
...input.action.args
|
|
1739
|
+
];
|
|
1740
|
+
input.onEvent?.({
|
|
1741
|
+
type: "forge:processor-started",
|
|
1742
|
+
processor: { index: input.action.index, mainClass: input.action.mainClass },
|
|
1743
|
+
total: input.total
|
|
1744
|
+
});
|
|
1745
|
+
const stderrTail = [];
|
|
1746
|
+
const child = input.spawner.spawn(input.javaPath, args, { cwd: process.cwd() });
|
|
1747
|
+
child.stdout.on("data", () => {
|
|
1748
|
+
});
|
|
1749
|
+
child.stderr.on("data", (line) => {
|
|
1750
|
+
if (stderrTail.length >= MAX_PROCESSOR_STDERR_LINES) stderrTail.shift();
|
|
1751
|
+
stderrTail.push(line);
|
|
1752
|
+
});
|
|
1753
|
+
const exit = await child.exited;
|
|
1754
|
+
if (exit.code !== 0) {
|
|
1755
|
+
throw new MinecraftKitError(
|
|
1756
|
+
"FORGE_PROCESSOR_FAILED",
|
|
1757
|
+
`Forge processor exited with code ${exit.code ?? "(signal)"}: ${input.action.mainClass}`,
|
|
1758
|
+
{
|
|
1759
|
+
context: {
|
|
1760
|
+
exitCode: exit.code ?? void 0,
|
|
1761
|
+
mainClass: input.action.mainClass,
|
|
1762
|
+
stderr: stderrTail.join("\n")
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
input.onEvent?.({
|
|
1768
|
+
type: "forge:processor-completed",
|
|
1769
|
+
processor: { index: input.action.index, mainClass: input.action.mainClass },
|
|
1770
|
+
exitCode: exit.code ?? 0,
|
|
1771
|
+
durationMs: Date.now() - startedAt
|
|
1772
|
+
});
|
|
1773
|
+
for (const [outputPath, expectedSha1] of Object.entries(input.action.outputs)) {
|
|
1774
|
+
const sha1 = await sha1OfFileStreaming(outputPath);
|
|
1775
|
+
if (sha1 !== expectedSha1) {
|
|
1776
|
+
throw new MinecraftKitError(
|
|
1777
|
+
"FORGE_PROCESSOR_FAILED",
|
|
1778
|
+
`Processor output hash mismatch: ${outputPath}`,
|
|
1779
|
+
{ context: { filePath: outputPath, expectedHash: expectedSha1, actualHash: sha1 } }
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
input.onEvent?.({
|
|
1783
|
+
type: "forge:processor-output-verified",
|
|
1784
|
+
processor: { index: input.action.index, mainClass: input.action.mainClass },
|
|
1785
|
+
path: outputPath
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
async function sha1OfFileStreaming(filePath) {
|
|
1790
|
+
const hash = crypto2.createHash("sha1");
|
|
1791
|
+
await new Promise((resolve, reject) => {
|
|
1792
|
+
const stream = createReadStream(filePath);
|
|
1793
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1794
|
+
stream.on("end", () => resolve());
|
|
1795
|
+
stream.on("error", reject);
|
|
1796
|
+
});
|
|
1797
|
+
return hash.digest("hex");
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// src/install/runtime-install.ts
|
|
1801
|
+
async function planRuntimeInstall(input) {
|
|
1802
|
+
const runtimePlan = await planRuntimeDownloads({
|
|
1803
|
+
runtime: input.target.runtime,
|
|
1804
|
+
directory: input.target.directory,
|
|
1805
|
+
http: input.http,
|
|
1806
|
+
cache: input.cache,
|
|
1807
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1808
|
+
});
|
|
1809
|
+
const actions = runtimePlan.actions;
|
|
1810
|
+
const totalBytes = runtimePlan.actions.reduce(
|
|
1811
|
+
(sum, action) => sum + (action.expectedSize ?? 0),
|
|
1812
|
+
0
|
|
1813
|
+
);
|
|
1814
|
+
return {
|
|
1815
|
+
targetId: input.target.id,
|
|
1816
|
+
directory: input.target.directory,
|
|
1817
|
+
target: input.target,
|
|
1818
|
+
actions,
|
|
1819
|
+
totalActions: actions.length,
|
|
1820
|
+
totalBytes
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
async function planStandaloneRuntimeInstall(input) {
|
|
1824
|
+
const runtimePlan = await planRuntimeDownloads({
|
|
1825
|
+
runtime: input.runtime,
|
|
1826
|
+
directory: input.directory,
|
|
1827
|
+
http: input.http,
|
|
1828
|
+
cache: input.cache,
|
|
1829
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
1830
|
+
});
|
|
1831
|
+
const actions = runtimePlan.actions;
|
|
1832
|
+
const totalBytes = runtimePlan.actions.reduce(
|
|
1833
|
+
(sum, action) => sum + (action.expectedSize ?? 0),
|
|
1834
|
+
0
|
|
1835
|
+
);
|
|
1836
|
+
const target = {
|
|
1837
|
+
id: input.id,
|
|
1838
|
+
directory: input.directory,
|
|
1839
|
+
runtime: input.runtime,
|
|
1840
|
+
minecraft: void 0,
|
|
1841
|
+
loader: void 0
|
|
1842
|
+
};
|
|
1843
|
+
return {
|
|
1844
|
+
targetId: input.id,
|
|
1845
|
+
directory: input.directory,
|
|
1846
|
+
target,
|
|
1847
|
+
actions,
|
|
1848
|
+
totalActions: actions.length,
|
|
1849
|
+
totalBytes
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// src/constants/launch.ts
|
|
1854
|
+
var BASE_JVM_ARGS = [
|
|
1855
|
+
"-XX:+UnlockExperimentalVMOptions",
|
|
1856
|
+
"-XX:+UseG1GC",
|
|
1857
|
+
"-XX:G1NewSizePercent=20",
|
|
1858
|
+
"-XX:G1ReservePercent=20",
|
|
1859
|
+
"-XX:MaxGCPauseMillis=50",
|
|
1860
|
+
"-XX:G1HeapRegionSize=32M"
|
|
1861
|
+
];
|
|
1862
|
+
var LEGACY_JVM_ARGS = [
|
|
1863
|
+
"-Djava.library.path=${natives_directory}",
|
|
1864
|
+
"-Dminecraft.launcher.brand=${launcher_name}",
|
|
1865
|
+
"-Dminecraft.launcher.version=${launcher_version}",
|
|
1866
|
+
"-cp",
|
|
1867
|
+
"${classpath}"
|
|
1868
|
+
];
|
|
1869
|
+
var MACOS_JVM_ARGS = ["-Xdock:name=Minecraft"];
|
|
1870
|
+
|
|
1871
|
+
// src/launch/arguments.ts
|
|
1872
|
+
function flattenArguments(entries, context) {
|
|
1873
|
+
const result = [];
|
|
1874
|
+
for (const entry of entries) {
|
|
1875
|
+
if (typeof entry === "string") {
|
|
1876
|
+
result.push(entry);
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
if (!evaluateRules(entry.rules, context)) continue;
|
|
1880
|
+
if (typeof entry.value === "string") {
|
|
1881
|
+
result.push(entry.value);
|
|
1882
|
+
} else {
|
|
1883
|
+
result.push(...entry.value);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
return result;
|
|
1887
|
+
}
|
|
1888
|
+
function splitLegacyArguments(raw) {
|
|
1889
|
+
return raw.trim().length === 0 ? [] : raw.trim().split(/\s+/);
|
|
1890
|
+
}
|
|
1891
|
+
function pickArguments(args, context) {
|
|
1892
|
+
return {
|
|
1893
|
+
game: flattenArguments(args?.game ?? [], context),
|
|
1894
|
+
jvm: flattenArguments(args?.jvm ?? [], context)
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/launch/placeholders.ts
|
|
1899
|
+
function substituteArg(raw, values) {
|
|
1900
|
+
return raw.replaceAll(/\$\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
|
|
1901
|
+
const value = values[key];
|
|
1902
|
+
if (value === void 0) {
|
|
1903
|
+
throw new MinecraftKitError("INVALID_INPUT", `Unknown launch placeholder: ${match}`, {
|
|
1904
|
+
context: { placeholder: key }
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
return value;
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
function substituteArgs(args, values) {
|
|
1911
|
+
return args.map((arg) => substituteArg(arg, values));
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// src/launch/args-composition.ts
|
|
1915
|
+
function composeArgs(input) {
|
|
1916
|
+
const minMb = input.options.memory?.minMb ?? DEFAULT_MIN_MB;
|
|
1917
|
+
const maxMb = input.options.memory?.maxMb ?? DEFAULT_MAX_MB;
|
|
1918
|
+
const memoryArgs = [`-Xms${minMb}M`, `-Xmx${maxMb}M`];
|
|
1919
|
+
const ruleContext = { system: input.target.runtime.system, features: input.features };
|
|
1920
|
+
let rawJvm;
|
|
1921
|
+
let rawGame;
|
|
1922
|
+
if (input.merged.arguments) {
|
|
1923
|
+
const picked = pickArguments(input.merged.arguments, ruleContext);
|
|
1924
|
+
rawJvm = picked.jvm;
|
|
1925
|
+
rawGame = picked.game;
|
|
1926
|
+
} else if (input.merged.minecraftArguments) {
|
|
1927
|
+
rawJvm = LEGACY_JVM_ARGS;
|
|
1928
|
+
rawGame = splitLegacyArguments(input.merged.minecraftArguments);
|
|
1929
|
+
} else {
|
|
1930
|
+
rawJvm = [];
|
|
1931
|
+
rawGame = [];
|
|
1932
|
+
}
|
|
1933
|
+
const macosArgs = input.target.runtime.system.os === "osx" ? MACOS_JVM_ARGS : [];
|
|
1934
|
+
const baseJvm = [...memoryArgs, ...BASE_JVM_ARGS, ...macosArgs];
|
|
1935
|
+
const substitutedJvm = substituteArgs(rawJvm, input.placeholderValues);
|
|
1936
|
+
const substitutedGame = substituteArgs(rawGame, input.placeholderValues);
|
|
1937
|
+
const jvmArgs = [...baseJvm, ...substitutedJvm];
|
|
1938
|
+
if (input.merged.logging?.client?.argument) {
|
|
1939
|
+
const logging = input.merged.logging.client;
|
|
1940
|
+
const loggingArg = substituteArgs([logging.argument], {
|
|
1941
|
+
...input.placeholderValues,
|
|
1942
|
+
path: targetPaths.loggingConfig(input.target.directory, logging.file.id)
|
|
1943
|
+
})[0];
|
|
1944
|
+
if (loggingArg !== void 0) jvmArgs.push(loggingArg);
|
|
1945
|
+
}
|
|
1946
|
+
const extraJvm = input.options.extraJvmArgs ?? [];
|
|
1947
|
+
const extraGame = input.options.extraGameArgs ?? [];
|
|
1948
|
+
const gameArgs = [...substitutedGame, ...extraGame];
|
|
1949
|
+
if (input.options.fullscreen === true) gameArgs.push("--fullscreen");
|
|
1950
|
+
if (input.options.resolution !== void 0 && rawGame.every((a) => !a.includes("--width"))) {
|
|
1951
|
+
gameArgs.push(
|
|
1952
|
+
"--width",
|
|
1953
|
+
input.options.resolution.width.toString(),
|
|
1954
|
+
"--height",
|
|
1955
|
+
input.options.resolution.height.toString()
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
return { jvmArgs: [...jvmArgs, ...extraJvm], gameArgs };
|
|
1959
|
+
}
|
|
1960
|
+
function buildClasspath(input) {
|
|
1961
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1962
|
+
const entries = [];
|
|
1963
|
+
for (const library of input.merged.libraries) {
|
|
1964
|
+
if (library.natives) continue;
|
|
1965
|
+
if (!evaluateRules(library.rules, { system: input.system })) continue;
|
|
1966
|
+
const relative = relativeFor(library);
|
|
1967
|
+
if (!relative) continue;
|
|
1968
|
+
const absolute = path.join(targetPaths.librariesDir(input.directory), relative);
|
|
1969
|
+
if (seen.has(absolute)) continue;
|
|
1970
|
+
seen.add(absolute);
|
|
1971
|
+
entries.push(absolute);
|
|
1972
|
+
}
|
|
1973
|
+
const versionJar = targetPaths.versionJar(input.directory, input.versionId);
|
|
1974
|
+
if (!seen.has(versionJar)) entries.push(versionJar);
|
|
1975
|
+
return entries;
|
|
1976
|
+
}
|
|
1977
|
+
function relativeFor(library) {
|
|
1978
|
+
if (library.downloads?.artifact?.path) return library.downloads.artifact.path;
|
|
1979
|
+
if (library.name) {
|
|
1980
|
+
const coord = parseMavenCoordinate(library.name);
|
|
1981
|
+
return mavenRelativePath(coord);
|
|
1982
|
+
}
|
|
1983
|
+
return null;
|
|
1984
|
+
}
|
|
1985
|
+
function offlineUuidFor(username) {
|
|
1986
|
+
const md5 = crypto2.createHash("md5");
|
|
1987
|
+
md5.update(`OfflinePlayer:${username}`, "utf8");
|
|
1988
|
+
const bytes = md5.digest();
|
|
1989
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 48;
|
|
1990
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
1991
|
+
return formatUuid(bytes);
|
|
1992
|
+
}
|
|
1993
|
+
function formatUuid(bytes) {
|
|
1994
|
+
const hex = bytes.toString("hex");
|
|
1995
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
1996
|
+
}
|
|
1997
|
+
function stripUuidDashes(uuid) {
|
|
1998
|
+
return uuid.replaceAll("-", "");
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/types/auth.ts
|
|
2002
|
+
var AuthModes = {
|
|
2003
|
+
/** Offline-mode play with a chosen username and synthetic UUID. */
|
|
2004
|
+
OFFLINE: "offline",
|
|
2005
|
+
/** Pre-authenticated session — caller provides the access token and identity. */
|
|
2006
|
+
ONLINE: "online"
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
// src/launch/placeholder-values.ts
|
|
2010
|
+
function buildPlaceholderValues(input) {
|
|
2011
|
+
const cpSeparator = process.platform === "win32" ? ";" : ":";
|
|
2012
|
+
const directory = input.target.directory;
|
|
2013
|
+
const username = input.auth.username;
|
|
2014
|
+
const uuid = input.auth.mode === AuthModes.OFFLINE ? input.auth.uuid ?? offlineUuidFor(username) : input.auth.uuid;
|
|
2015
|
+
const accessToken = input.auth.mode === AuthModes.OFFLINE ? "0" : input.auth.accessToken;
|
|
2016
|
+
const userType = input.auth.mode === AuthModes.OFFLINE ? "legacy" : input.auth.userType ?? "msa";
|
|
2017
|
+
const launcherName = input.options.launcherName ?? DEFAULT_LAUNCHER_NAME;
|
|
2018
|
+
const launcherVersion = input.options.launcherVersion ?? DEFAULT_LAUNCHER_VERSION;
|
|
2019
|
+
return {
|
|
2020
|
+
auth_player_name: username,
|
|
2021
|
+
version_name: input.versionId,
|
|
2022
|
+
game_directory: directory,
|
|
2023
|
+
assets_root: path.join(directory, ASSETS_DIR),
|
|
2024
|
+
assets_index_name: input.target.minecraft.manifest.assets,
|
|
2025
|
+
auth_uuid: stripUuidDashes(uuid),
|
|
2026
|
+
auth_access_token: accessToken,
|
|
2027
|
+
auth_session: `token:${accessToken}:${stripUuidDashes(uuid)}`,
|
|
2028
|
+
clientid: input.auth.mode === AuthModes.ONLINE ? input.auth.clientId ?? "" : "",
|
|
2029
|
+
auth_xuid: input.auth.mode === AuthModes.ONLINE ? input.auth.xuid ?? "" : "",
|
|
2030
|
+
user_type: userType,
|
|
2031
|
+
user_properties: "{}",
|
|
2032
|
+
version_type: input.target.minecraft.channel,
|
|
2033
|
+
game_assets: path.join(directory, ASSETS_LEGACY_DIR),
|
|
2034
|
+
natives_directory: targetPaths.nativesDir(directory, input.target.minecraft.version),
|
|
2035
|
+
classpath: input.classpath.join(cpSeparator),
|
|
2036
|
+
classpath_separator: cpSeparator,
|
|
2037
|
+
library_directory: path.join(directory, LIBRARIES_DIR),
|
|
2038
|
+
launcher_name: launcherName,
|
|
2039
|
+
launcher_version: launcherVersion,
|
|
2040
|
+
resolution_width: input.options.resolution?.width.toString() ?? "",
|
|
2041
|
+
resolution_height: input.options.resolution?.height.toString() ?? ""
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
// src/core/manifest-merge.ts
|
|
2046
|
+
function mergeManifest(parent, child) {
|
|
2047
|
+
const merged = {
|
|
2048
|
+
id: child.id || parent.id,
|
|
2049
|
+
type: child.type ?? parent.type,
|
|
2050
|
+
mainClass: child.mainClass ?? parent.mainClass,
|
|
2051
|
+
assetIndex: child.assetIndex ?? parent.assetIndex,
|
|
2052
|
+
assets: child.assets ?? parent.assets,
|
|
2053
|
+
downloads: { ...parent.downloads, ...child.downloads },
|
|
2054
|
+
libraries: mergeLibraries(parent.libraries, child.libraries),
|
|
2055
|
+
arguments: mergeArguments(parent.arguments, child.arguments),
|
|
2056
|
+
minecraftArguments: child.minecraftArguments ?? parent.minecraftArguments,
|
|
2057
|
+
javaVersion: child.javaVersion ?? parent.javaVersion,
|
|
2058
|
+
logging: child.logging ?? parent.logging,
|
|
2059
|
+
inheritsFrom: child.inheritsFrom ?? parent.inheritsFrom,
|
|
2060
|
+
releaseTime: child.releaseTime ?? parent.releaseTime,
|
|
2061
|
+
time: child.time ?? parent.time,
|
|
2062
|
+
minimumLauncherVersion: child.minimumLauncherVersion ?? parent.minimumLauncherVersion,
|
|
2063
|
+
complianceLevel: child.complianceLevel ?? parent.complianceLevel
|
|
2064
|
+
};
|
|
2065
|
+
return merged;
|
|
2066
|
+
}
|
|
2067
|
+
function mergeLibraries(parent, child) {
|
|
2068
|
+
return [...parent, ...child];
|
|
2069
|
+
}
|
|
2070
|
+
function mergeArguments(parent, child) {
|
|
2071
|
+
if (!parent && !child) return void 0;
|
|
2072
|
+
const parentGame = parent?.game ?? [];
|
|
2073
|
+
const parentJvm = parent?.jvm ?? [];
|
|
2074
|
+
const childGame = child?.game ?? [];
|
|
2075
|
+
const childJvm = child?.jvm ?? [];
|
|
2076
|
+
return {
|
|
2077
|
+
game: [...parentGame, ...childGame],
|
|
2078
|
+
jvm: [...parentJvm, ...childJvm]
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// src/launch/version-resolution.ts
|
|
2083
|
+
async function resolveLaunchVersion(target) {
|
|
2084
|
+
if (target.loader.type === Loaders.VANILLA) {
|
|
2085
|
+
return {
|
|
2086
|
+
versionId: target.minecraft.version,
|
|
2087
|
+
merged: target.minecraft.manifest,
|
|
2088
|
+
chain: [target.minecraft.version]
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
const versionId = await pickInstalledVersionId(target);
|
|
2092
|
+
const merged = await loadAndMerge(target.directory, versionId, target.minecraft.manifest);
|
|
2093
|
+
return { versionId, merged, chain: [versionId, target.minecraft.version] };
|
|
2094
|
+
}
|
|
2095
|
+
async function pickClientJarVersionId(directory, chain) {
|
|
2096
|
+
for (const id of chain) {
|
|
2097
|
+
const jar = targetPaths.versionJar(directory, id);
|
|
2098
|
+
if (await fileExists(jar)) return id;
|
|
2099
|
+
}
|
|
2100
|
+
const fallback = chain.at(-1);
|
|
2101
|
+
if (fallback === void 0) {
|
|
2102
|
+
throw new MinecraftKitError(
|
|
2103
|
+
"MANIFEST_NOT_FOUND",
|
|
2104
|
+
"Cannot resolve a client jar version id from an empty inheritsFrom chain"
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
return fallback;
|
|
2108
|
+
}
|
|
2109
|
+
async function pickInstalledVersionId(target) {
|
|
2110
|
+
if (target.loader.type === Loaders.FABRIC) {
|
|
2111
|
+
const candidate = target.loader.profile.id;
|
|
2112
|
+
const versionJsonPath = targetPaths.versionJson(target.directory, candidate);
|
|
2113
|
+
if (await fileExists(versionJsonPath)) return candidate;
|
|
2114
|
+
}
|
|
2115
|
+
if (target.loader.type === Loaders.FORGE) {
|
|
2116
|
+
const directories = await listChildDirectories(targetPaths.versionsDir(target.directory));
|
|
2117
|
+
for (const id of directories) {
|
|
2118
|
+
const versionJsonPath = targetPaths.versionJson(target.directory, id);
|
|
2119
|
+
if (!await fileExists(versionJsonPath)) continue;
|
|
2120
|
+
const text = await readText(versionJsonPath);
|
|
2121
|
+
try {
|
|
2122
|
+
const parsed = JSON.parse(text);
|
|
2123
|
+
if (parsed.inheritsFrom === target.minecraft.version && (id.includes("forge") || (parsed.id ?? "").includes("forge"))) {
|
|
2124
|
+
return id;
|
|
2125
|
+
}
|
|
2126
|
+
} catch {
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
throw new MinecraftKitError(
|
|
2131
|
+
"MANIFEST_NOT_FOUND",
|
|
2132
|
+
`Could not find an installed version JSON for target ${target.id}`,
|
|
2133
|
+
{ context: { targetId: target.id, loaderType: target.loader.type } }
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
async function loadAndMerge(directory, versionId, parentManifest) {
|
|
2137
|
+
const versionJsonPath = targetPaths.versionJson(directory, versionId);
|
|
2138
|
+
const text = await readText(versionJsonPath);
|
|
2139
|
+
let child;
|
|
2140
|
+
try {
|
|
2141
|
+
child = JSON.parse(text);
|
|
2142
|
+
} catch (cause) {
|
|
2143
|
+
throw new MinecraftKitError(
|
|
2144
|
+
"MANIFEST_INVALID",
|
|
2145
|
+
`Version JSON is not valid JSON: ${versionJsonPath}`,
|
|
2146
|
+
{ cause, context: { filePath: versionJsonPath } }
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
if (child.inheritsFrom !== void 0 && child.inheritsFrom !== parentManifest.id) {
|
|
2150
|
+
return mergeManifest(parentManifest, child);
|
|
2151
|
+
}
|
|
2152
|
+
return mergeManifest(parentManifest, child);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// src/launch/compose.ts
|
|
2156
|
+
async function composeLaunch(input) {
|
|
2157
|
+
const { target, options } = input;
|
|
2158
|
+
if (!options.auth.username || options.auth.username.length === 0) {
|
|
2159
|
+
throw new MinecraftKitError(
|
|
2160
|
+
"INVALID_INPUT",
|
|
2161
|
+
`Auth username must be non-empty (target ${target.id})`,
|
|
2162
|
+
{ context: { targetId: target.id } }
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
const resolved = await resolveLaunchVersion(target);
|
|
2166
|
+
const javaPath = targetPaths.runtimeJavaExecutable(
|
|
2167
|
+
target.directory,
|
|
2168
|
+
target.runtime.component,
|
|
2169
|
+
target.runtime.system.os,
|
|
2170
|
+
target.runtime.installRoot
|
|
2171
|
+
);
|
|
2172
|
+
const clientJarVersionId = await pickClientJarVersionId(target.directory, resolved.chain);
|
|
2173
|
+
const classpath = buildClasspath({
|
|
2174
|
+
directory: target.directory,
|
|
2175
|
+
versionId: clientJarVersionId,
|
|
2176
|
+
merged: resolved.merged,
|
|
2177
|
+
system: target.runtime.system
|
|
2178
|
+
});
|
|
2179
|
+
const features = buildFeatures(options);
|
|
2180
|
+
const placeholderValues = buildPlaceholderValues({
|
|
2181
|
+
target,
|
|
2182
|
+
versionId: resolved.versionId,
|
|
2183
|
+
auth: options.auth,
|
|
2184
|
+
classpath,
|
|
2185
|
+
options
|
|
2186
|
+
});
|
|
2187
|
+
const composed = composeArgs({
|
|
2188
|
+
target,
|
|
2189
|
+
merged: resolved.merged,
|
|
2190
|
+
options,
|
|
2191
|
+
placeholderValues,
|
|
2192
|
+
features
|
|
2193
|
+
});
|
|
2194
|
+
return {
|
|
2195
|
+
targetId: target.id,
|
|
2196
|
+
directory: target.directory,
|
|
2197
|
+
javaPath,
|
|
2198
|
+
mainClass: resolved.merged.mainClass,
|
|
2199
|
+
jvmArgs: composed.jvmArgs,
|
|
2200
|
+
gameArgs: composed.gameArgs,
|
|
2201
|
+
classpath,
|
|
2202
|
+
nativesDirectory: targetPaths.nativesDir(target.directory, target.minecraft.version),
|
|
2203
|
+
auth: options.auth,
|
|
2204
|
+
workingDirectory: target.directory
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
function buildFeatures(options) {
|
|
2208
|
+
const features = { ...options.features ?? {} };
|
|
2209
|
+
if (options.resolution !== void 0) {
|
|
2210
|
+
features.has_custom_resolution = true;
|
|
2211
|
+
}
|
|
2212
|
+
return features;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
// src/launch/runner.ts
|
|
2216
|
+
function runLaunch(input) {
|
|
2217
|
+
const composition = input.composition;
|
|
2218
|
+
const options = input.options ?? {};
|
|
2219
|
+
const args = [...composition.jvmArgs, composition.mainClass, ...composition.gameArgs];
|
|
2220
|
+
options.onEvent?.({
|
|
2221
|
+
type: "launch:starting",
|
|
2222
|
+
command: composition.javaPath,
|
|
2223
|
+
args,
|
|
2224
|
+
cwd: composition.workingDirectory
|
|
2225
|
+
});
|
|
2226
|
+
const child = input.spawner.spawn(composition.javaPath, args, {
|
|
2227
|
+
cwd: composition.workingDirectory,
|
|
2228
|
+
...composition.env !== void 0 ? { env: composition.env } : {}
|
|
2229
|
+
});
|
|
2230
|
+
options.onEvent?.({ type: "launch:started", pid: child.pid });
|
|
2231
|
+
child.stdout.on("data", (line) => {
|
|
2232
|
+
options.onEvent?.({ type: "launch:stdout", line });
|
|
2233
|
+
});
|
|
2234
|
+
child.stderr.on("data", (line) => {
|
|
2235
|
+
options.onEvent?.({ type: "launch:stderr", line });
|
|
2236
|
+
});
|
|
2237
|
+
const grace = options.killGracePeriodMs ?? DEFAULT_KILL_GRACE_MS;
|
|
2238
|
+
let aborted = false;
|
|
2239
|
+
const doAbort = (reason) => {
|
|
2240
|
+
if (aborted) return;
|
|
2241
|
+
aborted = true;
|
|
2242
|
+
options.onEvent?.({ type: "launch:aborted", reason });
|
|
2243
|
+
child.kill("SIGTERM");
|
|
2244
|
+
setTimeout(() => child.kill("SIGKILL"), grace).unref();
|
|
2245
|
+
};
|
|
2246
|
+
if (options.signal !== void 0) {
|
|
2247
|
+
if (options.signal.aborted) {
|
|
2248
|
+
doAbort(reasonFrom(options.signal.reason));
|
|
2249
|
+
} else {
|
|
2250
|
+
options.signal.addEventListener("abort", () => doAbort(reasonFrom(options.signal?.reason)), {
|
|
2251
|
+
once: true
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
const exited = (async () => {
|
|
2256
|
+
const { code, signal } = await child.exited;
|
|
2257
|
+
options.onEvent?.({ type: "launch:exited", code, signal });
|
|
2258
|
+
if (!aborted && code !== 0 && code !== null) {
|
|
2259
|
+
throw new MinecraftKitError(
|
|
2260
|
+
"LAUNCH_PROCESS_FAILED",
|
|
2261
|
+
`Minecraft process exited with code ${code}`,
|
|
2262
|
+
{ context: { exitCode: code } }
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
return { code, signal, aborted };
|
|
2266
|
+
})();
|
|
2267
|
+
return {
|
|
2268
|
+
pid: child.pid,
|
|
2269
|
+
exited,
|
|
2270
|
+
abort(reason) {
|
|
2271
|
+
doAbort(reason ?? "user");
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
function reasonFrom(value) {
|
|
2276
|
+
if (value === void 0) return "aborted";
|
|
2277
|
+
if (typeof value === "string") return value;
|
|
2278
|
+
if (value instanceof Error) return value.message;
|
|
2279
|
+
return String(value);
|
|
2280
|
+
}
|
|
2281
|
+
var ChildProcessSpawner = class {
|
|
2282
|
+
spawn(command, args, options) {
|
|
2283
|
+
const child = spawn(command, [...args], {
|
|
2284
|
+
cwd: options.cwd,
|
|
2285
|
+
env: options.env === void 0 ? process.env : { ...process.env, ...options.env },
|
|
2286
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2287
|
+
});
|
|
2288
|
+
const stdout = streamFromBuffer(child.stdout);
|
|
2289
|
+
const stderr = streamFromBuffer(child.stderr);
|
|
2290
|
+
const exited = new Promise((resolve) => {
|
|
2291
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
2292
|
+
});
|
|
2293
|
+
return {
|
|
2294
|
+
pid: child.pid ?? -1,
|
|
2295
|
+
stdout,
|
|
2296
|
+
stderr,
|
|
2297
|
+
exited,
|
|
2298
|
+
kill(signal) {
|
|
2299
|
+
return child.kill(signal);
|
|
2300
|
+
}
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
function streamFromBuffer(stream) {
|
|
2305
|
+
if (!stream) {
|
|
2306
|
+
return { on() {
|
|
2307
|
+
} };
|
|
2308
|
+
}
|
|
2309
|
+
let buffer = "";
|
|
2310
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2311
|
+
const emit = (line) => {
|
|
2312
|
+
for (const listener of listeners) listener(line);
|
|
2313
|
+
};
|
|
2314
|
+
stream.on("data", (chunk) => {
|
|
2315
|
+
const text = Buffer$1.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
2316
|
+
buffer += text;
|
|
2317
|
+
let index = buffer.indexOf("\n");
|
|
2318
|
+
while (index !== -1) {
|
|
2319
|
+
const line = buffer.slice(0, index).replace(/\r$/, "");
|
|
2320
|
+
buffer = buffer.slice(index + 1);
|
|
2321
|
+
emitBounded(emit, line);
|
|
2322
|
+
index = buffer.indexOf("\n");
|
|
2323
|
+
}
|
|
2324
|
+
while (buffer.length > SPAWNER_MAX_LINE_BYTES) {
|
|
2325
|
+
emit(buffer.slice(0, SPAWNER_MAX_LINE_BYTES));
|
|
2326
|
+
buffer = buffer.slice(SPAWNER_MAX_LINE_BYTES);
|
|
2327
|
+
}
|
|
2328
|
+
});
|
|
2329
|
+
stream.on("end", () => {
|
|
2330
|
+
if (buffer.length > 0) {
|
|
2331
|
+
emitBounded(emit, buffer);
|
|
2332
|
+
buffer = "";
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
return {
|
|
2336
|
+
on(_event, listener) {
|
|
2337
|
+
listeners.add(listener);
|
|
2338
|
+
}
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
function emitBounded(emit, line) {
|
|
2342
|
+
if (line.length <= SPAWNER_MAX_LINE_BYTES) {
|
|
2343
|
+
emit(line);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
for (let i = 0; i < line.length; i += SPAWNER_MAX_LINE_BYTES) {
|
|
2347
|
+
emit(line.slice(i, i + SPAWNER_MAX_LINE_BYTES));
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
// src/types/verify.ts
|
|
2352
|
+
var VerificationKinds = {
|
|
2353
|
+
MINECRAFT: "minecraft",
|
|
2354
|
+
FABRIC: "fabric",
|
|
2355
|
+
FORGE: "forge",
|
|
2356
|
+
RUNTIME: "runtime"
|
|
2357
|
+
};
|
|
2358
|
+
var VerifyFileStatuses = {
|
|
2359
|
+
OK: "ok",
|
|
2360
|
+
MISSING: "missing",
|
|
2361
|
+
CORRUPT: "corrupt",
|
|
2362
|
+
WRONG_SIZE: "wrong-size"
|
|
2363
|
+
};
|
|
2364
|
+
var VerifyFileCategories = {
|
|
2365
|
+
CLIENT_JAR: "client-jar",
|
|
2366
|
+
LIBRARY: "library",
|
|
2367
|
+
ASSET: "asset",
|
|
2368
|
+
ASSET_INDEX: "asset-index",
|
|
2369
|
+
NATIVE: "native",
|
|
2370
|
+
LOADER_LIBRARY: "loader-library",
|
|
2371
|
+
RUNTIME_FILE: "runtime-file",
|
|
2372
|
+
LOGGING_CONFIG: "logging-config"
|
|
2373
|
+
};
|
|
2374
|
+
|
|
2375
|
+
// src/repair/helpers.ts
|
|
2376
|
+
function asResultArray(from) {
|
|
2377
|
+
return Array.isArray(from) ? from : [from];
|
|
2378
|
+
}
|
|
2379
|
+
function buildIssueIndex(from) {
|
|
2380
|
+
const map = /* @__PURE__ */ new Map();
|
|
2381
|
+
for (const v of asResultArray(from)) {
|
|
2382
|
+
for (const issue of v.issues) {
|
|
2383
|
+
const set = map.get(issue.path);
|
|
2384
|
+
if (set) set.add(issue.category);
|
|
2385
|
+
else map.set(issue.path, /* @__PURE__ */ new Set([issue.category]));
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
return {
|
|
2389
|
+
has: (path16) => map.has(path16),
|
|
2390
|
+
hasNonNative: (path16) => {
|
|
2391
|
+
const cats = map.get(path16);
|
|
2392
|
+
if (!cats) return false;
|
|
2393
|
+
for (const c of cats) {
|
|
2394
|
+
if (c !== VerifyFileCategories.NATIVE) return true;
|
|
2395
|
+
}
|
|
2396
|
+
return false;
|
|
2397
|
+
},
|
|
2398
|
+
categoriesAt: (path16) => map.get(path16) ?? /* @__PURE__ */ new Set()
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
function sumDownloadBytes(actions) {
|
|
2402
|
+
return actions.reduce((sum, action) => {
|
|
2403
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
2404
|
+
return sum + (action.expectedSize ?? 0);
|
|
2405
|
+
}
|
|
2406
|
+
return sum;
|
|
2407
|
+
}, 0);
|
|
2408
|
+
}
|
|
2409
|
+
function buildRepairPlan(target, actions) {
|
|
2410
|
+
return {
|
|
2411
|
+
targetId: target.id,
|
|
2412
|
+
directory: target.directory,
|
|
2413
|
+
target,
|
|
2414
|
+
actions,
|
|
2415
|
+
totalActions: actions.length,
|
|
2416
|
+
totalBytes: sumDownloadBytes(actions)
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
async function planAspectRepair(input, aspectFilter, postprocess) {
|
|
2420
|
+
const installPlan = await planInstall({
|
|
2421
|
+
target: input.target,
|
|
2422
|
+
http: input.http,
|
|
2423
|
+
cache: input.cache,
|
|
2424
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2425
|
+
});
|
|
2426
|
+
const issues = buildIssueIndex(input.from);
|
|
2427
|
+
const actions = selectRepairActions({
|
|
2428
|
+
target: input.target,
|
|
2429
|
+
installPlan,
|
|
2430
|
+
issues,
|
|
2431
|
+
aspectFilter
|
|
2432
|
+
});
|
|
2433
|
+
postprocess?.({ actions, installPlan, issues });
|
|
2434
|
+
return buildRepairPlan(input.target, actions);
|
|
2435
|
+
}
|
|
2436
|
+
function selectRepairActions(input) {
|
|
2437
|
+
const matching = [];
|
|
2438
|
+
for (const action of input.installPlan.actions) {
|
|
2439
|
+
if (!input.aspectFilter(action)) continue;
|
|
2440
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
2441
|
+
if (input.issues.hasNonNative(action.target)) {
|
|
2442
|
+
matching.push(action);
|
|
2443
|
+
}
|
|
2444
|
+
} else if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
|
|
2445
|
+
if (input.issues.has(action.path)) {
|
|
2446
|
+
matching.push(action);
|
|
2447
|
+
}
|
|
2448
|
+
} else if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
|
|
2449
|
+
if (input.issues.has(action.source)) {
|
|
2450
|
+
matching.push(action);
|
|
2451
|
+
}
|
|
2452
|
+
} else {
|
|
2453
|
+
matching.push(action);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return matching;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// src/repair/fabric.ts
|
|
2460
|
+
async function planFabricRepair(input) {
|
|
2461
|
+
if (input.target.loader.type !== Loaders.FABRIC) {
|
|
2462
|
+
throw new MinecraftKitError(
|
|
2463
|
+
"INVALID_INPUT",
|
|
2464
|
+
`repair.fabric requires a Fabric target (got ${input.target.loader.type})`
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
const fabricJsonPath = targetPaths.versionJson(
|
|
2468
|
+
input.target.directory,
|
|
2469
|
+
input.target.loader.profile.id
|
|
2470
|
+
);
|
|
2471
|
+
return planAspectRepair(input, (action) => {
|
|
2472
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
2473
|
+
return action.category === "fabric-library";
|
|
2474
|
+
}
|
|
2475
|
+
if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
|
|
2476
|
+
return action.path === fabricJsonPath;
|
|
2477
|
+
}
|
|
2478
|
+
return false;
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// src/repair/forge.ts
|
|
2483
|
+
var FORGE_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
|
|
2484
|
+
"forge-library",
|
|
2485
|
+
"forge-installer"
|
|
2486
|
+
]);
|
|
2487
|
+
async function planForgeRepair(input) {
|
|
2488
|
+
if (input.target.loader.type !== Loaders.FORGE) {
|
|
2489
|
+
throw new MinecraftKitError(
|
|
2490
|
+
"INVALID_INPUT",
|
|
2491
|
+
`repair.forge requires a Forge target (got ${input.target.loader.type})`
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
const forgeJsonPath = targetPaths.versionJson(
|
|
2495
|
+
input.target.directory,
|
|
2496
|
+
input.target.loader.fullVersion
|
|
2497
|
+
);
|
|
2498
|
+
return planAspectRepair(
|
|
2499
|
+
input,
|
|
2500
|
+
(action) => {
|
|
2501
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
2502
|
+
return FORGE_DOWNLOAD_CATEGORIES.has(action.category);
|
|
2503
|
+
}
|
|
2504
|
+
if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
|
|
2505
|
+
return action.path === forgeJsonPath;
|
|
2506
|
+
}
|
|
2507
|
+
return false;
|
|
2508
|
+
},
|
|
2509
|
+
({ actions, installPlan, issues }) => {
|
|
2510
|
+
if (!issues.has(forgeJsonPath)) return;
|
|
2511
|
+
const alreadyIncluded = new Set(
|
|
2512
|
+
actions.filter((a) => a.kind === InstallActionKinds.DOWNLOAD_FILE).map((a) => a.target)
|
|
2513
|
+
);
|
|
2514
|
+
for (const action of installPlan.actions) {
|
|
2515
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "forge-library" && !alreadyIncluded.has(action.target)) {
|
|
2516
|
+
actions.push(action);
|
|
2517
|
+
} else if (action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR) {
|
|
2518
|
+
actions.push(action);
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
// src/repair/minecraft.ts
|
|
2526
|
+
var MINECRAFT_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
|
|
2527
|
+
"client-jar",
|
|
2528
|
+
"library",
|
|
2529
|
+
"asset-index",
|
|
2530
|
+
"asset",
|
|
2531
|
+
"logging-config"
|
|
2532
|
+
]);
|
|
2533
|
+
async function planMinecraftRepair(input) {
|
|
2534
|
+
const vanillaJsonPath = targetPaths.versionJson(
|
|
2535
|
+
input.target.directory,
|
|
2536
|
+
input.target.minecraft.version
|
|
2537
|
+
);
|
|
2538
|
+
return planAspectRepair(input, (action) => {
|
|
2539
|
+
if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
|
|
2540
|
+
return MINECRAFT_DOWNLOAD_CATEGORIES.has(action.category);
|
|
2541
|
+
}
|
|
2542
|
+
if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
|
|
2543
|
+
return action.path === vanillaJsonPath;
|
|
2544
|
+
}
|
|
2545
|
+
if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
|
|
2546
|
+
return true;
|
|
2547
|
+
}
|
|
2548
|
+
return false;
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
// src/repair/runner.ts
|
|
2553
|
+
async function runRepair(input) {
|
|
2554
|
+
const report = await runInstall({
|
|
2555
|
+
plan: {
|
|
2556
|
+
...input.plan,
|
|
2557
|
+
totalActions: input.plan.actions.length,
|
|
2558
|
+
totalBytes: input.plan.totalBytes
|
|
2559
|
+
},
|
|
2560
|
+
http: input.http,
|
|
2561
|
+
cache: input.cache,
|
|
2562
|
+
spawner: input.spawner,
|
|
2563
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
2564
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
2565
|
+
});
|
|
2566
|
+
return {
|
|
2567
|
+
targetId: report.targetId,
|
|
2568
|
+
bytesDownloaded: report.bytesDownloaded,
|
|
2569
|
+
actionsCompleted: report.actionsCompleted,
|
|
2570
|
+
durationMs: report.durationMs
|
|
2571
|
+
};
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// src/repair/runtime.ts
|
|
2575
|
+
async function planRuntimeRepair(input) {
|
|
2576
|
+
return planAspectRepair(
|
|
2577
|
+
input,
|
|
2578
|
+
(action) => action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "runtime-file"
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// src/types/runtime.ts
|
|
2583
|
+
var RuntimeComponents = {
|
|
2584
|
+
JRE_LEGACY: "jre-legacy"};
|
|
2585
|
+
var RuntimePreference = {
|
|
2586
|
+
/** Component declared by the Minecraft manifest. */
|
|
2587
|
+
RECOMMENDED: "recommended",
|
|
2588
|
+
/** Newest component available for the platform. */
|
|
2589
|
+
LATEST: "latest"
|
|
2590
|
+
};
|
|
2591
|
+
|
|
2592
|
+
// src/targets/index.ts
|
|
2593
|
+
var TargetsApi = class {
|
|
2594
|
+
constructor(ctx) {
|
|
2595
|
+
this.ctx = ctx;
|
|
2596
|
+
}
|
|
2597
|
+
ctx;
|
|
2598
|
+
/** The detected host system used by `resolve()` when no `system` is supplied. */
|
|
2599
|
+
get system() {
|
|
2600
|
+
return this.ctx.system;
|
|
2601
|
+
}
|
|
2602
|
+
/** Build a {@link Target} from already-resolved components. */
|
|
2603
|
+
create(input) {
|
|
2604
|
+
if (!input.id) {
|
|
2605
|
+
throw new MinecraftKitError("INVALID_INPUT", "Target id must be non-empty");
|
|
2606
|
+
}
|
|
2607
|
+
if (!input.directory) {
|
|
2608
|
+
throw new MinecraftKitError("INVALID_INPUT", "Target directory must be non-empty");
|
|
2609
|
+
}
|
|
2610
|
+
if (input.loader.minecraftVersion !== input.minecraft.version) {
|
|
2611
|
+
throw new MinecraftKitError(
|
|
2612
|
+
"INVALID_INPUT",
|
|
2613
|
+
`Loader Minecraft version (${input.loader.minecraftVersion}) does not match resolved Minecraft (${input.minecraft.version})`,
|
|
2614
|
+
{
|
|
2615
|
+
context: {
|
|
2616
|
+
loaderMinecraft: input.loader.minecraftVersion,
|
|
2617
|
+
minecraftVersion: input.minecraft.version
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
return {
|
|
2623
|
+
id: input.id,
|
|
2624
|
+
directory: input.directory,
|
|
2625
|
+
minecraft: input.minecraft,
|
|
2626
|
+
loader: input.loader,
|
|
2627
|
+
runtime: input.runtime
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
/** Sugar API: resolve every component then assemble a target. */
|
|
2631
|
+
async resolve(input) {
|
|
2632
|
+
const minecraft = await this.ctx.minecraft.resolve({
|
|
2633
|
+
version: input.minecraft.version,
|
|
2634
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2635
|
+
});
|
|
2636
|
+
const system = input.system ?? this.ctx.system;
|
|
2637
|
+
const componentOverride = input.runtime?.component;
|
|
2638
|
+
const runtimeComponent = componentOverride ?? minecraft.manifest.javaVersion?.component;
|
|
2639
|
+
const resolvedRuntime = await this.ctx.runtime.resolve({
|
|
2640
|
+
system,
|
|
2641
|
+
...runtimeComponent !== void 0 ? { component: runtimeComponent } : {},
|
|
2642
|
+
preference: input.runtime?.preference ?? RuntimePreference.RECOMMENDED,
|
|
2643
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2644
|
+
});
|
|
2645
|
+
const runtime = input.runtime?.installRoot !== void 0 ? { ...resolvedRuntime, installRoot: input.runtime.installRoot } : resolvedRuntime;
|
|
2646
|
+
let loader;
|
|
2647
|
+
if (input.loader.type === Loaders.VANILLA) {
|
|
2648
|
+
loader = {
|
|
2649
|
+
type: Loaders.VANILLA,
|
|
2650
|
+
minecraftVersion: minecraft.version,
|
|
2651
|
+
minecraft
|
|
2652
|
+
};
|
|
2653
|
+
} else if (input.loader.type === Loaders.FABRIC) {
|
|
2654
|
+
loader = await this.ctx.fabric.resolve({
|
|
2655
|
+
minecraftVersion: minecraft.version,
|
|
2656
|
+
...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
|
|
2657
|
+
...input.loader.version !== void 0 ? { loaderVersion: input.loader.version } : {},
|
|
2658
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2659
|
+
});
|
|
2660
|
+
} else {
|
|
2661
|
+
loader = await this.ctx.forge.resolve({
|
|
2662
|
+
minecraftVersion: minecraft.version,
|
|
2663
|
+
...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
|
|
2664
|
+
...input.loader.version !== void 0 ? { forgeVersion: input.loader.version } : {},
|
|
2665
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
return this.create({
|
|
2669
|
+
id: input.id,
|
|
2670
|
+
directory: input.directory,
|
|
2671
|
+
minecraft,
|
|
2672
|
+
loader,
|
|
2673
|
+
runtime
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
/** Scan a root directory for Minecraft installations. Returns only what is on disk. */
|
|
2677
|
+
async list(input) {
|
|
2678
|
+
if (!await dirExists(input.rootDir)) return [];
|
|
2679
|
+
const subdirs = await listChildDirectories(input.rootDir);
|
|
2680
|
+
const results = [];
|
|
2681
|
+
for (const id of subdirs) {
|
|
2682
|
+
const directory = path.join(input.rootDir, id);
|
|
2683
|
+
const discovered = await discoverInstallation(id, directory);
|
|
2684
|
+
if (discovered) results.push(discovered);
|
|
2685
|
+
}
|
|
2686
|
+
return results;
|
|
2687
|
+
}
|
|
2688
|
+
};
|
|
2689
|
+
async function discoverInstallation(id, directory) {
|
|
2690
|
+
const versionsDir = path.join(directory, VERSIONS_DIR);
|
|
2691
|
+
const librariesDir = path.join(directory, LIBRARIES_DIR);
|
|
2692
|
+
const assetsDir = path.join(directory, ASSETS_DIR);
|
|
2693
|
+
const looksLikeInstall = await dirExists(versionsDir) && (await dirExists(librariesDir) || await dirExists(assetsDir));
|
|
2694
|
+
if (!looksLikeInstall) return null;
|
|
2695
|
+
const versionDirs = await listChildDirectories(versionsDir);
|
|
2696
|
+
const minecraftVersions = [];
|
|
2697
|
+
const loaders = [];
|
|
2698
|
+
for (const versionId of versionDirs) {
|
|
2699
|
+
const hint = inferLoaderFromVersionId(versionId);
|
|
2700
|
+
if (hint) {
|
|
2701
|
+
loaders.push(hint);
|
|
2702
|
+
if (hint.minecraftVersion && !minecraftVersions.includes(hint.minecraftVersion)) {
|
|
2703
|
+
minecraftVersions.push(hint.minecraftVersion);
|
|
2704
|
+
}
|
|
2705
|
+
} else {
|
|
2706
|
+
minecraftVersions.push(versionId);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
const runtime = await discoverRuntime(directory);
|
|
2710
|
+
return { id, directory, minecraftVersions, loaders, ...runtime ? { runtime } : {} };
|
|
2711
|
+
}
|
|
2712
|
+
async function discoverRuntime(directory) {
|
|
2713
|
+
const runtimeDir = path.join(directory, RUNTIMES_DIR);
|
|
2714
|
+
if (!await dirExists(runtimeDir)) return void 0;
|
|
2715
|
+
let components;
|
|
2716
|
+
try {
|
|
2717
|
+
components = await listChildDirectories(runtimeDir);
|
|
2718
|
+
} catch {
|
|
2719
|
+
return void 0;
|
|
2720
|
+
}
|
|
2721
|
+
for (const component of components) {
|
|
2722
|
+
const root = path.join(runtimeDir, component);
|
|
2723
|
+
const javaPath = process.platform === "win32" ? path.join(root, "bin", "javaw.exe") : process.platform === "darwin" ? path.join(root, "jre.bundle", "Contents", "Home", "bin", "java") : path.join(root, "bin", "java");
|
|
2724
|
+
if (await fileExists(javaPath)) {
|
|
2725
|
+
return { component, javaPath };
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
return void 0;
|
|
2729
|
+
}
|
|
2730
|
+
function inferLoaderFromVersionId(versionId) {
|
|
2731
|
+
const fabricMatch = /^fabric-loader-([^-]+)-(.+)$/.exec(versionId);
|
|
2732
|
+
if (fabricMatch?.[1] && fabricMatch[2]) {
|
|
2733
|
+
return { type: Loaders.FABRIC, version: fabricMatch[1], minecraftVersion: fabricMatch[2] };
|
|
2734
|
+
}
|
|
2735
|
+
const forgeMatch = /^([^-]+)-forge-(.+)$/.exec(versionId);
|
|
2736
|
+
if (forgeMatch?.[1] && forgeMatch[2]) {
|
|
2737
|
+
return { type: Loaders.FORGE, minecraftVersion: forgeMatch[1], version: forgeMatch[2] };
|
|
2738
|
+
}
|
|
2739
|
+
return null;
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
// src/update/runner.ts
|
|
2743
|
+
async function planUpdate(input) {
|
|
2744
|
+
return planInstall({
|
|
2745
|
+
target: input.target,
|
|
2746
|
+
http: input.http,
|
|
2747
|
+
cache: input.cache,
|
|
2748
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
async function runUpdate(input) {
|
|
2752
|
+
const report = await runInstall({
|
|
2753
|
+
plan: input.plan,
|
|
2754
|
+
http: input.http,
|
|
2755
|
+
cache: input.cache,
|
|
2756
|
+
spawner: input.spawner,
|
|
2757
|
+
...input.signal !== void 0 ? { signal: input.signal } : {},
|
|
2758
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
2759
|
+
});
|
|
2760
|
+
return {
|
|
2761
|
+
targetId: report.targetId,
|
|
2762
|
+
bytesDownloaded: report.bytesDownloaded,
|
|
2763
|
+
actionsCompleted: report.actionsCompleted,
|
|
2764
|
+
actionsSkipped: report.actionsSkipped,
|
|
2765
|
+
durationMs: report.durationMs
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
async function sha1OfFile(filePath) {
|
|
2769
|
+
const hash = crypto2.createHash("sha1");
|
|
2770
|
+
await new Promise((resolve, reject) => {
|
|
2771
|
+
const stream = createReadStream(filePath);
|
|
2772
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
2773
|
+
stream.on("end", resolve);
|
|
2774
|
+
stream.on("error", reject);
|
|
2775
|
+
});
|
|
2776
|
+
return hash.digest("hex");
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
// src/verify/helpers.ts
|
|
2780
|
+
async function runVerification(input, check) {
|
|
2781
|
+
const startedAt = Date.now();
|
|
2782
|
+
const results = [];
|
|
2783
|
+
const record = (result) => {
|
|
2784
|
+
results.push(result);
|
|
2785
|
+
input.onEvent?.({ type: "verify:file-checked", file: result });
|
|
2786
|
+
};
|
|
2787
|
+
await check(record);
|
|
2788
|
+
return {
|
|
2789
|
+
targetId: input.targetId,
|
|
2790
|
+
kind: input.kind,
|
|
2791
|
+
isValid: results.every((r) => r.status === VerifyFileStatuses.OK),
|
|
2792
|
+
issues: results.filter((r) => r.status !== VerifyFileStatuses.OK),
|
|
2793
|
+
checkedFiles: results.length,
|
|
2794
|
+
durationMs: Date.now() - startedAt
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
async function verifyHashedFile(input) {
|
|
2798
|
+
if (!await fileExists(input.path)) {
|
|
2799
|
+
return {
|
|
2800
|
+
path: input.path,
|
|
2801
|
+
category: input.category,
|
|
2802
|
+
status: VerifyFileStatuses.MISSING,
|
|
2803
|
+
...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
|
|
2804
|
+
...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
|
|
2805
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
if (input.expectedSize !== void 0) {
|
|
2809
|
+
const size = await fileSize(input.path);
|
|
2810
|
+
if (size !== input.expectedSize) {
|
|
2811
|
+
return {
|
|
2812
|
+
path: input.path,
|
|
2813
|
+
category: input.category,
|
|
2814
|
+
status: VerifyFileStatuses.WRONG_SIZE,
|
|
2815
|
+
expectedSize: input.expectedSize,
|
|
2816
|
+
actualSize: size,
|
|
2817
|
+
...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
|
|
2818
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2819
|
+
};
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
if (input.expectedSha1 !== void 0) {
|
|
2823
|
+
const actualSha1 = await sha1OfFile(input.path);
|
|
2824
|
+
if (actualSha1 !== input.expectedSha1) {
|
|
2825
|
+
return {
|
|
2826
|
+
path: input.path,
|
|
2827
|
+
category: input.category,
|
|
2828
|
+
status: VerifyFileStatuses.CORRUPT,
|
|
2829
|
+
expectedSha1: input.expectedSha1,
|
|
2830
|
+
actualSha1,
|
|
2831
|
+
...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
|
|
2832
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return {
|
|
2837
|
+
path: input.path,
|
|
2838
|
+
category: input.category,
|
|
2839
|
+
status: VerifyFileStatuses.OK,
|
|
2840
|
+
...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
|
|
2841
|
+
...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
|
|
2842
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
async function verifyExistence(input) {
|
|
2846
|
+
if (await fileExists(input.path)) {
|
|
2847
|
+
return {
|
|
2848
|
+
path: input.path,
|
|
2849
|
+
category: input.category,
|
|
2850
|
+
status: VerifyFileStatuses.OK,
|
|
2851
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
return {
|
|
2855
|
+
path: input.path,
|
|
2856
|
+
category: input.category,
|
|
2857
|
+
status: VerifyFileStatuses.MISSING,
|
|
2858
|
+
...input.url !== void 0 ? { url: input.url } : {}
|
|
2859
|
+
};
|
|
2860
|
+
}
|
|
2861
|
+
async function findForgeVersionJsonPath(directory, minecraftVersion) {
|
|
2862
|
+
const versionsDir = targetPaths.versionsDir(directory);
|
|
2863
|
+
const dirs = await listChildDirectories(versionsDir);
|
|
2864
|
+
for (const id of dirs) {
|
|
2865
|
+
if (!id.startsWith(`${minecraftVersion}-forge-`)) continue;
|
|
2866
|
+
const jsonPath = targetPaths.versionJson(directory, id);
|
|
2867
|
+
if (!await fileExists(jsonPath)) {
|
|
2868
|
+
return jsonPath;
|
|
2869
|
+
}
|
|
2870
|
+
const parsed = await tryParseInheritsFrom(jsonPath);
|
|
2871
|
+
if (parsed === minecraftVersion) return jsonPath;
|
|
2872
|
+
}
|
|
2873
|
+
return null;
|
|
2874
|
+
}
|
|
2875
|
+
async function tryParseInheritsFrom(jsonPath) {
|
|
2876
|
+
try {
|
|
2877
|
+
const parsed = JSON.parse(await readText(jsonPath));
|
|
2878
|
+
return parsed.inheritsFrom;
|
|
2879
|
+
} catch {
|
|
2880
|
+
return void 0;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
// src/verify/fabric.ts
|
|
2885
|
+
async function verifyFabric(input) {
|
|
2886
|
+
if (input.target.loader.type !== Loaders.FABRIC) {
|
|
2887
|
+
throw new MinecraftKitError(
|
|
2888
|
+
"INVALID_INPUT",
|
|
2889
|
+
`verify.fabric requires a Fabric target (got ${input.target.loader.type})`
|
|
2890
|
+
);
|
|
2891
|
+
}
|
|
2892
|
+
const loader = input.target.loader;
|
|
2893
|
+
return runVerification(
|
|
2894
|
+
{
|
|
2895
|
+
targetId: input.target.id,
|
|
2896
|
+
kind: VerificationKinds.FABRIC,
|
|
2897
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
2898
|
+
},
|
|
2899
|
+
async (record) => {
|
|
2900
|
+
record(
|
|
2901
|
+
await verifyExistence({
|
|
2902
|
+
path: targetPaths.versionJson(input.target.directory, loader.profile.id),
|
|
2903
|
+
category: VerifyFileCategories.LOADER_LIBRARY
|
|
2904
|
+
})
|
|
2905
|
+
);
|
|
2906
|
+
const fabricLibraries = planLibraryDownloads({
|
|
2907
|
+
libraries: loader.profile.libraries,
|
|
2908
|
+
directory: input.target.directory,
|
|
2909
|
+
system: input.target.runtime.system,
|
|
2910
|
+
versionId: input.target.minecraft.version,
|
|
2911
|
+
category: "fabric-library"
|
|
2912
|
+
});
|
|
2913
|
+
for (const action of fabricLibraries.downloads) {
|
|
2914
|
+
record(
|
|
2915
|
+
await verifyHashedFile({
|
|
2916
|
+
path: action.target,
|
|
2917
|
+
expectedSha1: action.expectedSha1,
|
|
2918
|
+
expectedSize: action.expectedSize,
|
|
2919
|
+
...action.url ? { url: action.url } : {},
|
|
2920
|
+
category: VerifyFileCategories.LOADER_LIBRARY
|
|
2921
|
+
})
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
// src/verify/forge.ts
|
|
2929
|
+
async function verifyForge(input) {
|
|
2930
|
+
if (input.target.loader.type !== Loaders.FORGE) {
|
|
2931
|
+
throw new MinecraftKitError(
|
|
2932
|
+
"INVALID_INPUT",
|
|
2933
|
+
`verify.forge requires a Forge target (got ${input.target.loader.type})`
|
|
2934
|
+
);
|
|
2935
|
+
}
|
|
2936
|
+
return runVerification(
|
|
2937
|
+
{
|
|
2938
|
+
targetId: input.target.id,
|
|
2939
|
+
kind: VerificationKinds.FORGE,
|
|
2940
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
2941
|
+
},
|
|
2942
|
+
async (record) => {
|
|
2943
|
+
const forgeVersionJsonPath = await findForgeVersionJsonPath(
|
|
2944
|
+
input.target.directory,
|
|
2945
|
+
input.target.minecraft.version
|
|
2946
|
+
);
|
|
2947
|
+
if (forgeVersionJsonPath === null) return;
|
|
2948
|
+
record(
|
|
2949
|
+
await verifyExistence({
|
|
2950
|
+
path: forgeVersionJsonPath,
|
|
2951
|
+
category: VerifyFileCategories.LOADER_LIBRARY
|
|
2952
|
+
})
|
|
2953
|
+
);
|
|
2954
|
+
if (!await fileExists(forgeVersionJsonPath)) return;
|
|
2955
|
+
let parsed;
|
|
2956
|
+
try {
|
|
2957
|
+
parsed = JSON.parse(await readText(forgeVersionJsonPath));
|
|
2958
|
+
} catch {
|
|
2959
|
+
record({
|
|
2960
|
+
path: forgeVersionJsonPath,
|
|
2961
|
+
category: VerifyFileCategories.LOADER_LIBRARY,
|
|
2962
|
+
status: VerifyFileStatuses.CORRUPT
|
|
2963
|
+
});
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
const forgeLibraries = planLibraryDownloads({
|
|
2967
|
+
libraries: parsed.libraries,
|
|
2968
|
+
directory: input.target.directory,
|
|
2969
|
+
system: input.target.runtime.system,
|
|
2970
|
+
versionId: input.target.minecraft.version,
|
|
2971
|
+
category: "forge-library"
|
|
2972
|
+
});
|
|
2973
|
+
for (const action of forgeLibraries.downloads) {
|
|
2974
|
+
record(
|
|
2975
|
+
await verifyHashedFile({
|
|
2976
|
+
path: action.target,
|
|
2977
|
+
expectedSha1: action.expectedSha1,
|
|
2978
|
+
expectedSize: action.expectedSize,
|
|
2979
|
+
...action.url ? { url: action.url } : {},
|
|
2980
|
+
category: VerifyFileCategories.LOADER_LIBRARY
|
|
2981
|
+
})
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// src/verify/minecraft.ts
|
|
2989
|
+
async function verifyMinecraft(input) {
|
|
2990
|
+
return runVerification(
|
|
2991
|
+
{
|
|
2992
|
+
targetId: input.target.id,
|
|
2993
|
+
kind: VerificationKinds.MINECRAFT,
|
|
2994
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
2995
|
+
},
|
|
2996
|
+
async (record) => {
|
|
2997
|
+
const { directory, minecraft, runtime } = input.target;
|
|
2998
|
+
record(
|
|
2999
|
+
await verifyHashedFile({
|
|
3000
|
+
path: targetPaths.versionJar(directory, minecraft.version),
|
|
3001
|
+
expectedSha1: minecraft.manifest.downloads.client.sha1,
|
|
3002
|
+
expectedSize: minecraft.manifest.downloads.client.size,
|
|
3003
|
+
url: minecraft.manifest.downloads.client.url,
|
|
3004
|
+
category: VerifyFileCategories.CLIENT_JAR
|
|
3005
|
+
})
|
|
3006
|
+
);
|
|
3007
|
+
record(
|
|
3008
|
+
await verifyExistence({
|
|
3009
|
+
path: targetPaths.versionJson(directory, minecraft.version),
|
|
3010
|
+
category: VerifyFileCategories.CLIENT_JAR
|
|
3011
|
+
})
|
|
3012
|
+
);
|
|
3013
|
+
if (minecraft.manifest.logging?.client) {
|
|
3014
|
+
const logging = minecraft.manifest.logging.client;
|
|
3015
|
+
record(
|
|
3016
|
+
await verifyHashedFile({
|
|
3017
|
+
path: targetPaths.loggingConfig(directory, logging.file.id),
|
|
3018
|
+
expectedSha1: logging.file.sha1,
|
|
3019
|
+
expectedSize: logging.file.size,
|
|
3020
|
+
url: logging.file.url,
|
|
3021
|
+
category: VerifyFileCategories.LOGGING_CONFIG
|
|
3022
|
+
})
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
const libraryPlan = planLibraryDownloads({
|
|
3026
|
+
libraries: minecraft.manifest.libraries,
|
|
3027
|
+
directory,
|
|
3028
|
+
system: runtime.system,
|
|
3029
|
+
versionId: minecraft.version,
|
|
3030
|
+
category: "library"
|
|
3031
|
+
});
|
|
3032
|
+
for (const action of libraryPlan.downloads) {
|
|
3033
|
+
record(
|
|
3034
|
+
await verifyHashedFile({
|
|
3035
|
+
path: action.target,
|
|
3036
|
+
expectedSha1: action.expectedSha1,
|
|
3037
|
+
expectedSize: action.expectedSize,
|
|
3038
|
+
url: action.url,
|
|
3039
|
+
category: VerifyFileCategories.LIBRARY
|
|
3040
|
+
})
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
const indexUrl = minecraft.manifest.assetIndex.url;
|
|
3044
|
+
const indexPath = targetPaths.assetIndex(directory, minecraft.manifest.assetIndex.id);
|
|
3045
|
+
record(
|
|
3046
|
+
await verifyHashedFile({
|
|
3047
|
+
path: indexPath,
|
|
3048
|
+
expectedSha1: minecraft.manifest.assetIndex.sha1,
|
|
3049
|
+
expectedSize: minecraft.manifest.assetIndex.size,
|
|
3050
|
+
url: indexUrl,
|
|
3051
|
+
category: VerifyFileCategories.ASSET_INDEX
|
|
3052
|
+
})
|
|
3053
|
+
);
|
|
3054
|
+
const indexDocument = await fetchJson(input.http, input.cache, {
|
|
3055
|
+
url: indexUrl,
|
|
3056
|
+
cacheKey: `asset-index:${minecraft.manifest.assetIndex.id}:${minecraft.manifest.assetIndex.sha1}`,
|
|
3057
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3058
|
+
});
|
|
3059
|
+
const seenAssetHashes = /* @__PURE__ */ new Set();
|
|
3060
|
+
for (const entry of Object.values(indexDocument.objects)) {
|
|
3061
|
+
if (seenAssetHashes.has(entry.hash)) continue;
|
|
3062
|
+
seenAssetHashes.add(entry.hash);
|
|
3063
|
+
record(
|
|
3064
|
+
await verifyHashedFile({
|
|
3065
|
+
path: targetPaths.assetObject(directory, entry.hash),
|
|
3066
|
+
expectedSha1: entry.hash,
|
|
3067
|
+
expectedSize: entry.size,
|
|
3068
|
+
category: VerifyFileCategories.ASSET
|
|
3069
|
+
})
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
const nativesDir = targetPaths.nativesDir(directory, minecraft.version);
|
|
3073
|
+
if (!await fileExists(nativesDir)) {
|
|
3074
|
+
for (const extraction of libraryPlan.nativeExtractions) {
|
|
3075
|
+
record({
|
|
3076
|
+
path: extraction.source,
|
|
3077
|
+
category: VerifyFileCategories.NATIVE,
|
|
3078
|
+
status: VerifyFileStatuses.MISSING
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
);
|
|
3084
|
+
}
|
|
3085
|
+
async function verifyRuntime(input) {
|
|
3086
|
+
return runVerification(
|
|
3087
|
+
{
|
|
3088
|
+
targetId: input.target.id,
|
|
3089
|
+
kind: VerificationKinds.RUNTIME,
|
|
3090
|
+
...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
|
|
3091
|
+
},
|
|
3092
|
+
async (record) => {
|
|
3093
|
+
let manifest;
|
|
3094
|
+
try {
|
|
3095
|
+
manifest = await fetchJson(input.http, input.cache, {
|
|
3096
|
+
url: input.target.runtime.manifestUrl,
|
|
3097
|
+
cacheKey: `runtime-manifest:${input.target.runtime.component}:${input.target.runtime.platformKey}:${input.target.runtime.manifestSha1}`,
|
|
3098
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3099
|
+
});
|
|
3100
|
+
} catch {
|
|
3101
|
+
record({
|
|
3102
|
+
path: input.target.runtime.manifestUrl,
|
|
3103
|
+
category: VerifyFileCategories.RUNTIME_FILE,
|
|
3104
|
+
status: VerifyFileStatuses.MISSING
|
|
3105
|
+
});
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
const runtimeRoot = targetPaths.runtimeRoot(
|
|
3109
|
+
input.target.directory,
|
|
3110
|
+
input.target.runtime.component,
|
|
3111
|
+
input.target.runtime.installRoot
|
|
3112
|
+
);
|
|
3113
|
+
for (const [relative, entry] of Object.entries(manifest.files)) {
|
|
3114
|
+
if (entry.type !== "file") continue;
|
|
3115
|
+
record(
|
|
3116
|
+
await verifyHashedFile({
|
|
3117
|
+
path: path.join(runtimeRoot, relative),
|
|
3118
|
+
expectedSha1: entry.downloads.raw.sha1,
|
|
3119
|
+
expectedSize: entry.downloads.raw.size,
|
|
3120
|
+
url: entry.downloads.raw.url,
|
|
3121
|
+
category: VerifyFileCategories.RUNTIME_FILE
|
|
3122
|
+
})
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
);
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
// src/versions/fabric.ts
|
|
3130
|
+
var FabricVersionsApi = class {
|
|
3131
|
+
constructor(ctx) {
|
|
3132
|
+
this.ctx = ctx;
|
|
3133
|
+
}
|
|
3134
|
+
ctx;
|
|
3135
|
+
/** List Fabric loader versions, optionally constrained to a Minecraft version. */
|
|
3136
|
+
async list(input = {}) {
|
|
3137
|
+
if (input.minecraftVersion === void 0) {
|
|
3138
|
+
return fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3139
|
+
url: ApiEndpoints.fabric.loaderVersions(),
|
|
3140
|
+
cacheKey: "fabric-loader-all",
|
|
3141
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
const compat = await fetchJson(
|
|
3145
|
+
this.ctx.http,
|
|
3146
|
+
this.ctx.cache,
|
|
3147
|
+
{
|
|
3148
|
+
url: ApiEndpoints.fabric.loaderForGame(input.minecraftVersion),
|
|
3149
|
+
cacheKey: `fabric-loader-mc:${input.minecraftVersion}`,
|
|
3150
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3151
|
+
}
|
|
3152
|
+
);
|
|
3153
|
+
return compat.map((c) => c.loader);
|
|
3154
|
+
}
|
|
3155
|
+
/** Resolve a Fabric loader version against a Minecraft version. */
|
|
3156
|
+
async resolve(input) {
|
|
3157
|
+
const loaders = await this.list({
|
|
3158
|
+
minecraftVersion: input.minecraftVersion,
|
|
3159
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3160
|
+
});
|
|
3161
|
+
if (loaders.length === 0) {
|
|
3162
|
+
throw new MinecraftKitError(
|
|
3163
|
+
"MANIFEST_NOT_FOUND",
|
|
3164
|
+
`No Fabric loader available for Minecraft ${input.minecraftVersion}`,
|
|
3165
|
+
{ context: { version: input.minecraftVersion } }
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
3168
|
+
const chosen = pickFabricLoader(loaders, input);
|
|
3169
|
+
if (!chosen) {
|
|
3170
|
+
throw new MinecraftKitError(
|
|
3171
|
+
"MANIFEST_NOT_FOUND",
|
|
3172
|
+
`Fabric loader version not found: ${input.loaderVersion ?? "(none matched)"}`,
|
|
3173
|
+
{ context: { version: input.loaderVersion } }
|
|
3174
|
+
);
|
|
3175
|
+
}
|
|
3176
|
+
const profile = await fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3177
|
+
url: ApiEndpoints.fabric.profile(input.minecraftVersion, chosen.version),
|
|
3178
|
+
cacheKey: `fabric-profile:${input.minecraftVersion}:${chosen.version}`,
|
|
3179
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3180
|
+
});
|
|
3181
|
+
return {
|
|
3182
|
+
type: Loaders.FABRIC,
|
|
3183
|
+
minecraftVersion: input.minecraftVersion,
|
|
3184
|
+
loaderVersion: chosen.version,
|
|
3185
|
+
profile
|
|
3186
|
+
};
|
|
3187
|
+
}
|
|
3188
|
+
};
|
|
3189
|
+
function pickFabricLoader(loaders, input) {
|
|
3190
|
+
if (input.loaderVersion !== void 0) {
|
|
3191
|
+
return loaders.find((l) => l.version === input.loaderVersion);
|
|
3192
|
+
}
|
|
3193
|
+
const preference = input.preference ?? VersionPreference.LATEST;
|
|
3194
|
+
if (preference === VersionPreference.RECOMMENDED) {
|
|
3195
|
+
const stable = loaders.find((l) => l.stable);
|
|
3196
|
+
if (stable) return stable;
|
|
3197
|
+
}
|
|
3198
|
+
return loaders[0];
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
// src/core/xml.ts
|
|
3202
|
+
function parseMavenMetadataVersions(xml) {
|
|
3203
|
+
const versions = [];
|
|
3204
|
+
const regex = /<version>\s*([^<]+?)\s*<\/version>/g;
|
|
3205
|
+
for (const match of xml.matchAll(regex)) {
|
|
3206
|
+
if (match[1]) versions.push(match[1]);
|
|
3207
|
+
}
|
|
3208
|
+
return versions;
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
// src/versions/forge.ts
|
|
3212
|
+
var ForgeVersionsApi = class {
|
|
3213
|
+
constructor(ctx) {
|
|
3214
|
+
this.ctx = ctx;
|
|
3215
|
+
}
|
|
3216
|
+
ctx;
|
|
3217
|
+
/** List Forge builds (across all Minecraft versions, or filtered to one). */
|
|
3218
|
+
async list(input = {}) {
|
|
3219
|
+
const xml = await fetchText(this.ctx.http, this.ctx.cache, {
|
|
3220
|
+
url: ApiEndpoints.forge.mavenMetadata(),
|
|
3221
|
+
cacheKey: "forge-maven-metadata",
|
|
3222
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3223
|
+
});
|
|
3224
|
+
const allVersions = parseMavenMetadataVersions(xml);
|
|
3225
|
+
const promotions = await fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3226
|
+
url: ApiEndpoints.forge.promotions(),
|
|
3227
|
+
cacheKey: "forge-promotions",
|
|
3228
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3229
|
+
});
|
|
3230
|
+
const summaries = allVersions.map((fullVersion) => buildSummary(fullVersion, promotions)).filter((s) => s !== null);
|
|
3231
|
+
if (input.minecraftVersion === void 0) return summaries;
|
|
3232
|
+
return summaries.filter((s) => s.minecraftVersion === input.minecraftVersion);
|
|
3233
|
+
}
|
|
3234
|
+
/** Resolve a Forge build for a Minecraft version. */
|
|
3235
|
+
async resolve(input) {
|
|
3236
|
+
const builds = await this.list({
|
|
3237
|
+
minecraftVersion: input.minecraftVersion,
|
|
3238
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3239
|
+
});
|
|
3240
|
+
if (builds.length === 0) {
|
|
3241
|
+
throw new MinecraftKitError(
|
|
3242
|
+
"MANIFEST_NOT_FOUND",
|
|
3243
|
+
`No Forge build available for Minecraft ${input.minecraftVersion}`,
|
|
3244
|
+
{ context: { version: input.minecraftVersion } }
|
|
3245
|
+
);
|
|
3246
|
+
}
|
|
3247
|
+
const chosen = pickForge(builds, input);
|
|
3248
|
+
if (!chosen) {
|
|
3249
|
+
throw new MinecraftKitError(
|
|
3250
|
+
"MANIFEST_NOT_FOUND",
|
|
3251
|
+
`Forge build not found for ${input.minecraftVersion}: ${input.forgeVersion ?? "(none matched)"}`,
|
|
3252
|
+
{ context: { version: input.forgeVersion } }
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
return {
|
|
3256
|
+
type: Loaders.FORGE,
|
|
3257
|
+
minecraftVersion: chosen.minecraftVersion,
|
|
3258
|
+
forgeVersion: chosen.forgeVersion,
|
|
3259
|
+
fullVersion: chosen.fullVersion,
|
|
3260
|
+
installerUrl: ApiEndpoints.forge.installer(chosen.fullVersion)
|
|
3261
|
+
};
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
function buildSummary(fullVersion, promotions) {
|
|
3265
|
+
const dashIndex = fullVersion.indexOf("-");
|
|
3266
|
+
if (dashIndex <= 0 || dashIndex === fullVersion.length - 1) return null;
|
|
3267
|
+
const minecraftVersion = fullVersion.slice(0, dashIndex);
|
|
3268
|
+
const forgeVersion = fullVersion.slice(dashIndex + 1);
|
|
3269
|
+
const promos = promotions.promos;
|
|
3270
|
+
const recommended = promos[`${minecraftVersion}-recommended`];
|
|
3271
|
+
const latest = promos[`${minecraftVersion}-latest`];
|
|
3272
|
+
return {
|
|
3273
|
+
fullVersion,
|
|
3274
|
+
minecraftVersion,
|
|
3275
|
+
forgeVersion,
|
|
3276
|
+
isRecommended: recommended === forgeVersion,
|
|
3277
|
+
isLatest: latest === forgeVersion
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
function pickForge(builds, input) {
|
|
3281
|
+
if (input.forgeVersion !== void 0) {
|
|
3282
|
+
return builds.find(
|
|
3283
|
+
(b) => b.forgeVersion === input.forgeVersion || b.fullVersion === input.forgeVersion
|
|
3284
|
+
);
|
|
3285
|
+
}
|
|
3286
|
+
const preference = input.preference ?? VersionPreference.RECOMMENDED;
|
|
3287
|
+
if (preference === VersionPreference.RECOMMENDED) {
|
|
3288
|
+
const recommended = builds.find((b) => b.isRecommended);
|
|
3289
|
+
if (recommended) return recommended;
|
|
3290
|
+
}
|
|
3291
|
+
const latest = builds.find((b) => b.isLatest);
|
|
3292
|
+
if (latest) return latest;
|
|
3293
|
+
return builds[builds.length - 1];
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
// src/versions/minecraft.ts
|
|
3297
|
+
var MinecraftVersionsApi = class {
|
|
3298
|
+
constructor(ctx) {
|
|
3299
|
+
this.ctx = ctx;
|
|
3300
|
+
}
|
|
3301
|
+
ctx;
|
|
3302
|
+
/** List all Minecraft versions, optionally filtered by channel. */
|
|
3303
|
+
async list(input = {}) {
|
|
3304
|
+
const root = await this.fetchManifestRoot(input.signal);
|
|
3305
|
+
if (input.channel === void 0) return root.versions;
|
|
3306
|
+
return root.versions.filter((v) => v.type === input.channel);
|
|
3307
|
+
}
|
|
3308
|
+
/** Return the latest version on the given channel (defaults to RELEASE). */
|
|
3309
|
+
async latest(input = {}) {
|
|
3310
|
+
const root = await this.fetchManifestRoot(input.signal);
|
|
3311
|
+
const targetId = input.channel === "snapshot" ? root.latest.snapshot : root.latest.release;
|
|
3312
|
+
const summary = root.versions.find((v) => v.id === targetId);
|
|
3313
|
+
if (!summary) {
|
|
3314
|
+
throw new MinecraftKitError(
|
|
3315
|
+
"MANIFEST_NOT_FOUND",
|
|
3316
|
+
`Latest version ${targetId} not found in manifest`
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
return summary;
|
|
3320
|
+
}
|
|
3321
|
+
/** Return a single version summary or throw `MANIFEST_NOT_FOUND`. */
|
|
3322
|
+
async get(input) {
|
|
3323
|
+
const root = await this.fetchManifestRoot(input.signal);
|
|
3324
|
+
const summary = root.versions.find((v) => v.id === input.version);
|
|
3325
|
+
if (!summary) {
|
|
3326
|
+
throw new MinecraftKitError(
|
|
3327
|
+
"MANIFEST_NOT_FOUND",
|
|
3328
|
+
`Minecraft version not found: ${input.version}`,
|
|
3329
|
+
{ context: { version: input.version } }
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
return summary;
|
|
3333
|
+
}
|
|
3334
|
+
/** Fetch and parse the per-version manifest in addition to the summary. */
|
|
3335
|
+
async resolve(input) {
|
|
3336
|
+
const summary = await this.get(input);
|
|
3337
|
+
const manifest = await fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3338
|
+
url: summary.url,
|
|
3339
|
+
cacheKey: `minecraft-manifest:${summary.id}:${summary.sha1}`,
|
|
3340
|
+
...input.signal !== void 0 ? { signal: input.signal } : {}
|
|
3341
|
+
});
|
|
3342
|
+
if (!manifest.id || !manifest.mainClass) {
|
|
3343
|
+
throw new MinecraftKitError(
|
|
3344
|
+
"MANIFEST_INVALID",
|
|
3345
|
+
`Per-version manifest is missing required fields: ${summary.id}`,
|
|
3346
|
+
{ context: { version: summary.id, url: summary.url } }
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
3349
|
+
return {
|
|
3350
|
+
version: summary.id,
|
|
3351
|
+
channel: summary.type,
|
|
3352
|
+
manifest,
|
|
3353
|
+
summary
|
|
3354
|
+
};
|
|
3355
|
+
}
|
|
3356
|
+
async fetchManifestRoot(signal) {
|
|
3357
|
+
return fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3358
|
+
url: ApiEndpoints.mojang.versionManifest(),
|
|
3359
|
+
cacheKey: "minecraft-version-manifest-v2",
|
|
3360
|
+
...signal !== void 0 ? { signal } : {}
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
};
|
|
3364
|
+
|
|
3365
|
+
// src/constants/runtime.ts
|
|
3366
|
+
var FALLBACK_COMPONENT = RuntimeComponents.JRE_LEGACY;
|
|
3367
|
+
|
|
3368
|
+
// src/versions/runtime.ts
|
|
3369
|
+
var RuntimeVersionsApi = class {
|
|
3370
|
+
constructor(ctx) {
|
|
3371
|
+
this.ctx = ctx;
|
|
3372
|
+
}
|
|
3373
|
+
ctx;
|
|
3374
|
+
/** List available runtime entries for the host platform. */
|
|
3375
|
+
async list(input) {
|
|
3376
|
+
const platformKey = pickPlatformKey(input.system);
|
|
3377
|
+
const index = await this.fetchIndex(input.signal);
|
|
3378
|
+
const platform = index[platformKey];
|
|
3379
|
+
if (!platform) return [];
|
|
3380
|
+
const entries = [];
|
|
3381
|
+
for (const [component, items] of Object.entries(platform)) {
|
|
3382
|
+
for (const item of items) {
|
|
3383
|
+
entries.push({
|
|
3384
|
+
component,
|
|
3385
|
+
platformKey,
|
|
3386
|
+
versionName: item.version.name,
|
|
3387
|
+
released: item.version.released,
|
|
3388
|
+
manifestUrl: item.manifest.url
|
|
3389
|
+
});
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
return entries;
|
|
3393
|
+
}
|
|
3394
|
+
/** Resolve a single runtime for the host platform and Minecraft version. */
|
|
3395
|
+
async resolve(input) {
|
|
3396
|
+
const platformKey = pickPlatformKey(input.system);
|
|
3397
|
+
const index = await this.fetchIndex(input.signal);
|
|
3398
|
+
const platform = index[platformKey];
|
|
3399
|
+
if (!platform) {
|
|
3400
|
+
throw new MinecraftKitError(
|
|
3401
|
+
"RUNTIME_UNSUPPORTED_PLATFORM",
|
|
3402
|
+
`No runtimes published for platform: ${platformKey}`,
|
|
3403
|
+
{ context: { platform: platformKey } }
|
|
3404
|
+
);
|
|
3405
|
+
}
|
|
3406
|
+
const component = input.component ?? FALLBACK_COMPONENT;
|
|
3407
|
+
const candidates = platform[component] ?? [];
|
|
3408
|
+
if (candidates.length === 0) {
|
|
3409
|
+
const all = Object.entries(platform);
|
|
3410
|
+
const preference = input.preference ?? RuntimePreference.RECOMMENDED;
|
|
3411
|
+
if (preference === RuntimePreference.LATEST) {
|
|
3412
|
+
const fallback = pickLatestAcrossComponents(all);
|
|
3413
|
+
if (fallback) {
|
|
3414
|
+
return toResolved(fallback.component, platformKey, fallback.entry, input.system);
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
throw new MinecraftKitError(
|
|
3418
|
+
"RUNTIME_NOT_FOUND",
|
|
3419
|
+
`Runtime component ${component} not available on ${platformKey}`,
|
|
3420
|
+
{ context: { platform: platformKey, version: component } }
|
|
3421
|
+
);
|
|
3422
|
+
}
|
|
3423
|
+
const entry = candidates[0];
|
|
3424
|
+
if (!entry) {
|
|
3425
|
+
throw new MinecraftKitError(
|
|
3426
|
+
"RUNTIME_NOT_FOUND",
|
|
3427
|
+
`Runtime component ${component} list is empty for ${platformKey}`,
|
|
3428
|
+
{ context: { platform: platformKey, version: component } }
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
return toResolved(component, platformKey, entry, input.system);
|
|
3432
|
+
}
|
|
3433
|
+
async fetchIndex(signal) {
|
|
3434
|
+
return fetchJson(this.ctx.http, this.ctx.cache, {
|
|
3435
|
+
url: ApiEndpoints.mojang.runtimeIndex(),
|
|
3436
|
+
cacheKey: "mojang-runtime-index",
|
|
3437
|
+
...signal !== void 0 ? { signal } : {}
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
};
|
|
3441
|
+
function pickPlatformKey(system) {
|
|
3442
|
+
const archMap = RUNTIME_PLATFORM_KEYS[system.os];
|
|
3443
|
+
return archMap[system.arch];
|
|
3444
|
+
}
|
|
3445
|
+
function pickLatestAcrossComponents(entries) {
|
|
3446
|
+
let bestComponent = null;
|
|
3447
|
+
let bestEntry = null;
|
|
3448
|
+
for (const [component, list] of entries) {
|
|
3449
|
+
for (const entry of list) {
|
|
3450
|
+
if (!bestEntry || entry.version.released > bestEntry.version.released) {
|
|
3451
|
+
bestComponent = component;
|
|
3452
|
+
bestEntry = entry;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
if (!bestComponent || !bestEntry) return null;
|
|
3457
|
+
return { component: bestComponent, entry: bestEntry };
|
|
3458
|
+
}
|
|
3459
|
+
function toResolved(component, platformKey, entry, system) {
|
|
3460
|
+
return {
|
|
3461
|
+
component,
|
|
3462
|
+
platformKey,
|
|
3463
|
+
versionName: entry.version.name,
|
|
3464
|
+
system,
|
|
3465
|
+
manifestUrl: entry.manifest.url,
|
|
3466
|
+
manifestSha1: entry.manifest.sha1
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// src/kit.ts
|
|
3471
|
+
var MinecraftKit = class {
|
|
3472
|
+
versions;
|
|
3473
|
+
targets;
|
|
3474
|
+
install;
|
|
3475
|
+
update;
|
|
3476
|
+
verify;
|
|
3477
|
+
repair;
|
|
3478
|
+
launch;
|
|
3479
|
+
/** Cache surface useful for advanced consumers (e.g. clearing between operations). */
|
|
3480
|
+
cache;
|
|
3481
|
+
constructor(options = {}) {
|
|
3482
|
+
const http = options.httpClient ?? new FetchHttpClient();
|
|
3483
|
+
const cache = options.cache ?? createMemoryCache();
|
|
3484
|
+
const logger = options.logger ?? silentLogger;
|
|
3485
|
+
const system = options.system ?? detectSystem();
|
|
3486
|
+
const spawner = options.spawner ?? new ChildProcessSpawner();
|
|
3487
|
+
const ctx = { http, cache, logger };
|
|
3488
|
+
const minecraft = new MinecraftVersionsApi(ctx);
|
|
3489
|
+
const fabric = new FabricVersionsApi(ctx);
|
|
3490
|
+
const forge = new ForgeVersionsApi(ctx);
|
|
3491
|
+
const runtime = new RuntimeVersionsApi(ctx);
|
|
3492
|
+
this.versions = { minecraft, fabric, forge, runtime };
|
|
3493
|
+
this.targets = new TargetsApi({ minecraft, fabric, forge, runtime, system });
|
|
3494
|
+
this.cache = cache;
|
|
3495
|
+
const carry = (opts) => ({
|
|
3496
|
+
...opts?.signal !== void 0 ? { signal: opts.signal } : {},
|
|
3497
|
+
...opts?.onEvent !== void 0 ? { onEvent: opts.onEvent } : {}
|
|
3498
|
+
});
|
|
3499
|
+
const runInstallPlan = (plan, opts) => runInstall({ plan, http, cache, spawner, ...carry(opts) });
|
|
3500
|
+
this.install = {
|
|
3501
|
+
plan: (target, opts) => planInstall({ target, http, cache, ...carry(opts) }),
|
|
3502
|
+
run: runInstallPlan,
|
|
3503
|
+
runtime: {
|
|
3504
|
+
plan: (target, opts) => planRuntimeInstall({ target, http, cache, ...carry(opts) }),
|
|
3505
|
+
run: runInstallPlan,
|
|
3506
|
+
standalonePlan: (input) => planStandaloneRuntimeInstall({ ...input, http, cache })
|
|
3507
|
+
}
|
|
3508
|
+
};
|
|
3509
|
+
this.update = {
|
|
3510
|
+
plan: (target, opts) => planUpdate({ target, http, cache, ...carry(opts) }),
|
|
3511
|
+
run: (plan, opts) => runUpdate({ plan, http, cache, spawner, ...carry(opts) })
|
|
3512
|
+
};
|
|
3513
|
+
const verifyArgs = (target, opts) => ({
|
|
3514
|
+
target,
|
|
3515
|
+
http,
|
|
3516
|
+
cache,
|
|
3517
|
+
...carry(opts)
|
|
3518
|
+
});
|
|
3519
|
+
this.verify = {
|
|
3520
|
+
minecraft: { run: (target, opts) => verifyMinecraft(verifyArgs(target, opts)) },
|
|
3521
|
+
fabric: { run: (target, opts) => verifyFabric(verifyArgs(target, opts)) },
|
|
3522
|
+
forge: { run: (target, opts) => verifyForge(verifyArgs(target, opts)) },
|
|
3523
|
+
runtime: { run: (target, opts) => verifyRuntime(verifyArgs(target, opts)) }
|
|
3524
|
+
};
|
|
3525
|
+
const repairArgs = (target, opts) => ({
|
|
3526
|
+
target,
|
|
3527
|
+
from: opts.from,
|
|
3528
|
+
http,
|
|
3529
|
+
cache,
|
|
3530
|
+
...carry({ ...opts.signal !== void 0 ? { signal: opts.signal } : {} })
|
|
3531
|
+
});
|
|
3532
|
+
const runRepairPlan = (plan, opts) => runRepair({ plan, http, cache, spawner, ...carry(opts) });
|
|
3533
|
+
this.repair = {
|
|
3534
|
+
minecraft: {
|
|
3535
|
+
plan: (target, opts) => planMinecraftRepair(repairArgs(target, opts)),
|
|
3536
|
+
run: runRepairPlan
|
|
3537
|
+
},
|
|
3538
|
+
fabric: {
|
|
3539
|
+
plan: (target, opts) => planFabricRepair(repairArgs(target, opts)),
|
|
3540
|
+
run: runRepairPlan
|
|
3541
|
+
},
|
|
3542
|
+
forge: {
|
|
3543
|
+
plan: (target, opts) => planForgeRepair(repairArgs(target, opts)),
|
|
3544
|
+
run: runRepairPlan
|
|
3545
|
+
},
|
|
3546
|
+
runtime: {
|
|
3547
|
+
plan: (target, opts) => planRuntimeRepair(repairArgs(target, opts)),
|
|
3548
|
+
run: runRepairPlan
|
|
3549
|
+
}
|
|
3550
|
+
};
|
|
3551
|
+
this.launch = {
|
|
3552
|
+
compose: (target, opts) => composeLaunch({ target, options: opts }),
|
|
3553
|
+
run: (composition, opts) => runLaunch({
|
|
3554
|
+
composition,
|
|
3555
|
+
...opts !== void 0 ? { options: opts } : {},
|
|
3556
|
+
spawner
|
|
3557
|
+
})
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
};
|
|
3561
|
+
|
|
3562
|
+
// src/cli/error-format.ts
|
|
3563
|
+
function formatUserError(error) {
|
|
3564
|
+
if (isMinecraftKitError(error)) {
|
|
3565
|
+
const status = typeof error.context.httpStatus === "number" ? error.context.httpStatus : void 0;
|
|
3566
|
+
if (error.code === "NETWORK_HTTP_ERROR" && status !== void 0) {
|
|
3567
|
+
if (status === 400 || status === 404) {
|
|
3568
|
+
return "No matching data is available for that combination.";
|
|
3569
|
+
}
|
|
3570
|
+
if (status === 408) return "The server took too long to respond. Please retry.";
|
|
3571
|
+
if (status === 429)
|
|
3572
|
+
return "The metadata server is rate-limiting requests. Please retry shortly.";
|
|
3573
|
+
if (status >= 500 && status < 600) {
|
|
3574
|
+
return "The metadata server returned an error. Please retry later.";
|
|
3575
|
+
}
|
|
3576
|
+
return `Unexpected HTTP ${status} from the metadata server.`;
|
|
3577
|
+
}
|
|
3578
|
+
switch (error.code) {
|
|
3579
|
+
case "NETWORK_TIMEOUT":
|
|
3580
|
+
return "Network request timed out. Check your internet connection and retry.";
|
|
3581
|
+
case "NETWORK_ABORTED":
|
|
3582
|
+
return "The request was aborted.";
|
|
3583
|
+
case "MANIFEST_NOT_FOUND":
|
|
3584
|
+
return "Requested item is not available \u2014 try a different selection.";
|
|
3585
|
+
case "MANIFEST_INVALID":
|
|
3586
|
+
return "The metadata server returned a malformed response. Please retry.";
|
|
3587
|
+
case "INTEGRITY_HASH_MISMATCH":
|
|
3588
|
+
return "A downloaded file failed its hash check. Re-running install / repair will retry.";
|
|
3589
|
+
case "INTEGRITY_SIZE_MISMATCH":
|
|
3590
|
+
return "A downloaded file had the wrong size. Re-running install / repair will retry.";
|
|
3591
|
+
case "RUNTIME_NOT_FOUND":
|
|
3592
|
+
return "No runtime is published for that combination.";
|
|
3593
|
+
case "RUNTIME_UNSUPPORTED_PLATFORM":
|
|
3594
|
+
return "Your platform is not in Mojang's published runtime list.";
|
|
3595
|
+
case "FORGE_INSTALLER_INVALID":
|
|
3596
|
+
return "Forge installer appears to be corrupt or in an unsupported format.";
|
|
3597
|
+
case "FORGE_PROCESSOR_FAILED":
|
|
3598
|
+
return "A Forge processor failed during install.";
|
|
3599
|
+
case "LAUNCH_JAVA_NOT_FOUND":
|
|
3600
|
+
return "Could not find a Java executable. Install the runtime first.";
|
|
3601
|
+
case "LAUNCH_PROCESS_FAILED":
|
|
3602
|
+
return "Minecraft exited with an error.";
|
|
3603
|
+
case "LAUNCH_ABORTED":
|
|
3604
|
+
return "Operation was aborted.";
|
|
3605
|
+
case "INVALID_INPUT":
|
|
3606
|
+
return error.message;
|
|
3607
|
+
case "FILESYSTEM_PATH_TRAVERSAL":
|
|
3608
|
+
return "An archive entry tried to escape the install directory and was rejected.";
|
|
3609
|
+
case "FILESYSTEM_WRITE_ERROR":
|
|
3610
|
+
case "FILESYSTEM_READ_ERROR":
|
|
3611
|
+
return `Filesystem error: ${error.message}`;
|
|
3612
|
+
default:
|
|
3613
|
+
return error.message;
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
if (error instanceof Error) return error.message;
|
|
3617
|
+
return String(error);
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
// src/cli/progress.ts
|
|
3621
|
+
var DEFAULT_RENDER_INTERVAL_MS = 250;
|
|
3622
|
+
var SPEED_WINDOW_MS = 5e3;
|
|
3623
|
+
var BAR_WIDTH = 12;
|
|
3624
|
+
var SAFETY_MARGIN = 1;
|
|
3625
|
+
var ProgressRenderer = class {
|
|
3626
|
+
ui;
|
|
3627
|
+
label;
|
|
3628
|
+
totalActions;
|
|
3629
|
+
totalBytes;
|
|
3630
|
+
now;
|
|
3631
|
+
minRenderIntervalMs;
|
|
3632
|
+
columnsFn;
|
|
3633
|
+
speedSamples = [];
|
|
3634
|
+
spinner;
|
|
3635
|
+
/** Authoritative bytes for files that have completed. */
|
|
3636
|
+
completedBytes = 0;
|
|
3637
|
+
/** Latest progress bytes for files that are currently downloading. */
|
|
3638
|
+
activeBytes = /* @__PURE__ */ new Map();
|
|
3639
|
+
/** Files for which `download:started` fired but `download:completed/failed` has not. */
|
|
3640
|
+
activeTargets = /* @__PURE__ */ new Set();
|
|
3641
|
+
startedAt;
|
|
3642
|
+
lastRenderAt = 0;
|
|
3643
|
+
lastRenderedLine = null;
|
|
3644
|
+
currentPhase = "idle";
|
|
3645
|
+
filesCompleted = 0;
|
|
3646
|
+
filesSkipped = 0;
|
|
3647
|
+
filesFailed = 0;
|
|
3648
|
+
active = false;
|
|
3649
|
+
constructor(input) {
|
|
3650
|
+
this.ui = input.ui;
|
|
3651
|
+
this.label = input.label;
|
|
3652
|
+
if (input.totalActions !== void 0) this.totalActions = input.totalActions;
|
|
3653
|
+
if (input.totalBytes !== void 0) this.totalBytes = input.totalBytes;
|
|
3654
|
+
this.now = input.now ?? (() => Date.now());
|
|
3655
|
+
this.minRenderIntervalMs = input.minRenderIntervalMs ?? DEFAULT_RENDER_INTERVAL_MS;
|
|
3656
|
+
this.columnsFn = input.columns ?? (() => typeof process.stdout.columns === "number" ? process.stdout.columns : 0);
|
|
3657
|
+
this.spinner = this.ui.spinner();
|
|
3658
|
+
this.startedAt = this.now();
|
|
3659
|
+
}
|
|
3660
|
+
/** Start the spinner and return a listener function suitable for `kit.install.run({ onEvent })`. */
|
|
3661
|
+
attach() {
|
|
3662
|
+
this.active = true;
|
|
3663
|
+
this.startedAt = this.now();
|
|
3664
|
+
this.lastRenderAt = 0;
|
|
3665
|
+
this.lastRenderedLine = null;
|
|
3666
|
+
this.spinner.start(`${this.label}\u2026`);
|
|
3667
|
+
return (event) => this.handle(event);
|
|
3668
|
+
}
|
|
3669
|
+
/** Stop the spinner with a final summary line and return the metrics. */
|
|
3670
|
+
finish() {
|
|
3671
|
+
if (!this.active) return this.summary();
|
|
3672
|
+
this.active = false;
|
|
3673
|
+
const summary = this.summary();
|
|
3674
|
+
this.spinner.stop(this.summaryLine(summary));
|
|
3675
|
+
return summary;
|
|
3676
|
+
}
|
|
3677
|
+
/** Stop with a failure message instead of the summary. */
|
|
3678
|
+
fail(message) {
|
|
3679
|
+
if (!this.active) return;
|
|
3680
|
+
this.active = false;
|
|
3681
|
+
this.spinner.stop(`${this.label} failed: ${message}`);
|
|
3682
|
+
}
|
|
3683
|
+
/** Snapshot of current metrics. */
|
|
3684
|
+
summary() {
|
|
3685
|
+
const durationMs = Math.max(0, this.now() - this.startedAt);
|
|
3686
|
+
const bytes = this.bytesDownloadedNow();
|
|
3687
|
+
const avgSpeedBps = durationMs > 0 ? bytes * 1e3 / durationMs : 0;
|
|
3688
|
+
return {
|
|
3689
|
+
filesDownloaded: this.filesCompleted,
|
|
3690
|
+
filesSkipped: this.filesSkipped,
|
|
3691
|
+
filesFailed: this.filesFailed,
|
|
3692
|
+
bytesDownloaded: bytes,
|
|
3693
|
+
durationMs,
|
|
3694
|
+
avgSpeedBps
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3697
|
+
/** Handle one event. Re-rendering is throttled to {@link minRenderIntervalMs}. */
|
|
3698
|
+
handle(event) {
|
|
3699
|
+
let forceRender = false;
|
|
3700
|
+
switch (event.type) {
|
|
3701
|
+
case "install:phase-changed":
|
|
3702
|
+
case "repair:phase-changed":
|
|
3703
|
+
this.currentPhase = event.phase;
|
|
3704
|
+
forceRender = true;
|
|
3705
|
+
break;
|
|
3706
|
+
case "download:started":
|
|
3707
|
+
this.activeTargets.add(event.file.target);
|
|
3708
|
+
this.activeBytes.set(event.file.target, 0);
|
|
3709
|
+
forceRender = true;
|
|
3710
|
+
break;
|
|
3711
|
+
case "download:progress": {
|
|
3712
|
+
const previous = this.activeBytes.get(event.file.target) ?? 0;
|
|
3713
|
+
if (event.bytesDownloaded > previous) {
|
|
3714
|
+
const ts = this.now();
|
|
3715
|
+
this.speedSamples.push({ ts, bytes: event.bytesDownloaded - previous });
|
|
3716
|
+
const cutoff = ts - SPEED_WINDOW_MS;
|
|
3717
|
+
while (this.speedSamples.length > 0 && this.speedSamples[0].ts < cutoff) {
|
|
3718
|
+
this.speedSamples.shift();
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
this.activeBytes.set(event.file.target, event.bytesDownloaded);
|
|
3722
|
+
break;
|
|
3723
|
+
}
|
|
3724
|
+
case "download:completed":
|
|
3725
|
+
this.filesCompleted++;
|
|
3726
|
+
this.completedBytes += event.bytes;
|
|
3727
|
+
this.activeTargets.delete(event.file.target);
|
|
3728
|
+
this.activeBytes.delete(event.file.target);
|
|
3729
|
+
forceRender = true;
|
|
3730
|
+
break;
|
|
3731
|
+
case "download:skipped":
|
|
3732
|
+
this.filesSkipped++;
|
|
3733
|
+
this.filesCompleted++;
|
|
3734
|
+
forceRender = true;
|
|
3735
|
+
break;
|
|
3736
|
+
case "download:failed":
|
|
3737
|
+
if (event.willRetry) {
|
|
3738
|
+
this.activeBytes.set(event.file.target, 0);
|
|
3739
|
+
} else {
|
|
3740
|
+
this.filesFailed++;
|
|
3741
|
+
this.activeTargets.delete(event.file.target);
|
|
3742
|
+
this.activeBytes.delete(event.file.target);
|
|
3743
|
+
}
|
|
3744
|
+
forceRender = true;
|
|
3745
|
+
break;
|
|
3746
|
+
case "archive:extracted":
|
|
3747
|
+
case "forge:processor-started":
|
|
3748
|
+
case "forge:processor-completed":
|
|
3749
|
+
forceRender = true;
|
|
3750
|
+
break;
|
|
3751
|
+
}
|
|
3752
|
+
this.maybeRender(forceRender);
|
|
3753
|
+
}
|
|
3754
|
+
maybeRender(force) {
|
|
3755
|
+
if (!this.active) return;
|
|
3756
|
+
const ts = this.now();
|
|
3757
|
+
if (!force && ts - this.lastRenderAt < this.minRenderIntervalMs) return;
|
|
3758
|
+
const line = this.formatLine();
|
|
3759
|
+
if (line === this.lastRenderedLine) return;
|
|
3760
|
+
this.lastRenderAt = ts;
|
|
3761
|
+
this.lastRenderedLine = line;
|
|
3762
|
+
this.spinner.message(line);
|
|
3763
|
+
}
|
|
3764
|
+
bytesDownloadedNow() {
|
|
3765
|
+
let sum = this.completedBytes;
|
|
3766
|
+
for (const v of this.activeBytes.values()) sum += v;
|
|
3767
|
+
return sum;
|
|
3768
|
+
}
|
|
3769
|
+
formatLine() {
|
|
3770
|
+
const phase = shortPhase(this.currentPhase);
|
|
3771
|
+
const bytes = this.bytesDownloadedNow();
|
|
3772
|
+
const speed = this.computeSpeedBps();
|
|
3773
|
+
const active = this.activeTargets.size;
|
|
3774
|
+
const ratio = this.computeRatio();
|
|
3775
|
+
const fileCounter = this.totalActions !== void 0 ? `${this.filesCompleted}/${this.totalActions}` : `${this.filesCompleted}`;
|
|
3776
|
+
const segments = [`[${phase}]`, fileCounter, formatBytes(bytes)];
|
|
3777
|
+
if (speed > 0) segments.push(`${formatBytes(speed)}/s`);
|
|
3778
|
+
segments.push(`active ${active}`);
|
|
3779
|
+
if (ratio !== null) segments.push(`${this.formatBar(ratio)} ${(ratio * 100).toFixed(0)}%`);
|
|
3780
|
+
const line = segments.join(" \xB7 ");
|
|
3781
|
+
return clipToColumns(line, this.columnsFn());
|
|
3782
|
+
}
|
|
3783
|
+
formatBar(ratio) {
|
|
3784
|
+
const filled = Math.round(ratio * BAR_WIDTH);
|
|
3785
|
+
return `${"\u2588".repeat(filled)}${"\u2591".repeat(BAR_WIDTH - filled)}`;
|
|
3786
|
+
}
|
|
3787
|
+
computeRatio() {
|
|
3788
|
+
if (this.totalBytes !== void 0 && this.totalBytes > 0) {
|
|
3789
|
+
return Math.min(1, this.bytesDownloadedNow() / this.totalBytes);
|
|
3790
|
+
}
|
|
3791
|
+
if (this.totalActions !== void 0 && this.totalActions > 0) {
|
|
3792
|
+
return Math.min(1, this.filesCompleted / this.totalActions);
|
|
3793
|
+
}
|
|
3794
|
+
return null;
|
|
3795
|
+
}
|
|
3796
|
+
computeSpeedBps() {
|
|
3797
|
+
if (this.speedSamples.length === 0) return 0;
|
|
3798
|
+
const oldest = this.speedSamples[0].ts;
|
|
3799
|
+
const elapsed = Math.max(1, this.now() - oldest);
|
|
3800
|
+
const bytes = this.speedSamples.reduce((sum, sample) => sum + sample.bytes, 0);
|
|
3801
|
+
return bytes * 1e3 / elapsed;
|
|
3802
|
+
}
|
|
3803
|
+
summaryLine(summary) {
|
|
3804
|
+
const total = this.totalActions !== void 0 ? `/${this.totalActions}` : "";
|
|
3805
|
+
const parts = [`${this.label} done`, `${summary.filesDownloaded}${total} files`];
|
|
3806
|
+
if (summary.filesSkipped > 0) parts.push(`${summary.filesSkipped} skipped`);
|
|
3807
|
+
if (summary.filesFailed > 0) parts.push(`${summary.filesFailed} failed`);
|
|
3808
|
+
parts.push(`${formatBytes(summary.bytesDownloaded)}`);
|
|
3809
|
+
if (summary.avgSpeedBps > 0) parts.push(`avg ${formatBytes(summary.avgSpeedBps)}/s`);
|
|
3810
|
+
parts.push(`in ${formatDuration(summary.durationMs)}`);
|
|
3811
|
+
return clipToColumns(parts.join(" \xB7 "), this.columnsFn());
|
|
3812
|
+
}
|
|
3813
|
+
};
|
|
3814
|
+
function shortPhase(phase) {
|
|
3815
|
+
if (phase === "idle") return "starting";
|
|
3816
|
+
for (const prefix of [
|
|
3817
|
+
"downloading-",
|
|
3818
|
+
"installing-",
|
|
3819
|
+
"extracting-",
|
|
3820
|
+
"repairing-",
|
|
3821
|
+
"running-",
|
|
3822
|
+
"writing-"
|
|
3823
|
+
]) {
|
|
3824
|
+
if (phase.startsWith(prefix)) return phase.slice(prefix.length);
|
|
3825
|
+
}
|
|
3826
|
+
return phase;
|
|
3827
|
+
}
|
|
3828
|
+
function clipToColumns(line, cols) {
|
|
3829
|
+
if (cols <= 0) return line;
|
|
3830
|
+
const limit = cols - SAFETY_MARGIN;
|
|
3831
|
+
if (limit <= 0) return "";
|
|
3832
|
+
if (line.length <= limit) return line;
|
|
3833
|
+
return `${line.slice(0, Math.max(0, limit - 1))}\u2026`;
|
|
3834
|
+
}
|
|
3835
|
+
function formatBytes(bytes) {
|
|
3836
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "\u2014";
|
|
3837
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
3838
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
3839
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
3840
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
3841
|
+
}
|
|
3842
|
+
function formatDuration(ms) {
|
|
3843
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
3844
|
+
const seconds = Math.round(ms / 1e3);
|
|
3845
|
+
if (seconds < 60) return `${seconds}s`;
|
|
3846
|
+
const minutes = Math.floor(seconds / 60);
|
|
3847
|
+
const remSec = seconds % 60;
|
|
3848
|
+
if (minutes < 60) return `${minutes}m${remSec.toString().padStart(2, "0")}s`;
|
|
3849
|
+
const hours = Math.floor(minutes / 60);
|
|
3850
|
+
const remMin = minutes % 60;
|
|
3851
|
+
return `${hours}h${remMin.toString().padStart(2, "0")}m${remSec.toString().padStart(2, "0")}s`;
|
|
3852
|
+
}
|
|
3853
|
+
|
|
3854
|
+
// src/cli/scenarios/install-helpers.ts
|
|
3855
|
+
async function runInstallWithProgress(ctx, target, label) {
|
|
3856
|
+
const planSpinner = ctx.ui.spinner();
|
|
3857
|
+
planSpinner.start(`Planning ${label}\u2026`);
|
|
3858
|
+
let plan;
|
|
3859
|
+
try {
|
|
3860
|
+
plan = await ctx.kit.install.plan(target);
|
|
3861
|
+
planSpinner.stop(`Plan ready: ${plan.totalActions} actions, ${formatBytes(plan.totalBytes)}.`);
|
|
3862
|
+
} catch (error) {
|
|
3863
|
+
planSpinner.stop("Planning failed.");
|
|
3864
|
+
throw error;
|
|
3865
|
+
}
|
|
3866
|
+
const renderer = new ProgressRenderer({
|
|
3867
|
+
ui: ctx.ui,
|
|
3868
|
+
label: `Install ${label}`,
|
|
3869
|
+
totalActions: plan.totalActions,
|
|
3870
|
+
totalBytes: plan.totalBytes
|
|
3871
|
+
});
|
|
3872
|
+
const onEvent = renderer.attach();
|
|
3873
|
+
try {
|
|
3874
|
+
await ctx.kit.install.run(plan, { onEvent });
|
|
3875
|
+
const summary = renderer.finish();
|
|
3876
|
+
ctx.ui.note("Install summary", formatSummary(summary));
|
|
3877
|
+
} catch (error) {
|
|
3878
|
+
renderer.fail(formatUserError(error));
|
|
3879
|
+
throw error;
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
async function runInstallFromSelection(ctx, sel) {
|
|
3883
|
+
const v = sel.version;
|
|
3884
|
+
const dir = sel.directory;
|
|
3885
|
+
const loaderInput = buildLoaderInput(sel);
|
|
3886
|
+
const runtimeInput = sel.runtimeOverride !== null ? { runtime: { component: sel.runtimeOverride } } : {};
|
|
3887
|
+
let target;
|
|
3888
|
+
try {
|
|
3889
|
+
target = await ctx.kit.targets.resolve({
|
|
3890
|
+
id: path.basename(dir),
|
|
3891
|
+
directory: dir,
|
|
3892
|
+
minecraft: { version: v.id },
|
|
3893
|
+
loader: loaderInput,
|
|
3894
|
+
...runtimeInput
|
|
3895
|
+
});
|
|
3896
|
+
} catch (error) {
|
|
3897
|
+
ctx.ui.log("error", formatUserError(error));
|
|
3898
|
+
return "install-type";
|
|
3899
|
+
}
|
|
3900
|
+
try {
|
|
3901
|
+
await runInstallWithProgress(ctx, target, describeLoader(sel));
|
|
3902
|
+
return "ok";
|
|
3903
|
+
} catch {
|
|
3904
|
+
return "directory";
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
async function runStandaloneRuntimeInstallWithProgress(ctx, input) {
|
|
3908
|
+
const label = `runtime ${input.runtime.component}`;
|
|
3909
|
+
const planSpinner = ctx.ui.spinner();
|
|
3910
|
+
planSpinner.start(`Planning ${label}\u2026`);
|
|
3911
|
+
let plan;
|
|
3912
|
+
try {
|
|
3913
|
+
plan = await ctx.kit.install.runtime.standalonePlan({
|
|
3914
|
+
id: input.id,
|
|
3915
|
+
directory: input.directory,
|
|
3916
|
+
runtime: input.runtime
|
|
3917
|
+
});
|
|
3918
|
+
planSpinner.stop(`Plan ready: ${plan.totalActions} files, ${formatBytes(plan.totalBytes)}.`);
|
|
3919
|
+
} catch (error) {
|
|
3920
|
+
planSpinner.stop("Planning failed.");
|
|
3921
|
+
throw error;
|
|
3922
|
+
}
|
|
3923
|
+
const renderer = new ProgressRenderer({
|
|
3924
|
+
ui: ctx.ui,
|
|
3925
|
+
label: `Install ${label}`,
|
|
3926
|
+
totalActions: plan.totalActions,
|
|
3927
|
+
totalBytes: plan.totalBytes
|
|
3928
|
+
});
|
|
3929
|
+
const onEvent = renderer.attach();
|
|
3930
|
+
try {
|
|
3931
|
+
await ctx.kit.install.runtime.run(plan, { onEvent });
|
|
3932
|
+
const summary = renderer.finish();
|
|
3933
|
+
ctx.ui.note("Runtime install summary", formatSummary(summary));
|
|
3934
|
+
} catch (error) {
|
|
3935
|
+
renderer.fail(formatUserError(error));
|
|
3936
|
+
throw error;
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
function buildLoaderInput(sel) {
|
|
3940
|
+
if (sel.installType === Loaders.VANILLA) {
|
|
3941
|
+
return { type: Loaders.VANILLA };
|
|
3942
|
+
}
|
|
3943
|
+
if (sel.installType === Loaders.FABRIC) {
|
|
3944
|
+
return { type: Loaders.FABRIC, version: sel.fabricLoader };
|
|
3945
|
+
}
|
|
3946
|
+
return { type: Loaders.FORGE, version: sel.forgeBuild };
|
|
3947
|
+
}
|
|
3948
|
+
function describeLoader(sel) {
|
|
3949
|
+
const v = sel.version.id;
|
|
3950
|
+
if (sel.installType === Loaders.VANILLA) return `Vanilla ${v}`;
|
|
3951
|
+
if (sel.installType === Loaders.FABRIC) return `Fabric ${sel.fabricLoader} on ${v}`;
|
|
3952
|
+
return `Forge ${sel.forgeLabel ?? sel.forgeBuild} on ${v}`;
|
|
3953
|
+
}
|
|
3954
|
+
function summaryRows(sel) {
|
|
3955
|
+
const v = sel.version;
|
|
3956
|
+
const rows = [
|
|
3957
|
+
["Minecraft", v.id],
|
|
3958
|
+
["Type", labelForType(sel.installType)]
|
|
3959
|
+
];
|
|
3960
|
+
if (sel.installType === Loaders.FABRIC && sel.fabricLoader) {
|
|
3961
|
+
rows.push(["Fabric", sel.fabricLoader]);
|
|
3962
|
+
}
|
|
3963
|
+
if (sel.installType === Loaders.FORGE && (sel.forgeLabel || sel.forgeBuild)) {
|
|
3964
|
+
rows.push(["Forge", sel.forgeLabel ?? sel.forgeBuild ?? ""]);
|
|
3965
|
+
}
|
|
3966
|
+
rows.push(["Runtime", sel.runtimeOverride ?? "auto-detect"]);
|
|
3967
|
+
rows.push(["Directory", sel.directory]);
|
|
3968
|
+
return rows;
|
|
3969
|
+
}
|
|
3970
|
+
function labelForType(type) {
|
|
3971
|
+
if (type === Loaders.FABRIC) return "Fabric";
|
|
3972
|
+
if (type === Loaders.FORGE) return "Forge (modern)";
|
|
3973
|
+
return "Vanilla";
|
|
3974
|
+
}
|
|
3975
|
+
function previousFromDirectory(sel) {
|
|
3976
|
+
if (sel.installType === Loaders.FABRIC) return "fabric-loader";
|
|
3977
|
+
if (sel.installType === Loaders.FORGE) return "forge-build";
|
|
3978
|
+
return "install-type";
|
|
3979
|
+
}
|
|
3980
|
+
function defaultIdFromSelection(sel) {
|
|
3981
|
+
const v = sel.version.id;
|
|
3982
|
+
if (sel.installType === Loaders.FABRIC) {
|
|
3983
|
+
return defaultIdFor("fabric", `${v}-${sel.fabricLoader ?? ""}`);
|
|
3984
|
+
}
|
|
3985
|
+
if (sel.installType === Loaders.FORGE) {
|
|
3986
|
+
return defaultIdFor("forge", `${v}-${sel.forgeBuild ?? ""}`);
|
|
3987
|
+
}
|
|
3988
|
+
return defaultIdFor("vanilla", v);
|
|
3989
|
+
}
|
|
3990
|
+
function defaultIdFor(loader, suffix) {
|
|
3991
|
+
return `${loader}-${suffix}`.replace(/[^a-zA-Z0-9._-]+/g, "-").toLowerCase();
|
|
3992
|
+
}
|
|
3993
|
+
function formatDetailed(entry) {
|
|
3994
|
+
const versions = entry.minecraftVersions.length === 0 ? "(none)" : entry.minecraftVersions.join(", ");
|
|
3995
|
+
const loaders = entry.loaders.length === 0 ? "(none)" : entry.loaders.map((l) => `${l.type}${l.version ? ` ${l.version}` : ""}`).join(", ");
|
|
3996
|
+
const runtimePath = entry.runtime?.javaPath ?? "(none detected)";
|
|
3997
|
+
const runtimeComponent = entry.runtime?.component ?? "(unknown)";
|
|
3998
|
+
const runtimeVersion = entry.runtime?.javaVersion ?? "(unknown)";
|
|
3999
|
+
return [
|
|
4000
|
+
`Directory: ${entry.directory}`,
|
|
4001
|
+
`Minecraft: ${versions}`,
|
|
4002
|
+
`Loaders: ${loaders}`,
|
|
4003
|
+
`Runtime path: ${runtimePath}`,
|
|
4004
|
+
`Runtime component: ${runtimeComponent}`,
|
|
4005
|
+
`Runtime version: ${runtimeVersion}`
|
|
4006
|
+
].join("\n");
|
|
4007
|
+
}
|
|
4008
|
+
function formatSummary(summary) {
|
|
4009
|
+
const lines = [
|
|
4010
|
+
`Files downloaded: ${summary.filesDownloaded}`,
|
|
4011
|
+
`Files skipped: ${summary.filesSkipped}`
|
|
4012
|
+
];
|
|
4013
|
+
if (summary.filesFailed > 0) {
|
|
4014
|
+
lines.push(`Files failed: ${summary.filesFailed}`);
|
|
4015
|
+
}
|
|
4016
|
+
lines.push(
|
|
4017
|
+
`Bytes downloaded: ${formatBytes(summary.bytesDownloaded)}`,
|
|
4018
|
+
`Average speed: ${formatBytes(summary.avgSpeedBps)}/s`,
|
|
4019
|
+
`Duration: ${formatDuration(summary.durationMs)}`
|
|
4020
|
+
);
|
|
4021
|
+
return lines.join("\n");
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
// src/types/minecraft.ts
|
|
4025
|
+
var MinecraftChannels = {
|
|
4026
|
+
RELEASE: "release",
|
|
4027
|
+
SNAPSHOT: "snapshot",
|
|
4028
|
+
OLD_BETA: "old_beta",
|
|
4029
|
+
OLD_ALPHA: "old_alpha"
|
|
4030
|
+
};
|
|
4031
|
+
|
|
4032
|
+
// src/cli/scenarios/types.ts
|
|
4033
|
+
var CHANNEL_OPTIONS = [
|
|
4034
|
+
{ label: "Release", value: MinecraftChannels.RELEASE, hint: "stable releases (recommended)" },
|
|
4035
|
+
{ label: "Snapshot", value: MinecraftChannels.SNAPSHOT, hint: "weekly development builds" },
|
|
4036
|
+
{ label: "Old versions", value: "old", hint: "old_beta + old_alpha" },
|
|
4037
|
+
{ label: "All", value: "all", hint: "every channel combined" }
|
|
4038
|
+
];
|
|
4039
|
+
|
|
4040
|
+
// src/cli/scenarios/pickers.ts
|
|
4041
|
+
async function pickChannel(ui) {
|
|
4042
|
+
return ui.select({
|
|
4043
|
+
message: "Select Minecraft channel",
|
|
4044
|
+
options: CHANNEL_OPTIONS,
|
|
4045
|
+
allowCancel: true
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
async function pickMinecraftVersion(ctx, channel) {
|
|
4049
|
+
const spinner = ctx.ui.spinner();
|
|
4050
|
+
spinner.start("Loading Minecraft versions\u2026");
|
|
4051
|
+
let versions;
|
|
4052
|
+
try {
|
|
4053
|
+
versions = await ctx.kit.versions.minecraft.list();
|
|
4054
|
+
spinner.stop(`${versions.length} versions loaded.`);
|
|
4055
|
+
} catch (error) {
|
|
4056
|
+
spinner.stop("Failed to load versions.");
|
|
4057
|
+
ctx.ui.log("error", formatUserError(error));
|
|
4058
|
+
return { kind: "back" };
|
|
4059
|
+
}
|
|
4060
|
+
const filtered = filterVersionsByChannel(versions, channel);
|
|
4061
|
+
if (filtered.length === 0) {
|
|
4062
|
+
ctx.ui.log("warn", "No versions in that channel.");
|
|
4063
|
+
return { kind: "back" };
|
|
4064
|
+
}
|
|
4065
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4066
|
+
const unique = [];
|
|
4067
|
+
for (const v of filtered) {
|
|
4068
|
+
if (seen.has(v.id)) continue;
|
|
4069
|
+
seen.add(v.id);
|
|
4070
|
+
unique.push(v);
|
|
4071
|
+
}
|
|
4072
|
+
const sorted = [...unique].sort(
|
|
4073
|
+
(a, b) => (b.releaseTime ?? "").localeCompare(a.releaseTime ?? "")
|
|
4074
|
+
);
|
|
4075
|
+
const options = sorted.map((v) => ({
|
|
4076
|
+
label: v.id,
|
|
4077
|
+
value: v,
|
|
4078
|
+
hint: `${v.type} \xB7 ${(v.releaseTime ?? "").slice(0, 10)}`
|
|
4079
|
+
}));
|
|
4080
|
+
return ctx.ui.searchableSelect({
|
|
4081
|
+
message: "Select Minecraft version",
|
|
4082
|
+
options,
|
|
4083
|
+
allowBack: true,
|
|
4084
|
+
allowCancel: true
|
|
4085
|
+
});
|
|
4086
|
+
}
|
|
4087
|
+
async function pickRuntime(ctx) {
|
|
4088
|
+
const initial = await ctx.ui.select({
|
|
4089
|
+
message: "Select Java/runtime",
|
|
4090
|
+
options: [
|
|
4091
|
+
{
|
|
4092
|
+
label: "Use recommended",
|
|
4093
|
+
value: "auto",
|
|
4094
|
+
hint: "use the runtime declared by the version manifest"
|
|
4095
|
+
},
|
|
4096
|
+
{
|
|
4097
|
+
label: "Pick specific component\u2026",
|
|
4098
|
+
value: "specific",
|
|
4099
|
+
hint: "choose from Mojang's published runtimes"
|
|
4100
|
+
}
|
|
4101
|
+
],
|
|
4102
|
+
allowBack: true,
|
|
4103
|
+
allowCancel: true
|
|
4104
|
+
});
|
|
4105
|
+
if (initial.kind !== "ok") return initial;
|
|
4106
|
+
if (initial.value === "auto") return { kind: "ok", value: null };
|
|
4107
|
+
const spinner = ctx.ui.spinner();
|
|
4108
|
+
spinner.start("Loading runtime components\u2026");
|
|
4109
|
+
try {
|
|
4110
|
+
const list = await ctx.kit.versions.runtime.list({
|
|
4111
|
+
system: ctx.kit.targets.system
|
|
4112
|
+
});
|
|
4113
|
+
spinner.stop(
|
|
4114
|
+
`${list.length} runtime entr${list.length === 1 ? "y" : "ies"} for your platform.`
|
|
4115
|
+
);
|
|
4116
|
+
if (list.length === 0) {
|
|
4117
|
+
ctx.ui.log("warn", "No published runtimes for this platform \u2014 falling back to auto-detect.");
|
|
4118
|
+
return { kind: "ok", value: null };
|
|
4119
|
+
}
|
|
4120
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4121
|
+
const components = [];
|
|
4122
|
+
for (const entry of list) {
|
|
4123
|
+
if (seen.has(entry.component)) continue;
|
|
4124
|
+
seen.add(entry.component);
|
|
4125
|
+
components.push({
|
|
4126
|
+
component: entry.component,
|
|
4127
|
+
versionName: entry.versionName
|
|
4128
|
+
});
|
|
4129
|
+
}
|
|
4130
|
+
const choice = await ctx.ui.select({
|
|
4131
|
+
message: "Select runtime component",
|
|
4132
|
+
options: components.map((c) => ({
|
|
4133
|
+
label: c.component,
|
|
4134
|
+
value: c.component,
|
|
4135
|
+
hint: c.versionName
|
|
4136
|
+
})),
|
|
4137
|
+
allowBack: true,
|
|
4138
|
+
allowCancel: true
|
|
4139
|
+
});
|
|
4140
|
+
if (choice.kind !== "ok") return choice;
|
|
4141
|
+
return { kind: "ok", value: choice.value };
|
|
4142
|
+
} catch (error) {
|
|
4143
|
+
spinner.stop("Failed to load runtimes.");
|
|
4144
|
+
ctx.ui.log("warn", `${formatUserError(error)} Falling back to auto-detect.`);
|
|
4145
|
+
return { kind: "ok", value: null };
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
async function pickInstallType(ui) {
|
|
4149
|
+
return ui.select({
|
|
4150
|
+
message: "Select installation type",
|
|
4151
|
+
options: [
|
|
4152
|
+
{ label: "Vanilla", value: Loaders.VANILLA, hint: "no mod loader" },
|
|
4153
|
+
{
|
|
4154
|
+
label: "Fabric",
|
|
4155
|
+
value: Loaders.FABRIC,
|
|
4156
|
+
hint: "lightweight modern loader"
|
|
4157
|
+
},
|
|
4158
|
+
{ label: "Forge", value: Loaders.FORGE, hint: "modern Forge (1.13+)" }
|
|
4159
|
+
],
|
|
4160
|
+
allowBack: true,
|
|
4161
|
+
allowCancel: true
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4164
|
+
async function pickFabricLoader2(ctx, minecraftVersion) {
|
|
4165
|
+
const spinner = ctx.ui.spinner();
|
|
4166
|
+
spinner.start(`Loading Fabric loaders for ${minecraftVersion}\u2026`);
|
|
4167
|
+
try {
|
|
4168
|
+
const loaders = await ctx.kit.versions.fabric.list({ minecraftVersion });
|
|
4169
|
+
spinner.stop(`${loaders.length} Fabric loader(s).`);
|
|
4170
|
+
if (loaders.length === 0) {
|
|
4171
|
+
return { kind: "incompatible" };
|
|
4172
|
+
}
|
|
4173
|
+
const options = loaders.map((loader) => ({
|
|
4174
|
+
label: loader.version,
|
|
4175
|
+
value: loader.version,
|
|
4176
|
+
hint: loader.stable ? "stable" : "unstable"
|
|
4177
|
+
}));
|
|
4178
|
+
return await ctx.ui.searchableSelect({
|
|
4179
|
+
message: "Select Fabric loader",
|
|
4180
|
+
options,
|
|
4181
|
+
allowBack: true,
|
|
4182
|
+
allowCancel: true
|
|
4183
|
+
});
|
|
4184
|
+
} catch (error) {
|
|
4185
|
+
spinner.stop("Failed to load Fabric loaders.");
|
|
4186
|
+
ctx.ui.log("warn", formatUserError(error));
|
|
4187
|
+
return { kind: "incompatible" };
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
async function pickForgeBuild(ctx, minecraftVersion) {
|
|
4191
|
+
const spinner = ctx.ui.spinner();
|
|
4192
|
+
spinner.start(`Loading Forge builds for ${minecraftVersion}\u2026`);
|
|
4193
|
+
try {
|
|
4194
|
+
const builds = await ctx.kit.versions.forge.list({ minecraftVersion });
|
|
4195
|
+
spinner.stop(`${builds.length} Forge build(s).`);
|
|
4196
|
+
if (builds.length === 0) {
|
|
4197
|
+
return { kind: "incompatible" };
|
|
4198
|
+
}
|
|
4199
|
+
const recommended = builds.find((b) => b.isRecommended);
|
|
4200
|
+
const latest = builds.find((b) => b.isLatest);
|
|
4201
|
+
const options = [];
|
|
4202
|
+
if (recommended) {
|
|
4203
|
+
options.push({
|
|
4204
|
+
label: `Recommended (${recommended.forgeVersion})`,
|
|
4205
|
+
value: recommended.forgeVersion,
|
|
4206
|
+
hint: "promoted -recommended"
|
|
4207
|
+
});
|
|
4208
|
+
}
|
|
4209
|
+
if (latest && latest.forgeVersion !== recommended?.forgeVersion) {
|
|
4210
|
+
options.push({
|
|
4211
|
+
label: `Latest (${latest.forgeVersion})`,
|
|
4212
|
+
value: latest.forgeVersion,
|
|
4213
|
+
hint: "promoted -latest"
|
|
4214
|
+
});
|
|
4215
|
+
}
|
|
4216
|
+
for (const build of builds) {
|
|
4217
|
+
if (build.isRecommended || build.isLatest) continue;
|
|
4218
|
+
options.push({
|
|
4219
|
+
label: build.forgeVersion,
|
|
4220
|
+
value: build.forgeVersion,
|
|
4221
|
+
hint: build.fullVersion
|
|
4222
|
+
});
|
|
4223
|
+
}
|
|
4224
|
+
const result = await ctx.ui.searchableSelect({
|
|
4225
|
+
message: "Select Forge build",
|
|
4226
|
+
options,
|
|
4227
|
+
allowBack: true,
|
|
4228
|
+
allowCancel: true
|
|
4229
|
+
});
|
|
4230
|
+
if (result.kind !== "ok") return result;
|
|
4231
|
+
const matched = options.find((o) => o.value === result.value);
|
|
4232
|
+
return {
|
|
4233
|
+
kind: "ok",
|
|
4234
|
+
value: result.value,
|
|
4235
|
+
label: matched?.label ?? result.value
|
|
4236
|
+
};
|
|
4237
|
+
} catch (error) {
|
|
4238
|
+
spinner.stop("Failed to load Forge builds.");
|
|
4239
|
+
ctx.ui.log("warn", formatUserError(error));
|
|
4240
|
+
return { kind: "incompatible" };
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
async function pickDirectory(ctx, suggestedId) {
|
|
4244
|
+
const defaultPath = path.join(ctx.rootDir, suggestedId);
|
|
4245
|
+
const choice = await ctx.ui.select({
|
|
4246
|
+
message: "Where should the installation live?",
|
|
4247
|
+
options: [
|
|
4248
|
+
{ label: `Default: ${defaultPath}`, value: "default" },
|
|
4249
|
+
{ label: "Custom path\u2026", value: "custom" }
|
|
4250
|
+
],
|
|
4251
|
+
allowBack: true,
|
|
4252
|
+
allowCancel: true
|
|
4253
|
+
});
|
|
4254
|
+
if (choice.kind !== "ok") return choice;
|
|
4255
|
+
if (choice.value === "default") return { kind: "ok", value: defaultPath };
|
|
4256
|
+
const text = await ctx.ui.text({
|
|
4257
|
+
message: "Custom installation directory",
|
|
4258
|
+
placeholder: defaultPath,
|
|
4259
|
+
initial: defaultPath,
|
|
4260
|
+
validate: (s) => s.trim().length === 0 ? "Path must be non-empty" : void 0,
|
|
4261
|
+
allowBack: true
|
|
4262
|
+
});
|
|
4263
|
+
if (text.kind !== "ok") return text;
|
|
4264
|
+
return { kind: "ok", value: (text.value ?? "").trim() };
|
|
4265
|
+
}
|
|
4266
|
+
async function confirmInstall(ctx, rows) {
|
|
4267
|
+
ctx.ui.note("Summary", rows.map(([k, v]) => `${k.padEnd(11)} ${v}`).join("\n"));
|
|
4268
|
+
return ctx.ui.confirm({
|
|
4269
|
+
message: "Proceed with install?",
|
|
4270
|
+
initial: true,
|
|
4271
|
+
allowBack: true
|
|
4272
|
+
});
|
|
4273
|
+
}
|
|
4274
|
+
async function pickRuntimeComponent(ctx) {
|
|
4275
|
+
const spinner = ctx.ui.spinner();
|
|
4276
|
+
spinner.start("Loading runtime components\u2026");
|
|
4277
|
+
let entries;
|
|
4278
|
+
try {
|
|
4279
|
+
entries = await ctx.kit.versions.runtime.list({
|
|
4280
|
+
system: ctx.kit.targets.system
|
|
4281
|
+
});
|
|
4282
|
+
spinner.stop(
|
|
4283
|
+
`${entries.length} runtime entr${entries.length === 1 ? "y" : "ies"} for your platform.`
|
|
4284
|
+
);
|
|
4285
|
+
} catch (error) {
|
|
4286
|
+
spinner.stop("Failed to load runtimes.");
|
|
4287
|
+
ctx.ui.log("error", formatUserError(error));
|
|
4288
|
+
return { kind: "cancel" };
|
|
4289
|
+
}
|
|
4290
|
+
if (entries.length === 0) {
|
|
4291
|
+
ctx.ui.log("warn", "No published runtimes for this platform.");
|
|
4292
|
+
return { kind: "cancel" };
|
|
4293
|
+
}
|
|
4294
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4295
|
+
const componentChoices = [];
|
|
4296
|
+
for (const entry of entries) {
|
|
4297
|
+
if (seen.has(entry.component)) continue;
|
|
4298
|
+
seen.add(entry.component);
|
|
4299
|
+
componentChoices.push({
|
|
4300
|
+
component: entry.component,
|
|
4301
|
+
versionName: entry.versionName
|
|
4302
|
+
});
|
|
4303
|
+
}
|
|
4304
|
+
const choice = await ctx.ui.select({
|
|
4305
|
+
message: "Select Java runtime component",
|
|
4306
|
+
options: componentChoices.map((c) => ({
|
|
4307
|
+
label: c.component,
|
|
4308
|
+
value: c.component,
|
|
4309
|
+
hint: c.versionName
|
|
4310
|
+
})),
|
|
4311
|
+
allowBack: true,
|
|
4312
|
+
allowCancel: true
|
|
4313
|
+
});
|
|
4314
|
+
if (choice.kind !== "ok") return choice;
|
|
4315
|
+
const picked = componentChoices.find((c) => c.component === choice.value);
|
|
4316
|
+
if (!picked) return { kind: "cancel" };
|
|
4317
|
+
return { kind: "ok", value: picked };
|
|
4318
|
+
}
|
|
4319
|
+
async function pickRuntimeInstallRoot(ctx) {
|
|
4320
|
+
const choice = await ctx.ui.select({
|
|
4321
|
+
message: "Where should the runtime files live?",
|
|
4322
|
+
options: [
|
|
4323
|
+
{
|
|
4324
|
+
label: "Per-target (default)",
|
|
4325
|
+
value: "per-target",
|
|
4326
|
+
hint: "<directory>/runtime/<component>"
|
|
4327
|
+
},
|
|
4328
|
+
{
|
|
4329
|
+
label: "Custom shared install root\u2026",
|
|
4330
|
+
value: "custom",
|
|
4331
|
+
hint: "absolute path containing component directories"
|
|
4332
|
+
}
|
|
4333
|
+
],
|
|
4334
|
+
allowBack: true,
|
|
4335
|
+
allowCancel: true
|
|
4336
|
+
});
|
|
4337
|
+
if (choice.kind !== "ok") return choice;
|
|
4338
|
+
if (choice.value === "per-target") return { kind: "ok", value: null };
|
|
4339
|
+
const text = await ctx.ui.text({
|
|
4340
|
+
message: "Custom runtime install root (absolute path)",
|
|
4341
|
+
placeholder: "C:\\shared\\jre",
|
|
4342
|
+
validate: (s) => s.trim().length === 0 ? "Path must be non-empty" : void 0,
|
|
4343
|
+
allowBack: true
|
|
4344
|
+
});
|
|
4345
|
+
if (text.kind !== "ok") return text;
|
|
4346
|
+
const trimmed = (text.value ?? "").trim();
|
|
4347
|
+
if (trimmed.length === 0) return { kind: "ok", value: null };
|
|
4348
|
+
return { kind: "ok", value: trimmed };
|
|
4349
|
+
}
|
|
4350
|
+
async function pickInstalledTarget(ctx) {
|
|
4351
|
+
let list;
|
|
4352
|
+
try {
|
|
4353
|
+
list = await ctx.kit.targets.list({ rootDir: ctx.rootDir });
|
|
4354
|
+
} catch (error) {
|
|
4355
|
+
ctx.ui.log("error", formatUserError(error));
|
|
4356
|
+
return null;
|
|
4357
|
+
}
|
|
4358
|
+
if (list.length === 0) {
|
|
4359
|
+
ctx.ui.log("warn", `No installations under ${ctx.rootDir}. Install one first.`);
|
|
4360
|
+
return null;
|
|
4361
|
+
}
|
|
4362
|
+
const choice = await ctx.ui.select({
|
|
4363
|
+
message: "Pick an installation",
|
|
4364
|
+
options: list.map((entry2) => ({
|
|
4365
|
+
label: entry2.id,
|
|
4366
|
+
value: entry2.id,
|
|
4367
|
+
hint: entry2.minecraftVersions.join(", ") || "no versions"
|
|
4368
|
+
})),
|
|
4369
|
+
allowCancel: true
|
|
4370
|
+
});
|
|
4371
|
+
if (choice.kind !== "ok") return null;
|
|
4372
|
+
const entry = list.find((e) => e.id === choice.value);
|
|
4373
|
+
if (!entry) return null;
|
|
4374
|
+
const mcVersion = await pickMinecraftVersionFromEntry(ctx, entry);
|
|
4375
|
+
if (!mcVersion) return null;
|
|
4376
|
+
const loaderHint = entry.loaders.find(
|
|
4377
|
+
(l) => !l.minecraftVersion || l.minecraftVersion === mcVersion
|
|
4378
|
+
);
|
|
4379
|
+
try {
|
|
4380
|
+
return await ctx.kit.targets.resolve({
|
|
4381
|
+
id: entry.id,
|
|
4382
|
+
directory: entry.directory,
|
|
4383
|
+
minecraft: { version: mcVersion },
|
|
4384
|
+
loader: loaderHintToInput(loaderHint)
|
|
4385
|
+
});
|
|
4386
|
+
} catch (error) {
|
|
4387
|
+
ctx.ui.log("error", formatUserError(error));
|
|
4388
|
+
return null;
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
async function pickMinecraftVersionFromEntry(ctx, entry) {
|
|
4392
|
+
if (entry.minecraftVersions.length === 0) {
|
|
4393
|
+
ctx.ui.log("warn", "Installation has no Minecraft versions on disk.");
|
|
4394
|
+
return null;
|
|
4395
|
+
}
|
|
4396
|
+
if (entry.minecraftVersions.length === 1) {
|
|
4397
|
+
return entry.minecraftVersions[0] ?? null;
|
|
4398
|
+
}
|
|
4399
|
+
const choice = await ctx.ui.select({
|
|
4400
|
+
message: "Multiple Minecraft versions on disk \u2014 pick one",
|
|
4401
|
+
options: entry.minecraftVersions.map((v) => ({ label: v, value: v })),
|
|
4402
|
+
allowCancel: true
|
|
4403
|
+
});
|
|
4404
|
+
if (choice.kind !== "ok") return null;
|
|
4405
|
+
return choice.value;
|
|
4406
|
+
}
|
|
4407
|
+
function loaderHintToInput(hint) {
|
|
4408
|
+
if (!hint) return { type: Loaders.VANILLA };
|
|
4409
|
+
if (hint.type === Loaders.FABRIC) {
|
|
4410
|
+
return hint.version ? { type: Loaders.FABRIC, version: hint.version } : { type: Loaders.FABRIC, preference: VersionPreference.LATEST };
|
|
4411
|
+
}
|
|
4412
|
+
if (hint.type === Loaders.FORGE) {
|
|
4413
|
+
return hint.version ? { type: Loaders.FORGE, version: hint.version } : { type: Loaders.FORGE, preference: VersionPreference.RECOMMENDED };
|
|
4414
|
+
}
|
|
4415
|
+
return { type: Loaders.VANILLA };
|
|
4416
|
+
}
|
|
4417
|
+
function filterVersionsByChannel(versions, channel) {
|
|
4418
|
+
if (channel === "all") return versions;
|
|
4419
|
+
if (channel === "old") {
|
|
4420
|
+
return versions.filter(
|
|
4421
|
+
(v) => v.type === MinecraftChannels.OLD_BETA || v.type === MinecraftChannels.OLD_ALPHA
|
|
4422
|
+
);
|
|
4423
|
+
}
|
|
4424
|
+
return versions.filter((v) => v.type === channel);
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
// src/cli/scenarios/install.ts
|
|
4428
|
+
async function scenarioInstallMinecraft(ctx) {
|
|
4429
|
+
const sel = {
|
|
4430
|
+
channel: null,
|
|
4431
|
+
version: null,
|
|
4432
|
+
runtimeOverride: null,
|
|
4433
|
+
installType: null,
|
|
4434
|
+
fabricLoader: null,
|
|
4435
|
+
forgeBuild: null,
|
|
4436
|
+
forgeLabel: null,
|
|
4437
|
+
directory: null
|
|
4438
|
+
};
|
|
4439
|
+
let step = "channel";
|
|
4440
|
+
while (true) {
|
|
4441
|
+
if (step === "channel") {
|
|
4442
|
+
const r = await pickChannel(ctx.ui);
|
|
4443
|
+
if (r.kind !== "ok") return "cancelled";
|
|
4444
|
+
sel.channel = r.value;
|
|
4445
|
+
step = "version";
|
|
4446
|
+
} else if (step === "version") {
|
|
4447
|
+
const r = await pickMinecraftVersion(ctx, sel.channel);
|
|
4448
|
+
if (r.kind === "cancel") return "cancelled";
|
|
4449
|
+
if (r.kind === "back") {
|
|
4450
|
+
step = "channel";
|
|
4451
|
+
continue;
|
|
4452
|
+
}
|
|
4453
|
+
sel.version = r.value;
|
|
4454
|
+
step = "runtime";
|
|
4455
|
+
} else if (step === "runtime") {
|
|
4456
|
+
const r = await pickRuntime(ctx);
|
|
4457
|
+
if (r.kind === "cancel") return "cancelled";
|
|
4458
|
+
if (r.kind === "back") {
|
|
4459
|
+
step = "version";
|
|
4460
|
+
continue;
|
|
4461
|
+
}
|
|
4462
|
+
sel.runtimeOverride = r.value;
|
|
4463
|
+
step = "install-type";
|
|
4464
|
+
} else if (step === "install-type") {
|
|
4465
|
+
const r = await pickInstallType(ctx.ui);
|
|
4466
|
+
if (r.kind === "cancel") return "cancelled";
|
|
4467
|
+
if (r.kind === "back") {
|
|
4468
|
+
step = "runtime";
|
|
4469
|
+
continue;
|
|
4470
|
+
}
|
|
4471
|
+
sel.installType = r.value;
|
|
4472
|
+
if (r.value === Loaders.VANILLA) {
|
|
4473
|
+
step = "directory";
|
|
4474
|
+
} else if (r.value === Loaders.FABRIC) {
|
|
4475
|
+
step = "fabric-loader";
|
|
4476
|
+
} else {
|
|
4477
|
+
step = "forge-build";
|
|
4478
|
+
}
|
|
4479
|
+
} else if (step === "fabric-loader") {
|
|
4480
|
+
const r = await pickFabricLoader2(ctx, sel.version.id);
|
|
4481
|
+
if (r.kind === "cancel") return "cancelled";
|
|
4482
|
+
if (r.kind === "back") {
|
|
4483
|
+
step = "install-type";
|
|
4484
|
+
continue;
|
|
4485
|
+
}
|
|
4486
|
+
if (r.kind === "incompatible") {
|
|
4487
|
+
ctx.ui.log(
|
|
4488
|
+
"warn",
|
|
4489
|
+
`Fabric is not available for Minecraft ${sel.version.id}. Pick another version or install type.`
|
|
4490
|
+
);
|
|
4491
|
+
step = "install-type";
|
|
4492
|
+
continue;
|
|
4493
|
+
}
|
|
4494
|
+
sel.fabricLoader = r.value;
|
|
4495
|
+
step = "directory";
|
|
4496
|
+
} else if (step === "forge-build") {
|
|
4497
|
+
const r = await pickForgeBuild(ctx, sel.version.id);
|
|
4498
|
+
if (r.kind === "cancel") return "cancelled";
|
|
4499
|
+
if (r.kind === "back") {
|
|
4500
|
+
step = "install-type";
|
|
4501
|
+
continue;
|
|
4502
|
+
}
|
|
4503
|
+
if (r.kind === "incompatible") {
|
|
4504
|
+
ctx.ui.log(
|
|
4505
|
+
"warn",
|
|
4506
|
+
`Forge is not available for Minecraft ${sel.version.id}. Pick another version or install type.`
|
|
4507
|
+
);
|
|
4508
|
+
step = "install-type";
|
|
4509
|
+
continue;
|
|
4510
|
+
}
|
|
4511
|
+
sel.forgeBuild = r.value;
|
|
4512
|
+
sel.forgeLabel = r.label;
|
|
4513
|
+
step = "directory";
|
|
4514
|
+
} else if (step === "directory") {
|
|
4515
|
+
const r = await pickDirectory(ctx, defaultIdFromSelection(sel));
|
|
4516
|
+
if (r.kind === "cancel") return "cancelled";
|
|
4517
|
+
if (r.kind === "back") {
|
|
4518
|
+
step = previousFromDirectory(sel);
|
|
4519
|
+
continue;
|
|
4520
|
+
}
|
|
4521
|
+
sel.directory = r.value;
|
|
4522
|
+
step = "summary";
|
|
4523
|
+
} else {
|
|
4524
|
+
const ok = await confirmInstall(ctx, summaryRows(sel));
|
|
4525
|
+
if (ok.kind === "cancel") return "cancelled";
|
|
4526
|
+
if (ok.kind === "back") {
|
|
4527
|
+
step = "directory";
|
|
4528
|
+
continue;
|
|
4529
|
+
}
|
|
4530
|
+
if (!ok.value) return "cancelled";
|
|
4531
|
+
const result = await runInstallFromSelection(ctx, sel);
|
|
4532
|
+
if (result === "ok") return "completed";
|
|
4533
|
+
step = result;
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4537
|
+
async function scenarioInstallRuntime(ctx) {
|
|
4538
|
+
let step = "component";
|
|
4539
|
+
let component = null;
|
|
4540
|
+
let versionLabel = null;
|
|
4541
|
+
let directory = null;
|
|
4542
|
+
let installRoot = null;
|
|
4543
|
+
while (true) {
|
|
4544
|
+
if (step === "component") {
|
|
4545
|
+
const r = await pickRuntimeComponent(ctx);
|
|
4546
|
+
if (r.kind === "cancel") return "cancelled";
|
|
4547
|
+
if (r.kind === "back") return "cancelled";
|
|
4548
|
+
component = r.value.component;
|
|
4549
|
+
versionLabel = r.value.versionName;
|
|
4550
|
+
step = "directory";
|
|
4551
|
+
} else if (step === "directory") {
|
|
4552
|
+
const r = await pickDirectory(ctx, defaultIdFor("runtime", component ?? "java"));
|
|
4553
|
+
if (r.kind === "cancel") return "cancelled";
|
|
4554
|
+
if (r.kind === "back") {
|
|
4555
|
+
step = "component";
|
|
4556
|
+
continue;
|
|
4557
|
+
}
|
|
4558
|
+
directory = r.value;
|
|
4559
|
+
step = "install-root";
|
|
4560
|
+
} else if (step === "install-root") {
|
|
4561
|
+
const r = await pickRuntimeInstallRoot(ctx);
|
|
4562
|
+
if (r.kind === "cancel") return "cancelled";
|
|
4563
|
+
if (r.kind === "back") {
|
|
4564
|
+
step = "directory";
|
|
4565
|
+
continue;
|
|
4566
|
+
}
|
|
4567
|
+
installRoot = r.value;
|
|
4568
|
+
step = "summary";
|
|
4569
|
+
} else {
|
|
4570
|
+
const dir = directory;
|
|
4571
|
+
const comp = component;
|
|
4572
|
+
const ok = await confirmInstall(ctx, [
|
|
4573
|
+
["Goal", "Install Mojang Java runtime"],
|
|
4574
|
+
["Component", `${comp}${versionLabel ? ` (${versionLabel})` : ""}`],
|
|
4575
|
+
["Directory", dir],
|
|
4576
|
+
["Install root", installRoot ?? `${dir}/runtime (per-target)`]
|
|
4577
|
+
]);
|
|
4578
|
+
if (ok.kind === "cancel") return "cancelled";
|
|
4579
|
+
if (ok.kind === "back") {
|
|
4580
|
+
step = "install-root";
|
|
4581
|
+
continue;
|
|
4582
|
+
}
|
|
4583
|
+
if (!ok.value) return "cancelled";
|
|
4584
|
+
try {
|
|
4585
|
+
const runtime = await ctx.kit.versions.runtime.resolve({
|
|
4586
|
+
system: ctx.kit.targets.system,
|
|
4587
|
+
component: comp
|
|
4588
|
+
});
|
|
4589
|
+
const finalRuntime = installRoot !== null ? { ...runtime, installRoot } : runtime;
|
|
4590
|
+
await runStandaloneRuntimeInstallWithProgress(ctx, {
|
|
4591
|
+
id: path.basename(dir),
|
|
4592
|
+
directory: dir,
|
|
4593
|
+
runtime: finalRuntime
|
|
4594
|
+
});
|
|
4595
|
+
return "completed";
|
|
4596
|
+
} catch (error) {
|
|
4597
|
+
ctx.ui.log("error", formatUserError(error));
|
|
4598
|
+
step = "component";
|
|
4599
|
+
}
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4603
|
+
|
|
4604
|
+
// src/cli/scenarios/verify-repair.ts
|
|
4605
|
+
async function verifyAllAspects(ctx, target) {
|
|
4606
|
+
const results = [];
|
|
4607
|
+
results.push(await ctx.kit.verify.minecraft.run(target));
|
|
4608
|
+
if (target.loader.type === Loaders.FABRIC) {
|
|
4609
|
+
results.push(await ctx.kit.verify.fabric.run(target));
|
|
4610
|
+
} else if (target.loader.type === Loaders.FORGE) {
|
|
4611
|
+
results.push(await ctx.kit.verify.forge.run(target));
|
|
4612
|
+
}
|
|
4613
|
+
results.push(await ctx.kit.verify.runtime.run(target));
|
|
4614
|
+
return results;
|
|
4615
|
+
}
|
|
4616
|
+
function summarizeVerifications(results) {
|
|
4617
|
+
const perKind = results.map((r) => ({ kind: r.kind, count: r.issues.length }));
|
|
4618
|
+
const totalIssues = perKind.reduce((sum, p) => sum + p.count, 0);
|
|
4619
|
+
return { totalIssues, perKind };
|
|
4620
|
+
}
|
|
4621
|
+
async function scenarioVerify(ctx) {
|
|
4622
|
+
const target = await pickInstalledTarget(ctx);
|
|
4623
|
+
if (!target) return "cancelled";
|
|
4624
|
+
const spinner = ctx.ui.spinner();
|
|
4625
|
+
spinner.start("Verifying\u2026");
|
|
4626
|
+
try {
|
|
4627
|
+
const results = await verifyAllAspects(ctx, target);
|
|
4628
|
+
spinner.stop("Verification complete.");
|
|
4629
|
+
const { totalIssues, perKind } = summarizeVerifications(results);
|
|
4630
|
+
if (totalIssues === 0) {
|
|
4631
|
+
ctx.ui.log("success", `${target.id} is clean.`);
|
|
4632
|
+
} else {
|
|
4633
|
+
const breakdown = perKind.filter((p) => p.count > 0).map((p) => `${p.kind}: ${p.count}`).join(", ");
|
|
4634
|
+
ctx.ui.log(
|
|
4635
|
+
"warn",
|
|
4636
|
+
`${target.id}: ${totalIssues} issue(s) (${breakdown}). Run "Repair" to fix.`
|
|
4637
|
+
);
|
|
4638
|
+
}
|
|
4639
|
+
return "completed";
|
|
4640
|
+
} catch (error) {
|
|
4641
|
+
spinner.stop("Verification failed.");
|
|
4642
|
+
ctx.ui.log("error", formatUserError(error));
|
|
4643
|
+
return "cancelled";
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
async function scenarioRepair(ctx) {
|
|
4647
|
+
const target = await pickInstalledTarget(ctx);
|
|
4648
|
+
if (!target) return "cancelled";
|
|
4649
|
+
const verifySpinner = ctx.ui.spinner();
|
|
4650
|
+
verifySpinner.start("Verifying installation\u2026");
|
|
4651
|
+
try {
|
|
4652
|
+
const verifications = await verifyAllAspects(ctx, target);
|
|
4653
|
+
const { totalIssues, perKind } = summarizeVerifications(verifications);
|
|
4654
|
+
if (totalIssues === 0) {
|
|
4655
|
+
verifySpinner.stop("Nothing to repair.");
|
|
4656
|
+
ctx.ui.log("success", "Installation is already clean.");
|
|
4657
|
+
return "completed";
|
|
4658
|
+
}
|
|
4659
|
+
const breakdown = perKind.filter((p) => p.count > 0).map((p) => `${p.kind}: ${p.count}`).join(", ");
|
|
4660
|
+
verifySpinner.stop(`Found ${totalIssues} issue(s) \u2014 ${breakdown}.`);
|
|
4661
|
+
const ok = await ctx.ui.confirm({ message: `Repair ${totalIssues} item(s)?`, initial: true });
|
|
4662
|
+
if (ok.kind !== "ok" || !ok.value) return "cancelled";
|
|
4663
|
+
const aspects = [
|
|
4664
|
+
{ key: "minecraft", verification: verifications.find((v) => v.kind === "minecraft") },
|
|
4665
|
+
{ key: "fabric", verification: verifications.find((v) => v.kind === "fabric") },
|
|
4666
|
+
{ key: "forge", verification: verifications.find((v) => v.kind === "forge") },
|
|
4667
|
+
{ key: "runtime", verification: verifications.find((v) => v.kind === "runtime") }
|
|
4668
|
+
];
|
|
4669
|
+
for (const aspect of aspects) {
|
|
4670
|
+
if (!aspect.verification || aspect.verification.issues.length === 0) continue;
|
|
4671
|
+
const plan = await ctx.kit.repair[aspect.key].plan(target, { from: aspect.verification });
|
|
4672
|
+
if (plan.totalActions === 0) continue;
|
|
4673
|
+
const renderer = new ProgressRenderer({
|
|
4674
|
+
ui: ctx.ui,
|
|
4675
|
+
label: `Repair ${aspect.key}`,
|
|
4676
|
+
totalActions: plan.totalActions,
|
|
4677
|
+
totalBytes: plan.totalBytes
|
|
4678
|
+
});
|
|
4679
|
+
const onEvent = renderer.attach();
|
|
4680
|
+
try {
|
|
4681
|
+
await ctx.kit.repair[aspect.key].run(plan, { onEvent });
|
|
4682
|
+
const summary = renderer.finish();
|
|
4683
|
+
ctx.ui.note(`Repair ${aspect.key} summary`, formatSummary(summary));
|
|
4684
|
+
} catch (error) {
|
|
4685
|
+
renderer.fail(formatUserError(error));
|
|
4686
|
+
throw error;
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
return "completed";
|
|
4690
|
+
} catch (error) {
|
|
4691
|
+
ctx.ui.log("error", formatUserError(error));
|
|
4692
|
+
return "cancelled";
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
// src/cli/scenarios/launch.ts
|
|
4697
|
+
async function scenarioLaunch(ctx) {
|
|
4698
|
+
const target = await pickInstalledTarget(ctx);
|
|
4699
|
+
if (!target) return "cancelled";
|
|
4700
|
+
const usernameOutcome = await ctx.ui.text({
|
|
4701
|
+
message: "Player username",
|
|
4702
|
+
placeholder: "Player",
|
|
4703
|
+
initial: "Player",
|
|
4704
|
+
allowBack: true,
|
|
4705
|
+
validate: (s) => s.trim().length === 0 ? "Username must be non-empty" : void 0
|
|
4706
|
+
});
|
|
4707
|
+
if (usernameOutcome.kind !== "ok") return "cancelled";
|
|
4708
|
+
try {
|
|
4709
|
+
const composition = await ctx.kit.launch.compose(target, {
|
|
4710
|
+
auth: { mode: AuthModes.OFFLINE, username: usernameOutcome.value.trim() }
|
|
4711
|
+
});
|
|
4712
|
+
ctx.ui.note(
|
|
4713
|
+
"Launch summary",
|
|
4714
|
+
[
|
|
4715
|
+
`Java: ${composition.javaPath}`,
|
|
4716
|
+
`Main class: ${composition.mainClass}`,
|
|
4717
|
+
`Classpath: ${composition.classpath.length} entries`,
|
|
4718
|
+
`Natives: ${composition.nativesDirectory}`,
|
|
4719
|
+
`Working: ${composition.workingDirectory}`
|
|
4720
|
+
].join("\n")
|
|
4721
|
+
);
|
|
4722
|
+
const ok = await ctx.ui.confirm({ message: "Spawn Minecraft now?", initial: true });
|
|
4723
|
+
if (ok.kind !== "ok" || !ok.value) return "cancelled";
|
|
4724
|
+
const session = ctx.kit.launch.run(composition, {
|
|
4725
|
+
onEvent: (event) => {
|
|
4726
|
+
if (event.type === "launch:stdout" || event.type === "launch:stderr") {
|
|
4727
|
+
ctx.ui.log(event.type === "launch:stderr" ? "warn" : "info", event.line);
|
|
4728
|
+
}
|
|
4729
|
+
}
|
|
4730
|
+
});
|
|
4731
|
+
ctx.ui.log("info", `Started PID ${session.pid}. Waiting for exit\u2026`);
|
|
4732
|
+
await session.exited;
|
|
4733
|
+
ctx.ui.log("success", "Game exited.");
|
|
4734
|
+
return "completed";
|
|
4735
|
+
} catch (error) {
|
|
4736
|
+
ctx.ui.log("error", formatUserError(error));
|
|
4737
|
+
return "cancelled";
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
// src/cli/scenarios/inspect.ts
|
|
4742
|
+
async function scenarioInspect(ctx) {
|
|
4743
|
+
try {
|
|
4744
|
+
const list = await ctx.kit.targets.list({ rootDir: ctx.rootDir });
|
|
4745
|
+
if (list.length === 0) {
|
|
4746
|
+
ctx.ui.log("warn", "No installations found. Install one first.");
|
|
4747
|
+
return "completed";
|
|
4748
|
+
}
|
|
4749
|
+
const choice = await ctx.ui.select({
|
|
4750
|
+
message: "Pick an installation to inspect",
|
|
4751
|
+
options: list.map((entry2) => ({
|
|
4752
|
+
label: entry2.id,
|
|
4753
|
+
value: entry2.id,
|
|
4754
|
+
hint: entry2.minecraftVersions.join(", ") || "no versions"
|
|
4755
|
+
})),
|
|
4756
|
+
allowCancel: true
|
|
4757
|
+
});
|
|
4758
|
+
if (choice.kind !== "ok") return "cancelled";
|
|
4759
|
+
const entry = list.find((e) => e.id === choice.value);
|
|
4760
|
+
if (!entry) return "cancelled";
|
|
4761
|
+
ctx.ui.note(`Inspect: ${entry.id}`, formatDetailed(entry));
|
|
4762
|
+
return "completed";
|
|
4763
|
+
} catch (error) {
|
|
4764
|
+
ctx.ui.log("error", formatUserError(error));
|
|
4765
|
+
return "cancelled";
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
|
|
4769
|
+
// src/cli/ui.ts
|
|
4770
|
+
var BACK = /* @__PURE__ */ Symbol("mckit:back");
|
|
4771
|
+
var CANCEL = /* @__PURE__ */ Symbol("mckit:cancel");
|
|
4772
|
+
async function createClackUi() {
|
|
4773
|
+
const clack = await import('@clack/prompts');
|
|
4774
|
+
return buildUi(clack);
|
|
4775
|
+
}
|
|
4776
|
+
var MAX_VISIBLE_OPTIONS = 50;
|
|
4777
|
+
var DEFAULT_SEARCH_THRESHOLD = 30;
|
|
4778
|
+
function buildUi(clack) {
|
|
4779
|
+
return {
|
|
4780
|
+
intro: (m) => clack.intro(m),
|
|
4781
|
+
outro: (m) => clack.outro(m),
|
|
4782
|
+
write: (m) => process.stdout.write(`${m}
|
|
4783
|
+
`),
|
|
4784
|
+
note: (title, body) => clack.note(body, title),
|
|
4785
|
+
log: (level, message) => clack.log[level](message),
|
|
4786
|
+
text: async (input) => {
|
|
4787
|
+
const value = await clack.text({
|
|
4788
|
+
message: input.message,
|
|
4789
|
+
...input.placeholder !== void 0 ? { placeholder: input.placeholder } : {},
|
|
4790
|
+
...input.initial !== void 0 ? { initialValue: input.initial } : {},
|
|
4791
|
+
...input.validate !== void 0 ? { validate: input.validate } : {}
|
|
4792
|
+
});
|
|
4793
|
+
if (clack.isCancel(value)) return { kind: "cancel" };
|
|
4794
|
+
return { kind: "ok", value: typeof value === "string" ? value : "" };
|
|
4795
|
+
},
|
|
4796
|
+
select: async (input) => runSelect(clack, input),
|
|
4797
|
+
searchableSelect: async (input) => searchableSelect(clack, input),
|
|
4798
|
+
confirm: async (input) => {
|
|
4799
|
+
const value = await clack.confirm({
|
|
4800
|
+
message: input.message,
|
|
4801
|
+
...input.initial !== void 0 ? { initialValue: input.initial } : {}
|
|
4802
|
+
});
|
|
4803
|
+
if (clack.isCancel(value)) return { kind: "cancel" };
|
|
4804
|
+
return { kind: "ok", value };
|
|
4805
|
+
},
|
|
4806
|
+
// Bypass clack.spinner() for progress: clack's spinner sometimes prints a fresh line
|
|
4807
|
+
// per update on Windows / older versions, defeating in-place rendering. Our own
|
|
4808
|
+
// {@link createInPlaceSpinner} writes raw ANSI escapes to stdout so updates always
|
|
4809
|
+
// overwrite the previous line.
|
|
4810
|
+
spinner: () => createInPlaceSpinner()
|
|
4811
|
+
};
|
|
4812
|
+
}
|
|
4813
|
+
var DEFAULT_OUT = {
|
|
4814
|
+
write(chunk) {
|
|
4815
|
+
process.stdout.write(chunk);
|
|
4816
|
+
},
|
|
4817
|
+
get isTTY() {
|
|
4818
|
+
return process.stdout.isTTY === true;
|
|
4819
|
+
}
|
|
4820
|
+
};
|
|
4821
|
+
function createInPlaceSpinner(input = {}) {
|
|
4822
|
+
const out = input.out ?? DEFAULT_OUT;
|
|
4823
|
+
let started = false;
|
|
4824
|
+
let lastLine = "";
|
|
4825
|
+
return {
|
|
4826
|
+
start(message) {
|
|
4827
|
+
if (started) {
|
|
4828
|
+
if (message !== lastLine) {
|
|
4829
|
+
lastLine = message;
|
|
4830
|
+
if (out.isTTY) {
|
|
4831
|
+
out.write(`\r\x1B[2K${message}`);
|
|
4832
|
+
} else {
|
|
4833
|
+
out.write(`${message}
|
|
4834
|
+
`);
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
return;
|
|
4838
|
+
}
|
|
4839
|
+
started = true;
|
|
4840
|
+
lastLine = message;
|
|
4841
|
+
if (out.isTTY) {
|
|
4842
|
+
out.write(message);
|
|
4843
|
+
} else {
|
|
4844
|
+
out.write(`${message}
|
|
4845
|
+
`);
|
|
4846
|
+
}
|
|
4847
|
+
},
|
|
4848
|
+
message(message) {
|
|
4849
|
+
if (!started) return;
|
|
4850
|
+
if (message === lastLine) return;
|
|
4851
|
+
lastLine = message;
|
|
4852
|
+
if (out.isTTY) {
|
|
4853
|
+
out.write(`\r\x1B[2K${message}`);
|
|
4854
|
+
}
|
|
4855
|
+
},
|
|
4856
|
+
stop(message) {
|
|
4857
|
+
if (!started) {
|
|
4858
|
+
if (message !== void 0) {
|
|
4859
|
+
out.write(`${message}
|
|
4860
|
+
`);
|
|
4861
|
+
}
|
|
4862
|
+
return;
|
|
4863
|
+
}
|
|
4864
|
+
const finalText = message ?? lastLine;
|
|
4865
|
+
if (out.isTTY) {
|
|
4866
|
+
out.write(`\r\x1B[2K${finalText}
|
|
4867
|
+
`);
|
|
4868
|
+
} else {
|
|
4869
|
+
out.write(`${finalText}
|
|
4870
|
+
`);
|
|
4871
|
+
}
|
|
4872
|
+
started = false;
|
|
4873
|
+
lastLine = "";
|
|
4874
|
+
}
|
|
4875
|
+
};
|
|
4876
|
+
}
|
|
4877
|
+
async function runSelect(clack, input) {
|
|
4878
|
+
const augmentedOptions = input.options.map(
|
|
4879
|
+
(option) => {
|
|
4880
|
+
const opt = {
|
|
4881
|
+
label: option.label,
|
|
4882
|
+
value: option.value
|
|
4883
|
+
};
|
|
4884
|
+
if (option.hint !== void 0) opt.hint = option.hint;
|
|
4885
|
+
return opt;
|
|
4886
|
+
}
|
|
4887
|
+
);
|
|
4888
|
+
if (input.allowBack === true) {
|
|
4889
|
+
augmentedOptions.push({ label: "\u2190 Back", value: BACK });
|
|
4890
|
+
}
|
|
4891
|
+
if (input.allowCancel === true) {
|
|
4892
|
+
augmentedOptions.push({ label: "\u2715 Cancel", value: CANCEL });
|
|
4893
|
+
}
|
|
4894
|
+
const result = await clack.select({
|
|
4895
|
+
message: input.message,
|
|
4896
|
+
options: augmentedOptions,
|
|
4897
|
+
maxItems: 12,
|
|
4898
|
+
...input.initialValue !== void 0 ? { initialValue: input.initialValue } : {}
|
|
4899
|
+
});
|
|
4900
|
+
if (clack.isCancel(result)) return { kind: "cancel" };
|
|
4901
|
+
if (result === BACK) return { kind: "back" };
|
|
4902
|
+
if (result === CANCEL) return { kind: "cancel" };
|
|
4903
|
+
return { kind: "ok", value: result };
|
|
4904
|
+
}
|
|
4905
|
+
async function searchableSelect(clack, input) {
|
|
4906
|
+
const threshold = input.searchThreshold ?? DEFAULT_SEARCH_THRESHOLD;
|
|
4907
|
+
if (input.options.length <= threshold) {
|
|
4908
|
+
return runSelect(clack, input);
|
|
4909
|
+
}
|
|
4910
|
+
const trimmed = input.options.slice(0, MAX_VISIBLE_OPTIONS);
|
|
4911
|
+
const truncatedHint = input.options.length > MAX_VISIBLE_OPTIONS ? ` (top ${MAX_VISIBLE_OPTIONS} of ${input.options.length})` : "";
|
|
4912
|
+
return runSelect(clack, {
|
|
4913
|
+
message: `${input.message}${truncatedHint}`,
|
|
4914
|
+
options: trimmed,
|
|
4915
|
+
...input.allowBack === true ? { allowBack: true } : {},
|
|
4916
|
+
...input.allowCancel === true ? { allowCancel: true } : {}
|
|
4917
|
+
});
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
// src/cli/main.ts
|
|
4921
|
+
var SCENARIO_KEYS = {
|
|
4922
|
+
INSTALL_MC: "install-minecraft",
|
|
4923
|
+
INSTALL_RUNTIME: "install-runtime",
|
|
4924
|
+
VERIFY: "verify",
|
|
4925
|
+
REPAIR: "repair",
|
|
4926
|
+
LAUNCH: "launch",
|
|
4927
|
+
INSPECT: "inspect",
|
|
4928
|
+
EXIT: "exit"
|
|
4929
|
+
};
|
|
4930
|
+
async function runCli(input) {
|
|
4931
|
+
if (input.args.includes("--help") || input.args.includes("-h")) {
|
|
4932
|
+
input.ui.note(
|
|
4933
|
+
"mckit \u2014 minecraft-kit CLI",
|
|
4934
|
+
"Run with no arguments for the interactive menu.\n--version, --help, --debug"
|
|
4935
|
+
);
|
|
4936
|
+
return 0;
|
|
4937
|
+
}
|
|
4938
|
+
if (input.args.includes("--version") || input.args.includes("-v")) {
|
|
4939
|
+
input.ui.log("info", "0.1.0");
|
|
4940
|
+
return 0;
|
|
4941
|
+
}
|
|
4942
|
+
const debug = input.args.includes("--debug");
|
|
4943
|
+
const kit = input.kit ?? new MinecraftKit();
|
|
4944
|
+
const ctx = { kit, ui: input.ui, rootDir: input.rootDir };
|
|
4945
|
+
input.ui.intro("mckit \u2014 Minecraft launcher kit");
|
|
4946
|
+
while (true) {
|
|
4947
|
+
const choice = await input.ui.select({
|
|
4948
|
+
message: "What would you like to do?",
|
|
4949
|
+
options: MAIN_MENU
|
|
4950
|
+
});
|
|
4951
|
+
if (choice.kind !== "ok" || choice.value === SCENARIO_KEYS.EXIT) {
|
|
4952
|
+
input.ui.outro("Goodbye.");
|
|
4953
|
+
return 0;
|
|
4954
|
+
}
|
|
4955
|
+
try {
|
|
4956
|
+
const outcome = await dispatch(choice.value, ctx);
|
|
4957
|
+
if (outcome === "cancelled") {
|
|
4958
|
+
input.ui.log("info", "Operation cancelled.");
|
|
4959
|
+
}
|
|
4960
|
+
} catch (error) {
|
|
4961
|
+
if (debug) {
|
|
4962
|
+
input.ui.log("error", `${error instanceof Error ? error.stack : String(error)}`);
|
|
4963
|
+
} else {
|
|
4964
|
+
input.ui.log("error", formatUserError(error));
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
}
|
|
4969
|
+
var MAIN_MENU = [
|
|
4970
|
+
{ label: "Install Minecraft", value: SCENARIO_KEYS.INSTALL_MC, hint: "Vanilla / Fabric / Forge" },
|
|
4971
|
+
{ label: "Install Java/runtime", value: SCENARIO_KEYS.INSTALL_RUNTIME },
|
|
4972
|
+
{ label: "Verify installation", value: SCENARIO_KEYS.VERIFY },
|
|
4973
|
+
{ label: "Repair installation", value: SCENARIO_KEYS.REPAIR },
|
|
4974
|
+
{ label: "Launch Minecraft", value: SCENARIO_KEYS.LAUNCH },
|
|
4975
|
+
{ label: "Inspect installation", value: SCENARIO_KEYS.INSPECT },
|
|
4976
|
+
{ label: "Exit", value: SCENARIO_KEYS.EXIT }
|
|
4977
|
+
];
|
|
4978
|
+
async function dispatch(choice, ctx) {
|
|
4979
|
+
switch (choice) {
|
|
4980
|
+
case SCENARIO_KEYS.INSTALL_MC:
|
|
4981
|
+
return scenarioInstallMinecraft(ctx);
|
|
4982
|
+
case SCENARIO_KEYS.INSTALL_RUNTIME:
|
|
4983
|
+
return scenarioInstallRuntime(ctx);
|
|
4984
|
+
case SCENARIO_KEYS.VERIFY:
|
|
4985
|
+
return scenarioVerify(ctx);
|
|
4986
|
+
case SCENARIO_KEYS.REPAIR:
|
|
4987
|
+
return scenarioRepair(ctx);
|
|
4988
|
+
case SCENARIO_KEYS.LAUNCH:
|
|
4989
|
+
return scenarioLaunch(ctx);
|
|
4990
|
+
case SCENARIO_KEYS.INSPECT:
|
|
4991
|
+
return scenarioInspect(ctx);
|
|
4992
|
+
default:
|
|
4993
|
+
ctx.ui.log("warn", `Unknown action: ${choice}`);
|
|
4994
|
+
return "cancelled";
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
async function bin() {
|
|
4998
|
+
const ui = await createClackUi();
|
|
4999
|
+
const code = await runCli({
|
|
5000
|
+
args: process2.argv.slice(2),
|
|
5001
|
+
ui,
|
|
5002
|
+
rootDir: process2.cwd()
|
|
5003
|
+
});
|
|
5004
|
+
process2.exit(code);
|
|
5005
|
+
}
|
|
5006
|
+
|
|
5007
|
+
// src/cli/index.ts
|
|
5008
|
+
void bin();
|
|
5009
|
+
//# sourceMappingURL=index.js.map
|
|
5010
|
+
//# sourceMappingURL=index.js.map
|