@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.
@@ -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