@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.
package/dist/index.cjs CHANGED
@@ -9,7 +9,7 @@ var yauzl = require('yauzl');
9
9
  var crypto2 = require('crypto');
10
10
  var fs = require('fs/promises');
11
11
  var stream = require('stream');
12
- var pLimit = require('p-limit');
12
+ var async_hooks = require('async_hooks');
13
13
  var buffer = require('buffer');
14
14
  var child_process = require('child_process');
15
15
 
@@ -20,7 +20,6 @@ var path__default = /*#__PURE__*/_interopDefault(path);
20
20
  var yauzl__default = /*#__PURE__*/_interopDefault(yauzl);
21
21
  var crypto2__default = /*#__PURE__*/_interopDefault(crypto2);
22
22
  var fs__default = /*#__PURE__*/_interopDefault(fs);
23
- var pLimit__default = /*#__PURE__*/_interopDefault(pLimit);
24
23
 
25
24
  // src/types/logger.ts
26
25
  var LogLevels = {
@@ -1123,6 +1122,12 @@ async function downloadFile(http, input) {
1123
1122
  const sourceIterable = response.stream();
1124
1123
  const counting = (async function* () {
1125
1124
  for await (const chunk of sourceIterable) {
1125
+ if (input.pauseController?.paused) {
1126
+ await input.pauseController.waitWhilePaused();
1127
+ }
1128
+ if (input.signal?.aborted) {
1129
+ throw new MinecraftKitError("LAUNCH_ABORTED", "Download aborted by signal");
1130
+ }
1126
1131
  bytesDownloaded += chunk.byteLength;
1127
1132
  hash.update(chunk);
1128
1133
  input.onEvent?.({
@@ -1451,7 +1456,8 @@ function substituteToken(raw, tokens) {
1451
1456
  });
1452
1457
  }
1453
1458
  function stripLiteralPrefix(value) {
1454
- return value.startsWith("'") ? value.slice(1) : value;
1459
+ const stripped = value.startsWith("'") ? value.slice(1) : value;
1460
+ return stripped.endsWith("'") ? stripped.slice(0, -1) : stripped;
1455
1461
  }
1456
1462
  async function planRuntimeDownloads(input) {
1457
1463
  const manifest = await fetchJson(input.http, input.cache, {
@@ -1574,6 +1580,123 @@ async function planInstall(input) {
1574
1580
  totalBytes
1575
1581
  };
1576
1582
  }
1583
+
1584
+ // node_modules/yocto-queue/index.js
1585
+ var Node = class {
1586
+ value;
1587
+ next;
1588
+ constructor(value) {
1589
+ this.value = value;
1590
+ }
1591
+ };
1592
+ var Queue = class {
1593
+ #head;
1594
+ #tail;
1595
+ #size;
1596
+ constructor() {
1597
+ this.clear();
1598
+ }
1599
+ enqueue(value) {
1600
+ const node = new Node(value);
1601
+ if (this.#head) {
1602
+ this.#tail.next = node;
1603
+ this.#tail = node;
1604
+ } else {
1605
+ this.#head = node;
1606
+ this.#tail = node;
1607
+ }
1608
+ this.#size++;
1609
+ }
1610
+ dequeue() {
1611
+ const current = this.#head;
1612
+ if (!current) {
1613
+ return;
1614
+ }
1615
+ this.#head = this.#head.next;
1616
+ this.#size--;
1617
+ if (!this.#head) {
1618
+ this.#tail = void 0;
1619
+ }
1620
+ return current.value;
1621
+ }
1622
+ peek() {
1623
+ if (!this.#head) {
1624
+ return;
1625
+ }
1626
+ return this.#head.value;
1627
+ }
1628
+ clear() {
1629
+ this.#head = void 0;
1630
+ this.#tail = void 0;
1631
+ this.#size = 0;
1632
+ }
1633
+ get size() {
1634
+ return this.#size;
1635
+ }
1636
+ *[Symbol.iterator]() {
1637
+ let current = this.#head;
1638
+ while (current) {
1639
+ yield current.value;
1640
+ current = current.next;
1641
+ }
1642
+ }
1643
+ *drain() {
1644
+ while (this.#head) {
1645
+ yield this.dequeue();
1646
+ }
1647
+ }
1648
+ };
1649
+ function pLimit(concurrency) {
1650
+ if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
1651
+ throw new TypeError("Expected `concurrency` to be a number from 1 and up");
1652
+ }
1653
+ const queue = new Queue();
1654
+ let activeCount = 0;
1655
+ const next = () => {
1656
+ activeCount--;
1657
+ if (queue.size > 0) {
1658
+ queue.dequeue()();
1659
+ }
1660
+ };
1661
+ const run = async (function_, resolve, arguments_) => {
1662
+ activeCount++;
1663
+ const result = (async () => function_(...arguments_))();
1664
+ resolve(result);
1665
+ try {
1666
+ await result;
1667
+ } catch {
1668
+ }
1669
+ next();
1670
+ };
1671
+ const enqueue = (function_, resolve, arguments_) => {
1672
+ queue.enqueue(
1673
+ async_hooks.AsyncResource.bind(run.bind(void 0, function_, resolve, arguments_))
1674
+ );
1675
+ (async () => {
1676
+ await Promise.resolve();
1677
+ if (activeCount < concurrency && queue.size > 0) {
1678
+ queue.dequeue()();
1679
+ }
1680
+ })();
1681
+ };
1682
+ const generator = (function_, ...arguments_) => new Promise((resolve) => {
1683
+ enqueue(function_, resolve, arguments_);
1684
+ });
1685
+ Object.defineProperties(generator, {
1686
+ activeCount: {
1687
+ get: () => activeCount
1688
+ },
1689
+ pendingCount: {
1690
+ get: () => queue.size
1691
+ },
1692
+ clearQueue: {
1693
+ value() {
1694
+ queue.clear();
1695
+ }
1696
+ }
1697
+ });
1698
+ return generator;
1699
+ }
1577
1700
  async function materializeRuntimeExtras(input) {
1578
1701
  const root = targetPaths.runtimeRoot(
1579
1702
  input.directory,
@@ -1638,6 +1761,16 @@ function errorMessage(error) {
1638
1761
  }
1639
1762
 
1640
1763
  // src/install/runner.ts
1764
+ var DOWNLOAD_GROUPS = [
1765
+ { categories: ["runtime-file"], phase: InstallPhases.INSTALLING_RUNTIME },
1766
+ { categories: ["client-jar"], phase: InstallPhases.DOWNLOADING_CLIENT_JAR },
1767
+ { categories: ["library"], phase: InstallPhases.DOWNLOADING_LIBRARIES },
1768
+ { categories: ["asset-index"], phase: InstallPhases.DOWNLOADING_ASSET_INDEX },
1769
+ { categories: ["asset"], phase: InstallPhases.DOWNLOADING_ASSETS },
1770
+ { categories: ["logging-config"], phase: InstallPhases.WRITING_FILES },
1771
+ { categories: ["fabric-library"], phase: InstallPhases.INSTALLING_FABRIC },
1772
+ { categories: ["forge-installer", "forge-library"], phase: InstallPhases.INSTALLING_FORGE }
1773
+ ];
1641
1774
  async function runInstall(input) {
1642
1775
  const startedAt = Date.now();
1643
1776
  let bytesDownloaded = 0;
@@ -1650,50 +1783,89 @@ async function runInstall(input) {
1650
1783
  onEvent?.({ type: "install:phase-changed", phase, previous: currentPhase });
1651
1784
  currentPhase = phase;
1652
1785
  };
1653
- const downloads = input.plan.actions.filter(isDownload);
1786
+ const checkpoint = async () => {
1787
+ if (input.signal?.aborted) {
1788
+ throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1789
+ }
1790
+ await input.pauseController?.waitWhilePaused();
1791
+ if (input.signal?.aborted) {
1792
+ throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1793
+ }
1794
+ };
1795
+ const categoryFilter = input.actionCategories;
1796
+ const downloads = input.plan.actions.filter(isDownload).filter((a) => categoryFilter ? categoryFilter.has(a.category) : true);
1654
1797
  const natives = input.plan.actions.filter(isNative);
1655
1798
  const writeActions = input.plan.actions.filter(isWrite);
1656
1799
  const processors = input.plan.actions.filter(isProcessor);
1657
1800
  enterPhase(InstallPhases.PLANNING);
1658
- enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
1659
- const limit = pLimit__default.default(input.concurrency ?? DOWNLOAD_CONCURRENCY);
1660
- await Promise.all(
1661
- downloads.map(
1662
- (action) => limit(async () => {
1663
- if (input.signal?.aborted) {
1664
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1665
- }
1666
- const result = await downloadFile(input.http, {
1667
- url: action.url,
1668
- target: action.target,
1669
- ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1670
- ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1671
- ...action.category !== void 0 ? { category: action.category } : {},
1672
- ...input.signal !== void 0 ? { signal: input.signal } : {},
1673
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
1674
- });
1675
- bytesDownloaded += result.bytesDownloaded;
1676
- if (result.skipped) actionsSkipped++;
1677
- actionsCompleted++;
1678
- })
1679
- )
1801
+ const limit = pLimit(input.concurrency ?? DOWNLOAD_CONCURRENCY);
1802
+ for (const group of DOWNLOAD_GROUPS) {
1803
+ const groupActions = downloads.filter((action) => group.categories.includes(action.category));
1804
+ if (groupActions.length === 0) continue;
1805
+ await checkpoint();
1806
+ enterPhase(group.phase);
1807
+ await Promise.all(
1808
+ groupActions.map(
1809
+ (action) => limit(async () => {
1810
+ await checkpoint();
1811
+ const result = await downloadFile(input.http, {
1812
+ url: action.url,
1813
+ target: action.target,
1814
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1815
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1816
+ ...action.category !== void 0 ? { category: action.category } : {},
1817
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
1818
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1819
+ ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1820
+ });
1821
+ bytesDownloaded += result.bytesDownloaded;
1822
+ if (result.skipped) actionsSkipped++;
1823
+ actionsCompleted++;
1824
+ })
1825
+ )
1826
+ );
1827
+ }
1828
+ const ungrouped = downloads.filter(
1829
+ (action) => !DOWNLOAD_GROUPS.some((g) => g.categories.includes(action.category))
1680
1830
  );
1831
+ if (ungrouped.length > 0) {
1832
+ await checkpoint();
1833
+ enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
1834
+ await Promise.all(
1835
+ ungrouped.map(
1836
+ (action) => limit(async () => {
1837
+ await checkpoint();
1838
+ const result = await downloadFile(input.http, {
1839
+ url: action.url,
1840
+ target: action.target,
1841
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1842
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1843
+ ...action.category !== void 0 ? { category: action.category } : {},
1844
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
1845
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1846
+ ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1847
+ });
1848
+ bytesDownloaded += result.bytesDownloaded;
1849
+ if (result.skipped) actionsSkipped++;
1850
+ actionsCompleted++;
1851
+ })
1852
+ )
1853
+ );
1854
+ }
1681
1855
  if (writeActions.length > 0) {
1856
+ await checkpoint();
1682
1857
  enterPhase(InstallPhases.WRITING_FILES);
1683
1858
  for (const action of writeActions) {
1684
- if (input.signal?.aborted) {
1685
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1686
- }
1859
+ await checkpoint();
1687
1860
  await atomicWrite(action.path, action.content);
1688
1861
  actionsCompleted++;
1689
1862
  }
1690
1863
  }
1691
1864
  if (natives.length > 0) {
1865
+ await checkpoint();
1692
1866
  enterPhase(InstallPhases.EXTRACTING_NATIVES);
1693
1867
  for (const action of natives) {
1694
- if (input.signal?.aborted) {
1695
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1696
- }
1868
+ await checkpoint();
1697
1869
  const { fileCount } = await extractAllToDir(action.source, action.destination, {
1698
1870
  excludePrefixes: action.exclude
1699
1871
  });
@@ -1707,6 +1879,7 @@ async function runInstall(input) {
1707
1879
  }
1708
1880
  }
1709
1881
  if (input.plan.target.runtime !== void 0) {
1882
+ await checkpoint();
1710
1883
  enterPhase(InstallPhases.INSTALLING_RUNTIME);
1711
1884
  const runtimePlan = await planRuntimeDownloads({
1712
1885
  runtime: input.plan.target.runtime,
@@ -1722,6 +1895,7 @@ async function runInstall(input) {
1722
1895
  });
1723
1896
  }
1724
1897
  if (processors.length > 0) {
1898
+ await checkpoint();
1725
1899
  enterPhase(InstallPhases.RUNNING_FORGE_PROCESSORS);
1726
1900
  if (input.plan.target.loader.type !== Loaders.FORGE) {
1727
1901
  throw new MinecraftKitError(
@@ -1736,9 +1910,7 @@ async function runInstall(input) {
1736
1910
  input.plan.target.runtime.installRoot
1737
1911
  );
1738
1912
  for (const action of processors) {
1739
- if (input.signal?.aborted) {
1740
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1741
- }
1913
+ await checkpoint();
1742
1914
  await runProcessor({
1743
1915
  action,
1744
1916
  javaPath,
@@ -1953,6 +2125,33 @@ function pickArguments(args, context) {
1953
2125
  };
1954
2126
  }
1955
2127
 
2128
+ // src/launch/jvm-compat.ts
2129
+ var FLAG_MIN_JAVA = [
2130
+ { prefix: "--sun-misc-unsafe-memory-access", minJava: 23 },
2131
+ { prefix: "--enable-native-access", minJava: 17 },
2132
+ { prefix: "-XX:+UseCompactObjectHeaders", minJava: 24 },
2133
+ { prefix: "-XX:+UseZGC", minJava: 15 }
2134
+ ];
2135
+ function filterArgsForJava(input) {
2136
+ if (!Number.isFinite(input.javaMajor) || input.javaMajor <= 0) return input.args;
2137
+ const out = [];
2138
+ for (const arg of input.args) {
2139
+ const incompatible = FLAG_MIN_JAVA.find(
2140
+ ({ prefix }) => arg === prefix || arg.startsWith(`${prefix}=`) || arg.startsWith(`${prefix} `)
2141
+ );
2142
+ if (incompatible && input.javaMajor < incompatible.minJava) {
2143
+ input.logger?.log(
2144
+ "warn",
2145
+ `Dropping JVM arg "${arg}" \u2014 requires Java ${incompatible.minJava}, runtime is Java ${input.javaMajor}`,
2146
+ { flag: arg, minJava: incompatible.minJava, runtimeJava: input.javaMajor }
2147
+ );
2148
+ continue;
2149
+ }
2150
+ out.push(arg);
2151
+ }
2152
+ return out;
2153
+ }
2154
+
1956
2155
  // src/launch/placeholders.ts
1957
2156
  function substituteArg(raw, values) {
1958
2157
  return raw.replaceAll(/\$\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
@@ -1992,7 +2191,13 @@ function composeArgs(input) {
1992
2191
  const baseJvm = [...memoryArgs, ...BASE_JVM_ARGS, ...macosArgs];
1993
2192
  const substitutedJvm = substituteArgs(rawJvm, input.placeholderValues);
1994
2193
  const substitutedGame = substituteArgs(rawGame, input.placeholderValues);
1995
- const jvmArgs = [...baseJvm, ...substitutedJvm];
2194
+ const javaMajor = input.target.runtime.majorVersion;
2195
+ const filteredManifestJvm = javaMajor !== void 0 ? filterArgsForJava({
2196
+ args: substitutedJvm,
2197
+ javaMajor,
2198
+ logger: input.logger ?? silentLogger
2199
+ }) : substitutedJvm;
2200
+ const jvmArgs = [...baseJvm, ...filteredManifestJvm];
1996
2201
  if (input.merged.logging?.client?.argument) {
1997
2202
  const logging = input.merged.logging.client;
1998
2203
  const loggingArg = substituteArgs([logging.argument], {
@@ -2122,8 +2327,28 @@ function mergeManifest(parent, child) {
2122
2327
  };
2123
2328
  return merged;
2124
2329
  }
2330
+ function libraryDedupeKey(library) {
2331
+ if (!library.name) return null;
2332
+ try {
2333
+ const coord = parseMavenCoordinate(library.name);
2334
+ const classifier = coord.classifier ? `:${coord.classifier}` : "";
2335
+ return `${coord.group}:${coord.artifact}${classifier}`;
2336
+ } catch {
2337
+ return null;
2338
+ }
2339
+ }
2125
2340
  function mergeLibraries(parent, child) {
2126
- return [...parent, ...child];
2341
+ const byKey = /* @__PURE__ */ new Map();
2342
+ const unkeyed = [];
2343
+ for (const lib of [...parent, ...child]) {
2344
+ const key = libraryDedupeKey(lib);
2345
+ if (key === null) {
2346
+ unkeyed.push(lib);
2347
+ continue;
2348
+ }
2349
+ byKey.set(key, lib);
2350
+ }
2351
+ return [...byKey.values(), ...unkeyed];
2127
2352
  }
2128
2353
  function mergeArguments(parent, child) {
2129
2354
  if (!parent && !child) return void 0;
@@ -2247,7 +2472,8 @@ async function composeLaunch(input) {
2247
2472
  merged: resolved.merged,
2248
2473
  options,
2249
2474
  placeholderValues,
2250
- features
2475
+ features,
2476
+ logger: input.logger ?? silentLogger
2251
2477
  });
2252
2478
  return {
2253
2479
  targetId: target.id,
@@ -2429,193 +2655,553 @@ var VerifyFileCategories = {
2429
2655
  RUNTIME_FILE: "runtime-file",
2430
2656
  LOGGING_CONFIG: "logging-config"
2431
2657
  };
2658
+ async function sha1OfFile(filePath) {
2659
+ const hash = crypto2__default.default.createHash("sha1");
2660
+ await new Promise((resolve, reject) => {
2661
+ const stream = fs$1.createReadStream(filePath);
2662
+ stream.on("data", (chunk) => hash.update(chunk));
2663
+ stream.on("end", resolve);
2664
+ stream.on("error", reject);
2665
+ });
2666
+ return hash.digest("hex");
2667
+ }
2432
2668
 
2433
- // src/repair/helpers.ts
2434
- function asResultArray(from) {
2435
- return Array.isArray(from) ? from : [from];
2669
+ // src/verify/helpers.ts
2670
+ async function runVerification(input, check) {
2671
+ const startedAt = Date.now();
2672
+ const results = [];
2673
+ const record = (result) => {
2674
+ results.push(result);
2675
+ input.onEvent?.({ type: "verify:file-checked", file: result });
2676
+ };
2677
+ await check(record);
2678
+ return {
2679
+ targetId: input.targetId,
2680
+ kind: input.kind,
2681
+ isValid: results.every((r) => r.status === VerifyFileStatuses.OK),
2682
+ issues: results.filter((r) => r.status !== VerifyFileStatuses.OK),
2683
+ checkedFiles: results.length,
2684
+ durationMs: Date.now() - startedAt
2685
+ };
2436
2686
  }
2437
- function buildIssueIndex(from) {
2438
- const map = /* @__PURE__ */ new Map();
2439
- for (const v of asResultArray(from)) {
2440
- for (const issue of v.issues) {
2441
- const set = map.get(issue.path);
2442
- if (set) set.add(issue.category);
2443
- else map.set(issue.path, /* @__PURE__ */ new Set([issue.category]));
2687
+ async function verifyHashedFile(input) {
2688
+ if (!await fileExists(input.path)) {
2689
+ return {
2690
+ path: input.path,
2691
+ category: input.category,
2692
+ status: VerifyFileStatuses.MISSING,
2693
+ ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2694
+ ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2695
+ ...input.url !== void 0 ? { url: input.url } : {}
2696
+ };
2697
+ }
2698
+ if (input.expectedSize !== void 0) {
2699
+ const size = await fileSize(input.path);
2700
+ if (size !== input.expectedSize) {
2701
+ return {
2702
+ path: input.path,
2703
+ category: input.category,
2704
+ status: VerifyFileStatuses.WRONG_SIZE,
2705
+ expectedSize: input.expectedSize,
2706
+ actualSize: size,
2707
+ ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2708
+ ...input.url !== void 0 ? { url: input.url } : {}
2709
+ };
2710
+ }
2711
+ }
2712
+ if (input.expectedSha1 !== void 0) {
2713
+ const actualSha1 = await sha1OfFile(input.path);
2714
+ if (actualSha1 !== input.expectedSha1) {
2715
+ return {
2716
+ path: input.path,
2717
+ category: input.category,
2718
+ status: VerifyFileStatuses.CORRUPT,
2719
+ expectedSha1: input.expectedSha1,
2720
+ actualSha1,
2721
+ ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2722
+ ...input.url !== void 0 ? { url: input.url } : {}
2723
+ };
2444
2724
  }
2445
2725
  }
2446
2726
  return {
2447
- has: (path13) => map.has(path13),
2448
- hasNonNative: (path13) => {
2449
- const cats = map.get(path13);
2450
- if (!cats) return false;
2451
- for (const c of cats) {
2452
- if (c !== VerifyFileCategories.NATIVE) return true;
2453
- }
2454
- return false;
2455
- },
2456
- categoriesAt: (path13) => map.get(path13) ?? /* @__PURE__ */ new Set()
2727
+ path: input.path,
2728
+ category: input.category,
2729
+ status: VerifyFileStatuses.OK,
2730
+ ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2731
+ ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2732
+ ...input.url !== void 0 ? { url: input.url } : {}
2457
2733
  };
2458
2734
  }
2459
- function sumDownloadBytes(actions) {
2460
- return actions.reduce((sum, action) => {
2461
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2462
- return sum + (action.expectedSize ?? 0);
2463
- }
2464
- return sum;
2465
- }, 0);
2466
- }
2467
- function buildRepairPlan(target, actions) {
2735
+ async function verifyExistence(input) {
2736
+ if (await fileExists(input.path)) {
2737
+ return {
2738
+ path: input.path,
2739
+ category: input.category,
2740
+ status: VerifyFileStatuses.OK,
2741
+ ...input.url !== void 0 ? { url: input.url } : {}
2742
+ };
2743
+ }
2468
2744
  return {
2469
- targetId: target.id,
2470
- directory: target.directory,
2471
- target,
2472
- actions,
2473
- totalActions: actions.length,
2474
- totalBytes: sumDownloadBytes(actions)
2745
+ path: input.path,
2746
+ category: input.category,
2747
+ status: VerifyFileStatuses.MISSING,
2748
+ ...input.url !== void 0 ? { url: input.url } : {}
2475
2749
  };
2476
2750
  }
2477
- async function planAspectRepair(input, aspectFilter, postprocess) {
2478
- const installPlan = await planInstall({
2479
- target: input.target,
2480
- http: input.http,
2481
- cache: input.cache,
2482
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2483
- });
2484
- const issues = buildIssueIndex(input.from);
2485
- const actions = selectRepairActions({
2486
- target: input.target,
2487
- installPlan,
2488
- issues,
2489
- aspectFilter
2490
- });
2491
- postprocess?.({ actions, installPlan, issues });
2492
- return buildRepairPlan(input.target, actions);
2493
- }
2494
- function selectRepairActions(input) {
2495
- const matching = [];
2496
- for (const action of input.installPlan.actions) {
2497
- if (!input.aspectFilter(action)) continue;
2498
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2499
- if (input.issues.hasNonNative(action.target)) {
2500
- matching.push(action);
2501
- }
2502
- } else if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2503
- if (input.issues.has(action.path)) {
2504
- matching.push(action);
2505
- }
2506
- } else if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
2507
- if (input.issues.has(action.source)) {
2508
- matching.push(action);
2509
- }
2510
- } else {
2511
- matching.push(action);
2751
+ async function findForgeVersionJsonPath(directory, minecraftVersion) {
2752
+ const versionsDir = targetPaths.versionsDir(directory);
2753
+ const dirs = await listChildDirectories(versionsDir);
2754
+ for (const id of dirs) {
2755
+ if (!id.startsWith(`${minecraftVersion}-forge-`)) continue;
2756
+ const jsonPath = targetPaths.versionJson(directory, id);
2757
+ if (!await fileExists(jsonPath)) {
2758
+ return jsonPath;
2512
2759
  }
2760
+ const parsed = await tryParseInheritsFrom(jsonPath);
2761
+ if (parsed === minecraftVersion) return jsonPath;
2762
+ }
2763
+ return null;
2764
+ }
2765
+ async function tryParseInheritsFrom(jsonPath) {
2766
+ try {
2767
+ const parsed = JSON.parse(await readText(jsonPath));
2768
+ return parsed.inheritsFrom;
2769
+ } catch {
2770
+ return void 0;
2513
2771
  }
2514
- return matching;
2515
2772
  }
2516
2773
 
2517
- // src/repair/fabric.ts
2518
- async function planFabricRepair(input) {
2774
+ // src/verify/fabric.ts
2775
+ async function verifyFabric(input) {
2519
2776
  if (input.target.loader.type !== Loaders.FABRIC) {
2520
2777
  throw new MinecraftKitError(
2521
2778
  "INVALID_INPUT",
2522
- `repair.fabric requires a Fabric target (got ${input.target.loader.type})`
2779
+ `verify.fabric requires a Fabric target (got ${input.target.loader.type})`
2523
2780
  );
2524
2781
  }
2525
- const fabricJsonPath = targetPaths.versionJson(
2526
- input.target.directory,
2527
- input.target.loader.profile.id
2528
- );
2529
- return planAspectRepair(input, (action) => {
2530
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2531
- return action.category === "fabric-library";
2532
- }
2533
- if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2534
- return action.path === fabricJsonPath;
2782
+ const loader = input.target.loader;
2783
+ return runVerification(
2784
+ {
2785
+ targetId: input.target.id,
2786
+ kind: VerificationKinds.FABRIC,
2787
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2788
+ },
2789
+ async (record) => {
2790
+ record(
2791
+ await verifyExistence({
2792
+ path: targetPaths.versionJson(input.target.directory, loader.profile.id),
2793
+ category: VerifyFileCategories.LOADER_LIBRARY
2794
+ })
2795
+ );
2796
+ const fabricLibraries = planLibraryDownloads({
2797
+ libraries: loader.profile.libraries,
2798
+ directory: input.target.directory,
2799
+ system: input.target.runtime.system,
2800
+ versionId: input.target.minecraft.version,
2801
+ category: "fabric-library"
2802
+ });
2803
+ for (const action of fabricLibraries.downloads) {
2804
+ record(
2805
+ await verifyHashedFile({
2806
+ path: action.target,
2807
+ expectedSha1: action.expectedSha1,
2808
+ expectedSize: action.expectedSize,
2809
+ ...action.url ? { url: action.url } : {},
2810
+ category: VerifyFileCategories.LOADER_LIBRARY
2811
+ })
2812
+ );
2813
+ }
2535
2814
  }
2536
- return false;
2537
- });
2815
+ );
2538
2816
  }
2539
2817
 
2540
- // src/repair/forge.ts
2541
- var FORGE_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
2542
- "forge-library",
2543
- "forge-installer"
2544
- ]);
2545
- async function planForgeRepair(input) {
2818
+ // src/verify/forge.ts
2819
+ async function verifyForge(input) {
2546
2820
  if (input.target.loader.type !== Loaders.FORGE) {
2547
2821
  throw new MinecraftKitError(
2548
2822
  "INVALID_INPUT",
2549
- `repair.forge requires a Forge target (got ${input.target.loader.type})`
2823
+ `verify.forge requires a Forge target (got ${input.target.loader.type})`
2550
2824
  );
2551
2825
  }
2552
- const forgeJsonPath = targetPaths.versionJson(
2553
- input.target.directory,
2554
- input.target.loader.fullVersion
2555
- );
2556
- return planAspectRepair(
2557
- input,
2558
- (action) => {
2559
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2560
- return FORGE_DOWNLOAD_CATEGORIES.has(action.category);
2561
- }
2562
- if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2563
- return action.path === forgeJsonPath;
2564
- }
2565
- return false;
2826
+ return runVerification(
2827
+ {
2828
+ targetId: input.target.id,
2829
+ kind: VerificationKinds.FORGE,
2830
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2566
2831
  },
2567
- ({ actions, installPlan, issues }) => {
2568
- if (!issues.has(forgeJsonPath)) return;
2569
- const alreadyIncluded = new Set(
2570
- actions.filter((a) => a.kind === InstallActionKinds.DOWNLOAD_FILE).map((a) => a.target)
2832
+ async (record) => {
2833
+ const forgeVersionJsonPath = await findForgeVersionJsonPath(
2834
+ input.target.directory,
2835
+ input.target.minecraft.version
2571
2836
  );
2572
- for (const action of installPlan.actions) {
2573
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "forge-library" && !alreadyIncluded.has(action.target)) {
2574
- actions.push(action);
2575
- } else if (action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR) {
2576
- actions.push(action);
2577
- }
2837
+ if (forgeVersionJsonPath === null) return;
2838
+ record(
2839
+ await verifyExistence({
2840
+ path: forgeVersionJsonPath,
2841
+ category: VerifyFileCategories.LOADER_LIBRARY
2842
+ })
2843
+ );
2844
+ if (!await fileExists(forgeVersionJsonPath)) return;
2845
+ let parsed;
2846
+ try {
2847
+ parsed = JSON.parse(await readText(forgeVersionJsonPath));
2848
+ } catch {
2849
+ record({
2850
+ path: forgeVersionJsonPath,
2851
+ category: VerifyFileCategories.LOADER_LIBRARY,
2852
+ status: VerifyFileStatuses.CORRUPT
2853
+ });
2854
+ return;
2855
+ }
2856
+ const forgeLibraries = planLibraryDownloads({
2857
+ libraries: parsed.libraries,
2858
+ directory: input.target.directory,
2859
+ system: input.target.runtime.system,
2860
+ versionId: input.target.minecraft.version,
2861
+ category: "forge-library"
2862
+ });
2863
+ for (const action of forgeLibraries.downloads) {
2864
+ record(
2865
+ await verifyHashedFile({
2866
+ path: action.target,
2867
+ expectedSha1: action.expectedSha1,
2868
+ expectedSize: action.expectedSize,
2869
+ ...action.url ? { url: action.url } : {},
2870
+ category: VerifyFileCategories.LOADER_LIBRARY
2871
+ })
2872
+ );
2578
2873
  }
2579
2874
  }
2580
2875
  );
2581
2876
  }
2582
2877
 
2583
- // src/repair/minecraft.ts
2584
- var MINECRAFT_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
2585
- "client-jar",
2586
- "library",
2587
- "asset-index",
2588
- "asset",
2589
- "logging-config"
2590
- ]);
2591
- async function planMinecraftRepair(input) {
2592
- const vanillaJsonPath = targetPaths.versionJson(
2593
- input.target.directory,
2594
- input.target.minecraft.version
2595
- );
2596
- return planAspectRepair(input, (action) => {
2597
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2598
- return MINECRAFT_DOWNLOAD_CATEGORIES.has(action.category);
2599
- }
2600
- if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2601
- return action.path === vanillaJsonPath;
2602
- }
2603
- if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
2604
- return true;
2878
+ // src/verify/minecraft.ts
2879
+ async function verifyMinecraft(input) {
2880
+ return runVerification(
2881
+ {
2882
+ targetId: input.target.id,
2883
+ kind: VerificationKinds.MINECRAFT,
2884
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2885
+ },
2886
+ async (record) => {
2887
+ const { directory, minecraft, runtime } = input.target;
2888
+ record(
2889
+ await verifyHashedFile({
2890
+ path: targetPaths.versionJar(directory, minecraft.version),
2891
+ expectedSha1: minecraft.manifest.downloads.client.sha1,
2892
+ expectedSize: minecraft.manifest.downloads.client.size,
2893
+ url: minecraft.manifest.downloads.client.url,
2894
+ category: VerifyFileCategories.CLIENT_JAR
2895
+ })
2896
+ );
2897
+ record(
2898
+ await verifyExistence({
2899
+ path: targetPaths.versionJson(directory, minecraft.version),
2900
+ category: VerifyFileCategories.CLIENT_JAR
2901
+ })
2902
+ );
2903
+ if (minecraft.manifest.logging?.client) {
2904
+ const logging = minecraft.manifest.logging.client;
2905
+ record(
2906
+ await verifyHashedFile({
2907
+ path: targetPaths.loggingConfig(directory, logging.file.id),
2908
+ expectedSha1: logging.file.sha1,
2909
+ expectedSize: logging.file.size,
2910
+ url: logging.file.url,
2911
+ category: VerifyFileCategories.LOGGING_CONFIG
2912
+ })
2913
+ );
2914
+ }
2915
+ const libraryPlan = planLibraryDownloads({
2916
+ libraries: minecraft.manifest.libraries,
2917
+ directory,
2918
+ system: runtime.system,
2919
+ versionId: minecraft.version,
2920
+ category: "library"
2921
+ });
2922
+ for (const action of libraryPlan.downloads) {
2923
+ record(
2924
+ await verifyHashedFile({
2925
+ path: action.target,
2926
+ expectedSha1: action.expectedSha1,
2927
+ expectedSize: action.expectedSize,
2928
+ url: action.url,
2929
+ category: VerifyFileCategories.LIBRARY
2930
+ })
2931
+ );
2932
+ }
2933
+ const indexUrl = minecraft.manifest.assetIndex.url;
2934
+ const indexPath = targetPaths.assetIndex(directory, minecraft.manifest.assetIndex.id);
2935
+ record(
2936
+ await verifyHashedFile({
2937
+ path: indexPath,
2938
+ expectedSha1: minecraft.manifest.assetIndex.sha1,
2939
+ expectedSize: minecraft.manifest.assetIndex.size,
2940
+ url: indexUrl,
2941
+ category: VerifyFileCategories.ASSET_INDEX
2942
+ })
2943
+ );
2944
+ const indexDocument = await fetchJson(input.http, input.cache, {
2945
+ url: indexUrl,
2946
+ cacheKey: `asset-index:${minecraft.manifest.assetIndex.id}:${minecraft.manifest.assetIndex.sha1}`,
2947
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
2948
+ });
2949
+ const seenAssetHashes = /* @__PURE__ */ new Set();
2950
+ for (const entry of Object.values(indexDocument.objects)) {
2951
+ if (seenAssetHashes.has(entry.hash)) continue;
2952
+ seenAssetHashes.add(entry.hash);
2953
+ record(
2954
+ await verifyHashedFile({
2955
+ path: targetPaths.assetObject(directory, entry.hash),
2956
+ expectedSha1: entry.hash,
2957
+ expectedSize: entry.size,
2958
+ category: VerifyFileCategories.ASSET
2959
+ })
2960
+ );
2961
+ }
2962
+ const nativesDir = targetPaths.nativesDir(directory, minecraft.version);
2963
+ if (!await fileExists(nativesDir)) {
2964
+ for (const extraction of libraryPlan.nativeExtractions) {
2965
+ record({
2966
+ path: extraction.source,
2967
+ category: VerifyFileCategories.NATIVE,
2968
+ status: VerifyFileStatuses.MISSING
2969
+ });
2970
+ }
2971
+ }
2605
2972
  }
2606
- return false;
2607
- });
2973
+ );
2608
2974
  }
2609
-
2610
- // src/repair/runner.ts
2611
- async function runRepair(input) {
2612
- const report = await runInstall({
2613
- plan: {
2614
- ...input.plan,
2615
- totalActions: input.plan.actions.length,
2616
- totalBytes: input.plan.totalBytes
2975
+ async function verifyRuntime(input) {
2976
+ return runVerification(
2977
+ {
2978
+ targetId: input.target.id,
2979
+ kind: VerificationKinds.RUNTIME,
2980
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2617
2981
  },
2618
- http: input.http,
2982
+ async (record) => {
2983
+ let manifest;
2984
+ try {
2985
+ manifest = await fetchJson(input.http, input.cache, {
2986
+ url: input.target.runtime.manifestUrl,
2987
+ cacheKey: `runtime-manifest:${input.target.runtime.component}:${input.target.runtime.platformKey}:${input.target.runtime.manifestSha1}`,
2988
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
2989
+ });
2990
+ } catch {
2991
+ record({
2992
+ path: input.target.runtime.manifestUrl,
2993
+ category: VerifyFileCategories.RUNTIME_FILE,
2994
+ status: VerifyFileStatuses.MISSING
2995
+ });
2996
+ return;
2997
+ }
2998
+ const runtimeRoot = targetPaths.runtimeRoot(
2999
+ input.target.directory,
3000
+ input.target.runtime.component,
3001
+ input.target.runtime.installRoot
3002
+ );
3003
+ for (const [relative, entry] of Object.entries(manifest.files)) {
3004
+ if (entry.type !== "file") continue;
3005
+ record(
3006
+ await verifyHashedFile({
3007
+ path: path__default.default.join(runtimeRoot, relative),
3008
+ expectedSha1: entry.downloads.raw.sha1,
3009
+ expectedSize: entry.downloads.raw.size,
3010
+ url: entry.downloads.raw.url,
3011
+ category: VerifyFileCategories.RUNTIME_FILE
3012
+ })
3013
+ );
3014
+ }
3015
+ }
3016
+ );
3017
+ }
3018
+
3019
+ // src/repair/helpers.ts
3020
+ function asResultArray(from) {
3021
+ return Array.isArray(from) ? from : [from];
3022
+ }
3023
+ function buildIssueIndex(from) {
3024
+ const map = /* @__PURE__ */ new Map();
3025
+ for (const v of asResultArray(from)) {
3026
+ for (const issue of v.issues) {
3027
+ const set = map.get(issue.path);
3028
+ if (set) set.add(issue.category);
3029
+ else map.set(issue.path, /* @__PURE__ */ new Set([issue.category]));
3030
+ }
3031
+ }
3032
+ return {
3033
+ has: (path13) => map.has(path13),
3034
+ hasNonNative: (path13) => {
3035
+ const cats = map.get(path13);
3036
+ if (!cats) return false;
3037
+ for (const c of cats) {
3038
+ if (c !== VerifyFileCategories.NATIVE) return true;
3039
+ }
3040
+ return false;
3041
+ },
3042
+ categoriesAt: (path13) => map.get(path13) ?? /* @__PURE__ */ new Set()
3043
+ };
3044
+ }
3045
+ function sumDownloadBytes(actions) {
3046
+ return actions.reduce((sum, action) => {
3047
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3048
+ return sum + (action.expectedSize ?? 0);
3049
+ }
3050
+ return sum;
3051
+ }, 0);
3052
+ }
3053
+ function buildRepairPlan(target, actions) {
3054
+ return {
3055
+ targetId: target.id,
3056
+ directory: target.directory,
3057
+ target,
3058
+ actions,
3059
+ totalActions: actions.length,
3060
+ totalBytes: sumDownloadBytes(actions)
3061
+ };
3062
+ }
3063
+ async function planAspectRepair(input, aspectFilter, postprocess) {
3064
+ const installPlan = await planInstall({
3065
+ target: input.target,
3066
+ http: input.http,
3067
+ cache: input.cache,
3068
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3069
+ });
3070
+ const issues = buildIssueIndex(input.from);
3071
+ const actions = selectRepairActions({
3072
+ target: input.target,
3073
+ installPlan,
3074
+ issues,
3075
+ aspectFilter
3076
+ });
3077
+ postprocess?.({ actions, installPlan, issues });
3078
+ return buildRepairPlan(input.target, actions);
3079
+ }
3080
+ function selectRepairActions(input) {
3081
+ const matching = [];
3082
+ for (const action of input.installPlan.actions) {
3083
+ if (!input.aspectFilter(action)) continue;
3084
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3085
+ if (input.issues.hasNonNative(action.target)) {
3086
+ matching.push(action);
3087
+ }
3088
+ } else if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3089
+ if (input.issues.has(action.path)) {
3090
+ matching.push(action);
3091
+ }
3092
+ } else if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
3093
+ if (input.issues.has(action.source)) {
3094
+ matching.push(action);
3095
+ }
3096
+ } else {
3097
+ matching.push(action);
3098
+ }
3099
+ }
3100
+ return matching;
3101
+ }
3102
+
3103
+ // src/repair/fabric.ts
3104
+ async function planFabricRepair(input) {
3105
+ if (input.target.loader.type !== Loaders.FABRIC) {
3106
+ throw new MinecraftKitError(
3107
+ "INVALID_INPUT",
3108
+ `repair.fabric requires a Fabric target (got ${input.target.loader.type})`
3109
+ );
3110
+ }
3111
+ const fabricJsonPath = targetPaths.versionJson(
3112
+ input.target.directory,
3113
+ input.target.loader.profile.id
3114
+ );
3115
+ return planAspectRepair(input, (action) => {
3116
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3117
+ return action.category === "fabric-library";
3118
+ }
3119
+ if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3120
+ return action.path === fabricJsonPath;
3121
+ }
3122
+ return false;
3123
+ });
3124
+ }
3125
+
3126
+ // src/repair/forge.ts
3127
+ var FORGE_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
3128
+ "forge-library",
3129
+ "forge-installer"
3130
+ ]);
3131
+ async function planForgeRepair(input) {
3132
+ if (input.target.loader.type !== Loaders.FORGE) {
3133
+ throw new MinecraftKitError(
3134
+ "INVALID_INPUT",
3135
+ `repair.forge requires a Forge target (got ${input.target.loader.type})`
3136
+ );
3137
+ }
3138
+ const forgeJsonPath = targetPaths.versionJson(
3139
+ input.target.directory,
3140
+ input.target.loader.fullVersion
3141
+ );
3142
+ return planAspectRepair(
3143
+ input,
3144
+ (action) => {
3145
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3146
+ return FORGE_DOWNLOAD_CATEGORIES.has(action.category);
3147
+ }
3148
+ if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3149
+ return action.path === forgeJsonPath;
3150
+ }
3151
+ return false;
3152
+ },
3153
+ ({ actions, installPlan, issues }) => {
3154
+ if (!issues.has(forgeJsonPath)) return;
3155
+ const alreadyIncluded = new Set(
3156
+ actions.filter((a) => a.kind === InstallActionKinds.DOWNLOAD_FILE).map((a) => a.target)
3157
+ );
3158
+ for (const action of installPlan.actions) {
3159
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "forge-library" && !alreadyIncluded.has(action.target)) {
3160
+ actions.push(action);
3161
+ } else if (action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR) {
3162
+ actions.push(action);
3163
+ }
3164
+ }
3165
+ }
3166
+ );
3167
+ }
3168
+
3169
+ // src/repair/minecraft.ts
3170
+ var MINECRAFT_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
3171
+ "client-jar",
3172
+ "library",
3173
+ "asset-index",
3174
+ "asset",
3175
+ "logging-config"
3176
+ ]);
3177
+ async function planMinecraftRepair(input) {
3178
+ const vanillaJsonPath = targetPaths.versionJson(
3179
+ input.target.directory,
3180
+ input.target.minecraft.version
3181
+ );
3182
+ return planAspectRepair(input, (action) => {
3183
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3184
+ return MINECRAFT_DOWNLOAD_CATEGORIES.has(action.category);
3185
+ }
3186
+ if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3187
+ return action.path === vanillaJsonPath;
3188
+ }
3189
+ if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
3190
+ return true;
3191
+ }
3192
+ return false;
3193
+ });
3194
+ }
3195
+
3196
+ // src/repair/runner.ts
3197
+ async function runRepair(input) {
3198
+ const report = await runInstall({
3199
+ plan: {
3200
+ ...input.plan,
3201
+ totalActions: input.plan.actions.length,
3202
+ totalBytes: input.plan.totalBytes
3203
+ },
3204
+ http: input.http,
2619
3205
  cache: input.cache,
2620
3206
  spawner: input.spawner,
2621
3207
  ...input.signal !== void 0 ? { signal: input.signal } : {},
@@ -2637,6 +3223,58 @@ async function planRuntimeRepair(input) {
2637
3223
  );
2638
3224
  }
2639
3225
 
3226
+ // src/repair/all.ts
3227
+ async function repairAll(input) {
3228
+ const startedAt = Date.now();
3229
+ const ctx = {
3230
+ target: input.target,
3231
+ http: input.http,
3232
+ cache: input.cache,
3233
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3234
+ };
3235
+ const verifications = [];
3236
+ const mc = await verifyMinecraft(ctx);
3237
+ verifications.push(mc);
3238
+ const rt = await verifyRuntime(ctx);
3239
+ verifications.push(rt);
3240
+ if (input.target.loader.type === Loaders.FABRIC) {
3241
+ verifications.push(await verifyFabric(ctx));
3242
+ } else if (input.target.loader.type === Loaders.FORGE) {
3243
+ verifications.push(await verifyForge(ctx));
3244
+ }
3245
+ const repairs = /* @__PURE__ */ new Map();
3246
+ let bytesDownloaded = 0;
3247
+ for (const verification of verifications) {
3248
+ if (verification.isValid) continue;
3249
+ const planner = PLANNERS[verification.kind];
3250
+ if (!planner) continue;
3251
+ const plan = await planner({ ...ctx, from: verification });
3252
+ if (plan.totalActions === 0) continue;
3253
+ const report = await runRepair({
3254
+ plan,
3255
+ http: input.http,
3256
+ cache: input.cache,
3257
+ spawner: input.spawner,
3258
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
3259
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3260
+ });
3261
+ repairs.set(verification.kind, report);
3262
+ bytesDownloaded += report.bytesDownloaded;
3263
+ }
3264
+ return {
3265
+ verifications,
3266
+ repairs,
3267
+ bytesDownloaded,
3268
+ durationMs: Date.now() - startedAt
3269
+ };
3270
+ }
3271
+ var PLANNERS = {
3272
+ minecraft: planMinecraftRepair,
3273
+ runtime: planRuntimeRepair,
3274
+ fabric: planFabricRepair,
3275
+ forge: planForgeRepair
3276
+ };
3277
+
2640
3278
  // src/types/runtime.ts
2641
3279
  var RuntimeComponents = {
2642
3280
  JRE_LEGACY: "jre-legacy",
@@ -2711,485 +3349,125 @@ var TargetsApi = class {
2711
3349
  const runtime = input.runtime?.installRoot !== void 0 ? { ...resolvedRuntime, installRoot: input.runtime.installRoot } : resolvedRuntime;
2712
3350
  let loader;
2713
3351
  if (input.loader.type === Loaders.VANILLA) {
2714
- loader = {
2715
- type: Loaders.VANILLA,
2716
- minecraftVersion: minecraft.version,
2717
- minecraft
2718
- };
2719
- } else if (input.loader.type === Loaders.FABRIC) {
2720
- loader = await this.ctx.fabric.resolve({
2721
- minecraftVersion: minecraft.version,
2722
- ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
2723
- ...input.loader.version !== void 0 ? { loaderVersion: input.loader.version } : {},
2724
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2725
- });
2726
- } else {
2727
- loader = await this.ctx.forge.resolve({
2728
- minecraftVersion: minecraft.version,
2729
- ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
2730
- ...input.loader.version !== void 0 ? { forgeVersion: input.loader.version } : {},
2731
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2732
- });
2733
- }
2734
- return this.create({
2735
- id: input.id,
2736
- directory: input.directory,
2737
- minecraft,
2738
- loader,
2739
- runtime
2740
- });
2741
- }
2742
- /** Scan a root directory for Minecraft installations. Returns only what is on disk. */
2743
- async list(input) {
2744
- if (!await dirExists(input.rootDir)) return [];
2745
- const subdirs = await listChildDirectories(input.rootDir);
2746
- const results = [];
2747
- for (const id of subdirs) {
2748
- const directory = path__default.default.join(input.rootDir, id);
2749
- const discovered = await discoverInstallation(id, directory);
2750
- if (discovered) results.push(discovered);
2751
- }
2752
- return results;
2753
- }
2754
- };
2755
- async function discoverInstallation(id, directory) {
2756
- const versionsDir = path__default.default.join(directory, VERSIONS_DIR);
2757
- const librariesDir = path__default.default.join(directory, LIBRARIES_DIR);
2758
- const assetsDir = path__default.default.join(directory, ASSETS_DIR);
2759
- const looksLikeInstall = await dirExists(versionsDir) && (await dirExists(librariesDir) || await dirExists(assetsDir));
2760
- if (!looksLikeInstall) return null;
2761
- const versionDirs = await listChildDirectories(versionsDir);
2762
- const minecraftVersions = [];
2763
- const loaders = [];
2764
- for (const versionId of versionDirs) {
2765
- const hint = inferLoaderFromVersionId(versionId);
2766
- if (hint) {
2767
- loaders.push(hint);
2768
- if (hint.minecraftVersion && !minecraftVersions.includes(hint.minecraftVersion)) {
2769
- minecraftVersions.push(hint.minecraftVersion);
2770
- }
2771
- } else {
2772
- minecraftVersions.push(versionId);
2773
- }
2774
- }
2775
- const runtime = await discoverRuntime(directory);
2776
- return { id, directory, minecraftVersions, loaders, ...runtime ? { runtime } : {} };
2777
- }
2778
- async function discoverRuntime(directory) {
2779
- const runtimeDir = path__default.default.join(directory, RUNTIMES_DIR);
2780
- if (!await dirExists(runtimeDir)) return void 0;
2781
- let components;
2782
- try {
2783
- components = await listChildDirectories(runtimeDir);
2784
- } catch {
2785
- return void 0;
2786
- }
2787
- for (const component of components) {
2788
- const root = path__default.default.join(runtimeDir, component);
2789
- const javaPath = process.platform === "win32" ? path__default.default.join(root, "bin", "javaw.exe") : process.platform === "darwin" ? path__default.default.join(root, "jre.bundle", "Contents", "Home", "bin", "java") : path__default.default.join(root, "bin", "java");
2790
- if (await fileExists(javaPath)) {
2791
- return { component, javaPath };
2792
- }
2793
- }
2794
- return void 0;
2795
- }
2796
- function inferLoaderFromVersionId(versionId) {
2797
- const fabricMatch = /^fabric-loader-([^-]+)-(.+)$/.exec(versionId);
2798
- if (fabricMatch?.[1] && fabricMatch[2]) {
2799
- return { type: Loaders.FABRIC, version: fabricMatch[1], minecraftVersion: fabricMatch[2] };
2800
- }
2801
- const forgeMatch = /^([^-]+)-forge-(.+)$/.exec(versionId);
2802
- if (forgeMatch?.[1] && forgeMatch[2]) {
2803
- return { type: Loaders.FORGE, minecraftVersion: forgeMatch[1], version: forgeMatch[2] };
2804
- }
2805
- return null;
2806
- }
2807
-
2808
- // src/update/runner.ts
2809
- async function planUpdate(input) {
2810
- return planInstall({
2811
- target: input.target,
2812
- http: input.http,
2813
- cache: input.cache,
2814
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2815
- });
2816
- }
2817
- async function runUpdate(input) {
2818
- const report = await runInstall({
2819
- plan: input.plan,
2820
- http: input.http,
2821
- cache: input.cache,
2822
- spawner: input.spawner,
2823
- ...input.signal !== void 0 ? { signal: input.signal } : {},
2824
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2825
- });
2826
- return {
2827
- targetId: report.targetId,
2828
- bytesDownloaded: report.bytesDownloaded,
2829
- actionsCompleted: report.actionsCompleted,
2830
- actionsSkipped: report.actionsSkipped,
2831
- durationMs: report.durationMs
2832
- };
2833
- }
2834
- async function sha1OfFile(filePath) {
2835
- const hash = crypto2__default.default.createHash("sha1");
2836
- await new Promise((resolve, reject) => {
2837
- const stream = fs$1.createReadStream(filePath);
2838
- stream.on("data", (chunk) => hash.update(chunk));
2839
- stream.on("end", resolve);
2840
- stream.on("error", reject);
2841
- });
2842
- return hash.digest("hex");
2843
- }
2844
-
2845
- // src/verify/helpers.ts
2846
- async function runVerification(input, check) {
2847
- const startedAt = Date.now();
2848
- const results = [];
2849
- const record = (result) => {
2850
- results.push(result);
2851
- input.onEvent?.({ type: "verify:file-checked", file: result });
2852
- };
2853
- await check(record);
2854
- return {
2855
- targetId: input.targetId,
2856
- kind: input.kind,
2857
- isValid: results.every((r) => r.status === VerifyFileStatuses.OK),
2858
- issues: results.filter((r) => r.status !== VerifyFileStatuses.OK),
2859
- checkedFiles: results.length,
2860
- durationMs: Date.now() - startedAt
2861
- };
2862
- }
2863
- async function verifyHashedFile(input) {
2864
- if (!await fileExists(input.path)) {
2865
- return {
2866
- path: input.path,
2867
- category: input.category,
2868
- status: VerifyFileStatuses.MISSING,
2869
- ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2870
- ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2871
- ...input.url !== void 0 ? { url: input.url } : {}
2872
- };
2873
- }
2874
- if (input.expectedSize !== void 0) {
2875
- const size = await fileSize(input.path);
2876
- if (size !== input.expectedSize) {
2877
- return {
2878
- path: input.path,
2879
- category: input.category,
2880
- status: VerifyFileStatuses.WRONG_SIZE,
2881
- expectedSize: input.expectedSize,
2882
- actualSize: size,
2883
- ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2884
- ...input.url !== void 0 ? { url: input.url } : {}
2885
- };
2886
- }
2887
- }
2888
- if (input.expectedSha1 !== void 0) {
2889
- const actualSha1 = await sha1OfFile(input.path);
2890
- if (actualSha1 !== input.expectedSha1) {
2891
- return {
2892
- path: input.path,
2893
- category: input.category,
2894
- status: VerifyFileStatuses.CORRUPT,
2895
- expectedSha1: input.expectedSha1,
2896
- actualSha1,
2897
- ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2898
- ...input.url !== void 0 ? { url: input.url } : {}
2899
- };
2900
- }
2901
- }
2902
- return {
2903
- path: input.path,
2904
- category: input.category,
2905
- status: VerifyFileStatuses.OK,
2906
- ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2907
- ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2908
- ...input.url !== void 0 ? { url: input.url } : {}
2909
- };
2910
- }
2911
- async function verifyExistence(input) {
2912
- if (await fileExists(input.path)) {
2913
- return {
2914
- path: input.path,
2915
- category: input.category,
2916
- status: VerifyFileStatuses.OK,
2917
- ...input.url !== void 0 ? { url: input.url } : {}
2918
- };
2919
- }
2920
- return {
2921
- path: input.path,
2922
- category: input.category,
2923
- status: VerifyFileStatuses.MISSING,
2924
- ...input.url !== void 0 ? { url: input.url } : {}
2925
- };
2926
- }
2927
- async function findForgeVersionJsonPath(directory, minecraftVersion) {
2928
- const versionsDir = targetPaths.versionsDir(directory);
2929
- const dirs = await listChildDirectories(versionsDir);
2930
- for (const id of dirs) {
2931
- if (!id.startsWith(`${minecraftVersion}-forge-`)) continue;
2932
- const jsonPath = targetPaths.versionJson(directory, id);
2933
- if (!await fileExists(jsonPath)) {
2934
- return jsonPath;
2935
- }
2936
- const parsed = await tryParseInheritsFrom(jsonPath);
2937
- if (parsed === minecraftVersion) return jsonPath;
2938
- }
2939
- return null;
2940
- }
2941
- async function tryParseInheritsFrom(jsonPath) {
2942
- try {
2943
- const parsed = JSON.parse(await readText(jsonPath));
2944
- return parsed.inheritsFrom;
2945
- } catch {
2946
- return void 0;
2947
- }
2948
- }
2949
-
2950
- // src/verify/fabric.ts
2951
- async function verifyFabric(input) {
2952
- if (input.target.loader.type !== Loaders.FABRIC) {
2953
- throw new MinecraftKitError(
2954
- "INVALID_INPUT",
2955
- `verify.fabric requires a Fabric target (got ${input.target.loader.type})`
2956
- );
2957
- }
2958
- const loader = input.target.loader;
2959
- return runVerification(
2960
- {
2961
- targetId: input.target.id,
2962
- kind: VerificationKinds.FABRIC,
2963
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2964
- },
2965
- async (record) => {
2966
- record(
2967
- await verifyExistence({
2968
- path: targetPaths.versionJson(input.target.directory, loader.profile.id),
2969
- category: VerifyFileCategories.LOADER_LIBRARY
2970
- })
2971
- );
2972
- const fabricLibraries = planLibraryDownloads({
2973
- libraries: loader.profile.libraries,
2974
- directory: input.target.directory,
2975
- system: input.target.runtime.system,
2976
- versionId: input.target.minecraft.version,
2977
- category: "fabric-library"
2978
- });
2979
- for (const action of fabricLibraries.downloads) {
2980
- record(
2981
- await verifyHashedFile({
2982
- path: action.target,
2983
- expectedSha1: action.expectedSha1,
2984
- expectedSize: action.expectedSize,
2985
- ...action.url ? { url: action.url } : {},
2986
- category: VerifyFileCategories.LOADER_LIBRARY
2987
- })
2988
- );
2989
- }
2990
- }
2991
- );
2992
- }
2993
-
2994
- // src/verify/forge.ts
2995
- async function verifyForge(input) {
2996
- if (input.target.loader.type !== Loaders.FORGE) {
2997
- throw new MinecraftKitError(
2998
- "INVALID_INPUT",
2999
- `verify.forge requires a Forge target (got ${input.target.loader.type})`
3000
- );
3001
- }
3002
- return runVerification(
3003
- {
3004
- targetId: input.target.id,
3005
- kind: VerificationKinds.FORGE,
3006
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3007
- },
3008
- async (record) => {
3009
- const forgeVersionJsonPath = await findForgeVersionJsonPath(
3010
- input.target.directory,
3011
- input.target.minecraft.version
3012
- );
3013
- if (forgeVersionJsonPath === null) return;
3014
- record(
3015
- await verifyExistence({
3016
- path: forgeVersionJsonPath,
3017
- category: VerifyFileCategories.LOADER_LIBRARY
3018
- })
3019
- );
3020
- if (!await fileExists(forgeVersionJsonPath)) return;
3021
- let parsed;
3022
- try {
3023
- parsed = JSON.parse(await readText(forgeVersionJsonPath));
3024
- } catch {
3025
- record({
3026
- path: forgeVersionJsonPath,
3027
- category: VerifyFileCategories.LOADER_LIBRARY,
3028
- status: VerifyFileStatuses.CORRUPT
3029
- });
3030
- return;
3031
- }
3032
- const forgeLibraries = planLibraryDownloads({
3033
- libraries: parsed.libraries,
3034
- directory: input.target.directory,
3035
- system: input.target.runtime.system,
3036
- versionId: input.target.minecraft.version,
3037
- category: "forge-library"
3038
- });
3039
- for (const action of forgeLibraries.downloads) {
3040
- record(
3041
- await verifyHashedFile({
3042
- path: action.target,
3043
- expectedSha1: action.expectedSha1,
3044
- expectedSize: action.expectedSize,
3045
- ...action.url ? { url: action.url } : {},
3046
- category: VerifyFileCategories.LOADER_LIBRARY
3047
- })
3048
- );
3049
- }
3050
- }
3051
- );
3052
- }
3053
-
3054
- // src/verify/minecraft.ts
3055
- async function verifyMinecraft(input) {
3056
- return runVerification(
3057
- {
3058
- targetId: input.target.id,
3059
- kind: VerificationKinds.MINECRAFT,
3060
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3061
- },
3062
- async (record) => {
3063
- const { directory, minecraft, runtime } = input.target;
3064
- record(
3065
- await verifyHashedFile({
3066
- path: targetPaths.versionJar(directory, minecraft.version),
3067
- expectedSha1: minecraft.manifest.downloads.client.sha1,
3068
- expectedSize: minecraft.manifest.downloads.client.size,
3069
- url: minecraft.manifest.downloads.client.url,
3070
- category: VerifyFileCategories.CLIENT_JAR
3071
- })
3072
- );
3073
- record(
3074
- await verifyExistence({
3075
- path: targetPaths.versionJson(directory, minecraft.version),
3076
- category: VerifyFileCategories.CLIENT_JAR
3077
- })
3078
- );
3079
- if (minecraft.manifest.logging?.client) {
3080
- const logging = minecraft.manifest.logging.client;
3081
- record(
3082
- await verifyHashedFile({
3083
- path: targetPaths.loggingConfig(directory, logging.file.id),
3084
- expectedSha1: logging.file.sha1,
3085
- expectedSize: logging.file.size,
3086
- url: logging.file.url,
3087
- category: VerifyFileCategories.LOGGING_CONFIG
3088
- })
3089
- );
3090
- }
3091
- const libraryPlan = planLibraryDownloads({
3092
- libraries: minecraft.manifest.libraries,
3093
- directory,
3094
- system: runtime.system,
3095
- versionId: minecraft.version,
3096
- category: "library"
3352
+ loader = {
3353
+ type: Loaders.VANILLA,
3354
+ minecraftVersion: minecraft.version,
3355
+ minecraft
3356
+ };
3357
+ } else if (input.loader.type === Loaders.FABRIC) {
3358
+ loader = await this.ctx.fabric.resolve({
3359
+ minecraftVersion: minecraft.version,
3360
+ ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
3361
+ ...input.loader.version !== void 0 ? { loaderVersion: input.loader.version } : {},
3362
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3097
3363
  });
3098
- for (const action of libraryPlan.downloads) {
3099
- record(
3100
- await verifyHashedFile({
3101
- path: action.target,
3102
- expectedSha1: action.expectedSha1,
3103
- expectedSize: action.expectedSize,
3104
- url: action.url,
3105
- category: VerifyFileCategories.LIBRARY
3106
- })
3107
- );
3108
- }
3109
- const indexUrl = minecraft.manifest.assetIndex.url;
3110
- const indexPath = targetPaths.assetIndex(directory, minecraft.manifest.assetIndex.id);
3111
- record(
3112
- await verifyHashedFile({
3113
- path: indexPath,
3114
- expectedSha1: minecraft.manifest.assetIndex.sha1,
3115
- expectedSize: minecraft.manifest.assetIndex.size,
3116
- url: indexUrl,
3117
- category: VerifyFileCategories.ASSET_INDEX
3118
- })
3119
- );
3120
- const indexDocument = await fetchJson(input.http, input.cache, {
3121
- url: indexUrl,
3122
- cacheKey: `asset-index:${minecraft.manifest.assetIndex.id}:${minecraft.manifest.assetIndex.sha1}`,
3364
+ } else {
3365
+ loader = await this.ctx.forge.resolve({
3366
+ minecraftVersion: minecraft.version,
3367
+ ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
3368
+ ...input.loader.version !== void 0 ? { forgeVersion: input.loader.version } : {},
3123
3369
  ...input.signal !== void 0 ? { signal: input.signal } : {}
3124
3370
  });
3125
- const seenAssetHashes = /* @__PURE__ */ new Set();
3126
- for (const entry of Object.values(indexDocument.objects)) {
3127
- if (seenAssetHashes.has(entry.hash)) continue;
3128
- seenAssetHashes.add(entry.hash);
3129
- record(
3130
- await verifyHashedFile({
3131
- path: targetPaths.assetObject(directory, entry.hash),
3132
- expectedSha1: entry.hash,
3133
- expectedSize: entry.size,
3134
- category: VerifyFileCategories.ASSET
3135
- })
3136
- );
3137
- }
3138
- const nativesDir = targetPaths.nativesDir(directory, minecraft.version);
3139
- if (!await fileExists(nativesDir)) {
3140
- for (const extraction of libraryPlan.nativeExtractions) {
3141
- record({
3142
- path: extraction.source,
3143
- category: VerifyFileCategories.NATIVE,
3144
- status: VerifyFileStatuses.MISSING
3145
- });
3146
- }
3371
+ }
3372
+ return this.create({
3373
+ id: input.id,
3374
+ directory: input.directory,
3375
+ minecraft,
3376
+ loader,
3377
+ runtime
3378
+ });
3379
+ }
3380
+ /** Scan a root directory for Minecraft installations. Returns only what is on disk. */
3381
+ async list(input) {
3382
+ if (!await dirExists(input.rootDir)) return [];
3383
+ const subdirs = await listChildDirectories(input.rootDir);
3384
+ const results = [];
3385
+ for (const id of subdirs) {
3386
+ const directory = path__default.default.join(input.rootDir, id);
3387
+ const discovered = await discoverInstallation(id, directory);
3388
+ if (discovered) results.push(discovered);
3389
+ }
3390
+ return results;
3391
+ }
3392
+ };
3393
+ async function discoverInstallation(id, directory) {
3394
+ const versionsDir = path__default.default.join(directory, VERSIONS_DIR);
3395
+ const librariesDir = path__default.default.join(directory, LIBRARIES_DIR);
3396
+ const assetsDir = path__default.default.join(directory, ASSETS_DIR);
3397
+ const looksLikeInstall = await dirExists(versionsDir) && (await dirExists(librariesDir) || await dirExists(assetsDir));
3398
+ if (!looksLikeInstall) return null;
3399
+ const versionDirs = await listChildDirectories(versionsDir);
3400
+ const minecraftVersions = [];
3401
+ const loaders = [];
3402
+ for (const versionId of versionDirs) {
3403
+ const hint = inferLoaderFromVersionId(versionId);
3404
+ if (hint) {
3405
+ loaders.push(hint);
3406
+ if (hint.minecraftVersion && !minecraftVersions.includes(hint.minecraftVersion)) {
3407
+ minecraftVersions.push(hint.minecraftVersion);
3147
3408
  }
3409
+ } else {
3410
+ minecraftVersions.push(versionId);
3148
3411
  }
3149
- );
3412
+ }
3413
+ const runtime = await discoverRuntime(directory);
3414
+ return { id, directory, minecraftVersions, loaders, ...runtime ? { runtime } : {} };
3150
3415
  }
3151
- async function verifyRuntime(input) {
3152
- return runVerification(
3153
- {
3154
- targetId: input.target.id,
3155
- kind: VerificationKinds.RUNTIME,
3156
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3157
- },
3158
- async (record) => {
3159
- let manifest;
3160
- try {
3161
- manifest = await fetchJson(input.http, input.cache, {
3162
- url: input.target.runtime.manifestUrl,
3163
- cacheKey: `runtime-manifest:${input.target.runtime.component}:${input.target.runtime.platformKey}:${input.target.runtime.manifestSha1}`,
3164
- ...input.signal !== void 0 ? { signal: input.signal } : {}
3165
- });
3166
- } catch {
3167
- record({
3168
- path: input.target.runtime.manifestUrl,
3169
- category: VerifyFileCategories.RUNTIME_FILE,
3170
- status: VerifyFileStatuses.MISSING
3171
- });
3172
- return;
3173
- }
3174
- const runtimeRoot = targetPaths.runtimeRoot(
3175
- input.target.directory,
3176
- input.target.runtime.component,
3177
- input.target.runtime.installRoot
3178
- );
3179
- for (const [relative, entry] of Object.entries(manifest.files)) {
3180
- if (entry.type !== "file") continue;
3181
- record(
3182
- await verifyHashedFile({
3183
- path: path__default.default.join(runtimeRoot, relative),
3184
- expectedSha1: entry.downloads.raw.sha1,
3185
- expectedSize: entry.downloads.raw.size,
3186
- url: entry.downloads.raw.url,
3187
- category: VerifyFileCategories.RUNTIME_FILE
3188
- })
3189
- );
3190
- }
3416
+ async function discoverRuntime(directory) {
3417
+ const runtimeDir = path__default.default.join(directory, RUNTIMES_DIR);
3418
+ if (!await dirExists(runtimeDir)) return void 0;
3419
+ let components;
3420
+ try {
3421
+ components = await listChildDirectories(runtimeDir);
3422
+ } catch {
3423
+ return void 0;
3424
+ }
3425
+ for (const component of components) {
3426
+ const root = path__default.default.join(runtimeDir, component);
3427
+ 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");
3428
+ if (await fileExists(javaPath)) {
3429
+ return { component, javaPath };
3191
3430
  }
3192
- );
3431
+ }
3432
+ return void 0;
3433
+ }
3434
+ function inferLoaderFromVersionId(versionId) {
3435
+ const fabricMatch = /^fabric-loader-([^-]+)-(.+)$/.exec(versionId);
3436
+ if (fabricMatch?.[1] && fabricMatch[2]) {
3437
+ return { type: Loaders.FABRIC, version: fabricMatch[1], minecraftVersion: fabricMatch[2] };
3438
+ }
3439
+ const forgeMatch = /^([^-]+)-forge-(.+)$/.exec(versionId);
3440
+ if (forgeMatch?.[1] && forgeMatch[2]) {
3441
+ return { type: Loaders.FORGE, minecraftVersion: forgeMatch[1], version: forgeMatch[2] };
3442
+ }
3443
+ return null;
3444
+ }
3445
+
3446
+ // src/update/runner.ts
3447
+ async function planUpdate(input) {
3448
+ return planInstall({
3449
+ target: input.target,
3450
+ http: input.http,
3451
+ cache: input.cache,
3452
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3453
+ });
3454
+ }
3455
+ async function runUpdate(input) {
3456
+ const report = await runInstall({
3457
+ plan: input.plan,
3458
+ http: input.http,
3459
+ cache: input.cache,
3460
+ spawner: input.spawner,
3461
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
3462
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3463
+ });
3464
+ return {
3465
+ targetId: report.targetId,
3466
+ bytesDownloaded: report.bytesDownloaded,
3467
+ actionsCompleted: report.actionsCompleted,
3468
+ actionsSkipped: report.actionsSkipped,
3469
+ durationMs: report.durationMs
3470
+ };
3193
3471
  }
3194
3472
 
3195
3473
  // src/versions/fabric.ts
@@ -3523,15 +3801,23 @@ function pickLatestAcrossComponents(entries) {
3523
3801
  return { component: bestComponent, entry: bestEntry };
3524
3802
  }
3525
3803
  function toResolved(component, platformKey, entry, system) {
3804
+ const majorVersion = parseMajorVersion(entry.version.name);
3526
3805
  return {
3527
3806
  component,
3528
3807
  platformKey,
3529
3808
  versionName: entry.version.name,
3809
+ ...majorVersion !== void 0 ? { majorVersion } : {},
3530
3810
  system,
3531
3811
  manifestUrl: entry.manifest.url,
3532
3812
  manifestSha1: entry.manifest.sha1
3533
3813
  };
3534
3814
  }
3815
+ function parseMajorVersion(versionName) {
3816
+ const match = /^(\d+)/.exec(versionName);
3817
+ if (!match || !match[1]) return void 0;
3818
+ const parsed = Number.parseInt(match[1], 10);
3819
+ return Number.isFinite(parsed) ? parsed : void 0;
3820
+ }
3535
3821
 
3536
3822
  // src/kit.ts
3537
3823
  var MinecraftKit = class {
@@ -3562,7 +3848,12 @@ var MinecraftKit = class {
3562
3848
  ...opts?.signal !== void 0 ? { signal: opts.signal } : {},
3563
3849
  ...opts?.onEvent !== void 0 ? { onEvent: opts.onEvent } : {}
3564
3850
  });
3565
- const runInstallPlan = (plan, opts) => runInstall({ plan, http, cache, spawner, ...carry(opts) });
3851
+ const carryInstall = (opts) => ({
3852
+ ...carry(opts),
3853
+ ...opts?.pauseController !== void 0 ? { pauseController: opts.pauseController } : {},
3854
+ ...opts?.actionCategories !== void 0 ? { actionCategories: opts.actionCategories } : {}
3855
+ });
3856
+ const runInstallPlan = (plan, opts) => runInstall({ plan, http, cache, spawner, ...carryInstall(opts) });
3566
3857
  this.install = {
3567
3858
  plan: (target, opts) => planInstall({ target, http, cache, ...carry(opts) }),
3568
3859
  run: runInstallPlan,
@@ -3612,10 +3903,17 @@ var MinecraftKit = class {
3612
3903
  runtime: {
3613
3904
  plan: (target, opts) => planRuntimeRepair(repairArgs(target, opts)),
3614
3905
  run: runRepairPlan
3615
- }
3906
+ },
3907
+ all: (target, opts) => repairAll({
3908
+ target,
3909
+ http,
3910
+ cache,
3911
+ spawner,
3912
+ ...carry(opts)
3913
+ })
3616
3914
  };
3617
3915
  this.launch = {
3618
- compose: (target, opts) => composeLaunch({ target, options: opts }),
3916
+ compose: (target, opts) => composeLaunch({ target, options: opts, logger }),
3619
3917
  run: (composition, opts) => runLaunch({
3620
3918
  composition,
3621
3919
  ...opts !== void 0 ? { options: opts } : {},
@@ -3625,6 +3923,247 @@ var MinecraftKit = class {
3625
3923
  }
3626
3924
  };
3627
3925
 
3926
+ // src/core/pause-controller.ts
3927
+ var PauseController = class {
3928
+ #paused = false;
3929
+ #waiters = [];
3930
+ get paused() {
3931
+ return this.#paused;
3932
+ }
3933
+ pause() {
3934
+ this.#paused = true;
3935
+ }
3936
+ resume() {
3937
+ this.#paused = false;
3938
+ const list = this.#waiters;
3939
+ this.#waiters = [];
3940
+ for (const resolve of list) resolve();
3941
+ }
3942
+ waitWhilePaused() {
3943
+ if (!this.#paused) return Promise.resolve();
3944
+ return new Promise((resolve) => this.#waiters.push(resolve));
3945
+ }
3946
+ };
3947
+
3948
+ // src/install/progress-tracker.ts
3949
+ var InstallStages = {
3950
+ PREPARE: "prepare",
3951
+ RUNTIME: "runtime",
3952
+ MINECRAFT: "minecraft",
3953
+ LOADER: "loader",
3954
+ FINALIZE: "finalize"
3955
+ };
3956
+ var STAGE_FOR_CATEGORY = {
3957
+ "runtime-file": InstallStages.RUNTIME,
3958
+ "client-jar": InstallStages.MINECRAFT,
3959
+ library: InstallStages.MINECRAFT,
3960
+ "asset-index": InstallStages.MINECRAFT,
3961
+ asset: InstallStages.MINECRAFT,
3962
+ "logging-config": InstallStages.MINECRAFT,
3963
+ "fabric-library": InstallStages.LOADER,
3964
+ "forge-library": InstallStages.LOADER,
3965
+ "forge-installer": InstallStages.LOADER
3966
+ };
3967
+ var STAGE_FOR_PHASE = {
3968
+ [InstallPhases.PLANNING]: InstallStages.PREPARE,
3969
+ [InstallPhases.DOWNLOADING_VERSION_MANIFEST]: InstallStages.PREPARE,
3970
+ [InstallPhases.INSTALLING_RUNTIME]: InstallStages.RUNTIME,
3971
+ [InstallPhases.DOWNLOADING_CLIENT_JAR]: InstallStages.MINECRAFT,
3972
+ [InstallPhases.DOWNLOADING_LIBRARIES]: InstallStages.MINECRAFT,
3973
+ [InstallPhases.DOWNLOADING_ASSET_INDEX]: InstallStages.MINECRAFT,
3974
+ [InstallPhases.DOWNLOADING_ASSETS]: InstallStages.MINECRAFT,
3975
+ [InstallPhases.EXTRACTING_NATIVES]: InstallStages.MINECRAFT,
3976
+ [InstallPhases.WRITING_FILES]: InstallStages.MINECRAFT,
3977
+ [InstallPhases.INSTALLING_FABRIC]: InstallStages.LOADER,
3978
+ [InstallPhases.INSTALLING_FORGE]: InstallStages.LOADER,
3979
+ [InstallPhases.RUNNING_FORGE_PROCESSORS]: InstallStages.LOADER,
3980
+ [InstallPhases.COMPLETED]: InstallStages.FINALIZE
3981
+ };
3982
+ var ALL_STAGES = [
3983
+ InstallStages.PREPARE,
3984
+ InstallStages.RUNTIME,
3985
+ InstallStages.MINECRAFT,
3986
+ InstallStages.LOADER,
3987
+ InstallStages.FINALIZE
3988
+ ];
3989
+ function createInstallProgressTracker(plan, options = {}) {
3990
+ const throttleMs = options.throttleMs ?? 100;
3991
+ const stageOfTarget = /* @__PURE__ */ new Map();
3992
+ const expectedSizeOf = /* @__PURE__ */ new Map();
3993
+ const stageTotals = {
3994
+ prepare: 0,
3995
+ runtime: 0,
3996
+ minecraft: 0,
3997
+ loader: 0,
3998
+ finalize: 0
3999
+ };
4000
+ let overallTotal = 0;
4001
+ for (const action of plan.actions) {
4002
+ if (action.kind !== "download-file") continue;
4003
+ const stage = STAGE_FOR_CATEGORY[action.category] ?? InstallStages.MINECRAFT;
4004
+ stageOfTarget.set(action.target, stage);
4005
+ const size = action.expectedSize ?? 0;
4006
+ expectedSizeOf.set(action.target, size);
4007
+ stageTotals[stage] += size;
4008
+ overallTotal += size;
4009
+ }
4010
+ const stageDone = {
4011
+ prepare: 0,
4012
+ runtime: 0,
4013
+ minecraft: 0,
4014
+ loader: 0,
4015
+ finalize: 0
4016
+ };
4017
+ const stageInFlight = {
4018
+ prepare: 0,
4019
+ runtime: 0,
4020
+ minecraft: 0,
4021
+ loader: 0,
4022
+ finalize: 0
4023
+ };
4024
+ let totalDone = 0;
4025
+ let totalInFlight = 0;
4026
+ const inFlightByTarget = /* @__PURE__ */ new Map();
4027
+ let currentStage = InstallStages.PREPARE;
4028
+ let currentFile;
4029
+ const listeners = /* @__PURE__ */ new Set();
4030
+ let lastPushAt = 0;
4031
+ let pending = false;
4032
+ let pendingTimer = null;
4033
+ let finished = false;
4034
+ const snapshot = () => {
4035
+ const stageTotal = stageTotals[currentStage];
4036
+ const stageBytes = stageDone[currentStage] + stageInFlight[currentStage];
4037
+ const overallBytes = totalDone + totalInFlight;
4038
+ return {
4039
+ stage: currentStage,
4040
+ stagePercent: stageTotal > 0 ? clamp(stageBytes / stageTotal * 100) : 0,
4041
+ overallPercent: overallTotal > 0 ? clamp(overallBytes / overallTotal * 100) : 0,
4042
+ bytesDownloaded: overallBytes,
4043
+ totalBytes: stageTotal,
4044
+ ...currentFile !== void 0 ? { currentFile } : {}
4045
+ };
4046
+ };
4047
+ const clearTimer = () => {
4048
+ if (pendingTimer) {
4049
+ clearTimeout(pendingTimer);
4050
+ pendingTimer = null;
4051
+ }
4052
+ };
4053
+ const push = () => {
4054
+ pending = false;
4055
+ clearTimer();
4056
+ lastPushAt = Date.now();
4057
+ const snap = snapshot();
4058
+ for (const listener of listeners) listener(snap);
4059
+ };
4060
+ const schedulePush = () => {
4061
+ if (finished) return;
4062
+ const elapsed = Date.now() - lastPushAt;
4063
+ if (elapsed >= throttleMs) {
4064
+ push();
4065
+ return;
4066
+ }
4067
+ if (pending) return;
4068
+ pending = true;
4069
+ pendingTimer = setTimeout(push, throttleMs - elapsed);
4070
+ };
4071
+ const onEvent = (event) => {
4072
+ switch (event.type) {
4073
+ case "install:phase-changed": {
4074
+ const next = STAGE_FOR_PHASE[event.phase];
4075
+ if (next && next !== currentStage) {
4076
+ currentStage = next;
4077
+ currentFile = void 0;
4078
+ push();
4079
+ }
4080
+ return;
4081
+ }
4082
+ case "download:started": {
4083
+ const stage = stageOfTarget.get(event.file.target) ?? currentStage;
4084
+ inFlightByTarget.set(event.file.target, { stage, bytes: 0 });
4085
+ currentFile = event.file.target;
4086
+ schedulePush();
4087
+ return;
4088
+ }
4089
+ case "download:progress": {
4090
+ const entry = inFlightByTarget.get(event.file.target);
4091
+ if (entry) {
4092
+ const delta = event.bytesDownloaded - entry.bytes;
4093
+ if (delta !== 0) {
4094
+ entry.bytes = event.bytesDownloaded;
4095
+ stageInFlight[entry.stage] += delta;
4096
+ totalInFlight += delta;
4097
+ }
4098
+ }
4099
+ currentFile = event.file.target;
4100
+ schedulePush();
4101
+ return;
4102
+ }
4103
+ case "download:skipped": {
4104
+ const stage = stageOfTarget.get(event.file.target);
4105
+ if (stage) {
4106
+ const size = expectedSizeOf.get(event.file.target) ?? 0;
4107
+ stageDone[stage] += size;
4108
+ totalDone += size;
4109
+ schedulePush();
4110
+ }
4111
+ return;
4112
+ }
4113
+ case "download:completed": {
4114
+ const entry = inFlightByTarget.get(event.file.target);
4115
+ if (entry) {
4116
+ const finalBytes = event.bytes ?? entry.bytes;
4117
+ stageInFlight[entry.stage] -= entry.bytes;
4118
+ totalInFlight -= entry.bytes;
4119
+ stageDone[entry.stage] += finalBytes;
4120
+ totalDone += finalBytes;
4121
+ inFlightByTarget.delete(event.file.target);
4122
+ } else {
4123
+ const stage = stageOfTarget.get(event.file.target);
4124
+ if (stage) {
4125
+ const bytes = event.bytes ?? expectedSizeOf.get(event.file.target) ?? 0;
4126
+ stageDone[stage] += bytes;
4127
+ totalDone += bytes;
4128
+ }
4129
+ }
4130
+ schedulePush();
4131
+ return;
4132
+ }
4133
+ default:
4134
+ return;
4135
+ }
4136
+ };
4137
+ return {
4138
+ onEvent,
4139
+ snapshot,
4140
+ subscribe(listener) {
4141
+ listeners.add(listener);
4142
+ listener(snapshot());
4143
+ return () => listeners.delete(listener);
4144
+ },
4145
+ finish() {
4146
+ finished = true;
4147
+ clearTimer();
4148
+ currentStage = InstallStages.FINALIZE;
4149
+ currentFile = void 0;
4150
+ totalDone = overallTotal;
4151
+ totalInFlight = 0;
4152
+ for (const stage of ALL_STAGES) {
4153
+ stageDone[stage] = stageTotals[stage];
4154
+ stageInFlight[stage] = 0;
4155
+ }
4156
+ const snap = snapshot();
4157
+ for (const listener of listeners) listener(snap);
4158
+ }
4159
+ };
4160
+ }
4161
+ function clamp(value) {
4162
+ if (value <= 0) return 0;
4163
+ if (value >= 100) return 100;
4164
+ return value;
4165
+ }
4166
+
3628
4167
  // src/types/events.ts
3629
4168
  var EventTypes = {
3630
4169
  INSTALL_PHASE_CHANGED: "install:phase-changed",
@@ -3749,6 +4288,7 @@ exports.HTTP_RETRY_MAX = HTTP_RETRY_MAX;
3749
4288
  exports.HTTP_TIMEOUT_MS = HTTP_TIMEOUT_MS;
3750
4289
  exports.InstallActionKinds = InstallActionKinds;
3751
4290
  exports.InstallPhases = InstallPhases;
4291
+ exports.InstallStages = InstallStages;
3752
4292
  exports.JAVA_EXECUTABLE = JAVA_EXECUTABLE;
3753
4293
  exports.LAUNCH_PLACEHOLDERS = LAUNCH_PLACEHOLDERS;
3754
4294
  exports.LEGACY_JVM_ARGS = LEGACY_JVM_ARGS;
@@ -3767,6 +4307,7 @@ exports.NODE_ARCH_TO_MOJANG_ARCH = NODE_ARCH_TO_MOJANG_ARCH;
3767
4307
  exports.NODE_PLATFORM_TO_MOJANG_OS = NODE_PLATFORM_TO_MOJANG_OS;
3768
4308
  exports.OperatingSystems = OperatingSystems;
3769
4309
  exports.PROGRESS_EVENT_INTERVAL_MS = PROGRESS_EVENT_INTERVAL_MS;
4310
+ exports.PauseController = PauseController;
3770
4311
  exports.RUNTIMES_DIR = RUNTIMES_DIR;
3771
4312
  exports.RUNTIME_PLATFORM_KEYS = RUNTIME_PLATFORM_KEYS;
3772
4313
  exports.RepairPhases = RepairPhases;
@@ -3782,20 +4323,26 @@ exports.VerifyFileCategories = VerifyFileCategories;
3782
4323
  exports.VerifyFileStatuses = VerifyFileStatuses;
3783
4324
  exports.VersionPreference = VersionPreference;
3784
4325
  exports.consoleLogger = consoleLogger;
4326
+ exports.createInstallProgressTracker = createInstallProgressTracker;
3785
4327
  exports.createMemoryCache = createMemoryCache;
3786
4328
  exports.detectSystem = detectSystem;
3787
4329
  exports.isErrorCode = isErrorCode;
3788
4330
  exports.isMinecraftKitError = isMinecraftKitError;
3789
4331
  exports.offlineUuidFor = offlineUuidFor;
4332
+ exports.parseMajorVersion = parseMajorVersion;
4333
+ exports.pickClientJarVersionId = pickClientJarVersionId;
3790
4334
  exports.planFabricRepair = planFabricRepair;
3791
4335
  exports.planForgeRepair = planForgeRepair;
3792
4336
  exports.planMinecraftRepair = planMinecraftRepair;
3793
4337
  exports.planRuntimeInstall = planRuntimeInstall;
3794
4338
  exports.planRuntimeRepair = planRuntimeRepair;
3795
4339
  exports.planStandaloneRuntimeInstall = planStandaloneRuntimeInstall;
4340
+ exports.repairAll = repairAll;
4341
+ exports.resolveLaunchVersion = resolveLaunchVersion;
3796
4342
  exports.runRepair = runRepair;
3797
4343
  exports.silentLogger = silentLogger;
3798
4344
  exports.stripUuidDashes = stripUuidDashes;
4345
+ exports.targetPaths = targetPaths;
3799
4346
  exports.verifyFabric = verifyFabric;
3800
4347
  exports.verifyForge = verifyForge;
3801
4348
  exports.verifyMinecraft = verifyMinecraft;