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