@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.mjs CHANGED
@@ -8,7 +8,7 @@ import yauzl from 'yauzl';
8
8
  import crypto2 from 'crypto';
9
9
  import fs from 'fs/promises';
10
10
  import { Readable } from 'stream';
11
- import pLimit from 'p-limit';
11
+ import { AsyncResource } from 'async_hooks';
12
12
  import { Buffer as Buffer$1 } from 'buffer';
13
13
  import { spawn } from 'child_process';
14
14
 
@@ -1113,6 +1113,12 @@ async function downloadFile(http, input) {
1113
1113
  const sourceIterable = response.stream();
1114
1114
  const counting = (async function* () {
1115
1115
  for await (const chunk of sourceIterable) {
1116
+ if (input.pauseController?.paused) {
1117
+ await input.pauseController.waitWhilePaused();
1118
+ }
1119
+ if (input.signal?.aborted) {
1120
+ throw new MinecraftKitError("LAUNCH_ABORTED", "Download aborted by signal");
1121
+ }
1116
1122
  bytesDownloaded += chunk.byteLength;
1117
1123
  hash.update(chunk);
1118
1124
  input.onEvent?.({
@@ -1441,7 +1447,8 @@ function substituteToken(raw, tokens) {
1441
1447
  });
1442
1448
  }
1443
1449
  function stripLiteralPrefix(value) {
1444
- return value.startsWith("'") ? value.slice(1) : value;
1450
+ const stripped = value.startsWith("'") ? value.slice(1) : value;
1451
+ return stripped.endsWith("'") ? stripped.slice(0, -1) : stripped;
1445
1452
  }
1446
1453
  async function planRuntimeDownloads(input) {
1447
1454
  const manifest = await fetchJson(input.http, input.cache, {
@@ -1564,6 +1571,123 @@ async function planInstall(input) {
1564
1571
  totalBytes
1565
1572
  };
1566
1573
  }
1574
+
1575
+ // node_modules/yocto-queue/index.js
1576
+ var Node = class {
1577
+ value;
1578
+ next;
1579
+ constructor(value) {
1580
+ this.value = value;
1581
+ }
1582
+ };
1583
+ var Queue = class {
1584
+ #head;
1585
+ #tail;
1586
+ #size;
1587
+ constructor() {
1588
+ this.clear();
1589
+ }
1590
+ enqueue(value) {
1591
+ const node = new Node(value);
1592
+ if (this.#head) {
1593
+ this.#tail.next = node;
1594
+ this.#tail = node;
1595
+ } else {
1596
+ this.#head = node;
1597
+ this.#tail = node;
1598
+ }
1599
+ this.#size++;
1600
+ }
1601
+ dequeue() {
1602
+ const current = this.#head;
1603
+ if (!current) {
1604
+ return;
1605
+ }
1606
+ this.#head = this.#head.next;
1607
+ this.#size--;
1608
+ if (!this.#head) {
1609
+ this.#tail = void 0;
1610
+ }
1611
+ return current.value;
1612
+ }
1613
+ peek() {
1614
+ if (!this.#head) {
1615
+ return;
1616
+ }
1617
+ return this.#head.value;
1618
+ }
1619
+ clear() {
1620
+ this.#head = void 0;
1621
+ this.#tail = void 0;
1622
+ this.#size = 0;
1623
+ }
1624
+ get size() {
1625
+ return this.#size;
1626
+ }
1627
+ *[Symbol.iterator]() {
1628
+ let current = this.#head;
1629
+ while (current) {
1630
+ yield current.value;
1631
+ current = current.next;
1632
+ }
1633
+ }
1634
+ *drain() {
1635
+ while (this.#head) {
1636
+ yield this.dequeue();
1637
+ }
1638
+ }
1639
+ };
1640
+ function pLimit(concurrency) {
1641
+ if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
1642
+ throw new TypeError("Expected `concurrency` to be a number from 1 and up");
1643
+ }
1644
+ const queue = new Queue();
1645
+ let activeCount = 0;
1646
+ const next = () => {
1647
+ activeCount--;
1648
+ if (queue.size > 0) {
1649
+ queue.dequeue()();
1650
+ }
1651
+ };
1652
+ const run = async (function_, resolve, arguments_) => {
1653
+ activeCount++;
1654
+ const result = (async () => function_(...arguments_))();
1655
+ resolve(result);
1656
+ try {
1657
+ await result;
1658
+ } catch {
1659
+ }
1660
+ next();
1661
+ };
1662
+ const enqueue = (function_, resolve, arguments_) => {
1663
+ queue.enqueue(
1664
+ AsyncResource.bind(run.bind(void 0, function_, resolve, arguments_))
1665
+ );
1666
+ (async () => {
1667
+ await Promise.resolve();
1668
+ if (activeCount < concurrency && queue.size > 0) {
1669
+ queue.dequeue()();
1670
+ }
1671
+ })();
1672
+ };
1673
+ const generator = (function_, ...arguments_) => new Promise((resolve) => {
1674
+ enqueue(function_, resolve, arguments_);
1675
+ });
1676
+ Object.defineProperties(generator, {
1677
+ activeCount: {
1678
+ get: () => activeCount
1679
+ },
1680
+ pendingCount: {
1681
+ get: () => queue.size
1682
+ },
1683
+ clearQueue: {
1684
+ value() {
1685
+ queue.clear();
1686
+ }
1687
+ }
1688
+ });
1689
+ return generator;
1690
+ }
1567
1691
  async function materializeRuntimeExtras(input) {
1568
1692
  const root = targetPaths.runtimeRoot(
1569
1693
  input.directory,
@@ -1628,6 +1752,16 @@ function errorMessage(error) {
1628
1752
  }
1629
1753
 
1630
1754
  // src/install/runner.ts
1755
+ var DOWNLOAD_GROUPS = [
1756
+ { categories: ["runtime-file"], phase: InstallPhases.INSTALLING_RUNTIME },
1757
+ { categories: ["client-jar"], phase: InstallPhases.DOWNLOADING_CLIENT_JAR },
1758
+ { categories: ["library"], phase: InstallPhases.DOWNLOADING_LIBRARIES },
1759
+ { categories: ["asset-index"], phase: InstallPhases.DOWNLOADING_ASSET_INDEX },
1760
+ { categories: ["asset"], phase: InstallPhases.DOWNLOADING_ASSETS },
1761
+ { categories: ["logging-config"], phase: InstallPhases.WRITING_FILES },
1762
+ { categories: ["fabric-library"], phase: InstallPhases.INSTALLING_FABRIC },
1763
+ { categories: ["forge-installer", "forge-library"], phase: InstallPhases.INSTALLING_FORGE }
1764
+ ];
1631
1765
  async function runInstall(input) {
1632
1766
  const startedAt = Date.now();
1633
1767
  let bytesDownloaded = 0;
@@ -1640,50 +1774,89 @@ async function runInstall(input) {
1640
1774
  onEvent?.({ type: "install:phase-changed", phase, previous: currentPhase });
1641
1775
  currentPhase = phase;
1642
1776
  };
1643
- const downloads = input.plan.actions.filter(isDownload);
1777
+ const checkpoint = async () => {
1778
+ if (input.signal?.aborted) {
1779
+ throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1780
+ }
1781
+ await input.pauseController?.waitWhilePaused();
1782
+ if (input.signal?.aborted) {
1783
+ throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1784
+ }
1785
+ };
1786
+ const categoryFilter = input.actionCategories;
1787
+ const downloads = input.plan.actions.filter(isDownload).filter((a) => categoryFilter ? categoryFilter.has(a.category) : true);
1644
1788
  const natives = input.plan.actions.filter(isNative);
1645
1789
  const writeActions = input.plan.actions.filter(isWrite);
1646
1790
  const processors = input.plan.actions.filter(isProcessor);
1647
1791
  enterPhase(InstallPhases.PLANNING);
1648
- enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
1649
1792
  const limit = pLimit(input.concurrency ?? DOWNLOAD_CONCURRENCY);
1650
- await Promise.all(
1651
- downloads.map(
1652
- (action) => limit(async () => {
1653
- if (input.signal?.aborted) {
1654
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1655
- }
1656
- const result = await downloadFile(input.http, {
1657
- url: action.url,
1658
- target: action.target,
1659
- ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1660
- ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1661
- ...action.category !== void 0 ? { category: action.category } : {},
1662
- ...input.signal !== void 0 ? { signal: input.signal } : {},
1663
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
1664
- });
1665
- bytesDownloaded += result.bytesDownloaded;
1666
- if (result.skipped) actionsSkipped++;
1667
- actionsCompleted++;
1668
- })
1669
- )
1793
+ for (const group of DOWNLOAD_GROUPS) {
1794
+ const groupActions = downloads.filter((action) => group.categories.includes(action.category));
1795
+ if (groupActions.length === 0) continue;
1796
+ await checkpoint();
1797
+ enterPhase(group.phase);
1798
+ await Promise.all(
1799
+ groupActions.map(
1800
+ (action) => limit(async () => {
1801
+ await checkpoint();
1802
+ const result = await downloadFile(input.http, {
1803
+ url: action.url,
1804
+ target: action.target,
1805
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1806
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1807
+ ...action.category !== void 0 ? { category: action.category } : {},
1808
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
1809
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1810
+ ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1811
+ });
1812
+ bytesDownloaded += result.bytesDownloaded;
1813
+ if (result.skipped) actionsSkipped++;
1814
+ actionsCompleted++;
1815
+ })
1816
+ )
1817
+ );
1818
+ }
1819
+ const ungrouped = downloads.filter(
1820
+ (action) => !DOWNLOAD_GROUPS.some((g) => g.categories.includes(action.category))
1670
1821
  );
1822
+ if (ungrouped.length > 0) {
1823
+ await checkpoint();
1824
+ enterPhase(InstallPhases.DOWNLOADING_LIBRARIES);
1825
+ await Promise.all(
1826
+ ungrouped.map(
1827
+ (action) => limit(async () => {
1828
+ await checkpoint();
1829
+ const result = await downloadFile(input.http, {
1830
+ url: action.url,
1831
+ target: action.target,
1832
+ ...action.expectedSha1 !== void 0 ? { expectedSha1: action.expectedSha1 } : {},
1833
+ ...action.expectedSize !== void 0 ? { expectedSize: action.expectedSize } : {},
1834
+ ...action.category !== void 0 ? { category: action.category } : {},
1835
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
1836
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {},
1837
+ ...input.pauseController !== void 0 ? { pauseController: input.pauseController } : {}
1838
+ });
1839
+ bytesDownloaded += result.bytesDownloaded;
1840
+ if (result.skipped) actionsSkipped++;
1841
+ actionsCompleted++;
1842
+ })
1843
+ )
1844
+ );
1845
+ }
1671
1846
  if (writeActions.length > 0) {
1847
+ await checkpoint();
1672
1848
  enterPhase(InstallPhases.WRITING_FILES);
1673
1849
  for (const action of writeActions) {
1674
- if (input.signal?.aborted) {
1675
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1676
- }
1850
+ await checkpoint();
1677
1851
  await atomicWrite(action.path, action.content);
1678
1852
  actionsCompleted++;
1679
1853
  }
1680
1854
  }
1681
1855
  if (natives.length > 0) {
1856
+ await checkpoint();
1682
1857
  enterPhase(InstallPhases.EXTRACTING_NATIVES);
1683
1858
  for (const action of natives) {
1684
- if (input.signal?.aborted) {
1685
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1686
- }
1859
+ await checkpoint();
1687
1860
  const { fileCount } = await extractAllToDir(action.source, action.destination, {
1688
1861
  excludePrefixes: action.exclude
1689
1862
  });
@@ -1697,6 +1870,7 @@ async function runInstall(input) {
1697
1870
  }
1698
1871
  }
1699
1872
  if (input.plan.target.runtime !== void 0) {
1873
+ await checkpoint();
1700
1874
  enterPhase(InstallPhases.INSTALLING_RUNTIME);
1701
1875
  const runtimePlan = await planRuntimeDownloads({
1702
1876
  runtime: input.plan.target.runtime,
@@ -1712,6 +1886,7 @@ async function runInstall(input) {
1712
1886
  });
1713
1887
  }
1714
1888
  if (processors.length > 0) {
1889
+ await checkpoint();
1715
1890
  enterPhase(InstallPhases.RUNNING_FORGE_PROCESSORS);
1716
1891
  if (input.plan.target.loader.type !== Loaders.FORGE) {
1717
1892
  throw new MinecraftKitError(
@@ -1726,9 +1901,7 @@ async function runInstall(input) {
1726
1901
  input.plan.target.runtime.installRoot
1727
1902
  );
1728
1903
  for (const action of processors) {
1729
- if (input.signal?.aborted) {
1730
- throw new MinecraftKitError("LAUNCH_ABORTED", "Install aborted by signal");
1731
- }
1904
+ await checkpoint();
1732
1905
  await runProcessor({
1733
1906
  action,
1734
1907
  javaPath,
@@ -1943,6 +2116,33 @@ function pickArguments(args, context) {
1943
2116
  };
1944
2117
  }
1945
2118
 
2119
+ // src/launch/jvm-compat.ts
2120
+ var FLAG_MIN_JAVA = [
2121
+ { prefix: "--sun-misc-unsafe-memory-access", minJava: 23 },
2122
+ { prefix: "--enable-native-access", minJava: 17 },
2123
+ { prefix: "-XX:+UseCompactObjectHeaders", minJava: 24 },
2124
+ { prefix: "-XX:+UseZGC", minJava: 15 }
2125
+ ];
2126
+ function filterArgsForJava(input) {
2127
+ if (!Number.isFinite(input.javaMajor) || input.javaMajor <= 0) return input.args;
2128
+ const out = [];
2129
+ for (const arg of input.args) {
2130
+ const incompatible = FLAG_MIN_JAVA.find(
2131
+ ({ prefix }) => arg === prefix || arg.startsWith(`${prefix}=`) || arg.startsWith(`${prefix} `)
2132
+ );
2133
+ if (incompatible && input.javaMajor < incompatible.minJava) {
2134
+ input.logger?.log(
2135
+ "warn",
2136
+ `Dropping JVM arg "${arg}" \u2014 requires Java ${incompatible.minJava}, runtime is Java ${input.javaMajor}`,
2137
+ { flag: arg, minJava: incompatible.minJava, runtimeJava: input.javaMajor }
2138
+ );
2139
+ continue;
2140
+ }
2141
+ out.push(arg);
2142
+ }
2143
+ return out;
2144
+ }
2145
+
1946
2146
  // src/launch/placeholders.ts
1947
2147
  function substituteArg(raw, values) {
1948
2148
  return raw.replaceAll(/\$\{([a-zA-Z0-9_]+)\}/g, (match, key) => {
@@ -1982,7 +2182,13 @@ function composeArgs(input) {
1982
2182
  const baseJvm = [...memoryArgs, ...BASE_JVM_ARGS, ...macosArgs];
1983
2183
  const substitutedJvm = substituteArgs(rawJvm, input.placeholderValues);
1984
2184
  const substitutedGame = substituteArgs(rawGame, input.placeholderValues);
1985
- const jvmArgs = [...baseJvm, ...substitutedJvm];
2185
+ const javaMajor = input.target.runtime.majorVersion;
2186
+ const filteredManifestJvm = javaMajor !== void 0 ? filterArgsForJava({
2187
+ args: substitutedJvm,
2188
+ javaMajor,
2189
+ logger: input.logger ?? silentLogger
2190
+ }) : substitutedJvm;
2191
+ const jvmArgs = [...baseJvm, ...filteredManifestJvm];
1986
2192
  if (input.merged.logging?.client?.argument) {
1987
2193
  const logging = input.merged.logging.client;
1988
2194
  const loggingArg = substituteArgs([logging.argument], {
@@ -2112,8 +2318,28 @@ function mergeManifest(parent, child) {
2112
2318
  };
2113
2319
  return merged;
2114
2320
  }
2321
+ function libraryDedupeKey(library) {
2322
+ if (!library.name) return null;
2323
+ try {
2324
+ const coord = parseMavenCoordinate(library.name);
2325
+ const classifier = coord.classifier ? `:${coord.classifier}` : "";
2326
+ return `${coord.group}:${coord.artifact}${classifier}`;
2327
+ } catch {
2328
+ return null;
2329
+ }
2330
+ }
2115
2331
  function mergeLibraries(parent, child) {
2116
- return [...parent, ...child];
2332
+ const byKey = /* @__PURE__ */ new Map();
2333
+ const unkeyed = [];
2334
+ for (const lib of [...parent, ...child]) {
2335
+ const key = libraryDedupeKey(lib);
2336
+ if (key === null) {
2337
+ unkeyed.push(lib);
2338
+ continue;
2339
+ }
2340
+ byKey.set(key, lib);
2341
+ }
2342
+ return [...byKey.values(), ...unkeyed];
2117
2343
  }
2118
2344
  function mergeArguments(parent, child) {
2119
2345
  if (!parent && !child) return void 0;
@@ -2237,7 +2463,8 @@ async function composeLaunch(input) {
2237
2463
  merged: resolved.merged,
2238
2464
  options,
2239
2465
  placeholderValues,
2240
- features
2466
+ features,
2467
+ logger: input.logger ?? silentLogger
2241
2468
  });
2242
2469
  return {
2243
2470
  targetId: target.id,
@@ -2419,193 +2646,553 @@ var VerifyFileCategories = {
2419
2646
  RUNTIME_FILE: "runtime-file",
2420
2647
  LOGGING_CONFIG: "logging-config"
2421
2648
  };
2649
+ async function sha1OfFile(filePath) {
2650
+ const hash = crypto2.createHash("sha1");
2651
+ await new Promise((resolve, reject) => {
2652
+ const stream = createReadStream(filePath);
2653
+ stream.on("data", (chunk) => hash.update(chunk));
2654
+ stream.on("end", resolve);
2655
+ stream.on("error", reject);
2656
+ });
2657
+ return hash.digest("hex");
2658
+ }
2422
2659
 
2423
- // src/repair/helpers.ts
2424
- function asResultArray(from) {
2425
- return Array.isArray(from) ? from : [from];
2660
+ // src/verify/helpers.ts
2661
+ async function runVerification(input, check) {
2662
+ const startedAt = Date.now();
2663
+ const results = [];
2664
+ const record = (result) => {
2665
+ results.push(result);
2666
+ input.onEvent?.({ type: "verify:file-checked", file: result });
2667
+ };
2668
+ await check(record);
2669
+ return {
2670
+ targetId: input.targetId,
2671
+ kind: input.kind,
2672
+ isValid: results.every((r) => r.status === VerifyFileStatuses.OK),
2673
+ issues: results.filter((r) => r.status !== VerifyFileStatuses.OK),
2674
+ checkedFiles: results.length,
2675
+ durationMs: Date.now() - startedAt
2676
+ };
2426
2677
  }
2427
- function buildIssueIndex(from) {
2428
- const map = /* @__PURE__ */ new Map();
2429
- for (const v of asResultArray(from)) {
2430
- for (const issue of v.issues) {
2431
- const set = map.get(issue.path);
2432
- if (set) set.add(issue.category);
2433
- else map.set(issue.path, /* @__PURE__ */ new Set([issue.category]));
2678
+ async function verifyHashedFile(input) {
2679
+ if (!await fileExists(input.path)) {
2680
+ return {
2681
+ path: input.path,
2682
+ category: input.category,
2683
+ status: VerifyFileStatuses.MISSING,
2684
+ ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2685
+ ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2686
+ ...input.url !== void 0 ? { url: input.url } : {}
2687
+ };
2688
+ }
2689
+ if (input.expectedSize !== void 0) {
2690
+ const size = await fileSize(input.path);
2691
+ if (size !== input.expectedSize) {
2692
+ return {
2693
+ path: input.path,
2694
+ category: input.category,
2695
+ status: VerifyFileStatuses.WRONG_SIZE,
2696
+ expectedSize: input.expectedSize,
2697
+ actualSize: size,
2698
+ ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2699
+ ...input.url !== void 0 ? { url: input.url } : {}
2700
+ };
2701
+ }
2702
+ }
2703
+ if (input.expectedSha1 !== void 0) {
2704
+ const actualSha1 = await sha1OfFile(input.path);
2705
+ if (actualSha1 !== input.expectedSha1) {
2706
+ return {
2707
+ path: input.path,
2708
+ category: input.category,
2709
+ status: VerifyFileStatuses.CORRUPT,
2710
+ expectedSha1: input.expectedSha1,
2711
+ actualSha1,
2712
+ ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2713
+ ...input.url !== void 0 ? { url: input.url } : {}
2714
+ };
2434
2715
  }
2435
2716
  }
2436
2717
  return {
2437
- has: (path13) => map.has(path13),
2438
- hasNonNative: (path13) => {
2439
- const cats = map.get(path13);
2440
- if (!cats) return false;
2441
- for (const c of cats) {
2442
- if (c !== VerifyFileCategories.NATIVE) return true;
2443
- }
2444
- return false;
2445
- },
2446
- categoriesAt: (path13) => map.get(path13) ?? /* @__PURE__ */ new Set()
2718
+ path: input.path,
2719
+ category: input.category,
2720
+ status: VerifyFileStatuses.OK,
2721
+ ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2722
+ ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2723
+ ...input.url !== void 0 ? { url: input.url } : {}
2447
2724
  };
2448
2725
  }
2449
- function sumDownloadBytes(actions) {
2450
- return actions.reduce((sum, action) => {
2451
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2452
- return sum + (action.expectedSize ?? 0);
2453
- }
2454
- return sum;
2455
- }, 0);
2456
- }
2457
- function buildRepairPlan(target, actions) {
2726
+ async function verifyExistence(input) {
2727
+ if (await fileExists(input.path)) {
2728
+ return {
2729
+ path: input.path,
2730
+ category: input.category,
2731
+ status: VerifyFileStatuses.OK,
2732
+ ...input.url !== void 0 ? { url: input.url } : {}
2733
+ };
2734
+ }
2458
2735
  return {
2459
- targetId: target.id,
2460
- directory: target.directory,
2461
- target,
2462
- actions,
2463
- totalActions: actions.length,
2464
- totalBytes: sumDownloadBytes(actions)
2736
+ path: input.path,
2737
+ category: input.category,
2738
+ status: VerifyFileStatuses.MISSING,
2739
+ ...input.url !== void 0 ? { url: input.url } : {}
2465
2740
  };
2466
2741
  }
2467
- async function planAspectRepair(input, aspectFilter, postprocess) {
2468
- const installPlan = await planInstall({
2469
- target: input.target,
2470
- http: input.http,
2471
- cache: input.cache,
2472
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2473
- });
2474
- const issues = buildIssueIndex(input.from);
2475
- const actions = selectRepairActions({
2476
- target: input.target,
2477
- installPlan,
2478
- issues,
2479
- aspectFilter
2480
- });
2481
- postprocess?.({ actions, installPlan, issues });
2482
- return buildRepairPlan(input.target, actions);
2483
- }
2484
- function selectRepairActions(input) {
2485
- const matching = [];
2486
- for (const action of input.installPlan.actions) {
2487
- if (!input.aspectFilter(action)) continue;
2488
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2489
- if (input.issues.hasNonNative(action.target)) {
2490
- matching.push(action);
2491
- }
2492
- } else if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2493
- if (input.issues.has(action.path)) {
2494
- matching.push(action);
2495
- }
2496
- } else if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
2497
- if (input.issues.has(action.source)) {
2498
- matching.push(action);
2499
- }
2500
- } else {
2501
- matching.push(action);
2742
+ async function findForgeVersionJsonPath(directory, minecraftVersion) {
2743
+ const versionsDir = targetPaths.versionsDir(directory);
2744
+ const dirs = await listChildDirectories(versionsDir);
2745
+ for (const id of dirs) {
2746
+ if (!id.startsWith(`${minecraftVersion}-forge-`)) continue;
2747
+ const jsonPath = targetPaths.versionJson(directory, id);
2748
+ if (!await fileExists(jsonPath)) {
2749
+ return jsonPath;
2502
2750
  }
2751
+ const parsed = await tryParseInheritsFrom(jsonPath);
2752
+ if (parsed === minecraftVersion) return jsonPath;
2753
+ }
2754
+ return null;
2755
+ }
2756
+ async function tryParseInheritsFrom(jsonPath) {
2757
+ try {
2758
+ const parsed = JSON.parse(await readText(jsonPath));
2759
+ return parsed.inheritsFrom;
2760
+ } catch {
2761
+ return void 0;
2503
2762
  }
2504
- return matching;
2505
2763
  }
2506
2764
 
2507
- // src/repair/fabric.ts
2508
- async function planFabricRepair(input) {
2765
+ // src/verify/fabric.ts
2766
+ async function verifyFabric(input) {
2509
2767
  if (input.target.loader.type !== Loaders.FABRIC) {
2510
2768
  throw new MinecraftKitError(
2511
2769
  "INVALID_INPUT",
2512
- `repair.fabric requires a Fabric target (got ${input.target.loader.type})`
2770
+ `verify.fabric requires a Fabric target (got ${input.target.loader.type})`
2513
2771
  );
2514
2772
  }
2515
- const fabricJsonPath = targetPaths.versionJson(
2516
- input.target.directory,
2517
- input.target.loader.profile.id
2518
- );
2519
- return planAspectRepair(input, (action) => {
2520
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2521
- return action.category === "fabric-library";
2522
- }
2523
- if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2524
- return action.path === fabricJsonPath;
2773
+ const loader = input.target.loader;
2774
+ return runVerification(
2775
+ {
2776
+ targetId: input.target.id,
2777
+ kind: VerificationKinds.FABRIC,
2778
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2779
+ },
2780
+ async (record) => {
2781
+ record(
2782
+ await verifyExistence({
2783
+ path: targetPaths.versionJson(input.target.directory, loader.profile.id),
2784
+ category: VerifyFileCategories.LOADER_LIBRARY
2785
+ })
2786
+ );
2787
+ const fabricLibraries = planLibraryDownloads({
2788
+ libraries: loader.profile.libraries,
2789
+ directory: input.target.directory,
2790
+ system: input.target.runtime.system,
2791
+ versionId: input.target.minecraft.version,
2792
+ category: "fabric-library"
2793
+ });
2794
+ for (const action of fabricLibraries.downloads) {
2795
+ record(
2796
+ await verifyHashedFile({
2797
+ path: action.target,
2798
+ expectedSha1: action.expectedSha1,
2799
+ expectedSize: action.expectedSize,
2800
+ ...action.url ? { url: action.url } : {},
2801
+ category: VerifyFileCategories.LOADER_LIBRARY
2802
+ })
2803
+ );
2804
+ }
2525
2805
  }
2526
- return false;
2527
- });
2806
+ );
2528
2807
  }
2529
2808
 
2530
- // src/repair/forge.ts
2531
- var FORGE_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
2532
- "forge-library",
2533
- "forge-installer"
2534
- ]);
2535
- async function planForgeRepair(input) {
2809
+ // src/verify/forge.ts
2810
+ async function verifyForge(input) {
2536
2811
  if (input.target.loader.type !== Loaders.FORGE) {
2537
2812
  throw new MinecraftKitError(
2538
2813
  "INVALID_INPUT",
2539
- `repair.forge requires a Forge target (got ${input.target.loader.type})`
2814
+ `verify.forge requires a Forge target (got ${input.target.loader.type})`
2540
2815
  );
2541
2816
  }
2542
- const forgeJsonPath = targetPaths.versionJson(
2543
- input.target.directory,
2544
- input.target.loader.fullVersion
2545
- );
2546
- return planAspectRepair(
2547
- input,
2548
- (action) => {
2549
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2550
- return FORGE_DOWNLOAD_CATEGORIES.has(action.category);
2551
- }
2552
- if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2553
- return action.path === forgeJsonPath;
2554
- }
2555
- return false;
2817
+ return runVerification(
2818
+ {
2819
+ targetId: input.target.id,
2820
+ kind: VerificationKinds.FORGE,
2821
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2556
2822
  },
2557
- ({ actions, installPlan, issues }) => {
2558
- if (!issues.has(forgeJsonPath)) return;
2559
- const alreadyIncluded = new Set(
2560
- actions.filter((a) => a.kind === InstallActionKinds.DOWNLOAD_FILE).map((a) => a.target)
2823
+ async (record) => {
2824
+ const forgeVersionJsonPath = await findForgeVersionJsonPath(
2825
+ input.target.directory,
2826
+ input.target.minecraft.version
2561
2827
  );
2562
- for (const action of installPlan.actions) {
2563
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "forge-library" && !alreadyIncluded.has(action.target)) {
2564
- actions.push(action);
2565
- } else if (action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR) {
2566
- actions.push(action);
2567
- }
2828
+ if (forgeVersionJsonPath === null) return;
2829
+ record(
2830
+ await verifyExistence({
2831
+ path: forgeVersionJsonPath,
2832
+ category: VerifyFileCategories.LOADER_LIBRARY
2833
+ })
2834
+ );
2835
+ if (!await fileExists(forgeVersionJsonPath)) return;
2836
+ let parsed;
2837
+ try {
2838
+ parsed = JSON.parse(await readText(forgeVersionJsonPath));
2839
+ } catch {
2840
+ record({
2841
+ path: forgeVersionJsonPath,
2842
+ category: VerifyFileCategories.LOADER_LIBRARY,
2843
+ status: VerifyFileStatuses.CORRUPT
2844
+ });
2845
+ return;
2846
+ }
2847
+ const forgeLibraries = planLibraryDownloads({
2848
+ libraries: parsed.libraries,
2849
+ directory: input.target.directory,
2850
+ system: input.target.runtime.system,
2851
+ versionId: input.target.minecraft.version,
2852
+ category: "forge-library"
2853
+ });
2854
+ for (const action of forgeLibraries.downloads) {
2855
+ record(
2856
+ await verifyHashedFile({
2857
+ path: action.target,
2858
+ expectedSha1: action.expectedSha1,
2859
+ expectedSize: action.expectedSize,
2860
+ ...action.url ? { url: action.url } : {},
2861
+ category: VerifyFileCategories.LOADER_LIBRARY
2862
+ })
2863
+ );
2568
2864
  }
2569
2865
  }
2570
2866
  );
2571
2867
  }
2572
2868
 
2573
- // src/repair/minecraft.ts
2574
- var MINECRAFT_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
2575
- "client-jar",
2576
- "library",
2577
- "asset-index",
2578
- "asset",
2579
- "logging-config"
2580
- ]);
2581
- async function planMinecraftRepair(input) {
2582
- const vanillaJsonPath = targetPaths.versionJson(
2583
- input.target.directory,
2584
- input.target.minecraft.version
2585
- );
2586
- return planAspectRepair(input, (action) => {
2587
- if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
2588
- return MINECRAFT_DOWNLOAD_CATEGORIES.has(action.category);
2589
- }
2590
- if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
2591
- return action.path === vanillaJsonPath;
2592
- }
2593
- if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
2594
- return true;
2869
+ // src/verify/minecraft.ts
2870
+ async function verifyMinecraft(input) {
2871
+ return runVerification(
2872
+ {
2873
+ targetId: input.target.id,
2874
+ kind: VerificationKinds.MINECRAFT,
2875
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2876
+ },
2877
+ async (record) => {
2878
+ const { directory, minecraft, runtime } = input.target;
2879
+ record(
2880
+ await verifyHashedFile({
2881
+ path: targetPaths.versionJar(directory, minecraft.version),
2882
+ expectedSha1: minecraft.manifest.downloads.client.sha1,
2883
+ expectedSize: minecraft.manifest.downloads.client.size,
2884
+ url: minecraft.manifest.downloads.client.url,
2885
+ category: VerifyFileCategories.CLIENT_JAR
2886
+ })
2887
+ );
2888
+ record(
2889
+ await verifyExistence({
2890
+ path: targetPaths.versionJson(directory, minecraft.version),
2891
+ category: VerifyFileCategories.CLIENT_JAR
2892
+ })
2893
+ );
2894
+ if (minecraft.manifest.logging?.client) {
2895
+ const logging = minecraft.manifest.logging.client;
2896
+ record(
2897
+ await verifyHashedFile({
2898
+ path: targetPaths.loggingConfig(directory, logging.file.id),
2899
+ expectedSha1: logging.file.sha1,
2900
+ expectedSize: logging.file.size,
2901
+ url: logging.file.url,
2902
+ category: VerifyFileCategories.LOGGING_CONFIG
2903
+ })
2904
+ );
2905
+ }
2906
+ const libraryPlan = planLibraryDownloads({
2907
+ libraries: minecraft.manifest.libraries,
2908
+ directory,
2909
+ system: runtime.system,
2910
+ versionId: minecraft.version,
2911
+ category: "library"
2912
+ });
2913
+ for (const action of libraryPlan.downloads) {
2914
+ record(
2915
+ await verifyHashedFile({
2916
+ path: action.target,
2917
+ expectedSha1: action.expectedSha1,
2918
+ expectedSize: action.expectedSize,
2919
+ url: action.url,
2920
+ category: VerifyFileCategories.LIBRARY
2921
+ })
2922
+ );
2923
+ }
2924
+ const indexUrl = minecraft.manifest.assetIndex.url;
2925
+ const indexPath = targetPaths.assetIndex(directory, minecraft.manifest.assetIndex.id);
2926
+ record(
2927
+ await verifyHashedFile({
2928
+ path: indexPath,
2929
+ expectedSha1: minecraft.manifest.assetIndex.sha1,
2930
+ expectedSize: minecraft.manifest.assetIndex.size,
2931
+ url: indexUrl,
2932
+ category: VerifyFileCategories.ASSET_INDEX
2933
+ })
2934
+ );
2935
+ const indexDocument = await fetchJson(input.http, input.cache, {
2936
+ url: indexUrl,
2937
+ cacheKey: `asset-index:${minecraft.manifest.assetIndex.id}:${minecraft.manifest.assetIndex.sha1}`,
2938
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
2939
+ });
2940
+ const seenAssetHashes = /* @__PURE__ */ new Set();
2941
+ for (const entry of Object.values(indexDocument.objects)) {
2942
+ if (seenAssetHashes.has(entry.hash)) continue;
2943
+ seenAssetHashes.add(entry.hash);
2944
+ record(
2945
+ await verifyHashedFile({
2946
+ path: targetPaths.assetObject(directory, entry.hash),
2947
+ expectedSha1: entry.hash,
2948
+ expectedSize: entry.size,
2949
+ category: VerifyFileCategories.ASSET
2950
+ })
2951
+ );
2952
+ }
2953
+ const nativesDir = targetPaths.nativesDir(directory, minecraft.version);
2954
+ if (!await fileExists(nativesDir)) {
2955
+ for (const extraction of libraryPlan.nativeExtractions) {
2956
+ record({
2957
+ path: extraction.source,
2958
+ category: VerifyFileCategories.NATIVE,
2959
+ status: VerifyFileStatuses.MISSING
2960
+ });
2961
+ }
2962
+ }
2595
2963
  }
2596
- return false;
2597
- });
2964
+ );
2598
2965
  }
2599
-
2600
- // src/repair/runner.ts
2601
- async function runRepair(input) {
2602
- const report = await runInstall({
2603
- plan: {
2604
- ...input.plan,
2605
- totalActions: input.plan.actions.length,
2606
- totalBytes: input.plan.totalBytes
2966
+ async function verifyRuntime(input) {
2967
+ return runVerification(
2968
+ {
2969
+ targetId: input.target.id,
2970
+ kind: VerificationKinds.RUNTIME,
2971
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2607
2972
  },
2608
- http: input.http,
2973
+ async (record) => {
2974
+ let manifest;
2975
+ try {
2976
+ manifest = await fetchJson(input.http, input.cache, {
2977
+ url: input.target.runtime.manifestUrl,
2978
+ cacheKey: `runtime-manifest:${input.target.runtime.component}:${input.target.runtime.platformKey}:${input.target.runtime.manifestSha1}`,
2979
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
2980
+ });
2981
+ } catch {
2982
+ record({
2983
+ path: input.target.runtime.manifestUrl,
2984
+ category: VerifyFileCategories.RUNTIME_FILE,
2985
+ status: VerifyFileStatuses.MISSING
2986
+ });
2987
+ return;
2988
+ }
2989
+ const runtimeRoot = targetPaths.runtimeRoot(
2990
+ input.target.directory,
2991
+ input.target.runtime.component,
2992
+ input.target.runtime.installRoot
2993
+ );
2994
+ for (const [relative, entry] of Object.entries(manifest.files)) {
2995
+ if (entry.type !== "file") continue;
2996
+ record(
2997
+ await verifyHashedFile({
2998
+ path: path.join(runtimeRoot, relative),
2999
+ expectedSha1: entry.downloads.raw.sha1,
3000
+ expectedSize: entry.downloads.raw.size,
3001
+ url: entry.downloads.raw.url,
3002
+ category: VerifyFileCategories.RUNTIME_FILE
3003
+ })
3004
+ );
3005
+ }
3006
+ }
3007
+ );
3008
+ }
3009
+
3010
+ // src/repair/helpers.ts
3011
+ function asResultArray(from) {
3012
+ return Array.isArray(from) ? from : [from];
3013
+ }
3014
+ function buildIssueIndex(from) {
3015
+ const map = /* @__PURE__ */ new Map();
3016
+ for (const v of asResultArray(from)) {
3017
+ for (const issue of v.issues) {
3018
+ const set = map.get(issue.path);
3019
+ if (set) set.add(issue.category);
3020
+ else map.set(issue.path, /* @__PURE__ */ new Set([issue.category]));
3021
+ }
3022
+ }
3023
+ return {
3024
+ has: (path13) => map.has(path13),
3025
+ hasNonNative: (path13) => {
3026
+ const cats = map.get(path13);
3027
+ if (!cats) return false;
3028
+ for (const c of cats) {
3029
+ if (c !== VerifyFileCategories.NATIVE) return true;
3030
+ }
3031
+ return false;
3032
+ },
3033
+ categoriesAt: (path13) => map.get(path13) ?? /* @__PURE__ */ new Set()
3034
+ };
3035
+ }
3036
+ function sumDownloadBytes(actions) {
3037
+ return actions.reduce((sum, action) => {
3038
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3039
+ return sum + (action.expectedSize ?? 0);
3040
+ }
3041
+ return sum;
3042
+ }, 0);
3043
+ }
3044
+ function buildRepairPlan(target, actions) {
3045
+ return {
3046
+ targetId: target.id,
3047
+ directory: target.directory,
3048
+ target,
3049
+ actions,
3050
+ totalActions: actions.length,
3051
+ totalBytes: sumDownloadBytes(actions)
3052
+ };
3053
+ }
3054
+ async function planAspectRepair(input, aspectFilter, postprocess) {
3055
+ const installPlan = await planInstall({
3056
+ target: input.target,
3057
+ http: input.http,
3058
+ cache: input.cache,
3059
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3060
+ });
3061
+ const issues = buildIssueIndex(input.from);
3062
+ const actions = selectRepairActions({
3063
+ target: input.target,
3064
+ installPlan,
3065
+ issues,
3066
+ aspectFilter
3067
+ });
3068
+ postprocess?.({ actions, installPlan, issues });
3069
+ return buildRepairPlan(input.target, actions);
3070
+ }
3071
+ function selectRepairActions(input) {
3072
+ const matching = [];
3073
+ for (const action of input.installPlan.actions) {
3074
+ if (!input.aspectFilter(action)) continue;
3075
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3076
+ if (input.issues.hasNonNative(action.target)) {
3077
+ matching.push(action);
3078
+ }
3079
+ } else if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3080
+ if (input.issues.has(action.path)) {
3081
+ matching.push(action);
3082
+ }
3083
+ } else if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
3084
+ if (input.issues.has(action.source)) {
3085
+ matching.push(action);
3086
+ }
3087
+ } else {
3088
+ matching.push(action);
3089
+ }
3090
+ }
3091
+ return matching;
3092
+ }
3093
+
3094
+ // src/repair/fabric.ts
3095
+ async function planFabricRepair(input) {
3096
+ if (input.target.loader.type !== Loaders.FABRIC) {
3097
+ throw new MinecraftKitError(
3098
+ "INVALID_INPUT",
3099
+ `repair.fabric requires a Fabric target (got ${input.target.loader.type})`
3100
+ );
3101
+ }
3102
+ const fabricJsonPath = targetPaths.versionJson(
3103
+ input.target.directory,
3104
+ input.target.loader.profile.id
3105
+ );
3106
+ return planAspectRepair(input, (action) => {
3107
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3108
+ return action.category === "fabric-library";
3109
+ }
3110
+ if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3111
+ return action.path === fabricJsonPath;
3112
+ }
3113
+ return false;
3114
+ });
3115
+ }
3116
+
3117
+ // src/repair/forge.ts
3118
+ var FORGE_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
3119
+ "forge-library",
3120
+ "forge-installer"
3121
+ ]);
3122
+ async function planForgeRepair(input) {
3123
+ if (input.target.loader.type !== Loaders.FORGE) {
3124
+ throw new MinecraftKitError(
3125
+ "INVALID_INPUT",
3126
+ `repair.forge requires a Forge target (got ${input.target.loader.type})`
3127
+ );
3128
+ }
3129
+ const forgeJsonPath = targetPaths.versionJson(
3130
+ input.target.directory,
3131
+ input.target.loader.fullVersion
3132
+ );
3133
+ return planAspectRepair(
3134
+ input,
3135
+ (action) => {
3136
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3137
+ return FORGE_DOWNLOAD_CATEGORIES.has(action.category);
3138
+ }
3139
+ if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3140
+ return action.path === forgeJsonPath;
3141
+ }
3142
+ return false;
3143
+ },
3144
+ ({ actions, installPlan, issues }) => {
3145
+ if (!issues.has(forgeJsonPath)) return;
3146
+ const alreadyIncluded = new Set(
3147
+ actions.filter((a) => a.kind === InstallActionKinds.DOWNLOAD_FILE).map((a) => a.target)
3148
+ );
3149
+ for (const action of installPlan.actions) {
3150
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE && action.category === "forge-library" && !alreadyIncluded.has(action.target)) {
3151
+ actions.push(action);
3152
+ } else if (action.kind === InstallActionKinds.RUN_FORGE_PROCESSOR) {
3153
+ actions.push(action);
3154
+ }
3155
+ }
3156
+ }
3157
+ );
3158
+ }
3159
+
3160
+ // src/repair/minecraft.ts
3161
+ var MINECRAFT_DOWNLOAD_CATEGORIES = /* @__PURE__ */ new Set([
3162
+ "client-jar",
3163
+ "library",
3164
+ "asset-index",
3165
+ "asset",
3166
+ "logging-config"
3167
+ ]);
3168
+ async function planMinecraftRepair(input) {
3169
+ const vanillaJsonPath = targetPaths.versionJson(
3170
+ input.target.directory,
3171
+ input.target.minecraft.version
3172
+ );
3173
+ return planAspectRepair(input, (action) => {
3174
+ if (action.kind === InstallActionKinds.DOWNLOAD_FILE) {
3175
+ return MINECRAFT_DOWNLOAD_CATEGORIES.has(action.category);
3176
+ }
3177
+ if (action.kind === InstallActionKinds.WRITE_VERSION_JSON) {
3178
+ return action.path === vanillaJsonPath;
3179
+ }
3180
+ if (action.kind === InstallActionKinds.EXTRACT_NATIVE) {
3181
+ return true;
3182
+ }
3183
+ return false;
3184
+ });
3185
+ }
3186
+
3187
+ // src/repair/runner.ts
3188
+ async function runRepair(input) {
3189
+ const report = await runInstall({
3190
+ plan: {
3191
+ ...input.plan,
3192
+ totalActions: input.plan.actions.length,
3193
+ totalBytes: input.plan.totalBytes
3194
+ },
3195
+ http: input.http,
2609
3196
  cache: input.cache,
2610
3197
  spawner: input.spawner,
2611
3198
  ...input.signal !== void 0 ? { signal: input.signal } : {},
@@ -2627,6 +3214,58 @@ async function planRuntimeRepair(input) {
2627
3214
  );
2628
3215
  }
2629
3216
 
3217
+ // src/repair/all.ts
3218
+ async function repairAll(input) {
3219
+ const startedAt = Date.now();
3220
+ const ctx = {
3221
+ target: input.target,
3222
+ http: input.http,
3223
+ cache: input.cache,
3224
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3225
+ };
3226
+ const verifications = [];
3227
+ const mc = await verifyMinecraft(ctx);
3228
+ verifications.push(mc);
3229
+ const rt = await verifyRuntime(ctx);
3230
+ verifications.push(rt);
3231
+ if (input.target.loader.type === Loaders.FABRIC) {
3232
+ verifications.push(await verifyFabric(ctx));
3233
+ } else if (input.target.loader.type === Loaders.FORGE) {
3234
+ verifications.push(await verifyForge(ctx));
3235
+ }
3236
+ const repairs = /* @__PURE__ */ new Map();
3237
+ let bytesDownloaded = 0;
3238
+ for (const verification of verifications) {
3239
+ if (verification.isValid) continue;
3240
+ const planner = PLANNERS[verification.kind];
3241
+ if (!planner) continue;
3242
+ const plan = await planner({ ...ctx, from: verification });
3243
+ if (plan.totalActions === 0) continue;
3244
+ const report = await runRepair({
3245
+ plan,
3246
+ http: input.http,
3247
+ cache: input.cache,
3248
+ spawner: input.spawner,
3249
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
3250
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3251
+ });
3252
+ repairs.set(verification.kind, report);
3253
+ bytesDownloaded += report.bytesDownloaded;
3254
+ }
3255
+ return {
3256
+ verifications,
3257
+ repairs,
3258
+ bytesDownloaded,
3259
+ durationMs: Date.now() - startedAt
3260
+ };
3261
+ }
3262
+ var PLANNERS = {
3263
+ minecraft: planMinecraftRepair,
3264
+ runtime: planRuntimeRepair,
3265
+ fabric: planFabricRepair,
3266
+ forge: planForgeRepair
3267
+ };
3268
+
2630
3269
  // src/types/runtime.ts
2631
3270
  var RuntimeComponents = {
2632
3271
  JRE_LEGACY: "jre-legacy",
@@ -2701,485 +3340,125 @@ var TargetsApi = class {
2701
3340
  const runtime = input.runtime?.installRoot !== void 0 ? { ...resolvedRuntime, installRoot: input.runtime.installRoot } : resolvedRuntime;
2702
3341
  let loader;
2703
3342
  if (input.loader.type === Loaders.VANILLA) {
2704
- loader = {
2705
- type: Loaders.VANILLA,
2706
- minecraftVersion: minecraft.version,
2707
- minecraft
2708
- };
2709
- } else if (input.loader.type === Loaders.FABRIC) {
2710
- loader = await this.ctx.fabric.resolve({
2711
- minecraftVersion: minecraft.version,
2712
- ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
2713
- ...input.loader.version !== void 0 ? { loaderVersion: input.loader.version } : {},
2714
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2715
- });
2716
- } else {
2717
- loader = await this.ctx.forge.resolve({
2718
- minecraftVersion: minecraft.version,
2719
- ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
2720
- ...input.loader.version !== void 0 ? { forgeVersion: input.loader.version } : {},
2721
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2722
- });
2723
- }
2724
- return this.create({
2725
- id: input.id,
2726
- directory: input.directory,
2727
- minecraft,
2728
- loader,
2729
- runtime
2730
- });
2731
- }
2732
- /** Scan a root directory for Minecraft installations. Returns only what is on disk. */
2733
- async list(input) {
2734
- if (!await dirExists(input.rootDir)) return [];
2735
- const subdirs = await listChildDirectories(input.rootDir);
2736
- const results = [];
2737
- for (const id of subdirs) {
2738
- const directory = path.join(input.rootDir, id);
2739
- const discovered = await discoverInstallation(id, directory);
2740
- if (discovered) results.push(discovered);
2741
- }
2742
- return results;
2743
- }
2744
- };
2745
- async function discoverInstallation(id, directory) {
2746
- const versionsDir = path.join(directory, VERSIONS_DIR);
2747
- const librariesDir = path.join(directory, LIBRARIES_DIR);
2748
- const assetsDir = path.join(directory, ASSETS_DIR);
2749
- const looksLikeInstall = await dirExists(versionsDir) && (await dirExists(librariesDir) || await dirExists(assetsDir));
2750
- if (!looksLikeInstall) return null;
2751
- const versionDirs = await listChildDirectories(versionsDir);
2752
- const minecraftVersions = [];
2753
- const loaders = [];
2754
- for (const versionId of versionDirs) {
2755
- const hint = inferLoaderFromVersionId(versionId);
2756
- if (hint) {
2757
- loaders.push(hint);
2758
- if (hint.minecraftVersion && !minecraftVersions.includes(hint.minecraftVersion)) {
2759
- minecraftVersions.push(hint.minecraftVersion);
2760
- }
2761
- } else {
2762
- minecraftVersions.push(versionId);
2763
- }
2764
- }
2765
- const runtime = await discoverRuntime(directory);
2766
- return { id, directory, minecraftVersions, loaders, ...runtime ? { runtime } : {} };
2767
- }
2768
- async function discoverRuntime(directory) {
2769
- const runtimeDir = path.join(directory, RUNTIMES_DIR);
2770
- if (!await dirExists(runtimeDir)) return void 0;
2771
- let components;
2772
- try {
2773
- components = await listChildDirectories(runtimeDir);
2774
- } catch {
2775
- return void 0;
2776
- }
2777
- for (const component of components) {
2778
- const root = path.join(runtimeDir, component);
2779
- const javaPath = process.platform === "win32" ? path.join(root, "bin", "javaw.exe") : process.platform === "darwin" ? path.join(root, "jre.bundle", "Contents", "Home", "bin", "java") : path.join(root, "bin", "java");
2780
- if (await fileExists(javaPath)) {
2781
- return { component, javaPath };
2782
- }
2783
- }
2784
- return void 0;
2785
- }
2786
- function inferLoaderFromVersionId(versionId) {
2787
- const fabricMatch = /^fabric-loader-([^-]+)-(.+)$/.exec(versionId);
2788
- if (fabricMatch?.[1] && fabricMatch[2]) {
2789
- return { type: Loaders.FABRIC, version: fabricMatch[1], minecraftVersion: fabricMatch[2] };
2790
- }
2791
- const forgeMatch = /^([^-]+)-forge-(.+)$/.exec(versionId);
2792
- if (forgeMatch?.[1] && forgeMatch[2]) {
2793
- return { type: Loaders.FORGE, minecraftVersion: forgeMatch[1], version: forgeMatch[2] };
2794
- }
2795
- return null;
2796
- }
2797
-
2798
- // src/update/runner.ts
2799
- async function planUpdate(input) {
2800
- return planInstall({
2801
- target: input.target,
2802
- http: input.http,
2803
- cache: input.cache,
2804
- ...input.signal !== void 0 ? { signal: input.signal } : {}
2805
- });
2806
- }
2807
- async function runUpdate(input) {
2808
- const report = await runInstall({
2809
- plan: input.plan,
2810
- http: input.http,
2811
- cache: input.cache,
2812
- spawner: input.spawner,
2813
- ...input.signal !== void 0 ? { signal: input.signal } : {},
2814
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2815
- });
2816
- return {
2817
- targetId: report.targetId,
2818
- bytesDownloaded: report.bytesDownloaded,
2819
- actionsCompleted: report.actionsCompleted,
2820
- actionsSkipped: report.actionsSkipped,
2821
- durationMs: report.durationMs
2822
- };
2823
- }
2824
- async function sha1OfFile(filePath) {
2825
- const hash = crypto2.createHash("sha1");
2826
- await new Promise((resolve, reject) => {
2827
- const stream = createReadStream(filePath);
2828
- stream.on("data", (chunk) => hash.update(chunk));
2829
- stream.on("end", resolve);
2830
- stream.on("error", reject);
2831
- });
2832
- return hash.digest("hex");
2833
- }
2834
-
2835
- // src/verify/helpers.ts
2836
- async function runVerification(input, check) {
2837
- const startedAt = Date.now();
2838
- const results = [];
2839
- const record = (result) => {
2840
- results.push(result);
2841
- input.onEvent?.({ type: "verify:file-checked", file: result });
2842
- };
2843
- await check(record);
2844
- return {
2845
- targetId: input.targetId,
2846
- kind: input.kind,
2847
- isValid: results.every((r) => r.status === VerifyFileStatuses.OK),
2848
- issues: results.filter((r) => r.status !== VerifyFileStatuses.OK),
2849
- checkedFiles: results.length,
2850
- durationMs: Date.now() - startedAt
2851
- };
2852
- }
2853
- async function verifyHashedFile(input) {
2854
- if (!await fileExists(input.path)) {
2855
- return {
2856
- path: input.path,
2857
- category: input.category,
2858
- status: VerifyFileStatuses.MISSING,
2859
- ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2860
- ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2861
- ...input.url !== void 0 ? { url: input.url } : {}
2862
- };
2863
- }
2864
- if (input.expectedSize !== void 0) {
2865
- const size = await fileSize(input.path);
2866
- if (size !== input.expectedSize) {
2867
- return {
2868
- path: input.path,
2869
- category: input.category,
2870
- status: VerifyFileStatuses.WRONG_SIZE,
2871
- expectedSize: input.expectedSize,
2872
- actualSize: size,
2873
- ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2874
- ...input.url !== void 0 ? { url: input.url } : {}
2875
- };
2876
- }
2877
- }
2878
- if (input.expectedSha1 !== void 0) {
2879
- const actualSha1 = await sha1OfFile(input.path);
2880
- if (actualSha1 !== input.expectedSha1) {
2881
- return {
2882
- path: input.path,
2883
- category: input.category,
2884
- status: VerifyFileStatuses.CORRUPT,
2885
- expectedSha1: input.expectedSha1,
2886
- actualSha1,
2887
- ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2888
- ...input.url !== void 0 ? { url: input.url } : {}
2889
- };
2890
- }
2891
- }
2892
- return {
2893
- path: input.path,
2894
- category: input.category,
2895
- status: VerifyFileStatuses.OK,
2896
- ...input.expectedSha1 !== void 0 ? { expectedSha1: input.expectedSha1 } : {},
2897
- ...input.expectedSize !== void 0 ? { expectedSize: input.expectedSize } : {},
2898
- ...input.url !== void 0 ? { url: input.url } : {}
2899
- };
2900
- }
2901
- async function verifyExistence(input) {
2902
- if (await fileExists(input.path)) {
2903
- return {
2904
- path: input.path,
2905
- category: input.category,
2906
- status: VerifyFileStatuses.OK,
2907
- ...input.url !== void 0 ? { url: input.url } : {}
2908
- };
2909
- }
2910
- return {
2911
- path: input.path,
2912
- category: input.category,
2913
- status: VerifyFileStatuses.MISSING,
2914
- ...input.url !== void 0 ? { url: input.url } : {}
2915
- };
2916
- }
2917
- async function findForgeVersionJsonPath(directory, minecraftVersion) {
2918
- const versionsDir = targetPaths.versionsDir(directory);
2919
- const dirs = await listChildDirectories(versionsDir);
2920
- for (const id of dirs) {
2921
- if (!id.startsWith(`${minecraftVersion}-forge-`)) continue;
2922
- const jsonPath = targetPaths.versionJson(directory, id);
2923
- if (!await fileExists(jsonPath)) {
2924
- return jsonPath;
2925
- }
2926
- const parsed = await tryParseInheritsFrom(jsonPath);
2927
- if (parsed === minecraftVersion) return jsonPath;
2928
- }
2929
- return null;
2930
- }
2931
- async function tryParseInheritsFrom(jsonPath) {
2932
- try {
2933
- const parsed = JSON.parse(await readText(jsonPath));
2934
- return parsed.inheritsFrom;
2935
- } catch {
2936
- return void 0;
2937
- }
2938
- }
2939
-
2940
- // src/verify/fabric.ts
2941
- async function verifyFabric(input) {
2942
- if (input.target.loader.type !== Loaders.FABRIC) {
2943
- throw new MinecraftKitError(
2944
- "INVALID_INPUT",
2945
- `verify.fabric requires a Fabric target (got ${input.target.loader.type})`
2946
- );
2947
- }
2948
- const loader = input.target.loader;
2949
- return runVerification(
2950
- {
2951
- targetId: input.target.id,
2952
- kind: VerificationKinds.FABRIC,
2953
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2954
- },
2955
- async (record) => {
2956
- record(
2957
- await verifyExistence({
2958
- path: targetPaths.versionJson(input.target.directory, loader.profile.id),
2959
- category: VerifyFileCategories.LOADER_LIBRARY
2960
- })
2961
- );
2962
- const fabricLibraries = planLibraryDownloads({
2963
- libraries: loader.profile.libraries,
2964
- directory: input.target.directory,
2965
- system: input.target.runtime.system,
2966
- versionId: input.target.minecraft.version,
2967
- category: "fabric-library"
2968
- });
2969
- for (const action of fabricLibraries.downloads) {
2970
- record(
2971
- await verifyHashedFile({
2972
- path: action.target,
2973
- expectedSha1: action.expectedSha1,
2974
- expectedSize: action.expectedSize,
2975
- ...action.url ? { url: action.url } : {},
2976
- category: VerifyFileCategories.LOADER_LIBRARY
2977
- })
2978
- );
2979
- }
2980
- }
2981
- );
2982
- }
2983
-
2984
- // src/verify/forge.ts
2985
- async function verifyForge(input) {
2986
- if (input.target.loader.type !== Loaders.FORGE) {
2987
- throw new MinecraftKitError(
2988
- "INVALID_INPUT",
2989
- `verify.forge requires a Forge target (got ${input.target.loader.type})`
2990
- );
2991
- }
2992
- return runVerification(
2993
- {
2994
- targetId: input.target.id,
2995
- kind: VerificationKinds.FORGE,
2996
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
2997
- },
2998
- async (record) => {
2999
- const forgeVersionJsonPath = await findForgeVersionJsonPath(
3000
- input.target.directory,
3001
- input.target.minecraft.version
3002
- );
3003
- if (forgeVersionJsonPath === null) return;
3004
- record(
3005
- await verifyExistence({
3006
- path: forgeVersionJsonPath,
3007
- category: VerifyFileCategories.LOADER_LIBRARY
3008
- })
3009
- );
3010
- if (!await fileExists(forgeVersionJsonPath)) return;
3011
- let parsed;
3012
- try {
3013
- parsed = JSON.parse(await readText(forgeVersionJsonPath));
3014
- } catch {
3015
- record({
3016
- path: forgeVersionJsonPath,
3017
- category: VerifyFileCategories.LOADER_LIBRARY,
3018
- status: VerifyFileStatuses.CORRUPT
3019
- });
3020
- return;
3021
- }
3022
- const forgeLibraries = planLibraryDownloads({
3023
- libraries: parsed.libraries,
3024
- directory: input.target.directory,
3025
- system: input.target.runtime.system,
3026
- versionId: input.target.minecraft.version,
3027
- category: "forge-library"
3028
- });
3029
- for (const action of forgeLibraries.downloads) {
3030
- record(
3031
- await verifyHashedFile({
3032
- path: action.target,
3033
- expectedSha1: action.expectedSha1,
3034
- expectedSize: action.expectedSize,
3035
- ...action.url ? { url: action.url } : {},
3036
- category: VerifyFileCategories.LOADER_LIBRARY
3037
- })
3038
- );
3039
- }
3040
- }
3041
- );
3042
- }
3043
-
3044
- // src/verify/minecraft.ts
3045
- async function verifyMinecraft(input) {
3046
- return runVerification(
3047
- {
3048
- targetId: input.target.id,
3049
- kind: VerificationKinds.MINECRAFT,
3050
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3051
- },
3052
- async (record) => {
3053
- const { directory, minecraft, runtime } = input.target;
3054
- record(
3055
- await verifyHashedFile({
3056
- path: targetPaths.versionJar(directory, minecraft.version),
3057
- expectedSha1: minecraft.manifest.downloads.client.sha1,
3058
- expectedSize: minecraft.manifest.downloads.client.size,
3059
- url: minecraft.manifest.downloads.client.url,
3060
- category: VerifyFileCategories.CLIENT_JAR
3061
- })
3062
- );
3063
- record(
3064
- await verifyExistence({
3065
- path: targetPaths.versionJson(directory, minecraft.version),
3066
- category: VerifyFileCategories.CLIENT_JAR
3067
- })
3068
- );
3069
- if (minecraft.manifest.logging?.client) {
3070
- const logging = minecraft.manifest.logging.client;
3071
- record(
3072
- await verifyHashedFile({
3073
- path: targetPaths.loggingConfig(directory, logging.file.id),
3074
- expectedSha1: logging.file.sha1,
3075
- expectedSize: logging.file.size,
3076
- url: logging.file.url,
3077
- category: VerifyFileCategories.LOGGING_CONFIG
3078
- })
3079
- );
3080
- }
3081
- const libraryPlan = planLibraryDownloads({
3082
- libraries: minecraft.manifest.libraries,
3083
- directory,
3084
- system: runtime.system,
3085
- versionId: minecraft.version,
3086
- category: "library"
3343
+ loader = {
3344
+ type: Loaders.VANILLA,
3345
+ minecraftVersion: minecraft.version,
3346
+ minecraft
3347
+ };
3348
+ } else if (input.loader.type === Loaders.FABRIC) {
3349
+ loader = await this.ctx.fabric.resolve({
3350
+ minecraftVersion: minecraft.version,
3351
+ ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
3352
+ ...input.loader.version !== void 0 ? { loaderVersion: input.loader.version } : {},
3353
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3087
3354
  });
3088
- for (const action of libraryPlan.downloads) {
3089
- record(
3090
- await verifyHashedFile({
3091
- path: action.target,
3092
- expectedSha1: action.expectedSha1,
3093
- expectedSize: action.expectedSize,
3094
- url: action.url,
3095
- category: VerifyFileCategories.LIBRARY
3096
- })
3097
- );
3098
- }
3099
- const indexUrl = minecraft.manifest.assetIndex.url;
3100
- const indexPath = targetPaths.assetIndex(directory, minecraft.manifest.assetIndex.id);
3101
- record(
3102
- await verifyHashedFile({
3103
- path: indexPath,
3104
- expectedSha1: minecraft.manifest.assetIndex.sha1,
3105
- expectedSize: minecraft.manifest.assetIndex.size,
3106
- url: indexUrl,
3107
- category: VerifyFileCategories.ASSET_INDEX
3108
- })
3109
- );
3110
- const indexDocument = await fetchJson(input.http, input.cache, {
3111
- url: indexUrl,
3112
- cacheKey: `asset-index:${minecraft.manifest.assetIndex.id}:${minecraft.manifest.assetIndex.sha1}`,
3355
+ } else {
3356
+ loader = await this.ctx.forge.resolve({
3357
+ minecraftVersion: minecraft.version,
3358
+ ...input.loader.preference !== void 0 ? { preference: input.loader.preference } : {},
3359
+ ...input.loader.version !== void 0 ? { forgeVersion: input.loader.version } : {},
3113
3360
  ...input.signal !== void 0 ? { signal: input.signal } : {}
3114
3361
  });
3115
- const seenAssetHashes = /* @__PURE__ */ new Set();
3116
- for (const entry of Object.values(indexDocument.objects)) {
3117
- if (seenAssetHashes.has(entry.hash)) continue;
3118
- seenAssetHashes.add(entry.hash);
3119
- record(
3120
- await verifyHashedFile({
3121
- path: targetPaths.assetObject(directory, entry.hash),
3122
- expectedSha1: entry.hash,
3123
- expectedSize: entry.size,
3124
- category: VerifyFileCategories.ASSET
3125
- })
3126
- );
3127
- }
3128
- const nativesDir = targetPaths.nativesDir(directory, minecraft.version);
3129
- if (!await fileExists(nativesDir)) {
3130
- for (const extraction of libraryPlan.nativeExtractions) {
3131
- record({
3132
- path: extraction.source,
3133
- category: VerifyFileCategories.NATIVE,
3134
- status: VerifyFileStatuses.MISSING
3135
- });
3136
- }
3362
+ }
3363
+ return this.create({
3364
+ id: input.id,
3365
+ directory: input.directory,
3366
+ minecraft,
3367
+ loader,
3368
+ runtime
3369
+ });
3370
+ }
3371
+ /** Scan a root directory for Minecraft installations. Returns only what is on disk. */
3372
+ async list(input) {
3373
+ if (!await dirExists(input.rootDir)) return [];
3374
+ const subdirs = await listChildDirectories(input.rootDir);
3375
+ const results = [];
3376
+ for (const id of subdirs) {
3377
+ const directory = path.join(input.rootDir, id);
3378
+ const discovered = await discoverInstallation(id, directory);
3379
+ if (discovered) results.push(discovered);
3380
+ }
3381
+ return results;
3382
+ }
3383
+ };
3384
+ async function discoverInstallation(id, directory) {
3385
+ const versionsDir = path.join(directory, VERSIONS_DIR);
3386
+ const librariesDir = path.join(directory, LIBRARIES_DIR);
3387
+ const assetsDir = path.join(directory, ASSETS_DIR);
3388
+ const looksLikeInstall = await dirExists(versionsDir) && (await dirExists(librariesDir) || await dirExists(assetsDir));
3389
+ if (!looksLikeInstall) return null;
3390
+ const versionDirs = await listChildDirectories(versionsDir);
3391
+ const minecraftVersions = [];
3392
+ const loaders = [];
3393
+ for (const versionId of versionDirs) {
3394
+ const hint = inferLoaderFromVersionId(versionId);
3395
+ if (hint) {
3396
+ loaders.push(hint);
3397
+ if (hint.minecraftVersion && !minecraftVersions.includes(hint.minecraftVersion)) {
3398
+ minecraftVersions.push(hint.minecraftVersion);
3137
3399
  }
3400
+ } else {
3401
+ minecraftVersions.push(versionId);
3138
3402
  }
3139
- );
3403
+ }
3404
+ const runtime = await discoverRuntime(directory);
3405
+ return { id, directory, minecraftVersions, loaders, ...runtime ? { runtime } : {} };
3140
3406
  }
3141
- async function verifyRuntime(input) {
3142
- return runVerification(
3143
- {
3144
- targetId: input.target.id,
3145
- kind: VerificationKinds.RUNTIME,
3146
- ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3147
- },
3148
- async (record) => {
3149
- let manifest;
3150
- try {
3151
- manifest = await fetchJson(input.http, input.cache, {
3152
- url: input.target.runtime.manifestUrl,
3153
- cacheKey: `runtime-manifest:${input.target.runtime.component}:${input.target.runtime.platformKey}:${input.target.runtime.manifestSha1}`,
3154
- ...input.signal !== void 0 ? { signal: input.signal } : {}
3155
- });
3156
- } catch {
3157
- record({
3158
- path: input.target.runtime.manifestUrl,
3159
- category: VerifyFileCategories.RUNTIME_FILE,
3160
- status: VerifyFileStatuses.MISSING
3161
- });
3162
- return;
3163
- }
3164
- const runtimeRoot = targetPaths.runtimeRoot(
3165
- input.target.directory,
3166
- input.target.runtime.component,
3167
- input.target.runtime.installRoot
3168
- );
3169
- for (const [relative, entry] of Object.entries(manifest.files)) {
3170
- if (entry.type !== "file") continue;
3171
- record(
3172
- await verifyHashedFile({
3173
- path: path.join(runtimeRoot, relative),
3174
- expectedSha1: entry.downloads.raw.sha1,
3175
- expectedSize: entry.downloads.raw.size,
3176
- url: entry.downloads.raw.url,
3177
- category: VerifyFileCategories.RUNTIME_FILE
3178
- })
3179
- );
3180
- }
3407
+ async function discoverRuntime(directory) {
3408
+ const runtimeDir = path.join(directory, RUNTIMES_DIR);
3409
+ if (!await dirExists(runtimeDir)) return void 0;
3410
+ let components;
3411
+ try {
3412
+ components = await listChildDirectories(runtimeDir);
3413
+ } catch {
3414
+ return void 0;
3415
+ }
3416
+ for (const component of components) {
3417
+ const root = path.join(runtimeDir, component);
3418
+ const javaPath = process.platform === "win32" ? path.join(root, "bin", "javaw.exe") : process.platform === "darwin" ? path.join(root, "jre.bundle", "Contents", "Home", "bin", "java") : path.join(root, "bin", "java");
3419
+ if (await fileExists(javaPath)) {
3420
+ return { component, javaPath };
3181
3421
  }
3182
- );
3422
+ }
3423
+ return void 0;
3424
+ }
3425
+ function inferLoaderFromVersionId(versionId) {
3426
+ const fabricMatch = /^fabric-loader-([^-]+)-(.+)$/.exec(versionId);
3427
+ if (fabricMatch?.[1] && fabricMatch[2]) {
3428
+ return { type: Loaders.FABRIC, version: fabricMatch[1], minecraftVersion: fabricMatch[2] };
3429
+ }
3430
+ const forgeMatch = /^([^-]+)-forge-(.+)$/.exec(versionId);
3431
+ if (forgeMatch?.[1] && forgeMatch[2]) {
3432
+ return { type: Loaders.FORGE, minecraftVersion: forgeMatch[1], version: forgeMatch[2] };
3433
+ }
3434
+ return null;
3435
+ }
3436
+
3437
+ // src/update/runner.ts
3438
+ async function planUpdate(input) {
3439
+ return planInstall({
3440
+ target: input.target,
3441
+ http: input.http,
3442
+ cache: input.cache,
3443
+ ...input.signal !== void 0 ? { signal: input.signal } : {}
3444
+ });
3445
+ }
3446
+ async function runUpdate(input) {
3447
+ const report = await runInstall({
3448
+ plan: input.plan,
3449
+ http: input.http,
3450
+ cache: input.cache,
3451
+ spawner: input.spawner,
3452
+ ...input.signal !== void 0 ? { signal: input.signal } : {},
3453
+ ...input.onEvent !== void 0 ? { onEvent: input.onEvent } : {}
3454
+ });
3455
+ return {
3456
+ targetId: report.targetId,
3457
+ bytesDownloaded: report.bytesDownloaded,
3458
+ actionsCompleted: report.actionsCompleted,
3459
+ actionsSkipped: report.actionsSkipped,
3460
+ durationMs: report.durationMs
3461
+ };
3183
3462
  }
3184
3463
 
3185
3464
  // src/versions/fabric.ts
@@ -3513,15 +3792,23 @@ function pickLatestAcrossComponents(entries) {
3513
3792
  return { component: bestComponent, entry: bestEntry };
3514
3793
  }
3515
3794
  function toResolved(component, platformKey, entry, system) {
3795
+ const majorVersion = parseMajorVersion(entry.version.name);
3516
3796
  return {
3517
3797
  component,
3518
3798
  platformKey,
3519
3799
  versionName: entry.version.name,
3800
+ ...majorVersion !== void 0 ? { majorVersion } : {},
3520
3801
  system,
3521
3802
  manifestUrl: entry.manifest.url,
3522
3803
  manifestSha1: entry.manifest.sha1
3523
3804
  };
3524
3805
  }
3806
+ function parseMajorVersion(versionName) {
3807
+ const match = /^(\d+)/.exec(versionName);
3808
+ if (!match || !match[1]) return void 0;
3809
+ const parsed = Number.parseInt(match[1], 10);
3810
+ return Number.isFinite(parsed) ? parsed : void 0;
3811
+ }
3525
3812
 
3526
3813
  // src/kit.ts
3527
3814
  var MinecraftKit = class {
@@ -3552,7 +3839,12 @@ var MinecraftKit = class {
3552
3839
  ...opts?.signal !== void 0 ? { signal: opts.signal } : {},
3553
3840
  ...opts?.onEvent !== void 0 ? { onEvent: opts.onEvent } : {}
3554
3841
  });
3555
- const runInstallPlan = (plan, opts) => runInstall({ plan, http, cache, spawner, ...carry(opts) });
3842
+ const carryInstall = (opts) => ({
3843
+ ...carry(opts),
3844
+ ...opts?.pauseController !== void 0 ? { pauseController: opts.pauseController } : {},
3845
+ ...opts?.actionCategories !== void 0 ? { actionCategories: opts.actionCategories } : {}
3846
+ });
3847
+ const runInstallPlan = (plan, opts) => runInstall({ plan, http, cache, spawner, ...carryInstall(opts) });
3556
3848
  this.install = {
3557
3849
  plan: (target, opts) => planInstall({ target, http, cache, ...carry(opts) }),
3558
3850
  run: runInstallPlan,
@@ -3602,10 +3894,17 @@ var MinecraftKit = class {
3602
3894
  runtime: {
3603
3895
  plan: (target, opts) => planRuntimeRepair(repairArgs(target, opts)),
3604
3896
  run: runRepairPlan
3605
- }
3897
+ },
3898
+ all: (target, opts) => repairAll({
3899
+ target,
3900
+ http,
3901
+ cache,
3902
+ spawner,
3903
+ ...carry(opts)
3904
+ })
3606
3905
  };
3607
3906
  this.launch = {
3608
- compose: (target, opts) => composeLaunch({ target, options: opts }),
3907
+ compose: (target, opts) => composeLaunch({ target, options: opts, logger }),
3609
3908
  run: (composition, opts) => runLaunch({
3610
3909
  composition,
3611
3910
  ...opts !== void 0 ? { options: opts } : {},
@@ -3615,6 +3914,247 @@ var MinecraftKit = class {
3615
3914
  }
3616
3915
  };
3617
3916
 
3917
+ // src/core/pause-controller.ts
3918
+ var PauseController = class {
3919
+ #paused = false;
3920
+ #waiters = [];
3921
+ get paused() {
3922
+ return this.#paused;
3923
+ }
3924
+ pause() {
3925
+ this.#paused = true;
3926
+ }
3927
+ resume() {
3928
+ this.#paused = false;
3929
+ const list = this.#waiters;
3930
+ this.#waiters = [];
3931
+ for (const resolve of list) resolve();
3932
+ }
3933
+ waitWhilePaused() {
3934
+ if (!this.#paused) return Promise.resolve();
3935
+ return new Promise((resolve) => this.#waiters.push(resolve));
3936
+ }
3937
+ };
3938
+
3939
+ // src/install/progress-tracker.ts
3940
+ var InstallStages = {
3941
+ PREPARE: "prepare",
3942
+ RUNTIME: "runtime",
3943
+ MINECRAFT: "minecraft",
3944
+ LOADER: "loader",
3945
+ FINALIZE: "finalize"
3946
+ };
3947
+ var STAGE_FOR_CATEGORY = {
3948
+ "runtime-file": InstallStages.RUNTIME,
3949
+ "client-jar": InstallStages.MINECRAFT,
3950
+ library: InstallStages.MINECRAFT,
3951
+ "asset-index": InstallStages.MINECRAFT,
3952
+ asset: InstallStages.MINECRAFT,
3953
+ "logging-config": InstallStages.MINECRAFT,
3954
+ "fabric-library": InstallStages.LOADER,
3955
+ "forge-library": InstallStages.LOADER,
3956
+ "forge-installer": InstallStages.LOADER
3957
+ };
3958
+ var STAGE_FOR_PHASE = {
3959
+ [InstallPhases.PLANNING]: InstallStages.PREPARE,
3960
+ [InstallPhases.DOWNLOADING_VERSION_MANIFEST]: InstallStages.PREPARE,
3961
+ [InstallPhases.INSTALLING_RUNTIME]: InstallStages.RUNTIME,
3962
+ [InstallPhases.DOWNLOADING_CLIENT_JAR]: InstallStages.MINECRAFT,
3963
+ [InstallPhases.DOWNLOADING_LIBRARIES]: InstallStages.MINECRAFT,
3964
+ [InstallPhases.DOWNLOADING_ASSET_INDEX]: InstallStages.MINECRAFT,
3965
+ [InstallPhases.DOWNLOADING_ASSETS]: InstallStages.MINECRAFT,
3966
+ [InstallPhases.EXTRACTING_NATIVES]: InstallStages.MINECRAFT,
3967
+ [InstallPhases.WRITING_FILES]: InstallStages.MINECRAFT,
3968
+ [InstallPhases.INSTALLING_FABRIC]: InstallStages.LOADER,
3969
+ [InstallPhases.INSTALLING_FORGE]: InstallStages.LOADER,
3970
+ [InstallPhases.RUNNING_FORGE_PROCESSORS]: InstallStages.LOADER,
3971
+ [InstallPhases.COMPLETED]: InstallStages.FINALIZE
3972
+ };
3973
+ var ALL_STAGES = [
3974
+ InstallStages.PREPARE,
3975
+ InstallStages.RUNTIME,
3976
+ InstallStages.MINECRAFT,
3977
+ InstallStages.LOADER,
3978
+ InstallStages.FINALIZE
3979
+ ];
3980
+ function createInstallProgressTracker(plan, options = {}) {
3981
+ const throttleMs = options.throttleMs ?? 100;
3982
+ const stageOfTarget = /* @__PURE__ */ new Map();
3983
+ const expectedSizeOf = /* @__PURE__ */ new Map();
3984
+ const stageTotals = {
3985
+ prepare: 0,
3986
+ runtime: 0,
3987
+ minecraft: 0,
3988
+ loader: 0,
3989
+ finalize: 0
3990
+ };
3991
+ let overallTotal = 0;
3992
+ for (const action of plan.actions) {
3993
+ if (action.kind !== "download-file") continue;
3994
+ const stage = STAGE_FOR_CATEGORY[action.category] ?? InstallStages.MINECRAFT;
3995
+ stageOfTarget.set(action.target, stage);
3996
+ const size = action.expectedSize ?? 0;
3997
+ expectedSizeOf.set(action.target, size);
3998
+ stageTotals[stage] += size;
3999
+ overallTotal += size;
4000
+ }
4001
+ const stageDone = {
4002
+ prepare: 0,
4003
+ runtime: 0,
4004
+ minecraft: 0,
4005
+ loader: 0,
4006
+ finalize: 0
4007
+ };
4008
+ const stageInFlight = {
4009
+ prepare: 0,
4010
+ runtime: 0,
4011
+ minecraft: 0,
4012
+ loader: 0,
4013
+ finalize: 0
4014
+ };
4015
+ let totalDone = 0;
4016
+ let totalInFlight = 0;
4017
+ const inFlightByTarget = /* @__PURE__ */ new Map();
4018
+ let currentStage = InstallStages.PREPARE;
4019
+ let currentFile;
4020
+ const listeners = /* @__PURE__ */ new Set();
4021
+ let lastPushAt = 0;
4022
+ let pending = false;
4023
+ let pendingTimer = null;
4024
+ let finished = false;
4025
+ const snapshot = () => {
4026
+ const stageTotal = stageTotals[currentStage];
4027
+ const stageBytes = stageDone[currentStage] + stageInFlight[currentStage];
4028
+ const overallBytes = totalDone + totalInFlight;
4029
+ return {
4030
+ stage: currentStage,
4031
+ stagePercent: stageTotal > 0 ? clamp(stageBytes / stageTotal * 100) : 0,
4032
+ overallPercent: overallTotal > 0 ? clamp(overallBytes / overallTotal * 100) : 0,
4033
+ bytesDownloaded: overallBytes,
4034
+ totalBytes: stageTotal,
4035
+ ...currentFile !== void 0 ? { currentFile } : {}
4036
+ };
4037
+ };
4038
+ const clearTimer = () => {
4039
+ if (pendingTimer) {
4040
+ clearTimeout(pendingTimer);
4041
+ pendingTimer = null;
4042
+ }
4043
+ };
4044
+ const push = () => {
4045
+ pending = false;
4046
+ clearTimer();
4047
+ lastPushAt = Date.now();
4048
+ const snap = snapshot();
4049
+ for (const listener of listeners) listener(snap);
4050
+ };
4051
+ const schedulePush = () => {
4052
+ if (finished) return;
4053
+ const elapsed = Date.now() - lastPushAt;
4054
+ if (elapsed >= throttleMs) {
4055
+ push();
4056
+ return;
4057
+ }
4058
+ if (pending) return;
4059
+ pending = true;
4060
+ pendingTimer = setTimeout(push, throttleMs - elapsed);
4061
+ };
4062
+ const onEvent = (event) => {
4063
+ switch (event.type) {
4064
+ case "install:phase-changed": {
4065
+ const next = STAGE_FOR_PHASE[event.phase];
4066
+ if (next && next !== currentStage) {
4067
+ currentStage = next;
4068
+ currentFile = void 0;
4069
+ push();
4070
+ }
4071
+ return;
4072
+ }
4073
+ case "download:started": {
4074
+ const stage = stageOfTarget.get(event.file.target) ?? currentStage;
4075
+ inFlightByTarget.set(event.file.target, { stage, bytes: 0 });
4076
+ currentFile = event.file.target;
4077
+ schedulePush();
4078
+ return;
4079
+ }
4080
+ case "download:progress": {
4081
+ const entry = inFlightByTarget.get(event.file.target);
4082
+ if (entry) {
4083
+ const delta = event.bytesDownloaded - entry.bytes;
4084
+ if (delta !== 0) {
4085
+ entry.bytes = event.bytesDownloaded;
4086
+ stageInFlight[entry.stage] += delta;
4087
+ totalInFlight += delta;
4088
+ }
4089
+ }
4090
+ currentFile = event.file.target;
4091
+ schedulePush();
4092
+ return;
4093
+ }
4094
+ case "download:skipped": {
4095
+ const stage = stageOfTarget.get(event.file.target);
4096
+ if (stage) {
4097
+ const size = expectedSizeOf.get(event.file.target) ?? 0;
4098
+ stageDone[stage] += size;
4099
+ totalDone += size;
4100
+ schedulePush();
4101
+ }
4102
+ return;
4103
+ }
4104
+ case "download:completed": {
4105
+ const entry = inFlightByTarget.get(event.file.target);
4106
+ if (entry) {
4107
+ const finalBytes = event.bytes ?? entry.bytes;
4108
+ stageInFlight[entry.stage] -= entry.bytes;
4109
+ totalInFlight -= entry.bytes;
4110
+ stageDone[entry.stage] += finalBytes;
4111
+ totalDone += finalBytes;
4112
+ inFlightByTarget.delete(event.file.target);
4113
+ } else {
4114
+ const stage = stageOfTarget.get(event.file.target);
4115
+ if (stage) {
4116
+ const bytes = event.bytes ?? expectedSizeOf.get(event.file.target) ?? 0;
4117
+ stageDone[stage] += bytes;
4118
+ totalDone += bytes;
4119
+ }
4120
+ }
4121
+ schedulePush();
4122
+ return;
4123
+ }
4124
+ default:
4125
+ return;
4126
+ }
4127
+ };
4128
+ return {
4129
+ onEvent,
4130
+ snapshot,
4131
+ subscribe(listener) {
4132
+ listeners.add(listener);
4133
+ listener(snapshot());
4134
+ return () => listeners.delete(listener);
4135
+ },
4136
+ finish() {
4137
+ finished = true;
4138
+ clearTimer();
4139
+ currentStage = InstallStages.FINALIZE;
4140
+ currentFile = void 0;
4141
+ totalDone = overallTotal;
4142
+ totalInFlight = 0;
4143
+ for (const stage of ALL_STAGES) {
4144
+ stageDone[stage] = stageTotals[stage];
4145
+ stageInFlight[stage] = 0;
4146
+ }
4147
+ const snap = snapshot();
4148
+ for (const listener of listeners) listener(snap);
4149
+ }
4150
+ };
4151
+ }
4152
+ function clamp(value) {
4153
+ if (value <= 0) return 0;
4154
+ if (value >= 100) return 100;
4155
+ return value;
4156
+ }
4157
+
3618
4158
  // src/types/events.ts
3619
4159
  var EventTypes = {
3620
4160
  INSTALL_PHASE_CHANGED: "install:phase-changed",
@@ -3699,6 +4239,6 @@ var LAUNCH_PLACEHOLDERS = {
3699
4239
  "${path}": "Path to the log4j config file (logging.client.argument only)."
3700
4240
  };
3701
4241
 
3702
- export { ASSETS_DIR, ASSETS_INDEXES_DIR, ASSETS_LEGACY_DIR, ASSETS_LOG_CONFIGS_DIR, ASSETS_OBJECTS_DIR, ASSETS_RESOURCES_DIR, ASSETS_VIRTUAL_DIR, ApiEndpoints, Architectures, AuthModes, BASE_JVM_ARGS, CACHE_MAX_ENTRIES, CACHE_TTL_MS, ChildProcessSpawner, DEFAULT_KILL_GRACE_MS, DEFAULT_LAUNCHER_NAME, DEFAULT_LAUNCHER_VERSION, DEFAULT_LIBRARY_REPOSITORY, DEFAULT_MAX_MB, DEFAULT_MIN_MB, DOWNLOAD_CONCURRENCY, EXTRACTION_MAX_COMPRESSION_RATIO, EXTRACTION_MAX_ENTRY_COUNT, EXTRACTION_MAX_FILE_SIZE, EXTRACTION_MAX_TOTAL_SIZE, EventTypes, FABRIC_MAVEN_BASE, FALLBACK_COMPONENT, FORGE_INSTALLERS_DIR, FORGE_INSTALLER_MAX_SIZE, FORGE_MAVEN_BASE, FabricVersionsApi, FetchHttpClient, ForgeVersionsApi, HTTP_RETRY_BACKOFF_BASE_MS, HTTP_RETRY_BACKOFF_CAP_MS, HTTP_RETRY_MAX, HTTP_TIMEOUT_MS, InstallActionKinds, InstallPhases, JAVA_EXECUTABLE, LAUNCH_PLACEHOLDERS, LEGACY_JVM_ARGS, LIBRARIES_DIR, Loaders, LogLevels, MACOS_JVM_ARGS, MAC_RUNTIME_PREFIX, MAX_PROCESSOR_STDERR_LINES, MinecraftChannels, MinecraftKit, MinecraftKitError, MinecraftVersionsApi, NATIVES_DIR_NAME, NODE_ARCH_TO_MOJANG_ARCH, NODE_PLATFORM_TO_MOJANG_OS, OperatingSystems, PROGRESS_EVENT_INTERVAL_MS, RUNTIMES_DIR, RUNTIME_PLATFORM_KEYS, RepairPhases, RuntimeComponents, RuntimePreference, RuntimeVersionsApi, SPAWNER_MAX_LINE_BYTES, TargetsApi, USER_AGENT, VERSIONS_DIR, VerificationKinds, VerifyFileCategories, VerifyFileStatuses, VersionPreference, consoleLogger, createMemoryCache, detectSystem, isErrorCode, isMinecraftKitError, offlineUuidFor, planFabricRepair, planForgeRepair, planMinecraftRepair, planRuntimeInstall, planRuntimeRepair, planStandaloneRuntimeInstall, runRepair, silentLogger, stripUuidDashes, verifyFabric, verifyForge, verifyMinecraft, verifyRuntime };
4242
+ export { ASSETS_DIR, ASSETS_INDEXES_DIR, ASSETS_LEGACY_DIR, ASSETS_LOG_CONFIGS_DIR, ASSETS_OBJECTS_DIR, ASSETS_RESOURCES_DIR, ASSETS_VIRTUAL_DIR, ApiEndpoints, Architectures, AuthModes, BASE_JVM_ARGS, CACHE_MAX_ENTRIES, CACHE_TTL_MS, ChildProcessSpawner, DEFAULT_KILL_GRACE_MS, DEFAULT_LAUNCHER_NAME, DEFAULT_LAUNCHER_VERSION, DEFAULT_LIBRARY_REPOSITORY, DEFAULT_MAX_MB, DEFAULT_MIN_MB, DOWNLOAD_CONCURRENCY, EXTRACTION_MAX_COMPRESSION_RATIO, EXTRACTION_MAX_ENTRY_COUNT, EXTRACTION_MAX_FILE_SIZE, EXTRACTION_MAX_TOTAL_SIZE, EventTypes, FABRIC_MAVEN_BASE, FALLBACK_COMPONENT, FORGE_INSTALLERS_DIR, FORGE_INSTALLER_MAX_SIZE, FORGE_MAVEN_BASE, FabricVersionsApi, FetchHttpClient, ForgeVersionsApi, HTTP_RETRY_BACKOFF_BASE_MS, HTTP_RETRY_BACKOFF_CAP_MS, HTTP_RETRY_MAX, HTTP_TIMEOUT_MS, InstallActionKinds, InstallPhases, InstallStages, JAVA_EXECUTABLE, LAUNCH_PLACEHOLDERS, LEGACY_JVM_ARGS, LIBRARIES_DIR, Loaders, LogLevels, MACOS_JVM_ARGS, MAC_RUNTIME_PREFIX, MAX_PROCESSOR_STDERR_LINES, MinecraftChannels, MinecraftKit, MinecraftKitError, MinecraftVersionsApi, NATIVES_DIR_NAME, NODE_ARCH_TO_MOJANG_ARCH, NODE_PLATFORM_TO_MOJANG_OS, OperatingSystems, PROGRESS_EVENT_INTERVAL_MS, PauseController, RUNTIMES_DIR, RUNTIME_PLATFORM_KEYS, RepairPhases, RuntimeComponents, RuntimePreference, RuntimeVersionsApi, SPAWNER_MAX_LINE_BYTES, TargetsApi, USER_AGENT, VERSIONS_DIR, VerificationKinds, VerifyFileCategories, VerifyFileStatuses, VersionPreference, consoleLogger, createInstallProgressTracker, createMemoryCache, detectSystem, isErrorCode, isMinecraftKitError, offlineUuidFor, parseMajorVersion, pickClientJarVersionId, planFabricRepair, planForgeRepair, planMinecraftRepair, planRuntimeInstall, planRuntimeRepair, planStandaloneRuntimeInstall, repairAll, resolveLaunchVersion, runRepair, silentLogger, stripUuidDashes, targetPaths, verifyFabric, verifyForge, verifyMinecraft, verifyRuntime };
3703
4243
  //# sourceMappingURL=index.mjs.map
3704
4244
  //# sourceMappingURL=index.mjs.map