@loontail/minecraft-kit 0.5.0 → 0.6.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.
@@ -10,7 +10,7 @@ var yauzl = require('yauzl');
10
10
  var crypto2 = require('crypto');
11
11
  var fs = require('fs/promises');
12
12
  var stream = require('stream');
13
- var pLimit = require('p-limit');
13
+ var async_hooks = require('async_hooks');
14
14
  var buffer = require('buffer');
15
15
  var child_process = require('child_process');
16
16
 
@@ -22,7 +22,6 @@ var path__default = /*#__PURE__*/_interopDefault(path);
22
22
  var yauzl__default = /*#__PURE__*/_interopDefault(yauzl);
23
23
  var crypto2__default = /*#__PURE__*/_interopDefault(crypto2);
24
24
  var fs__default = /*#__PURE__*/_interopDefault(fs);
25
- var pLimit__default = /*#__PURE__*/_interopDefault(pLimit);
26
25
 
27
26
  // src/cli/main.ts
28
27
 
@@ -290,9 +289,14 @@ var targetPaths = {
290
289
  // src/types/install.ts
291
290
  var InstallPhases = {
292
291
  PLANNING: "planning",
292
+ DOWNLOADING_CLIENT_JAR: "downloading-client-jar",
293
293
  DOWNLOADING_LIBRARIES: "downloading-libraries",
294
+ DOWNLOADING_ASSET_INDEX: "downloading-asset-index",
295
+ DOWNLOADING_ASSETS: "downloading-assets",
294
296
  EXTRACTING_NATIVES: "extracting-natives",
295
297
  INSTALLING_RUNTIME: "installing-runtime",
298
+ INSTALLING_FABRIC: "installing-fabric",
299
+ INSTALLING_FORGE: "installing-forge",
296
300
  RUNNING_FORGE_PROCESSORS: "running-forge-processors",
297
301
  WRITING_FILES: "writing-files",
298
302
  COMPLETED: "completed"
@@ -1089,6 +1093,12 @@ async function downloadFile(http, input) {
1089
1093
  const sourceIterable = response.stream();
1090
1094
  const counting = (async function* () {
1091
1095
  for await (const chunk of sourceIterable) {
1096
+ if (input.pauseController?.paused) {
1097
+ await input.pauseController.waitWhilePaused();
1098
+ }
1099
+ if (input.signal?.aborted) {
1100
+ throw new MinecraftKitError("LAUNCH_ABORTED", "Download aborted by signal");
1101
+ }
1092
1102
  bytesDownloaded += chunk.byteLength;
1093
1103
  hash.update(chunk);
1094
1104
  input.onEvent?.({
@@ -1417,7 +1427,8 @@ function substituteToken(raw, tokens) {
1417
1427
  });
1418
1428
  }
1419
1429
  function stripLiteralPrefix(value) {
1420
- return value.startsWith("'") ? value.slice(1) : value;
1430
+ const stripped = value.startsWith("'") ? value.slice(1) : value;
1431
+ return stripped.endsWith("'") ? stripped.slice(0, -1) : stripped;
1421
1432
  }
1422
1433
  async function planRuntimeDownloads(input) {
1423
1434
  const manifest = await fetchJson(input.http, input.cache, {
@@ -1540,6 +1551,123 @@ async function planInstall(input) {
1540
1551
  totalBytes
1541
1552
  };
1542
1553
  }
1554
+
1555
+ // node_modules/yocto-queue/index.js
1556
+ var Node = class {
1557
+ value;
1558
+ next;
1559
+ constructor(value) {
1560
+ this.value = value;
1561
+ }
1562
+ };
1563
+ var Queue = class {
1564
+ #head;
1565
+ #tail;
1566
+ #size;
1567
+ constructor() {
1568
+ this.clear();
1569
+ }
1570
+ enqueue(value) {
1571
+ const node = new Node(value);
1572
+ if (this.#head) {
1573
+ this.#tail.next = node;
1574
+ this.#tail = node;
1575
+ } else {
1576
+ this.#head = node;
1577
+ this.#tail = node;
1578
+ }
1579
+ this.#size++;
1580
+ }
1581
+ dequeue() {
1582
+ const current = this.#head;
1583
+ if (!current) {
1584
+ return;
1585
+ }
1586
+ this.#head = this.#head.next;
1587
+ this.#size--;
1588
+ if (!this.#head) {
1589
+ this.#tail = void 0;
1590
+ }
1591
+ return current.value;
1592
+ }
1593
+ peek() {
1594
+ if (!this.#head) {
1595
+ return;
1596
+ }
1597
+ return this.#head.value;
1598
+ }
1599
+ clear() {
1600
+ this.#head = void 0;
1601
+ this.#tail = void 0;
1602
+ this.#size = 0;
1603
+ }
1604
+ get size() {
1605
+ return this.#size;
1606
+ }
1607
+ *[Symbol.iterator]() {
1608
+ let current = this.#head;
1609
+ while (current) {
1610
+ yield current.value;
1611
+ current = current.next;
1612
+ }
1613
+ }
1614
+ *drain() {
1615
+ while (this.#head) {
1616
+ yield this.dequeue();
1617
+ }
1618
+ }
1619
+ };
1620
+ function pLimit(concurrency) {
1621
+ if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
1622
+ throw new TypeError("Expected `concurrency` to be a number from 1 and up");
1623
+ }
1624
+ const queue = new Queue();
1625
+ let activeCount = 0;
1626
+ const next = () => {
1627
+ activeCount--;
1628
+ if (queue.size > 0) {
1629
+ queue.dequeue()();
1630
+ }
1631
+ };
1632
+ const run = async (function_, resolve, arguments_) => {
1633
+ activeCount++;
1634
+ const result = (async () => function_(...arguments_))();
1635
+ resolve(result);
1636
+ try {
1637
+ await result;
1638
+ } catch {
1639
+ }
1640
+ next();
1641
+ };
1642
+ const enqueue = (function_, resolve, arguments_) => {
1643
+ queue.enqueue(
1644
+ async_hooks.AsyncResource.bind(run.bind(void 0, function_, resolve, arguments_))
1645
+ );
1646
+ (async () => {
1647
+ await Promise.resolve();
1648
+ if (activeCount < concurrency && queue.size > 0) {
1649
+ queue.dequeue()();
1650
+ }
1651
+ })();
1652
+ };
1653
+ const generator = (function_, ...arguments_) => new Promise((resolve) => {
1654
+ enqueue(function_, resolve, arguments_);
1655
+ });
1656
+ Object.defineProperties(generator, {
1657
+ activeCount: {
1658
+ get: () => activeCount
1659
+ },
1660
+ pendingCount: {
1661
+ get: () => queue.size
1662
+ },
1663
+ clearQueue: {
1664
+ value() {
1665
+ queue.clear();
1666
+ }
1667
+ }
1668
+ });
1669
+ return generator;
1670
+ }
1543
1671
  async function materializeRuntimeExtras(input) {
1544
1672
  const root = targetPaths.runtimeRoot(
1545
1673
  input.directory,
@@ -1604,6 +1732,16 @@ function errorMessage(error) {
1604
1732
  }
1605
1733
 
1606
1734
  // src/install/runner.ts
1735
+ var DOWNLOAD_GROUPS = [
1736
+ { categories: ["runtime-file"], phase: InstallPhases.INSTALLING_RUNTIME },
1737
+ { categories: ["client-jar"], phase: InstallPhases.DOWNLOADING_CLIENT_JAR },
1738
+ { categories: ["library"], phase: InstallPhases.DOWNLOADING_LIBRARIES },
1739
+ { categories: ["asset-index"], phase: InstallPhases.DOWNLOADING_ASSET_INDEX },
1740
+ { categories: ["asset"], phase: InstallPhases.DOWNLOADING_ASSETS },
1741
+ { categories: ["logging-config"], phase: InstallPhases.WRITING_FILES },
1742
+ { categories: ["fabric-library"], phase: InstallPhases.INSTALLING_FABRIC },
1743
+ { categories: ["forge-installer", "forge-library"], phase: InstallPhases.INSTALLING_FORGE }
1744
+ ];
1607
1745
  async function runInstall(input) {
1608
1746
  const startedAt = Date.now();
1609
1747
  let bytesDownloaded = 0;
@@ -1616,50 +1754,89 @@ async function runInstall(input) {
1616
1754
  onEvent?.({ type: "install:phase-changed", phase, previous: currentPhase });
1617
1755
  currentPhase = phase;
1618
1756
  };
1619
- const downloads = input.plan.actions.filter(isDownload);
1757
+ const checkpoint = async () => {
1758
+ if (input.signal?.aborted) {
1759
+ throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1760
+ }
1761
+ await input.pauseController?.waitWhilePaused();
1762
+ if (input.signal?.aborted) {
1763
+ throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1764
+ }
1765
+ };
1766
+ const categoryFilter = input.actionCategories;
1767
+ const downloads = input.plan.actions.filter(isDownload).filter((a) => categoryFilter ? categoryFilter.has(a.category) : true);
1620
1768
  const natives = input.plan.actions.filter(isNative);
1621
1769
  const writeActions = input.plan.actions.filter(isWrite);
1622
1770
  const processors = input.plan.actions.filter(isProcessor);
1623
1771
  enterPhase(InstallPhases.PLANNING);
1624
- enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
1625
- const limit = pLimit__default.default(input.concurrency ?? DOWNLOAD_CONCURRENCY);
1626
- await Promise.all(
1627
- downloads.map(
1628
- (action) => limit(async () => {
1629
- if (input.signal?.aborted) {
1630
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1631
- }
1632
- const result = await downloadFile(input.http, {
1633
- url: action.url,
1634
- target: action.target,
1635
- ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1636
- ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1637
- ...action.category !== void 0 ? { category: action.category } : {},
1638
- ...input.signal !== void 0 ? { signal: input.signal } : {},
1639
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
1640
- });
1641
- bytesDownloaded += result.bytesDownloaded;
1642
- if (result.skipped) actionsSkipped++;
1643
- actionsCompleted++;
1644
- })
1645
- )
1772
+ const limit = pLimit(input.concurrency ?? DOWNLOAD_CONCURRENCY);
1773
+ for (const group of DOWNLOAD_GROUPS) {
1774
+ const groupActions = downloads.filter((action) => group.categories.includes(action.category));
1775
+ if (groupActions.length === 0) continue;
1776
+ await checkpoint();
1777
+ enterPhase(group.phase);
1778
+ await Promise.all(
1779
+ groupActions.map(
1780
+ (action) => limit(async () => {
1781
+ await checkpoint();
1782
+ const result = await downloadFile(input.http, {
1783
+ url: action.url,
1784
+ target: action.target,
1785
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1786
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1787
+ ...action.category !== void 0 ? { category: action.category } : {},
1788
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
1789
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1790
+ ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1791
+ });
1792
+ bytesDownloaded += result.bytesDownloaded;
1793
+ if (result.skipped) actionsSkipped++;
1794
+ actionsCompleted++;
1795
+ })
1796
+ )
1797
+ );
1798
+ }
1799
+ const ungrouped = downloads.filter(
1800
+ (action) => !DOWNLOAD_GROUPS.some((g) => g.categories.includes(action.category))
1646
1801
  );
1802
+ if (ungrouped.length > 0) {
1803
+ await checkpoint();
1804
+ enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
1805
+ await Promise.all(
1806
+ ungrouped.map(
1807
+ (action) => limit(async () => {
1808
+ await checkpoint();
1809
+ const result = await downloadFile(input.http, {
1810
+ url: action.url,
1811
+ target: action.target,
1812
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1813
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1814
+ ...action.category !== void 0 ? { category: action.category } : {},
1815
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
1816
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1817
+ ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1818
+ });
1819
+ bytesDownloaded += result.bytesDownloaded;
1820
+ if (result.skipped) actionsSkipped++;
1821
+ actionsCompleted++;
1822
+ })
1823
+ )
1824
+ );
1825
+ }
1647
1826
  if (writeActions.length > 0) {
1827
+ await checkpoint();
1648
1828
  enterPhase(InstallPhases.WRITING_FILES);
1649
1829
  for (const action of writeActions) {
1650
- if (input.signal?.aborted) {
1651
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1652
- }
1830
+ await checkpoint();
1653
1831
  await atomicWrite(action.path, action.content);
1654
1832
  actionsCompleted++;
1655
1833
  }
1656
1834
  }
1657
1835
  if (natives.length > 0) {
1836
+ await checkpoint();
1658
1837
  enterPhase(InstallPhases.EXTRACTING_NATIVES);
1659
1838
  for (const action of natives) {
1660
- if (input.signal?.aborted) {
1661
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1662
- }
1839
+ await checkpoint();
1663
1840
  const { fileCount } = await extractAllToDir(action.source, action.destination, {
1664
1841
  excludePrefixes: action.exclude
1665
1842
  });
@@ -1673,6 +1850,7 @@ async function runInstall(input) {
1673
1850
  }
1674
1851
  }
1675
1852
  if (input.plan.target.runtime !== void 0) {
1853
+ await checkpoint();
1676
1854
  enterPhase(InstallPhases.INSTALLING_RUNTIME);
1677
1855
  const runtimePlan = await planRuntimeDownloads({
1678
1856
  runtime: input.plan.target.runtime,
@@ -1688,6 +1866,7 @@ async function runInstall(input) {
1688
1866
  });
1689
1867
  }
1690
1868
  if (processors.length > 0) {
1869
+ await checkpoint();
1691
1870
  enterPhase(InstallPhases.RUNNING_FORGE_PROCESSORS);
1692
1871
  if (input.plan.target.loader.type !== Loaders.FORGE) {
1693
1872
  throw new MinecraftKitError(
@@ -1702,9 +1881,7 @@ async function runInstall(input) {
1702
1881
  input.plan.target.runtime.installRoot
1703
1882
  );
1704
1883
  for (const action of processors) {
1705
- if (input.signal?.aborted) {
1706
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1707
- }
1884
+ await checkpoint();
1708
1885
  await runProcessor({
1709
1886
  action,
1710
1887
  javaPath,
@@ -1919,6 +2096,33 @@ function pickArguments(args, context) {
1919
2096
  };
1920
2097
  }
1921
2098
 
2099
+ // src/launch/jvm-compat.ts
2100
+ var FLAG_MIN_JAVA = [
2101
+ { prefix: "--sun-misc-unsafe-memory-access", minJava: 23 },
2102
+ { prefix: "--enable-native-access", minJava: 17 },
2103
+ { prefix: "-XX:+UseCompactObjectHeaders", minJava: 24 },
2104
+ { prefix: "-XX:+UseZGC", minJava: 15 }
2105
+ ];
2106
+ function filterArgsForJava(input) {
2107
+ if (!Number.isFinite(input.javaMajor) || input.javaMajor <= 0) return input.args;
2108
+ const out = [];
2109
+ for (const arg of input.args) {
2110
+ const incompatible = FLAG_MIN_JAVA.find(
2111
+ ({ prefix }) => arg === prefix || arg.startsWith(`${prefix}=`) || arg.startsWith(`${prefix} `)
2112
+ );
2113
+ if (incompatible && input.javaMajor < incompatible.minJava) {
2114
+ input.logger?.log(
2115
+ "warn",
2116
+ `Dropping JVM arg "${arg}" \u2014 requires Java ${incompatible.minJava}, runtime is Java ${input.javaMajor}`,
2117
+ { flag: arg, minJava: incompatible.minJava, runtimeJava: input.javaMajor }
2118
+ );
2119
+ continue;
2120
+ }
2121
+ out.push(arg);
2122
+ }
2123
+ return out;
2124
+ }
2125
+
1922
2126
  // src/launch/placeholders.ts
1923
2127
  function substituteArg(raw, values) {
1924
2128
  return raw.replaceAll(/\$\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
@@ -1958,7 +2162,13 @@ function composeArgs(input) {
1958
2162
  const baseJvm = [...memoryArgs, ...BASE_JVM_ARGS, ...macosArgs];
1959
2163
  const substitutedJvm = substituteArgs(rawJvm, input.placeholderValues);
1960
2164
  const substitutedGame = substituteArgs(rawGame, input.placeholderValues);
1961
- const jvmArgs = [...baseJvm, ...substitutedJvm];
2165
+ const javaMajor = input.target.runtime.majorVersion;
2166
+ const filteredManifestJvm = javaMajor !== void 0 ? filterArgsForJava({
2167
+ args: substitutedJvm,
2168
+ javaMajor,
2169
+ logger: input.logger ?? silentLogger
2170
+ }) : substitutedJvm;
2171
+ const jvmArgs = [...baseJvm, ...filteredManifestJvm];
1962
2172
  if (input.merged.logging?.client?.argument) {
1963
2173
  const logging = input.merged.logging.client;
1964
2174
  const loggingArg = substituteArgs([logging.argument], {
@@ -2088,8 +2298,28 @@ function mergeManifest(parent, child) {
2088
2298
  };
2089
2299
  return merged;
2090
2300
  }
2301
+ function libraryDedupeKey(library) {
2302
+ if (!library.name) return null;
2303
+ try {
2304
+ const coord = parseMavenCoordinate(library.name);
2305
+ const classifier = coord.classifier ? `:${coord.classifier}` : "";
2306
+ return `${coord.group}:${coord.artifact}${classifier}`;
2307
+ } catch {
2308
+ return null;
2309
+ }
2310
+ }
2091
2311
  function mergeLibraries(parent, child) {
2092
- return [...parent, ...child];
2312
+ const byKey = /* @__PURE__ */ new Map();
2313
+ const unkeyed = [];
2314
+ for (const lib of [...parent, ...child]) {
2315
+ const key = libraryDedupeKey(lib);
2316
+ if (key === null) {
2317
+ unkeyed.push(lib);
2318
+ continue;
2319
+ }
2320
+ byKey.set(key, lib);
2321
+ }
2322
+ return [...byKey.values(), ...unkeyed];
2093
2323
  }
2094
2324
  function mergeArguments(parent, child) {
2095
2325
  if (!parent && !child) return void 0;
@@ -2213,7 +2443,8 @@ async function composeLaunch(input) {
2213
2443
  merged: resolved.merged,
2214
2444
  options,
2215
2445
  placeholderValues,
2216
- features
2446
+ features,
2447
+ logger: input.logger ?? silentLogger
2217
2448
  });
2218
2449
  return {
2219
2450
  targetId: target.id,
@@ -2395,546 +2626,212 @@ var VerifyFileCategories = {
2395
2626
  RUNTIME_FILE: "runtime-file",
2396
2627
  LOGGING_CONFIG: "logging-config"
2397
2628
  };
2629
+ async function sha1OfFile(filePath) {
2630
+ const hash = crypto2__default.default.createHash("sha1");
2631
+ await new Promise((resolve, reject) => {
2632
+ const stream = fs$1.createReadStream(filePath);
2633
+ stream.on("data", (chunk) => hash.update(chunk));
2634
+ stream.on("end", resolve);
2635
+ stream.on("error", reject);
2636
+ });
2637
+ return hash.digest("hex");
2638
+ }
2398
2639
 
2399
- // src/repair/helpers.ts
2400
- function asResultArray(from) {
2401
- return Array.isArray(from) ? from : [from];
2640
+ // src/verify/helpers.ts
2641
+ async function runVerification(input, check) {
2642
+ const startedAt = Date.now();
2643
+ const results = [];
2644
+ const record = (result) => {
2645
+ results.push(result);
2646
+ input.onEvent?.({ type: "verify:file-checked", file: result });
2647
+ };
2648
+ await check(record);
2649
+ return {
2650
+ targetId: input.targetId,
2651
+ kind: input.kind,
2652
+ isValid: results.every((r) => r.status === VerifyFileStatuses.OK),
2653
+ issues: results.filter((r) => r.status !== VerifyFileStatuses.OK),
2654
+ checkedFiles: results.length,
2655
+ durationMs: Date.now() - startedAt
2656
+ };
2402
2657
  }
2403
- function buildIssueIndex(from) {
2404
- const map = /* @__PURE__ */ new Map();
2405
- for (const v of asResultArray(from)) {
2406
- for (const issue of v.issues) {
2407
- const set = map.get(issue.path);
2408
- if (set) set.add(issue.category);
2409
- else map.set(issue.path, /* @__PURE__ */ new Set([issue.category]));
2658
+ async function verifyHashedFile(input) {
2659
+ if (!await fileExists(input.path)) {
2660
+ return {
2661
+ path: input.path,
2662
+ category: input.category,
2663
+ status: VerifyFileStatuses.MISSING,
2664
+ ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2665
+ ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2666
+ ...input.url !== void 0 ? { url: input.url } : {}
2667
+ };
2668
+ }
2669
+ if (input.expectedSize !== void 0) {
2670
+ const size = await fileSize(input.path);
2671
+ if (size !== input.expectedSize) {
2672
+ return {
2673
+ path: input.path,
2674
+ category: input.category,
2675
+ status: VerifyFileStatuses.WRONG_SIZE,
2676
+ expectedSize: input.expectedSize,
2677
+ actualSize: size,
2678
+ ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2679
+ ...input.url !== void 0 ? { url: input.url } : {}
2680
+ };
2681
+ }
2682
+ }
2683
+ if (input.expectedSha1 !== void 0) {
2684
+ const actualSha1 = await sha1OfFile(input.path);
2685
+ if (actualSha1 !== input.expectedSha1) {
2686
+ return {
2687
+ path: input.path,
2688
+ category: input.category,
2689
+ status: VerifyFileStatuses.CORRUPT,
2690
+ expectedSha1: input.expectedSha1,
2691
+ actualSha1,
2692
+ ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2693
+ ...input.url !== void 0 ? { url: input.url } : {}
2694
+ };
2410
2695
  }
2411
2696
  }
2412
2697
  return {
2413
- has: (path16) => map.has(path16),
2414
- hasNonNative: (path16) => {
2415
- const cats = map.get(path16);
2416
- if (!cats) return false;
2417
- for (const c of cats) {
2418
- if (c !== VerifyFileCategories.NATIVE) return true;
2419
- }
2420
- return false;
2421
- },
2422
- categoriesAt: (path16) => map.get(path16) ?? /* @__PURE__ */ new Set()
2698
+ path: input.path,
2699
+ category: input.category,
2700
+ status: VerifyFileStatuses.OK,
2701
+ ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2702
+ ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2703
+ ...input.url !== void 0 ? { url: input.url } : {}
2423
2704
  };
2424
2705
  }
2425
- function sumDownloadBytes(actions) {
2426
- return actions.reduce((sum, action) => {
2427
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2428
- return sum + (action.expectedSize ?? 0);
2429
- }
2430
- return sum;
2431
- }, 0);
2432
- }
2433
- function buildRepairPlan(target, actions) {
2706
+ async function verifyExistence(input) {
2707
+ if (await fileExists(input.path)) {
2708
+ return {
2709
+ path: input.path,
2710
+ category: input.category,
2711
+ status: VerifyFileStatuses.OK,
2712
+ ...input.url !== void 0 ? { url: input.url } : {}
2713
+ };
2714
+ }
2434
2715
  return {
2435
- targetId: target.id,
2436
- directory: target.directory,
2437
- target,
2438
- actions,
2439
- totalActions: actions.length,
2440
- totalBytes: sumDownloadBytes(actions)
2716
+ path: input.path,
2717
+ category: input.category,
2718
+ status: VerifyFileStatuses.MISSING,
2719
+ ...input.url !== void 0 ? { url: input.url } : {}
2441
2720
  };
2442
2721
  }
2443
- async function planAspectRepair(input, aspectFilter, postprocess) {
2444
- const installPlan = await planInstall({
2445
- target: input.target,
2446
- http: input.http,
2447
- cache: input.cache,
2448
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2449
- });
2450
- const issues = buildIssueIndex(input.from);
2451
- const actions = selectRepairActions({
2452
- target: input.target,
2453
- installPlan,
2454
- issues,
2455
- aspectFilter
2456
- });
2457
- postprocess?.({ actions, installPlan, issues });
2458
- return buildRepairPlan(input.target, actions);
2459
- }
2460
- function selectRepairActions(input) {
2461
- const matching = [];
2462
- for (const action of input.installPlan.actions) {
2463
- if (!input.aspectFilter(action)) continue;
2464
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2465
- if (input.issues.hasNonNative(action.target)) {
2466
- matching.push(action);
2467
- }
2468
- } else if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2469
- if (input.issues.has(action.path)) {
2470
- matching.push(action);
2471
- }
2472
- } else if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
2473
- if (input.issues.has(action.source)) {
2474
- matching.push(action);
2475
- }
2476
- } else {
2477
- matching.push(action);
2722
+ async function findForgeVersionJsonPath(directory, minecraftVersion) {
2723
+ const versionsDir = targetPaths.versionsDir(directory);
2724
+ const dirs = await listChildDirectories(versionsDir);
2725
+ for (const id of dirs) {
2726
+ if (!id.startsWith(`${minecraftVersion}-forge-`)) continue;
2727
+ const jsonPath = targetPaths.versionJson(directory, id);
2728
+ if (!await fileExists(jsonPath)) {
2729
+ return jsonPath;
2478
2730
  }
2731
+ const parsed = await tryParseInheritsFrom(jsonPath);
2732
+ if (parsed === minecraftVersion) return jsonPath;
2733
+ }
2734
+ return null;
2735
+ }
2736
+ async function tryParseInheritsFrom(jsonPath) {
2737
+ try {
2738
+ const parsed = JSON.parse(await readText(jsonPath));
2739
+ return parsed.inheritsFrom;
2740
+ } catch {
2741
+ return void 0;
2479
2742
  }
2480
- return matching;
2481
2743
  }
2482
2744
 
2483
- // src/repair/fabric.ts
2484
- async function planFabricRepair(input) {
2745
+ // src/verify/fabric.ts
2746
+ async function verifyFabric(input) {
2485
2747
  if (input.target.loader.type !== Loaders.FABRIC) {
2486
2748
  throw new MinecraftKitError(
2487
2749
  "INVALID_INPUT",
2488
- `repair.fabric requires a Fabric target (got ${input.target.loader.type})`
2750
+ `verify.fabric requires a Fabric target (got ${input.target.loader.type})`
2489
2751
  );
2490
2752
  }
2491
- const fabricJsonPath = targetPaths.versionJson(
2492
- input.target.directory,
2493
- input.target.loader.profile.id
2494
- );
2495
- return planAspectRepair(input, (action) => {
2496
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2497
- return action.category === "fabric-library";
2498
- }
2499
- if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2500
- return action.path === fabricJsonPath;
2501
- }
2502
- return false;
2503
- });
2504
- }
2505
-
2506
- // src/repair/forge.ts
2507
- var FORGE_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
2508
- "forge-library",
2509
- "forge-installer"
2510
- ]);
2511
- async function planForgeRepair(input) {
2512
- if (input.target.loader.type !== Loaders.FORGE) {
2513
- throw new MinecraftKitError(
2514
- "INVALID_INPUT",
2515
- `repair.forge requires a Forge target (got ${input.target.loader.type})`
2516
- );
2517
- }
2518
- const forgeJsonPath = targetPaths.versionJson(
2519
- input.target.directory,
2520
- input.target.loader.fullVersion
2521
- );
2522
- return planAspectRepair(
2523
- input,
2524
- (action) => {
2525
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2526
- return FORGE_DOWNLOAD_CATEGORIES.has(action.category);
2527
- }
2528
- if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2529
- return action.path === forgeJsonPath;
2530
- }
2531
- return false;
2532
- },
2533
- ({ actions, installPlan, issues }) => {
2534
- if (!issues.has(forgeJsonPath)) return;
2535
- const alreadyIncluded = new Set(
2536
- actions.filter((a) => a.kind === InstallActionKinds.DOWNLOAD_FILE).map((a) => a.target)
2537
- );
2538
- for (const action of installPlan.actions) {
2539
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "forge-library" && !alreadyIncluded.has(action.target)) {
2540
- actions.push(action);
2541
- } else if (action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR) {
2542
- actions.push(action);
2543
- }
2544
- }
2545
- }
2546
- );
2547
- }
2548
-
2549
- // src/repair/minecraft.ts
2550
- var MINECRAFT_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
2551
- "client-jar",
2552
- "library",
2553
- "asset-index",
2554
- "asset",
2555
- "logging-config"
2556
- ]);
2557
- async function planMinecraftRepair(input) {
2558
- const vanillaJsonPath = targetPaths.versionJson(
2559
- input.target.directory,
2560
- input.target.minecraft.version
2561
- );
2562
- return planAspectRepair(input, (action) => {
2563
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2564
- return MINECRAFT_DOWNLOAD_CATEGORIES.has(action.category);
2565
- }
2566
- if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2567
- return action.path === vanillaJsonPath;
2568
- }
2569
- if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
2570
- return true;
2571
- }
2572
- return false;
2573
- });
2574
- }
2575
-
2576
- // src/repair/runner.ts
2577
- async function runRepair(input) {
2578
- const report = await runInstall({
2579
- plan: {
2580
- ...input.plan,
2581
- totalActions: input.plan.actions.length,
2582
- totalBytes: input.plan.totalBytes
2753
+ const loader = input.target.loader;
2754
+ return runVerification(
2755
+ {
2756
+ targetId: input.target.id,
2757
+ kind: VerificationKinds.FABRIC,
2758
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2583
2759
  },
2584
- http: input.http,
2585
- cache: input.cache,
2586
- spawner: input.spawner,
2587
- ...input.signal !== void 0 ? { signal: input.signal } : {},
2588
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2589
- });
2590
- return {
2591
- targetId: report.targetId,
2592
- bytesDownloaded: report.bytesDownloaded,
2593
- actionsCompleted: report.actionsCompleted,
2594
- durationMs: report.durationMs
2595
- };
2596
- }
2597
-
2598
- // src/repair/runtime.ts
2599
- async function planRuntimeRepair(input) {
2600
- return planAspectRepair(
2601
- input,
2602
- (action) => action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "runtime-file"
2603
- );
2604
- }
2605
-
2606
- // src/types/runtime.ts
2607
- var RuntimeComponents = {
2608
- JRE_LEGACY: "jre-legacy"};
2609
- var RuntimePreference = {
2610
- /** Component declared by the Minecraft manifest. */
2611
- RECOMMENDED: "recommended",
2612
- /** Newest component available for the platform. */
2613
- LATEST: "latest"
2614
- };
2615
-
2616
- // src/targets/index.ts
2617
- var TargetsApi = class {
2618
- constructor(ctx) {
2619
- this.ctx = ctx;
2620
- }
2621
- ctx;
2622
- /** The detected host system used by `resolve()` when no `system` is supplied. */
2623
- get system() {
2624
- return this.ctx.system;
2625
- }
2626
- /** Build a {@link Target} from already-resolved components. */
2627
- create(input) {
2628
- if (!input.id) {
2629
- throw new MinecraftKitError("INVALID_INPUT", "Target id must be non-empty");
2630
- }
2631
- if (!input.directory) {
2632
- throw new MinecraftKitError("INVALID_INPUT", "Target directory must be non-empty");
2633
- }
2634
- if (input.loader.minecraftVersion !== input.minecraft.version) {
2635
- throw new MinecraftKitError(
2636
- "INVALID_INPUT",
2637
- `Loader Minecraft version (${input.loader.minecraftVersion}) does not match resolved Minecraft (${input.minecraft.version})`,
2638
- {
2639
- context: {
2640
- loaderMinecraft: input.loader.minecraftVersion,
2641
- minecraftVersion: input.minecraft.version
2642
- }
2643
- }
2760
+ async (record) => {
2761
+ record(
2762
+ await verifyExistence({
2763
+ path: targetPaths.versionJson(input.target.directory, loader.profile.id),
2764
+ category: VerifyFileCategories.LOADER_LIBRARY
2765
+ })
2644
2766
  );
2645
- }
2646
- return {
2647
- id: input.id,
2648
- directory: input.directory,
2649
- minecraft: input.minecraft,
2650
- loader: input.loader,
2651
- runtime: input.runtime
2652
- };
2653
- }
2654
- /** Sugar API: resolve every component then assemble a target. */
2655
- async resolve(input) {
2656
- const minecraft = await this.ctx.minecraft.resolve({
2657
- version: input.minecraft.version,
2658
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2659
- });
2660
- const system = input.system ?? this.ctx.system;
2661
- const componentOverride = input.runtime?.component;
2662
- const runtimeComponent = componentOverride ?? minecraft.manifest.javaVersion?.component;
2663
- const resolvedRuntime = await this.ctx.runtime.resolve({
2664
- system,
2665
- ...runtimeComponent !== void 0 ? { component: runtimeComponent } : {},
2666
- preference: input.runtime?.preference ?? RuntimePreference.RECOMMENDED,
2667
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2668
- });
2669
- const runtime = input.runtime?.installRoot !== void 0 ? { ...resolvedRuntime, installRoot: input.runtime.installRoot } : resolvedRuntime;
2670
- let loader;
2671
- if (input.loader.type === Loaders.VANILLA) {
2672
- loader = {
2673
- type: Loaders.VANILLA,
2674
- minecraftVersion: minecraft.version,
2675
- minecraft
2676
- };
2677
- } else if (input.loader.type === Loaders.FABRIC) {
2678
- loader = await this.ctx.fabric.resolve({
2679
- minecraftVersion: minecraft.version,
2680
- ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
2681
- ...input.loader.version !== void 0 ? { loaderVersion: input.loader.version } : {},
2682
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2683
- });
2684
- } else {
2685
- loader = await this.ctx.forge.resolve({
2686
- minecraftVersion: minecraft.version,
2687
- ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
2688
- ...input.loader.version !== void 0 ? { forgeVersion: input.loader.version } : {},
2689
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2767
+ const fabricLibraries = planLibraryDownloads({
2768
+ libraries: loader.profile.libraries,
2769
+ directory: input.target.directory,
2770
+ system: input.target.runtime.system,
2771
+ versionId: input.target.minecraft.version,
2772
+ category: "fabric-library"
2690
2773
  });
2774
+ for (const action of fabricLibraries.downloads) {
2775
+ record(
2776
+ await verifyHashedFile({
2777
+ path: action.target,
2778
+ expectedSha1: action.expectedSha1,
2779
+ expectedSize: action.expectedSize,
2780
+ ...action.url ? { url: action.url } : {},
2781
+ category: VerifyFileCategories.LOADER_LIBRARY
2782
+ })
2783
+ );
2784
+ }
2691
2785
  }
2692
- return this.create({
2693
- id: input.id,
2694
- directory: input.directory,
2695
- minecraft,
2696
- loader,
2697
- runtime
2698
- });
2699
- }
2700
- /** Scan a root directory for Minecraft installations. Returns only what is on disk. */
2701
- async list(input) {
2702
- if (!await dirExists(input.rootDir)) return [];
2703
- const subdirs = await listChildDirectories(input.rootDir);
2704
- const results = [];
2705
- for (const id of subdirs) {
2706
- const directory = path__default.default.join(input.rootDir, id);
2707
- const discovered = await discoverInstallation(id, directory);
2708
- if (discovered) results.push(discovered);
2709
- }
2710
- return results;
2711
- }
2712
- };
2713
- async function discoverInstallation(id, directory) {
2714
- const versionsDir = path__default.default.join(directory, VERSIONS_DIR);
2715
- const librariesDir = path__default.default.join(directory, LIBRARIES_DIR);
2716
- const assetsDir = path__default.default.join(directory, ASSETS_DIR);
2717
- const looksLikeInstall = await dirExists(versionsDir) && (await dirExists(librariesDir) || await dirExists(assetsDir));
2718
- if (!looksLikeInstall) return null;
2719
- const versionDirs = await listChildDirectories(versionsDir);
2720
- const minecraftVersions = [];
2721
- const loaders = [];
2722
- for (const versionId of versionDirs) {
2723
- const hint = inferLoaderFromVersionId(versionId);
2724
- if (hint) {
2725
- loaders.push(hint);
2726
- if (hint.minecraftVersion && !minecraftVersions.includes(hint.minecraftVersion)) {
2727
- minecraftVersions.push(hint.minecraftVersion);
2728
- }
2729
- } else {
2730
- minecraftVersions.push(versionId);
2731
- }
2732
- }
2733
- const runtime = await discoverRuntime(directory);
2734
- return { id, directory, minecraftVersions, loaders, ...runtime ? { runtime } : {} };
2735
- }
2736
- async function discoverRuntime(directory) {
2737
- const runtimeDir = path__default.default.join(directory, RUNTIMES_DIR);
2738
- if (!await dirExists(runtimeDir)) return void 0;
2739
- let components;
2740
- try {
2741
- components = await listChildDirectories(runtimeDir);
2742
- } catch {
2743
- return void 0;
2744
- }
2745
- for (const component of components) {
2746
- const root = path__default.default.join(runtimeDir, component);
2747
- const javaPath = process.platform === "win32" ? path__default.default.join(root, "bin", "javaw.exe") : process.platform === "darwin" ? path__default.default.join(root, "jre.bundle", "Contents", "Home", "bin", "java") : path__default.default.join(root, "bin", "java");
2748
- if (await fileExists(javaPath)) {
2749
- return { component, javaPath };
2750
- }
2751
- }
2752
- return void 0;
2753
- }
2754
- function inferLoaderFromVersionId(versionId) {
2755
- const fabricMatch = /^fabric-loader-([^-]+)-(.+)$/.exec(versionId);
2756
- if (fabricMatch?.[1] && fabricMatch[2]) {
2757
- return { type: Loaders.FABRIC, version: fabricMatch[1], minecraftVersion: fabricMatch[2] };
2758
- }
2759
- const forgeMatch = /^([^-]+)-forge-(.+)$/.exec(versionId);
2760
- if (forgeMatch?.[1] && forgeMatch[2]) {
2761
- return { type: Loaders.FORGE, minecraftVersion: forgeMatch[1], version: forgeMatch[2] };
2762
- }
2763
- return null;
2764
- }
2765
-
2766
- // src/update/runner.ts
2767
- async function planUpdate(input) {
2768
- return planInstall({
2769
- target: input.target,
2770
- http: input.http,
2771
- cache: input.cache,
2772
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2773
- });
2774
- }
2775
- async function runUpdate(input) {
2776
- const report = await runInstall({
2777
- plan: input.plan,
2778
- http: input.http,
2779
- cache: input.cache,
2780
- spawner: input.spawner,
2781
- ...input.signal !== void 0 ? { signal: input.signal } : {},
2782
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2783
- });
2784
- return {
2785
- targetId: report.targetId,
2786
- bytesDownloaded: report.bytesDownloaded,
2787
- actionsCompleted: report.actionsCompleted,
2788
- actionsSkipped: report.actionsSkipped,
2789
- durationMs: report.durationMs
2790
- };
2791
- }
2792
- async function sha1OfFile(filePath) {
2793
- const hash = crypto2__default.default.createHash("sha1");
2794
- await new Promise((resolve, reject) => {
2795
- const stream = fs$1.createReadStream(filePath);
2796
- stream.on("data", (chunk) => hash.update(chunk));
2797
- stream.on("end", resolve);
2798
- stream.on("error", reject);
2799
- });
2800
- return hash.digest("hex");
2801
- }
2802
-
2803
- // src/verify/helpers.ts
2804
- async function runVerification(input, check) {
2805
- const startedAt = Date.now();
2806
- const results = [];
2807
- const record = (result) => {
2808
- results.push(result);
2809
- input.onEvent?.({ type: "verify:file-checked", file: result });
2810
- };
2811
- await check(record);
2812
- return {
2813
- targetId: input.targetId,
2814
- kind: input.kind,
2815
- isValid: results.every((r) => r.status === VerifyFileStatuses.OK),
2816
- issues: results.filter((r) => r.status !== VerifyFileStatuses.OK),
2817
- checkedFiles: results.length,
2818
- durationMs: Date.now() - startedAt
2819
- };
2820
- }
2821
- async function verifyHashedFile(input) {
2822
- if (!await fileExists(input.path)) {
2823
- return {
2824
- path: input.path,
2825
- category: input.category,
2826
- status: VerifyFileStatuses.MISSING,
2827
- ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2828
- ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2829
- ...input.url !== void 0 ? { url: input.url } : {}
2830
- };
2831
- }
2832
- if (input.expectedSize !== void 0) {
2833
- const size = await fileSize(input.path);
2834
- if (size !== input.expectedSize) {
2835
- return {
2836
- path: input.path,
2837
- category: input.category,
2838
- status: VerifyFileStatuses.WRONG_SIZE,
2839
- expectedSize: input.expectedSize,
2840
- actualSize: size,
2841
- ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2842
- ...input.url !== void 0 ? { url: input.url } : {}
2843
- };
2844
- }
2845
- }
2846
- if (input.expectedSha1 !== void 0) {
2847
- const actualSha1 = await sha1OfFile(input.path);
2848
- if (actualSha1 !== input.expectedSha1) {
2849
- return {
2850
- path: input.path,
2851
- category: input.category,
2852
- status: VerifyFileStatuses.CORRUPT,
2853
- expectedSha1: input.expectedSha1,
2854
- actualSha1,
2855
- ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2856
- ...input.url !== void 0 ? { url: input.url } : {}
2857
- };
2858
- }
2859
- }
2860
- return {
2861
- path: input.path,
2862
- category: input.category,
2863
- status: VerifyFileStatuses.OK,
2864
- ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2865
- ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2866
- ...input.url !== void 0 ? { url: input.url } : {}
2867
- };
2868
- }
2869
- async function verifyExistence(input) {
2870
- if (await fileExists(input.path)) {
2871
- return {
2872
- path: input.path,
2873
- category: input.category,
2874
- status: VerifyFileStatuses.OK,
2875
- ...input.url !== void 0 ? { url: input.url } : {}
2876
- };
2877
- }
2878
- return {
2879
- path: input.path,
2880
- category: input.category,
2881
- status: VerifyFileStatuses.MISSING,
2882
- ...input.url !== void 0 ? { url: input.url } : {}
2883
- };
2884
- }
2885
- async function findForgeVersionJsonPath(directory, minecraftVersion) {
2886
- const versionsDir = targetPaths.versionsDir(directory);
2887
- const dirs = await listChildDirectories(versionsDir);
2888
- for (const id of dirs) {
2889
- if (!id.startsWith(`${minecraftVersion}-forge-`)) continue;
2890
- const jsonPath = targetPaths.versionJson(directory, id);
2891
- if (!await fileExists(jsonPath)) {
2892
- return jsonPath;
2893
- }
2894
- const parsed = await tryParseInheritsFrom(jsonPath);
2895
- if (parsed === minecraftVersion) return jsonPath;
2896
- }
2897
- return null;
2898
- }
2899
- async function tryParseInheritsFrom(jsonPath) {
2900
- try {
2901
- const parsed = JSON.parse(await readText(jsonPath));
2902
- return parsed.inheritsFrom;
2903
- } catch {
2904
- return void 0;
2905
- }
2786
+ );
2906
2787
  }
2907
2788
 
2908
- // src/verify/fabric.ts
2909
- async function verifyFabric(input) {
2910
- if (input.target.loader.type !== Loaders.FABRIC) {
2789
+ // src/verify/forge.ts
2790
+ async function verifyForge(input) {
2791
+ if (input.target.loader.type !== Loaders.FORGE) {
2911
2792
  throw new MinecraftKitError(
2912
2793
  "INVALID_INPUT",
2913
- `verify.fabric requires a Fabric target (got ${input.target.loader.type})`
2794
+ `verify.forge requires a Forge target (got ${input.target.loader.type})`
2914
2795
  );
2915
2796
  }
2916
- const loader = input.target.loader;
2917
2797
  return runVerification(
2918
2798
  {
2919
2799
  targetId: input.target.id,
2920
- kind: VerificationKinds.FABRIC,
2800
+ kind: VerificationKinds.FORGE,
2921
2801
  ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2922
2802
  },
2923
2803
  async (record) => {
2804
+ const forgeVersionJsonPath = await findForgeVersionJsonPath(
2805
+ input.target.directory,
2806
+ input.target.minecraft.version
2807
+ );
2808
+ if (forgeVersionJsonPath === null) return;
2924
2809
  record(
2925
2810
  await verifyExistence({
2926
- path: targetPaths.versionJson(input.target.directory, loader.profile.id),
2811
+ path: forgeVersionJsonPath,
2927
2812
  category: VerifyFileCategories.LOADER_LIBRARY
2928
2813
  })
2929
2814
  );
2930
- const fabricLibraries = planLibraryDownloads({
2931
- libraries: loader.profile.libraries,
2815
+ if (!await fileExists(forgeVersionJsonPath)) return;
2816
+ let parsed;
2817
+ try {
2818
+ parsed = JSON.parse(await readText(forgeVersionJsonPath));
2819
+ } catch {
2820
+ record({
2821
+ path: forgeVersionJsonPath,
2822
+ category: VerifyFileCategories.LOADER_LIBRARY,
2823
+ status: VerifyFileStatuses.CORRUPT
2824
+ });
2825
+ return;
2826
+ }
2827
+ const forgeLibraries = planLibraryDownloads({
2828
+ libraries: parsed.libraries,
2932
2829
  directory: input.target.directory,
2933
2830
  system: input.target.runtime.system,
2934
2831
  versionId: input.target.minecraft.version,
2935
- category: "fabric-library"
2832
+ category: "forge-library"
2936
2833
  });
2937
- for (const action of fabricLibraries.downloads) {
2834
+ for (const action of forgeLibraries.downloads) {
2938
2835
  record(
2939
2836
  await verifyHashedFile({
2940
2837
  path: action.target,
@@ -2949,205 +2846,591 @@ async function verifyFabric(input) {
2949
2846
  );
2950
2847
  }
2951
2848
 
2952
- // src/verify/forge.ts
2953
- async function verifyForge(input) {
2849
+ // src/verify/minecraft.ts
2850
+ async function verifyMinecraft(input) {
2851
+ return runVerification(
2852
+ {
2853
+ targetId: input.target.id,
2854
+ kind: VerificationKinds.MINECRAFT,
2855
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2856
+ },
2857
+ async (record) => {
2858
+ const { directory, minecraft, runtime } = input.target;
2859
+ record(
2860
+ await verifyHashedFile({
2861
+ path: targetPaths.versionJar(directory, minecraft.version),
2862
+ expectedSha1: minecraft.manifest.downloads.client.sha1,
2863
+ expectedSize: minecraft.manifest.downloads.client.size,
2864
+ url: minecraft.manifest.downloads.client.url,
2865
+ category: VerifyFileCategories.CLIENT_JAR
2866
+ })
2867
+ );
2868
+ record(
2869
+ await verifyExistence({
2870
+ path: targetPaths.versionJson(directory, minecraft.version),
2871
+ category: VerifyFileCategories.CLIENT_JAR
2872
+ })
2873
+ );
2874
+ if (minecraft.manifest.logging?.client) {
2875
+ const logging = minecraft.manifest.logging.client;
2876
+ record(
2877
+ await verifyHashedFile({
2878
+ path: targetPaths.loggingConfig(directory, logging.file.id),
2879
+ expectedSha1: logging.file.sha1,
2880
+ expectedSize: logging.file.size,
2881
+ url: logging.file.url,
2882
+ category: VerifyFileCategories.LOGGING_CONFIG
2883
+ })
2884
+ );
2885
+ }
2886
+ const libraryPlan = planLibraryDownloads({
2887
+ libraries: minecraft.manifest.libraries,
2888
+ directory,
2889
+ system: runtime.system,
2890
+ versionId: minecraft.version,
2891
+ category: "library"
2892
+ });
2893
+ for (const action of libraryPlan.downloads) {
2894
+ record(
2895
+ await verifyHashedFile({
2896
+ path: action.target,
2897
+ expectedSha1: action.expectedSha1,
2898
+ expectedSize: action.expectedSize,
2899
+ url: action.url,
2900
+ category: VerifyFileCategories.LIBRARY
2901
+ })
2902
+ );
2903
+ }
2904
+ const indexUrl = minecraft.manifest.assetIndex.url;
2905
+ const indexPath = targetPaths.assetIndex(directory, minecraft.manifest.assetIndex.id);
2906
+ record(
2907
+ await verifyHashedFile({
2908
+ path: indexPath,
2909
+ expectedSha1: minecraft.manifest.assetIndex.sha1,
2910
+ expectedSize: minecraft.manifest.assetIndex.size,
2911
+ url: indexUrl,
2912
+ category: VerifyFileCategories.ASSET_INDEX
2913
+ })
2914
+ );
2915
+ const indexDocument = await fetchJson(input.http, input.cache, {
2916
+ url: indexUrl,
2917
+ cacheKey: `asset-index:${minecraft.manifest.assetIndex.id}:${minecraft.manifest.assetIndex.sha1}`,
2918
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
2919
+ });
2920
+ const seenAssetHashes = /* @__PURE__ */ new Set();
2921
+ for (const entry of Object.values(indexDocument.objects)) {
2922
+ if (seenAssetHashes.has(entry.hash)) continue;
2923
+ seenAssetHashes.add(entry.hash);
2924
+ record(
2925
+ await verifyHashedFile({
2926
+ path: targetPaths.assetObject(directory, entry.hash),
2927
+ expectedSha1: entry.hash,
2928
+ expectedSize: entry.size,
2929
+ category: VerifyFileCategories.ASSET
2930
+ })
2931
+ );
2932
+ }
2933
+ const nativesDir = targetPaths.nativesDir(directory, minecraft.version);
2934
+ if (!await fileExists(nativesDir)) {
2935
+ for (const extraction of libraryPlan.nativeExtractions) {
2936
+ record({
2937
+ path: extraction.source,
2938
+ category: VerifyFileCategories.NATIVE,
2939
+ status: VerifyFileStatuses.MISSING
2940
+ });
2941
+ }
2942
+ }
2943
+ }
2944
+ );
2945
+ }
2946
+ async function verifyRuntime(input) {
2947
+ return runVerification(
2948
+ {
2949
+ targetId: input.target.id,
2950
+ kind: VerificationKinds.RUNTIME,
2951
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2952
+ },
2953
+ async (record) => {
2954
+ let manifest;
2955
+ try {
2956
+ manifest = await fetchJson(input.http, input.cache, {
2957
+ url: input.target.runtime.manifestUrl,
2958
+ cacheKey: `runtime-manifest:${input.target.runtime.component}:${input.target.runtime.platformKey}:${input.target.runtime.manifestSha1}`,
2959
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
2960
+ });
2961
+ } catch {
2962
+ record({
2963
+ path: input.target.runtime.manifestUrl,
2964
+ category: VerifyFileCategories.RUNTIME_FILE,
2965
+ status: VerifyFileStatuses.MISSING
2966
+ });
2967
+ return;
2968
+ }
2969
+ const runtimeRoot = targetPaths.runtimeRoot(
2970
+ input.target.directory,
2971
+ input.target.runtime.component,
2972
+ input.target.runtime.installRoot
2973
+ );
2974
+ for (const [relative, entry] of Object.entries(manifest.files)) {
2975
+ if (entry.type !== "file") continue;
2976
+ record(
2977
+ await verifyHashedFile({
2978
+ path: path__default.default.join(runtimeRoot, relative),
2979
+ expectedSha1: entry.downloads.raw.sha1,
2980
+ expectedSize: entry.downloads.raw.size,
2981
+ url: entry.downloads.raw.url,
2982
+ category: VerifyFileCategories.RUNTIME_FILE
2983
+ })
2984
+ );
2985
+ }
2986
+ }
2987
+ );
2988
+ }
2989
+
2990
+ // src/repair/helpers.ts
2991
+ function asResultArray(from) {
2992
+ return Array.isArray(from) ? from : [from];
2993
+ }
2994
+ function buildIssueIndex(from) {
2995
+ const map = /* @__PURE__ */ new Map();
2996
+ for (const v of asResultArray(from)) {
2997
+ for (const issue of v.issues) {
2998
+ const set = map.get(issue.path);
2999
+ if (set) set.add(issue.category);
3000
+ else map.set(issue.path, /* @__PURE__ */ new Set([issue.category]));
3001
+ }
3002
+ }
3003
+ return {
3004
+ has: (path16) => map.has(path16),
3005
+ hasNonNative: (path16) => {
3006
+ const cats = map.get(path16);
3007
+ if (!cats) return false;
3008
+ for (const c of cats) {
3009
+ if (c !== VerifyFileCategories.NATIVE) return true;
3010
+ }
3011
+ return false;
3012
+ },
3013
+ categoriesAt: (path16) => map.get(path16) ?? /* @__PURE__ */ new Set()
3014
+ };
3015
+ }
3016
+ function sumDownloadBytes(actions) {
3017
+ return actions.reduce((sum, action) => {
3018
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3019
+ return sum + (action.expectedSize ?? 0);
3020
+ }
3021
+ return sum;
3022
+ }, 0);
3023
+ }
3024
+ function buildRepairPlan(target, actions) {
3025
+ return {
3026
+ targetId: target.id,
3027
+ directory: target.directory,
3028
+ target,
3029
+ actions,
3030
+ totalActions: actions.length,
3031
+ totalBytes: sumDownloadBytes(actions)
3032
+ };
3033
+ }
3034
+ async function planAspectRepair(input, aspectFilter, postprocess) {
3035
+ const installPlan = await planInstall({
3036
+ target: input.target,
3037
+ http: input.http,
3038
+ cache: input.cache,
3039
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3040
+ });
3041
+ const issues = buildIssueIndex(input.from);
3042
+ const actions = selectRepairActions({
3043
+ target: input.target,
3044
+ installPlan,
3045
+ issues,
3046
+ aspectFilter
3047
+ });
3048
+ postprocess?.({ actions, installPlan, issues });
3049
+ return buildRepairPlan(input.target, actions);
3050
+ }
3051
+ function selectRepairActions(input) {
3052
+ const matching = [];
3053
+ for (const action of input.installPlan.actions) {
3054
+ if (!input.aspectFilter(action)) continue;
3055
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3056
+ if (input.issues.hasNonNative(action.target)) {
3057
+ matching.push(action);
3058
+ }
3059
+ } else if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3060
+ if (input.issues.has(action.path)) {
3061
+ matching.push(action);
3062
+ }
3063
+ } else if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
3064
+ if (input.issues.has(action.source)) {
3065
+ matching.push(action);
3066
+ }
3067
+ } else {
3068
+ matching.push(action);
3069
+ }
3070
+ }
3071
+ return matching;
3072
+ }
3073
+
3074
+ // src/repair/fabric.ts
3075
+ async function planFabricRepair(input) {
3076
+ if (input.target.loader.type !== Loaders.FABRIC) {
3077
+ throw new MinecraftKitError(
3078
+ "INVALID_INPUT",
3079
+ `repair.fabric requires a Fabric target (got ${input.target.loader.type})`
3080
+ );
3081
+ }
3082
+ const fabricJsonPath = targetPaths.versionJson(
3083
+ input.target.directory,
3084
+ input.target.loader.profile.id
3085
+ );
3086
+ return planAspectRepair(input, (action) => {
3087
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3088
+ return action.category === "fabric-library";
3089
+ }
3090
+ if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3091
+ return action.path === fabricJsonPath;
3092
+ }
3093
+ return false;
3094
+ });
3095
+ }
3096
+
3097
+ // src/repair/forge.ts
3098
+ var FORGE_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
3099
+ "forge-library",
3100
+ "forge-installer"
3101
+ ]);
3102
+ async function planForgeRepair(input) {
2954
3103
  if (input.target.loader.type !== Loaders.FORGE) {
2955
3104
  throw new MinecraftKitError(
2956
3105
  "INVALID_INPUT",
2957
- `verify.forge requires a Forge target (got ${input.target.loader.type})`
3106
+ `repair.forge requires a Forge target (got ${input.target.loader.type})`
2958
3107
  );
2959
3108
  }
2960
- return runVerification(
2961
- {
2962
- targetId: input.target.id,
2963
- kind: VerificationKinds.FORGE,
2964
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3109
+ const forgeJsonPath = targetPaths.versionJson(
3110
+ input.target.directory,
3111
+ input.target.loader.fullVersion
3112
+ );
3113
+ return planAspectRepair(
3114
+ input,
3115
+ (action) => {
3116
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3117
+ return FORGE_DOWNLOAD_CATEGORIES.has(action.category);
3118
+ }
3119
+ if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3120
+ return action.path === forgeJsonPath;
3121
+ }
3122
+ return false;
2965
3123
  },
2966
- async (record) => {
2967
- const forgeVersionJsonPath = await findForgeVersionJsonPath(
2968
- input.target.directory,
2969
- input.target.minecraft.version
2970
- );
2971
- if (forgeVersionJsonPath === null) return;
2972
- record(
2973
- await verifyExistence({
2974
- path: forgeVersionJsonPath,
2975
- category: VerifyFileCategories.LOADER_LIBRARY
2976
- })
3124
+ ({ actions, installPlan, issues }) => {
3125
+ if (!issues.has(forgeJsonPath)) return;
3126
+ const alreadyIncluded = new Set(
3127
+ actions.filter((a) => a.kind === InstallActionKinds.DOWNLOAD_FILE).map((a) => a.target)
2977
3128
  );
2978
- if (!await fileExists(forgeVersionJsonPath)) return;
2979
- let parsed;
2980
- try {
2981
- parsed = JSON.parse(await readText(forgeVersionJsonPath));
2982
- } catch {
2983
- record({
2984
- path: forgeVersionJsonPath,
2985
- category: VerifyFileCategories.LOADER_LIBRARY,
2986
- status: VerifyFileStatuses.CORRUPT
2987
- });
2988
- return;
2989
- }
2990
- const forgeLibraries = planLibraryDownloads({
2991
- libraries: parsed.libraries,
2992
- directory: input.target.directory,
2993
- system: input.target.runtime.system,
2994
- versionId: input.target.minecraft.version,
2995
- category: "forge-library"
2996
- });
2997
- for (const action of forgeLibraries.downloads) {
2998
- record(
2999
- await verifyHashedFile({
3000
- path: action.target,
3001
- expectedSha1: action.expectedSha1,
3002
- expectedSize: action.expectedSize,
3003
- ...action.url ? { url: action.url } : {},
3004
- category: VerifyFileCategories.LOADER_LIBRARY
3005
- })
3006
- );
3129
+ for (const action of installPlan.actions) {
3130
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "forge-library" && !alreadyIncluded.has(action.target)) {
3131
+ actions.push(action);
3132
+ } else if (action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR) {
3133
+ actions.push(action);
3134
+ }
3007
3135
  }
3008
3136
  }
3009
3137
  );
3010
3138
  }
3011
3139
 
3012
- // src/verify/minecraft.ts
3013
- async function verifyMinecraft(input) {
3014
- return runVerification(
3015
- {
3016
- targetId: input.target.id,
3017
- kind: VerificationKinds.MINECRAFT,
3018
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3140
+ // src/repair/minecraft.ts
3141
+ var MINECRAFT_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
3142
+ "client-jar",
3143
+ "library",
3144
+ "asset-index",
3145
+ "asset",
3146
+ "logging-config"
3147
+ ]);
3148
+ async function planMinecraftRepair(input) {
3149
+ const vanillaJsonPath = targetPaths.versionJson(
3150
+ input.target.directory,
3151
+ input.target.minecraft.version
3152
+ );
3153
+ return planAspectRepair(input, (action) => {
3154
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3155
+ return MINECRAFT_DOWNLOAD_CATEGORIES.has(action.category);
3156
+ }
3157
+ if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3158
+ return action.path === vanillaJsonPath;
3159
+ }
3160
+ if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
3161
+ return true;
3162
+ }
3163
+ return false;
3164
+ });
3165
+ }
3166
+
3167
+ // src/repair/runner.ts
3168
+ async function runRepair(input) {
3169
+ const report = await runInstall({
3170
+ plan: {
3171
+ ...input.plan,
3172
+ totalActions: input.plan.actions.length,
3173
+ totalBytes: input.plan.totalBytes
3019
3174
  },
3020
- async (record) => {
3021
- const { directory, minecraft, runtime } = input.target;
3022
- record(
3023
- await verifyHashedFile({
3024
- path: targetPaths.versionJar(directory, minecraft.version),
3025
- expectedSha1: minecraft.manifest.downloads.client.sha1,
3026
- expectedSize: minecraft.manifest.downloads.client.size,
3027
- url: minecraft.manifest.downloads.client.url,
3028
- category: VerifyFileCategories.CLIENT_JAR
3029
- })
3030
- );
3031
- record(
3032
- await verifyExistence({
3033
- path: targetPaths.versionJson(directory, minecraft.version),
3034
- category: VerifyFileCategories.CLIENT_JAR
3035
- })
3175
+ http: input.http,
3176
+ cache: input.cache,
3177
+ spawner: input.spawner,
3178
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
3179
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3180
+ });
3181
+ return {
3182
+ targetId: report.targetId,
3183
+ bytesDownloaded: report.bytesDownloaded,
3184
+ actionsCompleted: report.actionsCompleted,
3185
+ durationMs: report.durationMs
3186
+ };
3187
+ }
3188
+
3189
+ // src/repair/runtime.ts
3190
+ async function planRuntimeRepair(input) {
3191
+ return planAspectRepair(
3192
+ input,
3193
+ (action) => action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "runtime-file"
3194
+ );
3195
+ }
3196
+
3197
+ // src/repair/all.ts
3198
+ async function repairAll(input) {
3199
+ const startedAt = Date.now();
3200
+ const ctx = {
3201
+ target: input.target,
3202
+ http: input.http,
3203
+ cache: input.cache,
3204
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3205
+ };
3206
+ const verifications = [];
3207
+ const mc = await verifyMinecraft(ctx);
3208
+ verifications.push(mc);
3209
+ const rt = await verifyRuntime(ctx);
3210
+ verifications.push(rt);
3211
+ if (input.target.loader.type === Loaders.FABRIC) {
3212
+ verifications.push(await verifyFabric(ctx));
3213
+ } else if (input.target.loader.type === Loaders.FORGE) {
3214
+ verifications.push(await verifyForge(ctx));
3215
+ }
3216
+ const repairs = /* @__PURE__ */ new Map();
3217
+ let bytesDownloaded = 0;
3218
+ for (const verification of verifications) {
3219
+ if (verification.isValid) continue;
3220
+ const planner = PLANNERS[verification.kind];
3221
+ if (!planner) continue;
3222
+ const plan = await planner({ ...ctx, from: verification });
3223
+ if (plan.totalActions === 0) continue;
3224
+ const report = await runRepair({
3225
+ plan,
3226
+ http: input.http,
3227
+ cache: input.cache,
3228
+ spawner: input.spawner,
3229
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
3230
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3231
+ });
3232
+ repairs.set(verification.kind, report);
3233
+ bytesDownloaded += report.bytesDownloaded;
3234
+ }
3235
+ return {
3236
+ verifications,
3237
+ repairs,
3238
+ bytesDownloaded,
3239
+ durationMs: Date.now() - startedAt
3240
+ };
3241
+ }
3242
+ var PLANNERS = {
3243
+ minecraft: planMinecraftRepair,
3244
+ runtime: planRuntimeRepair,
3245
+ fabric: planFabricRepair,
3246
+ forge: planForgeRepair
3247
+ };
3248
+
3249
+ // src/types/runtime.ts
3250
+ var RuntimeComponents = {
3251
+ JRE_LEGACY: "jre-legacy"};
3252
+ var RuntimePreference = {
3253
+ /** Component declared by the Minecraft manifest. */
3254
+ RECOMMENDED: "recommended",
3255
+ /** Newest component available for the platform. */
3256
+ LATEST: "latest"
3257
+ };
3258
+
3259
+ // src/targets/index.ts
3260
+ var TargetsApi = class {
3261
+ constructor(ctx) {
3262
+ this.ctx = ctx;
3263
+ }
3264
+ ctx;
3265
+ /** The detected host system used by `resolve()` when no `system` is supplied. */
3266
+ get system() {
3267
+ return this.ctx.system;
3268
+ }
3269
+ /** Build a {@link Target} from already-resolved components. */
3270
+ create(input) {
3271
+ if (!input.id) {
3272
+ throw new MinecraftKitError("INVALID_INPUT", "Target id must be non-empty");
3273
+ }
3274
+ if (!input.directory) {
3275
+ throw new MinecraftKitError("INVALID_INPUT", "Target directory must be non-empty");
3276
+ }
3277
+ if (input.loader.minecraftVersion !== input.minecraft.version) {
3278
+ throw new MinecraftKitError(
3279
+ "INVALID_INPUT",
3280
+ `Loader Minecraft version (${input.loader.minecraftVersion}) does not match resolved Minecraft (${input.minecraft.version})`,
3281
+ {
3282
+ context: {
3283
+ loaderMinecraft: input.loader.minecraftVersion,
3284
+ minecraftVersion: input.minecraft.version
3285
+ }
3286
+ }
3036
3287
  );
3037
- if (minecraft.manifest.logging?.client) {
3038
- const logging = minecraft.manifest.logging.client;
3039
- record(
3040
- await verifyHashedFile({
3041
- path: targetPaths.loggingConfig(directory, logging.file.id),
3042
- expectedSha1: logging.file.sha1,
3043
- expectedSize: logging.file.size,
3044
- url: logging.file.url,
3045
- category: VerifyFileCategories.LOGGING_CONFIG
3046
- })
3047
- );
3048
- }
3049
- const libraryPlan = planLibraryDownloads({
3050
- libraries: minecraft.manifest.libraries,
3051
- directory,
3052
- system: runtime.system,
3053
- versionId: minecraft.version,
3054
- category: "library"
3288
+ }
3289
+ return {
3290
+ id: input.id,
3291
+ directory: input.directory,
3292
+ minecraft: input.minecraft,
3293
+ loader: input.loader,
3294
+ runtime: input.runtime
3295
+ };
3296
+ }
3297
+ /** Sugar API: resolve every component then assemble a target. */
3298
+ async resolve(input) {
3299
+ const minecraft = await this.ctx.minecraft.resolve({
3300
+ version: input.minecraft.version,
3301
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3302
+ });
3303
+ const system = input.system ?? this.ctx.system;
3304
+ const componentOverride = input.runtime?.component;
3305
+ const runtimeComponent = componentOverride ?? minecraft.manifest.javaVersion?.component;
3306
+ const resolvedRuntime = await this.ctx.runtime.resolve({
3307
+ system,
3308
+ ...runtimeComponent !== void 0 ? { component: runtimeComponent } : {},
3309
+ preference: input.runtime?.preference ?? RuntimePreference.RECOMMENDED,
3310
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3311
+ });
3312
+ const runtime = input.runtime?.installRoot !== void 0 ? { ...resolvedRuntime, installRoot: input.runtime.installRoot } : resolvedRuntime;
3313
+ let loader;
3314
+ if (input.loader.type === Loaders.VANILLA) {
3315
+ loader = {
3316
+ type: Loaders.VANILLA,
3317
+ minecraftVersion: minecraft.version,
3318
+ minecraft
3319
+ };
3320
+ } else if (input.loader.type === Loaders.FABRIC) {
3321
+ loader = await this.ctx.fabric.resolve({
3322
+ minecraftVersion: minecraft.version,
3323
+ ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
3324
+ ...input.loader.version !== void 0 ? { loaderVersion: input.loader.version } : {},
3325
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3055
3326
  });
3056
- for (const action of libraryPlan.downloads) {
3057
- record(
3058
- await verifyHashedFile({
3059
- path: action.target,
3060
- expectedSha1: action.expectedSha1,
3061
- expectedSize: action.expectedSize,
3062
- url: action.url,
3063
- category: VerifyFileCategories.LIBRARY
3064
- })
3065
- );
3066
- }
3067
- const indexUrl = minecraft.manifest.assetIndex.url;
3068
- const indexPath = targetPaths.assetIndex(directory, minecraft.manifest.assetIndex.id);
3069
- record(
3070
- await verifyHashedFile({
3071
- path: indexPath,
3072
- expectedSha1: minecraft.manifest.assetIndex.sha1,
3073
- expectedSize: minecraft.manifest.assetIndex.size,
3074
- url: indexUrl,
3075
- category: VerifyFileCategories.ASSET_INDEX
3076
- })
3077
- );
3078
- const indexDocument = await fetchJson(input.http, input.cache, {
3079
- url: indexUrl,
3080
- cacheKey: `asset-index:${minecraft.manifest.assetIndex.id}:${minecraft.manifest.assetIndex.sha1}`,
3327
+ } else {
3328
+ loader = await this.ctx.forge.resolve({
3329
+ minecraftVersion: minecraft.version,
3330
+ ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
3331
+ ...input.loader.version !== void 0 ? { forgeVersion: input.loader.version } : {},
3081
3332
  ...input.signal !== void 0 ? { signal: input.signal } : {}
3082
3333
  });
3083
- const seenAssetHashes = /* @__PURE__ */ new Set();
3084
- for (const entry of Object.values(indexDocument.objects)) {
3085
- if (seenAssetHashes.has(entry.hash)) continue;
3086
- seenAssetHashes.add(entry.hash);
3087
- record(
3088
- await verifyHashedFile({
3089
- path: targetPaths.assetObject(directory, entry.hash),
3090
- expectedSha1: entry.hash,
3091
- expectedSize: entry.size,
3092
- category: VerifyFileCategories.ASSET
3093
- })
3094
- );
3095
- }
3096
- const nativesDir = targetPaths.nativesDir(directory, minecraft.version);
3097
- if (!await fileExists(nativesDir)) {
3098
- for (const extraction of libraryPlan.nativeExtractions) {
3099
- record({
3100
- path: extraction.source,
3101
- category: VerifyFileCategories.NATIVE,
3102
- status: VerifyFileStatuses.MISSING
3103
- });
3104
- }
3334
+ }
3335
+ return this.create({
3336
+ id: input.id,
3337
+ directory: input.directory,
3338
+ minecraft,
3339
+ loader,
3340
+ runtime
3341
+ });
3342
+ }
3343
+ /** Scan a root directory for Minecraft installations. Returns only what is on disk. */
3344
+ async list(input) {
3345
+ if (!await dirExists(input.rootDir)) return [];
3346
+ const subdirs = await listChildDirectories(input.rootDir);
3347
+ const results = [];
3348
+ for (const id of subdirs) {
3349
+ const directory = path__default.default.join(input.rootDir, id);
3350
+ const discovered = await discoverInstallation(id, directory);
3351
+ if (discovered) results.push(discovered);
3352
+ }
3353
+ return results;
3354
+ }
3355
+ };
3356
+ async function discoverInstallation(id, directory) {
3357
+ const versionsDir = path__default.default.join(directory, VERSIONS_DIR);
3358
+ const librariesDir = path__default.default.join(directory, LIBRARIES_DIR);
3359
+ const assetsDir = path__default.default.join(directory, ASSETS_DIR);
3360
+ const looksLikeInstall = await dirExists(versionsDir) && (await dirExists(librariesDir) || await dirExists(assetsDir));
3361
+ if (!looksLikeInstall) return null;
3362
+ const versionDirs = await listChildDirectories(versionsDir);
3363
+ const minecraftVersions = [];
3364
+ const loaders = [];
3365
+ for (const versionId of versionDirs) {
3366
+ const hint = inferLoaderFromVersionId(versionId);
3367
+ if (hint) {
3368
+ loaders.push(hint);
3369
+ if (hint.minecraftVersion && !minecraftVersions.includes(hint.minecraftVersion)) {
3370
+ minecraftVersions.push(hint.minecraftVersion);
3105
3371
  }
3372
+ } else {
3373
+ minecraftVersions.push(versionId);
3106
3374
  }
3107
- );
3375
+ }
3376
+ const runtime = await discoverRuntime(directory);
3377
+ return { id, directory, minecraftVersions, loaders, ...runtime ? { runtime } : {} };
3108
3378
  }
3109
- async function verifyRuntime(input) {
3110
- return runVerification(
3111
- {
3112
- targetId: input.target.id,
3113
- kind: VerificationKinds.RUNTIME,
3114
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3115
- },
3116
- async (record) => {
3117
- let manifest;
3118
- try {
3119
- manifest = await fetchJson(input.http, input.cache, {
3120
- url: input.target.runtime.manifestUrl,
3121
- cacheKey: `runtime-manifest:${input.target.runtime.component}:${input.target.runtime.platformKey}:${input.target.runtime.manifestSha1}`,
3122
- ...input.signal !== void 0 ? { signal: input.signal } : {}
3123
- });
3124
- } catch {
3125
- record({
3126
- path: input.target.runtime.manifestUrl,
3127
- category: VerifyFileCategories.RUNTIME_FILE,
3128
- status: VerifyFileStatuses.MISSING
3129
- });
3130
- return;
3131
- }
3132
- const runtimeRoot = targetPaths.runtimeRoot(
3133
- input.target.directory,
3134
- input.target.runtime.component,
3135
- input.target.runtime.installRoot
3136
- );
3137
- for (const [relative, entry] of Object.entries(manifest.files)) {
3138
- if (entry.type !== "file") continue;
3139
- record(
3140
- await verifyHashedFile({
3141
- path: path__default.default.join(runtimeRoot, relative),
3142
- expectedSha1: entry.downloads.raw.sha1,
3143
- expectedSize: entry.downloads.raw.size,
3144
- url: entry.downloads.raw.url,
3145
- category: VerifyFileCategories.RUNTIME_FILE
3146
- })
3147
- );
3148
- }
3379
+ async function discoverRuntime(directory) {
3380
+ const runtimeDir = path__default.default.join(directory, RUNTIMES_DIR);
3381
+ if (!await dirExists(runtimeDir)) return void 0;
3382
+ let components;
3383
+ try {
3384
+ components = await listChildDirectories(runtimeDir);
3385
+ } catch {
3386
+ return void 0;
3387
+ }
3388
+ for (const component of components) {
3389
+ const root = path__default.default.join(runtimeDir, component);
3390
+ const javaPath = process.platform === "win32" ? path__default.default.join(root, "bin", "javaw.exe") : process.platform === "darwin" ? path__default.default.join(root, "jre.bundle", "Contents", "Home", "bin", "java") : path__default.default.join(root, "bin", "java");
3391
+ if (await fileExists(javaPath)) {
3392
+ return { component, javaPath };
3149
3393
  }
3150
- );
3394
+ }
3395
+ return void 0;
3396
+ }
3397
+ function inferLoaderFromVersionId(versionId) {
3398
+ const fabricMatch = /^fabric-loader-([^-]+)-(.+)$/.exec(versionId);
3399
+ if (fabricMatch?.[1] && fabricMatch[2]) {
3400
+ return { type: Loaders.FABRIC, version: fabricMatch[1], minecraftVersion: fabricMatch[2] };
3401
+ }
3402
+ const forgeMatch = /^([^-]+)-forge-(.+)$/.exec(versionId);
3403
+ if (forgeMatch?.[1] && forgeMatch[2]) {
3404
+ return { type: Loaders.FORGE, minecraftVersion: forgeMatch[1], version: forgeMatch[2] };
3405
+ }
3406
+ return null;
3407
+ }
3408
+
3409
+ // src/update/runner.ts
3410
+ async function planUpdate(input) {
3411
+ return planInstall({
3412
+ target: input.target,
3413
+ http: input.http,
3414
+ cache: input.cache,
3415
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3416
+ });
3417
+ }
3418
+ async function runUpdate(input) {
3419
+ const report = await runInstall({
3420
+ plan: input.plan,
3421
+ http: input.http,
3422
+ cache: input.cache,
3423
+ spawner: input.spawner,
3424
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
3425
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3426
+ });
3427
+ return {
3428
+ targetId: report.targetId,
3429
+ bytesDownloaded: report.bytesDownloaded,
3430
+ actionsCompleted: report.actionsCompleted,
3431
+ actionsSkipped: report.actionsSkipped,
3432
+ durationMs: report.durationMs
3433
+ };
3151
3434
  }
3152
3435
 
3153
3436
  // src/versions/fabric.ts
@@ -3481,15 +3764,23 @@ function pickLatestAcrossComponents(entries) {
3481
3764
  return { component: bestComponent, entry: bestEntry };
3482
3765
  }
3483
3766
  function toResolved(component, platformKey, entry, system) {
3767
+ const majorVersion = parseMajorVersion(entry.version.name);
3484
3768
  return {
3485
3769
  component,
3486
3770
  platformKey,
3487
3771
  versionName: entry.version.name,
3772
+ ...majorVersion !== void 0 ? { majorVersion } : {},
3488
3773
  system,
3489
3774
  manifestUrl: entry.manifest.url,
3490
3775
  manifestSha1: entry.manifest.sha1
3491
3776
  };
3492
3777
  }
3778
+ function parseMajorVersion(versionName) {
3779
+ const match = /^(\d+)/.exec(versionName);
3780
+ if (!match || !match[1]) return void 0;
3781
+ const parsed = Number.parseInt(match[1], 10);
3782
+ return Number.isFinite(parsed) ? parsed : void 0;
3783
+ }
3493
3784
 
3494
3785
  // src/kit.ts
3495
3786
  var MinecraftKit = class {
@@ -3520,7 +3811,12 @@ var MinecraftKit = class {
3520
3811
  ...opts?.signal !== void 0 ? { signal: opts.signal } : {},
3521
3812
  ...opts?.onEvent !== void 0 ? { onEvent: opts.onEvent } : {}
3522
3813
  });
3523
- const runInstallPlan = (plan, opts) => runInstall({ plan, http, cache, spawner, ...carry(opts) });
3814
+ const carryInstall = (opts) => ({
3815
+ ...carry(opts),
3816
+ ...opts?.pauseController !== void 0 ? { pauseController: opts.pauseController } : {},
3817
+ ...opts?.actionCategories !== void 0 ? { actionCategories: opts.actionCategories } : {}
3818
+ });
3819
+ const runInstallPlan = (plan, opts) => runInstall({ plan, http, cache, spawner, ...carryInstall(opts) });
3524
3820
  this.install = {
3525
3821
  plan: (target, opts) => planInstall({ target, http, cache, ...carry(opts) }),
3526
3822
  run: runInstallPlan,
@@ -3570,10 +3866,17 @@ var MinecraftKit = class {
3570
3866
  runtime: {
3571
3867
  plan: (target, opts) => planRuntimeRepair(repairArgs(target, opts)),
3572
3868
  run: runRepairPlan
3573
- }
3869
+ },
3870
+ all: (target, opts) => repairAll({
3871
+ target,
3872
+ http,
3873
+ cache,
3874
+ spawner,
3875
+ ...carry(opts)
3876
+ })
3574
3877
  };
3575
3878
  this.launch = {
3576
- compose: (target, opts) => composeLaunch({ target, options: opts }),
3879
+ compose: (target, opts) => composeLaunch({ target, options: opts, logger }),
3577
3880
  run: (composition, opts) => runLaunch({
3578
3881
  composition,
3579
3882
  ...opts !== void 0 ? { options: opts } : {},