From ccaee11472e411d789ddb9351e62994efece78a5 Mon Sep 17 00:00:00 2001 From: kisakay Date: Sun, 18 Oct 2026 18:44:09 +0200 Subject: [PATCH] SurviePote 2/4: atlas Nebula (serveur SurviePote-26.3, distribution.json, sans binaires) --- atlas/.github/FUNDING.yml | 3 + atlas/.gitignore | 91 + atlas/.nvmrc | 1 + atlas/LICENSE | 21 + atlas/README.md | 361 +++ atlas/SURVIEPOTE.md | 112 + atlas/eslint.config.mjs | 57 + atlas/package-lock.json | 2585 +++++++++++++++++ atlas/package.json | 57 + atlas/root/distribution.json | 432 +++ atlas/root/meta/distrometa.json | 4 + .../root/schemas/DistroMetaSchema.schema.json | 1 + .../root/schemas/ServerMetaSchema.schema.json | 1 + .../SurviePote-26.3/SurviePote-26.3.png | Bin 0 -> 6784 bytes .../servers/SurviePote-26.3/servermeta.json | 16 + atlas/src/index.ts | 453 +++ .../src/model/claritas/ClaritasLibraryType.ts | 5 + atlas/src/model/claritas/ClaritasResult.ts | 44 + atlas/src/model/fabric/FabricMeta.ts | 49 + atlas/src/model/fabric/FabricModJson.ts | 13 + atlas/src/model/forge/McModInfo.ts | 22 + atlas/src/model/forge/McModInfoList.ts | 7 + atlas/src/model/forge/ModsToml.ts | 29 + atlas/src/model/forge/PromotionsSlim.ts | 8 + atlas/src/model/forge/VersionManifestFG2.ts | 21 + atlas/src/model/forge/VersionManifestFG3.ts | 25 + atlas/src/model/nebula/DistroMeta.ts | 30 + atlas/src/model/nebula/ServerMeta.ts | 104 + atlas/src/model/resolver/Asset.ts | 9 + atlas/src/parser/CurseForgeParser.ts | 171 ++ atlas/src/resolver/BaseResolver.ts | 27 + atlas/src/resolver/Resolver.ts | 7 + atlas/src/resolver/fabric/Fabric.resolver.ts | 122 + atlas/src/resolver/forge/Forge.resolver.ts | 153 + .../forge/adapter/ForgeGradle2.resolver.ts | 255 ++ .../forge/adapter/ForgeGradle3.resolver.ts | 778 +++++ atlas/src/structure/BaseFileStructure.ts | 32 + atlas/src/structure/FileStructure.ts | 5 + atlas/src/structure/repo/BaseMavenRepo.ts | 99 + atlas/src/structure/repo/LibRepo.struct.ts | 33 + atlas/src/structure/repo/Repo.struct.ts | 57 + .../src/structure/repo/VersionRepo.struct.ts | 37 + .../structure/spec_model/BaseModel.struct.ts | 19 + .../spec_model/Distribution.struct.ts | 62 + .../src/structure/spec_model/Server.struct.ts | 254 ++ .../spec_model/SpecModelStructure.ts | 7 + .../spec_model/module/FabricMod.struct.ts | 91 + .../spec_model/module/File.struct.ts | 65 + .../spec_model/module/ForgeMod.struct.ts | 42 + .../spec_model/module/Library.struct.ts | 46 + .../structure/spec_model/module/Mod.struct.ts | 66 + .../spec_model/module/Module.struct.ts | 221 ++ .../module/ToggleableModule.struct.ts | 85 + .../module/forgemod/ForgeMod113.struct.ts | 153 + .../module/forgemod/ForgeMod17.struct.ts | 154 + atlas/src/util/LoggerUtil.ts | 48 + atlas/src/util/MavenUtil.ts | 100 + atlas/src/util/MetadataUtil.ts | 21 + atlas/src/util/MinecraftVersion.ts | 51 + atlas/src/util/SchemaUtil.ts | 77 + atlas/src/util/StringUtils.ts | 15 + atlas/src/util/VersionSegmented.ts | 7 + atlas/src/util/VersionSegmentedRegistry.ts | 55 + atlas/src/util/VersionUtil.ts | 139 + atlas/src/util/java/ClaritasWrapper.ts | 70 + atlas/src/util/java/JarExecutor.ts | 55 + atlas/src/util/java/JavaUtil.ts | 7 + atlas/src/util/java/PackXZExtractWrapper.ts | 30 + atlas/tsconfig.json | 12 + 69 files changed, 8289 insertions(+) create mode 100644 atlas/.github/FUNDING.yml create mode 100644 atlas/.gitignore create mode 100644 atlas/.nvmrc create mode 100644 atlas/LICENSE create mode 100644 atlas/README.md create mode 100644 atlas/SURVIEPOTE.md create mode 100644 atlas/eslint.config.mjs create mode 100644 atlas/package-lock.json create mode 100644 atlas/package.json create mode 100644 atlas/root/distribution.json create mode 100644 atlas/root/meta/distrometa.json create mode 100644 atlas/root/schemas/DistroMetaSchema.schema.json create mode 100644 atlas/root/schemas/ServerMetaSchema.schema.json create mode 100644 atlas/root/servers/SurviePote-26.3/SurviePote-26.3.png create mode 100644 atlas/root/servers/SurviePote-26.3/servermeta.json create mode 100644 atlas/src/index.ts create mode 100644 atlas/src/model/claritas/ClaritasLibraryType.ts create mode 100644 atlas/src/model/claritas/ClaritasResult.ts create mode 100644 atlas/src/model/fabric/FabricMeta.ts create mode 100644 atlas/src/model/fabric/FabricModJson.ts create mode 100644 atlas/src/model/forge/McModInfo.ts create mode 100644 atlas/src/model/forge/McModInfoList.ts create mode 100644 atlas/src/model/forge/ModsToml.ts create mode 100644 atlas/src/model/forge/PromotionsSlim.ts create mode 100644 atlas/src/model/forge/VersionManifestFG2.ts create mode 100644 atlas/src/model/forge/VersionManifestFG3.ts create mode 100644 atlas/src/model/nebula/DistroMeta.ts create mode 100644 atlas/src/model/nebula/ServerMeta.ts create mode 100644 atlas/src/model/resolver/Asset.ts create mode 100644 atlas/src/parser/CurseForgeParser.ts create mode 100644 atlas/src/resolver/BaseResolver.ts create mode 100644 atlas/src/resolver/Resolver.ts create mode 100644 atlas/src/resolver/fabric/Fabric.resolver.ts create mode 100644 atlas/src/resolver/forge/Forge.resolver.ts create mode 100644 atlas/src/resolver/forge/adapter/ForgeGradle2.resolver.ts create mode 100644 atlas/src/resolver/forge/adapter/ForgeGradle3.resolver.ts create mode 100644 atlas/src/structure/BaseFileStructure.ts create mode 100644 atlas/src/structure/FileStructure.ts create mode 100644 atlas/src/structure/repo/BaseMavenRepo.ts create mode 100644 atlas/src/structure/repo/LibRepo.struct.ts create mode 100644 atlas/src/structure/repo/Repo.struct.ts create mode 100644 atlas/src/structure/repo/VersionRepo.struct.ts create mode 100644 atlas/src/structure/spec_model/BaseModel.struct.ts create mode 100644 atlas/src/structure/spec_model/Distribution.struct.ts create mode 100644 atlas/src/structure/spec_model/Server.struct.ts create mode 100644 atlas/src/structure/spec_model/SpecModelStructure.ts create mode 100644 atlas/src/structure/spec_model/module/FabricMod.struct.ts create mode 100644 atlas/src/structure/spec_model/module/File.struct.ts create mode 100644 atlas/src/structure/spec_model/module/ForgeMod.struct.ts create mode 100644 atlas/src/structure/spec_model/module/Library.struct.ts create mode 100644 atlas/src/structure/spec_model/module/Mod.struct.ts create mode 100644 atlas/src/structure/spec_model/module/Module.struct.ts create mode 100644 atlas/src/structure/spec_model/module/ToggleableModule.struct.ts create mode 100644 atlas/src/structure/spec_model/module/forgemod/ForgeMod113.struct.ts create mode 100644 atlas/src/structure/spec_model/module/forgemod/ForgeMod17.struct.ts create mode 100644 atlas/src/util/LoggerUtil.ts create mode 100644 atlas/src/util/MavenUtil.ts create mode 100644 atlas/src/util/MetadataUtil.ts create mode 100644 atlas/src/util/MinecraftVersion.ts create mode 100644 atlas/src/util/SchemaUtil.ts create mode 100644 atlas/src/util/StringUtils.ts create mode 100644 atlas/src/util/VersionSegmented.ts create mode 100644 atlas/src/util/VersionSegmentedRegistry.ts create mode 100644 atlas/src/util/VersionUtil.ts create mode 100644 atlas/src/util/java/ClaritasWrapper.ts create mode 100644 atlas/src/util/java/JarExecutor.ts create mode 100644 atlas/src/util/java/JavaUtil.ts create mode 100644 atlas/src/util/java/PackXZExtractWrapper.ts create mode 100644 atlas/tsconfig.json diff --git a/atlas/.github/FUNDING.yml b/atlas/.github/FUNDING.yml new file mode 100644 index 0000000..e338313 --- /dev/null +++ b/atlas/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: dscalzi +patreon: dscalzi +custom: ['https://www.paypal.me/dscalzi'] diff --git a/atlas/.gitignore b/atlas/.gitignore new file mode 100644 index 0000000..b711a8b --- /dev/null +++ b/atlas/.gitignore @@ -0,0 +1,91 @@ +# Build directory +dist + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ \ No newline at end of file diff --git a/atlas/.nvmrc b/atlas/.nvmrc new file mode 100644 index 0000000..8fdd954 --- /dev/null +++ b/atlas/.nvmrc @@ -0,0 +1 @@ +22 \ No newline at end of file diff --git a/atlas/LICENSE b/atlas/LICENSE new file mode 100644 index 0000000..28957b6 --- /dev/null +++ b/atlas/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2020 Daniel D. Scalzi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/atlas/README.md b/atlas/README.md new file mode 100644 index 0000000..0b35c8e --- /dev/null +++ b/atlas/README.md @@ -0,0 +1,361 @@ +# Nebula + +Generate a distribution.json for Helios. Documentation on this format can be found [here][distro.md]. + +## Requirements + +* Node.js 22 +* Java 8+ (https://adoptium.net/) + * This is required to run the forge installer, process [XZ](https://tukaani.org/xz/format.html) files, and run bytecode analysis on mod files. + * Although 1.17 requires Java 16, the forge installer works with Java 8. + +## Setup + +1. Clone the repository +2. Install the dependencies (`npm i`) +3. Create a file called [`.env`][dotenvnpm] at the root directory of the cloned folder and set the required values. + +Example +```properties +JAVA_EXECUTABLE=C:\Program Files\Eclipse Adoptium\jdk-17.0.12.7-hotspot\bin\java.exe +ROOT=D:\TestRoot2 +BASE_URL=http://localhost:8080/ +HELIOS_DATA_FOLDER=C:\Users\user\AppData\Roaming\Helios Launcher +``` + +## Usage + +Nebula is still being developed. Usage may change, but it has remained stable for some time now. + +#### TL;DR (Too Long; Didn't Read) Usage + +This is the barebones overall usage. Please read the rest of this document. + +* Follow the setup instructions above. +* Run the `init root` command. +* Generate servers using the `g server` command. +* Put all files in their respective folders (documented below). +* Generate the distribution using the `g distro` command. +* When in doubt, reread this document and then ask on Discord. + +## Commands + +Commands will be documented here. You can run any command with the `--help` option to view more information. + +### Command Usage + +This explains how to run the commands listed below. There are a few ways to run commands, pick your preferred method. + +Example: To run `init root`, you would do `npm run start -- init root`. + +*Recommended* + +* Run **`npm run start -- `** + * *Why is this recommended? This command will compile the source code first.* + +*Other* + +* Build the project using **`npm run build`** +* Run **`node dist/index.js `** OR **`npm run faststart -- `** + * `faststart` is an alias to run the main file without building. + +> ***Note:*** +> - ***If you modify any files, you will have to rebuild the project.*** +> - ***After pulling from git, you will have to rebuild the project.*** +> +> ***npm start does this automatically.*** + +--- + +### Init + +Init commands are used for initializing empty file structures. + +Aliases: [`init`, `i`] + +__*Subcommands*__ + +--- + +#### Init Root + +Generate an empty standard file structure. JSON schemas will also be generated. + +`init root` + +--- + +### Generate + +Generate commands are used for generation. + +Aliases: [`generate`, `g`] + +__*SubCommands*__ + +--- + +#### Generate Server + +Generate an new server in the root directory. Options are provided to include forge in the generated server. + +`generate server ` + +Options: + +* `--forge ` Specify forge version. This is WITHOUT the minecraft version (ex. 14.23.5.2847) + * OPTIONAL (default: null) + * If not provided forge will not be enabled. + * You can provide either `latest` or `recommended` to use the latest/recommended version of forge. +* `--fabric ` Specify fabric loader version + * OPTIONAL (default: null) + * If not provided fabric will not be enabled. + * You can provide either `latest` or `recommended` to use the latest/recommended version of fabric. + +> [!NOTE] +> Forge and fabric cannot be used together on the same server. This command will fail if both are provided. + +> +> Example Usage +> +> `generate server Test1 1.12.2 --forge 14.23.5.2847` +> + +--- + +#### Generate Server from CurseForge Modpack + +Generate an new server in the root directory, including files and mods from an existing CurseForge modpack. + +`generate server-curseforge ` + +The cursforge modpack must be downloaded as a zip and placed into `${ROOT}/modpacks/curseforge`. Pass the name of the modpack as the `` argument. + +> +> Example Usage +> +> `generate server-curseforge WesterosCraft-Prod The+WesterosCraft+Modpack-2.1.6.zip` +> + +--- + +#### Generate Distribution + +Generate a distribution file from the root file structure. + +`generate distro [name]` + +Arguments: +* `name` The name of the distribution file. + * OPTIONAL (default: `distribution`) + +Options: + +* `--installLocal` Have the application install a copy of the generated distribution to the Helios data folder. + * OPTIONAL (default: false) + * This is useful to easily test the new distribution.json in dev mode on Helios. + * Tip: Set name to `distribution_dev` when using this option. +* `--discardOutput` Delete cached output after it is no longer required. May be useful if disk space is limited. + * OPTIONAL (default: false) +* `--invalidateCache` Invalidate and delete existing caches as they are encountered. Requires fresh cache generation. + * OPTIONAL (default: false) + +#### Notes + +As of Forge 1.13, the installer must be run to generate required files. The installer output is cached by default. This is done to speed up subsequent builds and allow Nebula to be run as a CI job. Options are provided to discard installer output (no caching) and invalidate caches (delete cached output and require fresh generation). To invalidate only a single version cache, manually delete the cached folder. + +> +> Example Usage +> +> `generate distro` +> +> `generate distro distribution_dev --installLocal` +> + +--- + +#### Generate Schemas + +Generate the JSON schemas used by Nebula's internal types (ex. Distro Meta and Server Meta schemas). This command should be used to update the schemas when a change to Nebula requires it. You may need to reopen your editor for the new JSON schemas to take effect. + +`generate schemas` + +--- + +### Latest Forge + +Get the latest version of Forge. + +`latest-forge ` + +--- + +### Recommended Forge + +Get the recommended version of Forge. If no recommended build is available, it will pull the latest version. + +`recommended-forge ` + +--- + +## File Structure Setup (Tentative) + +Nebula aims to provide users with an information preserving structure for storing files. The application will use this structure to generate a full distribution.json for HeliosLauncher. For coherency, the distribution structure is modularized and encapsulated by a directory pattern. These encapsulations will be explained below. They can be generated manually or by using the commands documented above. + +### Distribution Encapsulation + +The distribution object is represented by the main root. All command output will be stored in this directory. The structure is documented below. + +Ex. + +* `TestRoot` The root directory which encapsulates the distribution. + * `servers` All server files are stored in this directory. + +### Server Encapsulation + +Server objects are encapsulated in their own folders. The name of the server's folder contains both its id and version. + +Ex. + +* `servers` + * `TestServer-1.12.2` A server with id TestServer set to version 1.12.2. + +The server directory will contain files pertaining to that server. + +Ex. + +* `TestServer-1.12.2` + * `files` All modules of type `File`. + * `libraries` All modules of type `Library` + * `forgemods` All modules of type `ForgeMod`. + * `fabricmods` All modules of type `FabricMod`. + * This is a directory of toggleable modules. See the note below. + * `TestServer-1.12.2.png` Server icon file. + +#### Setting the Server Icon + +You can set the server icon in two ways. + +1. __*(Preferred)*__ Place your server icon in the root server directory as shown in the example above. Only jpg and png files will be looked at. The name of the file does not matter. +2. Paste the **full** URL to your server icon in the servermeta.json for your server. It is highly recommended to only use files that are hosted on your own servers. + +The value in servermeta.json will always be used so long as it is not empty and is a [valid url](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL). If it is empty or an [invalid url](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL), then the first method will be used. + +#### Toggleable Modules + +If a directory represents a toggleable mod, it will have three subdirectories. You must filter your files into these three. + +* `required` Modules that are required. +* `optionalon` Modules that are optional and enabled by default. +* `optionaloff` Modules that are optional and disabled by default. + +### Additional Metadata + +To preserve metadata that cannot be inferred via file structure, two files exist. Default values will be generated when applicable. Customize to fit your needs. These values should be self explanatory. If further details are required, see the [distribution.json specification document][distro.md]. + +#### ${ROOT}/meta/distrometa.json + +Represents the additiona metadata on the distribution object. + +A JSON schema is provided to assist editing this file. It should automatically be referenced when the default file is generated. + +Sample: + +```json +{ + "$schema": "file:///${ROOT}/schemas/DistroMetaSchema.schema.json", + "meta": { + "rss": "", + "discord": { + "clientId": "", + "smallImageText": "", + "smallImageKey": "" + } + } +} +``` + +#### servers/${YOUR_SERVER}/servermeta.json + +Represents the additional metadata on the server object (for a YOUR_SERVER). + +A JSON schema is provided to assist editing this file. It should automatically be referenced when the default file is generated. + +Sample: + +```json +{ + "$schema": "file:///${ROOT}/schemas/ServerMetaSchema.schema.json", + "meta": { + "version": "1.0.0", + "name": "Test (Minecraft 1.12.2)", + "description": "Test Running Minecraft 1.12.2 (Forge v14.23.5.2854)", + "icon": "How to set the server icon: https://github.com/dscalzi/Nebula#setting-the-server-icon", + "address": "localhost:25565", + "discord": { + "shortId": "1.12.2 Test Server", + "largeImageText": "1.12.2 Test Server", + "largeImageKey": "seal-circle" + }, + "mainServer": false, + "autoconnect": true + }, + "forge": { + "version": "14.23.5.2854" + }, + "untrackedFiles": [] +} +``` + +#### Untracked Files + +Untracked files is optional. MD5 hashes will not be generated for files matching the provided glob patterns. The launcher will not validate/update files without MD5 hashes. + +```json +{ + "untrackedFiles": [ + { + "appliesTo": ["files"], + "patterns": [ + "config/*.cfg", + "config/**/*.yml" + ] + } + ] +} +``` + +In the above example, all files of type `cfg` in the config directory will be untracked. Additionally, all files of type `yml` in the config directory and its subdirectories will be untracked. You can tweak these patterns to fit your needs, this is purely an example. The patterns will only be applied to the folders specified in `appliesTo`. As an example, valid values include `files`, `forgemods`, `libraries`, etc. + +```json +{ + "untrackedFiles": [ + { + "appliesTo": ["files"], + "patterns": [ + "config/*.cfg", + "config/**/*.yml" + ] + }, + { + "appliesTo": ["forgemods"], + "patterns": [ + "optionalon/*.jar" + ] + } + ] +} +``` + +Another example where all `optionalon` forgemods are untracked. **Untracking mods is NOT recommended. This is an example ONLY.** + +### Note on JSON Schemas + +The `$schema` property in a JSON file is a URL to a JSON schema file. This property is optional. Nebula provides schemas for internal types to make editing the JSON easier. Editors, such as Visual Studio Code, will use this schema file to validate the data and show useful information, like property descriptions. Valid properties will also be autocompleted. For detailed information, you may view the [JSON Schema Website][jsonschemawebsite]. + +Nebula will store JSON schemas in `${ROOT}/schemas`. This is so that they will always be in sync with your local version of Nebula. They will initially be generated by the `init root` command. To update the schemas, you can run the `generate schemas` command. + + +[dotenvnpm]: https://www.npmjs.com/package/dotenv +[distro.md]: https://github.com/dscalzi/HeliosLauncher/blob/master/docs/distro.md +[jsonschemawebsite]: https://json-schema.org/ diff --git a/atlas/SURVIEPOTE.md b/atlas/SURVIEPOTE.md new file mode 100644 index 0000000..fee6a5d --- /dev/null +++ b/atlas/SURVIEPOTE.md @@ -0,0 +1,112 @@ +# SurviePote — Atlas (Nebula) : configuration plug-and-play + +Génère `root/distribution.json` pour le launcher Helios (SurviePote, MC 26.3, Fabric Loader 0.20.0). +Le launcher lit la distribution à `https://mcpv.ihorizon.org/distribution.json` +(cf `launcher/app/assets/js/distromanager.js`). + +## 1. Arborescence `root/` + +``` +atlas/ +├── .env # ROOT=./root, BASE_URL, JAVA_EXECUTABLE (voir §2) +├── SURVIEPOTE.md # ce fichier +└── root/ # web root à servir (TNDev: NE PAS committer les .jar, voir .gitignore) + ├── distribution.json # fichier généré, servi à https://mcpv.ihorizon.org/distribution.json + ├── meta/ + │ └── distrometa.json # rss + discord client (globaux) + ├── schemas/ # schémas JSON régénérés par `init root` / `g schemas` + ├── modpacks/curseforge/ # vide (import CurseForge, inutilisé) + └── servers/ + └── SurviePote-26.3/ + ├── servermeta.json # nom, adresse, discord, main/autoconnect + ├── SurviePote-26.3.png # icône serveur (copiée de launcher/build/icon.png) + ├── fabricmods/ + │ ├── required/ # 31 mods partagés (client+serveur), TOUS requis + │ ├── optionalon/ # vide + │ └── optionaloff/ # vide + ├── files/ + │ ├── resourcepacks/FallPack_26.3_v3.1.zip # module File, chemin préservé + │ └── shaderpacks/Revelation-dev.zip # module File, chemin préservé + └── libraries/ # vide (libs Fabric résolues à la volée via FabricResolver) +``` + +## 2. `.env` + +```properties +JAVA_EXECUTABLE=/run/current-system/sw/bin/java +ROOT=./root +BASE_URL=https://mcpv.ihorizon.org/ +HELIOS_DATA_FOLDER= +# DEV ALTERNATIVE (décommenter en dev local): +# BASE_URL=http://localhost:8080/ +``` + +- `BASE_URL` **doit** finir par `/` : c'est le préfixe des URLs dans `distribution.json`. + En dev, passer à `http://localhost:8080/` puis régénérer (`g distro`). +- Écart volontaire : la consigne disait `/usr/bin/java`, inexistant sur NixOS ; + on pointe vers le binaire réel (`readlink -f $(which java)`). +- `HELIOS_DATA_FOLDER` vide = pas de `--installLocal` (pas de dossier Helios local). + +## 3. Re-générer + +```bash +cd atlas +npm install # une fois (Node 22 requis, testé en 24.14.0 OK) +npm run start -- init root # (re)crée root/meta, root/schemas, root/servers +npm run faststart -- g server SurviePote 26.3 --fabric 0.20.0 # un seul serveur +# ... peupler fabricmods/ + files/ (voir §4), éditer servermeta.json ... +npm run faststart -- g distro # (re)génère root/distribution.json +``` + +`npm run start` rebuild avant d'exécuter ; `faststart` réutilise `dist/` (plus rapide). +Ne **jamais** utiliser `--installLocal` ici (pas de Helios local). + +## 4. Où sont mods / files, tri, choix d'implémentation + +- Source mods : `~/.local/share/PrismLauncher/instances/26.3/minecraft/mods/*.jar` + (copie physique par `cp`, pas de symlink). +- `fabricmods/required/` (31) : tous les mods **sauf 15 client-only purs** (même logique + que `server/`) : sodium, iris, continuity, dynamic-fps, xaerominimap, xaeroworldmap, + skinlayers3d, waveycapes, capes, chat_heads, morechathistory, fast-ip-ping, modmenu, + appleskin, vanillarice. +- Tri : Nebula trie les mods par nom de fichier (`BaseModStructure.getSpecModel`), + donc « tri mods auto » = natif, rien à faire. +- **Choix resourcepack/shader** : `src/structure/spec_model/module/File.struct.ts` + (`MiscFileStructure extends ModuleStructure`, scan récursif, pas de namespaces + `required/optionalon/optionaloff`) — contrairement à `FabricModStructure` + (cf `ToggleableModule.struct.ts`). Les modules `File` sont donc **toujours requis**. + Le choix user « optionnels ON » n'est **pas exprimable** en natif : les mettre en + `fabricmods/optionalon` casserait le typage (`Type.FabricMod`, pas de `artifact.path`). + → FallPack + Revelation sont en `files/resourcepacks/` et `files/shaderpacks/` + (chemins relatifs préservés dans `artifact.path`, URLs sous `.../files/...`). + Pour du vrai optionnel, il faudrait patcher Nebula (rendre `MiscFileStructure` + toggleable) ou gérer ça côté launcher. + +## 5. Servir via HTTP + +Le web root = `atlas/root/` (les URLs sont relatives à `BASE_URL`). + +- Dev : `cd atlas/root && python3 -m http.server 8080` + (+ `.env` → `BASE_URL=http://localhost:8080/`, régénérer). +- Prod (TNDev) : servir `atlas/root/` depuis la racine `https://mcpv.ihorizon.org/` + (nginx `root` ou copie du contenu), de sorte que + `https://mcpv.ihorizon.org/distribution.json` réponde. + +## 6. TODO restants + +- [ ] Discord Rich Presence : `root/meta/distrometa.json` → `discord.clientId` vide + (+ `smallImageText/Key` vides) — mettre le vrai clientId ou supprimer l'objet. +- [ ] RSS : `root/meta/distrometa.json` → `rss` vide — mettre l'URL du flux ou laisser vide. +- [ ] Discord serveur : placeholder `SurviePote / seal-circle` dans `servermeta.json` — + valider `largeImageKey` (asset du portail Discord). +- [ ] Icône : `SurviePote-26.3.png` = copie de `launcher/build/icon.png` (143 K) ; + le champ `icon` de `servermeta.json` garde le texte d'aide (URL invalide), + Nebula utilise donc le PNG (voir `Server.struct.ts` : fallback fichier). + Remplacer par une vraie icône si besoin. +- [ ] `BASE_URL` finale : vérifier `https://mcpv.ihorizon.org/` + déploiement du contenu + de `root/` à la racine web, puis tester le launcher en prod. +- [ ] Client-only exclus (sodium, iris, xaero…) : si les clients doivent les recevoir + via le launcher, les ajouter en `fabricmods/optionalon/` (actuellement exclus, + logique serveur). +- [ ] Ne pas committer les binaires (`root/servers/**`, `root/distribution.json` à + décider) — ajouter au `.gitignore` si besoin. diff --git a/atlas/eslint.config.mjs b/atlas/eslint.config.mjs new file mode 100644 index 0000000..9004fc5 --- /dev/null +++ b/atlas/eslint.config.mjs @@ -0,0 +1,57 @@ +// @ts-check + +import eslint from '@eslint/js'; +import { defineConfig } from 'eslint/config'; +import tseslint from 'typescript-eslint'; +import stylistic from '@stylistic/eslint-plugin'; + +export default defineConfig( + { + ignores: ['**/dist/**', 'node_modules', 'eslint.config.mjs'], + }, + eslint.configs.recommended, + tseslint.configs.recommendedTypeChecked, + tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + parser: tseslint.parser, + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + files: ['**/*.ts', '**/*.mts', '**/*.cts', '**/*.tsx'], + plugins: { + '@stylistic': stylistic, + '@typescript-eslint': tseslint.plugin, + }, + rules: { + '@stylistic/semi': ['error', 'never'], + '@stylistic/quotes': ['error', 'single'], + '@stylistic/indent': ['error', 4], + '@stylistic/member-delimiter-style': ['error', { + multiline: { + delimiter: 'none', + requireLast: false + }, + singleline: { + delimiter: 'comma', + requireLast: false + } + }], + '@typescript-eslint/consistent-indexed-object-style': 'off', // Don't need to enforce types be Record + '@typescript-eslint/explicit-function-return-type': ['warn'], // Warn if no return type is set + '@typescript-eslint/no-unsafe-call': 'off', // There are instances where internal types are ambigious but it doesnt matter + '@typescript-eslint/no-unsafe-member-access': 'off', // Method calls, above reason + "@typescript-eslint/no-unused-vars": ["error", { // Allow unused caught errors + "caughtErrors": "none" + }], + '@typescript-eslint/prefer-nullish-coalescing': 'off', // Logical OR is fine depending on the circumstance + '@typescript-eslint/prefer-promise-reject-errors': 'off', // We reject with errors passed through event listeners that are untyped + '@typescript-eslint/require-await': 'off', // This is literally broken + '@typescript-eslint/restrict-template-expressions': 'off' // We make use of template expressions on objects + }, + } +); \ No newline at end of file diff --git a/atlas/package-lock.json b/atlas/package-lock.json new file mode 100644 index 0000000..3bc0b89 --- /dev/null +++ b/atlas/package-lock.json @@ -0,0 +1,2585 @@ +{ + "name": "nebula", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nebula", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "dotenv": "^17.2.3", + "fs-extra": "^11.3.3", + "got": "^14.6.5", + "helios-distribution-types": "^1.3.0", + "luxon": "^3.7.2", + "minimatch": "^10.1.1", + "node-stream-zip": "^1.15.0", + "toml": "^3.0.0", + "triple-beam": "^1.4.1", + "ts-json-schema-generator": "^2.4.0", + "winston": "^3.19.0", + "yargs": "^18.0.0" + }, + "devDependencies": { + "@stylistic/eslint-plugin": "^5.6.1", + "@types/fs-extra": "^11.0.4", + "@types/luxon": "^3.7.1", + "@types/node": "^20.19.27", + "@types/triple-beam": "^1.3.5", + "@types/yargs": "^17.0.35", + "eslint": "^9.39.2", + "rimraf": "^6.1.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.51.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==" + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==" + }, + "node_modules/@sindresorhus/is": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.1.tgz", + "integrity": "sha512-QWLl2P+rsCJeofkDNIT3WFmb6NrRud1SUYW8dIhXK/46XFV8Q/g7Bsvib0Askb0reRLe+WYPeeE+l5cH7SlkuQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.6.1.tgz", + "integrity": "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.0", + "@typescript-eslint/types": "^8.47.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/luxon": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.19.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.27.tgz", + "integrity": "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", + "integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/type-utils": "8.51.0", + "@typescript-eslint/utils": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.51.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz", + "integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz", + "integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.51.0", + "@typescript-eslint/types": "^8.51.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz", + "integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz", + "integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz", + "integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz", + "integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz", + "integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==", + "dev": true, + "dependencies": { + "@typescript-eslint/project-service": "8.51.0", + "@typescript-eslint/tsconfig-utils": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/visitor-keys": "8.51.0", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz", + "integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.51.0", + "@typescript-eslint/types": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz", + "integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.51.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/byte-counter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz", + "integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/cacheable-request": { + "version": "13.0.17", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.17.tgz", + "integrity": "sha512-tQm7K9zC0cJPpbJS8xZ+NUqJ1bZ78jEXc7/G8uqvQTSdEdbmrxdnvxGb7/piCPeICuRY/L82VVt8UA+qpJ8wyw==", + "dependencies": { + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", + "http-cache-semantics": "^4.2.0", + "keyv": "^5.5.4", + "mimic-response": "^4.0.0", + "normalize-url": "^8.1.0", + "responselike": "^4.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacheable-request/node_modules/keyv": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz", + "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz", + "integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==", + "dependencies": { + "mimic-response": "^4.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data-encoder": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.0.2.tgz", + "integrity": "sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==", + "engines": { + "node": ">= 18" + } + }, + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", + "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "dev": true, + "dependencies": { + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "path-scurry": "^2.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "14.6.5", + "resolved": "https://registry.npmjs.org/got/-/got-14.6.5.tgz", + "integrity": "sha512-Su87c0NNeg97de1sO02gy9I8EmE7DCJ1gzcFLcgGpYeq2PnLg4xz73MWrp6HjqbSsjb6Glf4UBDW6JNyZA6uSg==", + "dependencies": { + "@sindresorhus/is": "^7.0.1", + "byte-counter": "^0.1.0", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^13.0.12", + "decompress-response": "^10.0.0", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", + "keyv": "^5.5.3", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^4.0.1", + "responselike": "^4.0.2", + "type-fest": "^4.26.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/keyv": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.5.5.tgz", + "integrity": "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ==", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/helios-distribution-types": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/helios-distribution-types/-/helios-distribution-types-1.3.0.tgz", + "integrity": "sha512-MP66JRHvmuE9yDoZoKeFDh3stsHger0w/cRcJAlV7UYw5ztR3m/uLbWdbfFV68B1Yc0+hDIiuFsuJT/Ve9xuiw==" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==" + }, + "node_modules/http2-wrapper": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "11.2.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", + "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "engines": { + "node": ">=12" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", + "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-cancelable": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", + "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/responselike": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz", + "integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rimraf": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.2.tgz", + "integrity": "sha512-cFCkPslJv7BAXJsYlK1dZsbP8/ZNLkCAQ0bi1hf5EKX2QHegmDFEFA6QhuYJlk7UDdc+02JjO80YSOrWPpw06g==", + "dev": true, + "dependencies": { + "glob": "^13.0.0", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.3.0.tgz", + "integrity": "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-json-schema-generator": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-json-schema-generator/-/ts-json-schema-generator-2.4.0.tgz", + "integrity": "sha512-HbmNsgs58CfdJq0gpteRTxPXG26zumezOs+SB9tgky6MpqiFgQwieCn2MW70+sxpHouZ/w9LW0V6L4ZQO4y1Ug==", + "dependencies": { + "@types/json-schema": "^7.0.15", + "commander": "^13.1.0", + "glob": "^11.0.1", + "json5": "^2.2.3", + "normalize-path": "^3.0.0", + "safe-stable-stringify": "^2.5.0", + "tslib": "^2.8.1", + "typescript": "^5.8.2" + }, + "bin": { + "ts-json-schema-generator": "bin/ts-json-schema-generator.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/ts-json-schema-generator/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.51.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz", + "integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.51.0", + "@typescript-eslint/parser": "8.51.0", + "@typescript-eslint/typescript-estree": "8.51.0", + "@typescript-eslint/utils": "8.51.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/atlas/package.json b/atlas/package.json new file mode 100644 index 0000000..075e8af --- /dev/null +++ b/atlas/package.json @@ -0,0 +1,57 @@ +{ + "name": "nebula", + "version": "0.1.0", + "description": "Utility package to generate a distribution.json for Helios.", + "type": "module", + "exports": "./dist/index.js", + "scripts": { + "clean": "rimraf dist", + "tsc": "tsc", + "build": "npm run clean && npm run tsc", + "start": "npm run build && node dist/index.js", + "fs": "node dist/index.js", + "faststart": "node dist/index.js", + "lint": "eslint ." + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dscalzi/Nebula.git" + }, + "keywords": [ + "helios", + "nebula", + "distribution" + ], + "author": "Daniel Scalzi", + "license": "MIT", + "bugs": { + "url": "https://github.com/dscalzi/Nebula/issues" + }, + "homepage": "https://github.com/dscalzi/Nebula#readme", + "devDependencies": { + "@stylistic/eslint-plugin": "^5.6.1", + "@types/fs-extra": "^11.0.4", + "@types/luxon": "^3.7.1", + "@types/node": "^20.19.27", + "@types/triple-beam": "^1.3.5", + "@types/yargs": "^17.0.35", + "eslint": "^9.39.2", + "rimraf": "^6.1.1", + "typescript": "~5.9.3", + "typescript-eslint": "^8.51.0" + }, + "dependencies": { + "dotenv": "^17.2.3", + "fs-extra": "^11.3.3", + "got": "^14.6.5", + "helios-distribution-types": "^1.3.0", + "luxon": "^3.7.2", + "minimatch": "^10.1.1", + "node-stream-zip": "^1.15.0", + "toml": "^3.0.0", + "triple-beam": "^1.4.1", + "ts-json-schema-generator": "^2.4.0", + "winston": "^3.19.0", + "yargs": "^18.0.0" + } +} diff --git a/atlas/root/distribution.json b/atlas/root/distribution.json new file mode 100644 index 0000000..f17cf5c --- /dev/null +++ b/atlas/root/distribution.json @@ -0,0 +1,432 @@ +{ + "version": "1.0.0", + "servers": [ + { + "id": "SurviePote-26.3", + "name": "SurviePote", + "description": "Serveur survie moddée entre potes (Minecraft 26.3, Fabric).", + "icon": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/SurviePote-26.3.png", + "version": "1.0.0", + "address": "mcpv.ihorizon.org:25565", + "minecraftVersion": "26.3", + "mainServer": true, + "autoconnect": true, + "modules": [ + { + "id": "net.fabricmc:fabric-loader:0.20.0", + "name": "Fabric (fabric-loader)", + "type": "Fabric", + "artifact": { + "size": 1988875, + "MD5": "e09c87e1337e0c41f9c37d546a8faa5d", + "url": "https://mcpv.ihorizon.org/repo/lib/net/fabricmc/fabric-loader/0.20.0/fabric-loader-0.20.0.jar" + }, + "subModules": [ + { + "id": "26.3-fabric-0.20.0", + "name": "Fabric (version.json)", + "type": "VersionManifest", + "artifact": { + "size": 2768, + "MD5": "7867623da046314a5ba913ab06340387", + "url": "https://mcpv.ihorizon.org/repo/versions/26.3-fabric-0.20.0/26.3-fabric-0.20.0.json" + } + }, + { + "id": "org.ow2.asm:asm:9.11", + "name": "Fabric (asm)", + "type": "Library", + "artifact": { + "size": 131056, + "MD5": "67fe79a0c88ca77bf89ad0897cd0ffbc", + "url": "https://mcpv.ihorizon.org/repo/lib/org/ow2/asm/asm/9.11/asm-9.11.jar" + } + }, + { + "id": "org.ow2.asm:asm-analysis:9.11", + "name": "Fabric (asm-analysis)", + "type": "Library", + "artifact": { + "size": 39663, + "MD5": "1f0307062449c4990dd8f772f6fb452e", + "url": "https://mcpv.ihorizon.org/repo/lib/org/ow2/asm/asm-analysis/9.11/asm-analysis-9.11.jar" + } + }, + { + "id": "org.ow2.asm:asm-commons:9.11", + "name": "Fabric (asm-commons)", + "type": "Library", + "artifact": { + "size": 77917, + "MD5": "2771e9f87a5c9caf6abab10c3e995ea8", + "url": "https://mcpv.ihorizon.org/repo/lib/org/ow2/asm/asm-commons/9.11/asm-commons-9.11.jar" + } + }, + { + "id": "org.ow2.asm:asm-tree:9.11", + "name": "Fabric (asm-tree)", + "type": "Library", + "artifact": { + "size": 51954, + "MD5": "20ed5cf0fd4b739262b2c62303ebf32f", + "url": "https://mcpv.ihorizon.org/repo/lib/org/ow2/asm/asm-tree/9.11/asm-tree-9.11.jar" + } + }, + { + "id": "org.ow2.asm:asm-util:9.11", + "name": "Fabric (asm-util)", + "type": "Library", + "artifact": { + "size": 98078, + "MD5": "4e6556015e270658fc05bc2823f2da08", + "url": "https://mcpv.ihorizon.org/repo/lib/org/ow2/asm/asm-util/9.11/asm-util-9.11.jar" + } + }, + { + "id": "net.fabricmc:sponge-mixin:0.17.5+mixin.0.8.7", + "name": "Fabric (sponge-mixin)", + "type": "Library", + "artifact": { + "size": 1551593, + "MD5": "5fd69fb11801c52d88a854ffe05eb839", + "url": "https://mcpv.ihorizon.org/repo/lib/net/fabricmc/sponge-mixin/0.17.5+mixin.0.8.7/sponge-mixin-0.17.5+mixin.0.8.7.jar" + } + } + ] + }, + { + "id": "dev.architectury.utils:architectury:22.0.5@jar", + "name": "Architectury", + "type": "FabricMod", + "artifact": { + "size": 622542, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/architectury-fabric-22.0.5.jar", + "MD5": "907df9cd4c2d9df0d95ab4f649b0dafd" + } + }, + { + "id": "net.blay09.mods:balm:26.3.0.3@jar", + "name": "Balm", + "type": "FabricMod", + "artifact": { + "size": 1213047, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/balm-fabric-26.3-26.3.0.3.jar", + "MD5": "e436dfcb6eebef5adb7b68c2dc8afaa1" + } + }, + { + "id": "org.betterx:bclib:26.301.2@jar", + "name": "BCLib", + "type": "FabricMod", + "artifact": { + "size": 1250438, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/bclib-26.301.2.jar", + "MD5": "a9cfe49091794c3c4e499c3ec9d5e65f" + } + }, + { + "id": "generated.fabricmod:better:end-26.301.2@jar", + "name": "Better", + "type": "FabricMod", + "artifact": { + "size": 99419157, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/better-end-26.301.2.jar", + "MD5": "522b9df994d1a6111aa3d716d364073b" + } + }, + { + "id": "net.bettercombat:bettercombat:3.2.2@jar", + "name": "Better Combat", + "type": "FabricMod", + "artifact": { + "size": 931245, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/bettercombat-fabric-3.2.2+26.3.jar", + "MD5": "85ca794996f4599a9843623f2ac2ffce" + } + }, + { + "id": "generated.fabricmod:biomesoplenty:26.3.0.0.15@jar", + "name": "BiomesOPlenty", + "type": "FabricMod", + "artifact": { + "size": 23204575, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/BiomesOPlenty-fabric-26.3-26.3.0.0.15.jar", + "MD5": "53eb5271f71b948dff00ce05c33f39cc" + } + }, + { + "id": "me.shedaniel.autoconfig.example:cloth-config:26.3.159@jar", + "name": "Cloth Config v26.3", + "type": "FabricMod", + "artifact": { + "size": 1135009, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/cloth-config-fabric-26.3.159.jar", + "MD5": "ad876761ca3b7ace3b026bc901dd493e" + } + }, + { + "id": "generated.fabricmod:clumps:26.3.2@jar", + "name": "Clumps", + "type": "FabricMod", + "artifact": { + "size": 20154, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/Clumps-fabric-26.3-26.3.2.jar", + "MD5": "ee7ecb188801b1e506dadeee26d01a37" + } + }, + { + "id": "com.natamus:collective:8.58@jar", + "name": "Collective", + "type": "FabricMod", + "artifact": { + "size": 1294475, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/collective-26.3.0-8.58.jar", + "MD5": "19d0bb757fbc910514254261bb022e91" + } + }, + { + "id": "ivkond.mc.mods.eh:easy_homes:1.0.1+26.3@jar", + "name": "Easy Homes", + "type": "FabricMod", + "artifact": { + "size": 115668, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/easy_homes-fabric-1.0.1+26.3.jar", + "MD5": "b59adc60f71cb3bf517b876b64689c91" + } + }, + { + "id": "generated.fabricmod:fabric-api:0.163.0+26.3@jar", + "name": "Fabric API", + "type": "FabricMod", + "artifact": { + "size": 1774707, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/fabric-api-0.163.0+26.3.jar", + "MD5": "e3de98c7b25b1bdacd8005cd3030ed95" + } + }, + { + "id": "generated.fabricmod:fabric-language-kotlin:1.14.2+kotlin.2.4.21@jar", + "name": "Fabric Language Kotlin", + "type": "FabricMod", + "artifact": { + "size": 8142878, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/fabric-language-kotlin-1.14.2+kotlin.2.4.21.jar", + "MD5": "08b201c7d0e80ba7a3d77cca0b2d3d3f" + } + }, + { + "id": "glitchcore.fabric.core:glitchcore:26.3.0.0.5@jar", + "name": "GlitchCore", + "type": "FabricMod", + "artifact": { + "size": 339788, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/GlitchCore-fabric-26.3-26.3.0.0.5.jar", + "MD5": "298d2a20d0f2d9555951c5cce87c2003" + } + }, + { + "id": "com.blocklegend001:immersiveores:26.3-0.1@jar", + "name": "ImmersiveOres", + "type": "FabricMod", + "artifact": { + "size": 543252, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/ImmersiveOres-26.3-0.1.jar", + "MD5": "840a11c59c29eadd909cba3ce6f569bb" + } + }, + { + "id": "snownee.jade:jade:26.3.5+fabric@jar", + "name": "Jade", + "type": "FabricMod", + "artifact": { + "size": 1092685, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/Jade-mc26.3-Fabric-26.3.5.jar", + "MD5": "e446d8a9e2fd40d3bd0b88c1a9c7dc05" + } + }, + { + "id": "mezz.jei:jei:31.16.0.83@jar", + "name": "Just Enough Items", + "type": "FabricMod", + "artifact": { + "size": 7379654, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/jei-26.3-fabric-31.16.0.83.jar", + "MD5": "0f78657ef2dff197c9951cbd7a20c5c4" + } + }, + { + "id": "dev.nikrecs:leashall:1.5.0@jar", + "name": "LeashAll", + "type": "FabricMod", + "artifact": { + "size": 170235, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/leashall-fabric-26.3-1.5.0.jar", + "MD5": "f9118c5809c2b1f5b385302af1475681" + } + }, + { + "id": "net.mezzdev.config:mezz_config:0.6.8@jar", + "name": "MezzConfig", + "type": "FabricMod", + "artifact": { + "size": 368401, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/mezz_config-26.3-fabric-0.6.8.jar", + "MD5": "228595e27e2977ec10adfc05dce05489" + } + }, + { + "id": "com.chaosthedude:naturescompass:26.3-2.5.1-fabric@jar", + "name": "Nature's Compass", + "type": "FabricMod", + "artifact": { + "size": 232647, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/NaturesCompass-26.3-2.5.1-fabric.jar", + "MD5": "ee0b1ce7458afa5c4701c02b42a9f475" + } + }, + { + "id": "dev.tonimatas:packetfixer:3.3.6@jar", + "name": "Packet Fixer", + "type": "FabricMod", + "artifact": { + "size": 32477, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/PacketFixer-fabric-3.3.6.jar", + "MD5": "b15fd6703384c58c19d30624923460a5" + } + }, + { + "id": "net.lopymine.patpat.entrypoint.impl:patpat:1.3.2+26.3+fabric@jar", + "name": "PatPat", + "type": "FabricMod", + "artifact": { + "size": 1211460, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/PatPat-1.3.2+26.3+fabric.jar", + "MD5": "366d0fb54566f096ed320aa6ca1b84ba" + } + }, + { + "id": "generated.fabricmod:placeholder-api:3.2.0+26.3@jar", + "name": "Placeholder API", + "type": "FabricMod", + "artifact": { + "size": 254959, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/placeholder-api-3.2.0+26.3.jar", + "MD5": "ca2d9a353e202ce5c11177fb4a7668b1" + } + }, + { + "id": "com.zigythebird.playeranim:player_animation_library:1.2.8+mc.26.3@jar", + "name": "Player Animation Library", + "type": "FabricMod", + "artifact": { + "size": 406793, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/PlayerAnimationLibMerged-1.2.8+mc.26.3.jar", + "MD5": "e176ae70db08a26839282e6d373b5d97" + } + }, + { + "id": "net.blay09.mods:shogi:26.3.0.3@jar", + "name": "Shogi", + "type": "FabricMod", + "artifact": { + "size": 466899, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/shogi-fabric-26.3-26.3.0.3.jar", + "MD5": "7d4488104a6d0714838b5c801f50407d" + } + }, + { + "id": "com.jaquadro.minecraft:storagedrawers:26.3.0.1@jar", + "name": "Storage Drawers", + "type": "FabricMod", + "artifact": { + "size": 1224098, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/StorageDrawers-fabric-26.3.0.1.jar", + "MD5": "7194e41762c99278d25d4a2c7fe4cec6" + } + }, + { + "id": "terrablender.core:terrablender:26.3.0.0.9@jar", + "name": "TerraBlender", + "type": "FabricMod", + "artifact": { + "size": 358202, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/TerraBlender-fabric-26.3-26.3.0.0.9.jar", + "MD5": "310a79696ae06b2049c29f1689c86db1" + } + }, + { + "id": "com.serilum:treeharvester:10.3@jar", + "name": "Tree Harvester", + "type": "FabricMod", + "artifact": { + "size": 158451, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/treeharvester-26.3.0-10.3.jar", + "MD5": "63fe436df405aa30d6adf50ebf65737f" + } + }, + { + "id": "net.execheinz:upgrader:1.2.1@jar", + "name": "Upgrader", + "type": "FabricMod", + "artifact": { + "size": 107274, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/upgrader-fabric-1.2.1+26.3.jar", + "MD5": "1a0d1ad1d17ce8686e11cf081cf1acf4" + } + }, + { + "id": "de.miraculixx:veinminer:2.12.3@jar", + "name": "Veinminer", + "type": "FabricMod", + "artifact": { + "size": 524474, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/veinminer-fabric-2.12.3.jar", + "MD5": "d18e6fe6b403a3a00f722317cf681b24" + } + }, + { + "id": "net.blay09.mods:waystones:26.3.0.1@jar", + "name": "Waystones", + "type": "FabricMod", + "artifact": { + "size": 1211675, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/waystones-fabric-26.3-26.3.0.1.jar", + "MD5": "4329117f8e28f657fa79f464deda3fde" + } + }, + { + "id": "de.ambertation.wover.entrypoint:wover:26.301.2@jar", + "name": "WorldWeaver", + "type": "FabricMod", + "artifact": { + "size": 2999556, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/fabricmods/required/worldweaver-26.301.2.jar", + "MD5": "defce770e3f9661853c1d343fc870f7f" + } + }, + { + "id": "FallPack_26.3_v3.1.zip", + "name": "FallPack_26.3_v3.1.zip", + "type": "File", + "artifact": { + "size": 3060739, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/files/resourcepacks/FallPack_26.3_v3.1.zip", + "MD5": "920b09c92b88b0d879076883fb48df83", + "path": "resourcepacks/FallPack_26.3_v3.1.zip" + } + }, + { + "id": "Revelation-dev.zip", + "name": "Revelation-dev.zip", + "type": "File", + "artifact": { + "size": 66424586, + "url": "https://mcpv.ihorizon.org/servers/SurviePote-26.3/files/shaderpacks/Revelation-dev.zip", + "MD5": "29e0725fe9c23ceade72ac3d6925aa0a", + "path": "shaderpacks/Revelation-dev.zip" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/atlas/root/meta/distrometa.json b/atlas/root/meta/distrometa.json new file mode 100644 index 0000000..ce69884 --- /dev/null +++ b/atlas/root/meta/distrometa.json @@ -0,0 +1,4 @@ +{ + "$schema": "file:///home/kisakay/Documents/Code/HomeLabGitea/mc/atlas/root/schemas/DistroMetaSchema.schema.json", + "meta": {} +} \ No newline at end of file diff --git a/atlas/root/schemas/DistroMetaSchema.schema.json b/atlas/root/schemas/DistroMetaSchema.schema.json new file mode 100644 index 0000000..71d47b4 --- /dev/null +++ b/atlas/root/schemas/DistroMetaSchema.schema.json @@ -0,0 +1 @@ +{"$schema":"http://json-schema.org/draft-07/schema#","$ref":"#/definitions/DistroMetaSchema","definitions":{"DistroMetaSchema":{"type":"object","additionalProperties":false,"properties":{"$schema":{"type":"string","description":"URL to the JSON schema for this type of file. This is used by editors to validate and annotate the data."},"meta":{"type":"object","properties":{"rss":{"type":"string","description":"A URL to a RSS feed. Used for loading news."},"discord":{"type":"object","properties":{"clientId":{"type":"string","description":"Client ID for the Application registered with Discord."},"smallImageText":{"type":"string","description":"Tootltip for the smallImageKey."},"smallImageKey":{"type":"string","description":"Name of the uploaded image for the small profile artwork."}},"required":["clientId","smallImageText","smallImageKey"],"additionalProperties":false,"description":"Global settings for Discord Rich Presence."}},"required":["rss"],"additionalProperties":false,"description":"Distribution metadata to be forwarded to the distribution file."}},"required":["meta"]}}} \ No newline at end of file diff --git a/atlas/root/schemas/ServerMetaSchema.schema.json b/atlas/root/schemas/ServerMetaSchema.schema.json new file mode 100644 index 0000000..a142fb7 --- /dev/null +++ b/atlas/root/schemas/ServerMetaSchema.schema.json @@ -0,0 +1 @@ +{"$schema":"http://json-schema.org/draft-07/schema#","$ref":"#/definitions/ServerMetaSchema","definitions":{"ServerMetaSchema":{"type":"object","additionalProperties":false,"properties":{"$schema":{"type":"string","description":"URL to the JSON schema for this type of file. This is used by editors to validate and annotate the data."},"meta":{"type":"object","properties":{"version":{"type":"string","description":"The version of the server configuration."},"name":{"type":"string","description":"The name of the server. This is what users see on the UI."},"description":{"type":"string","description":"A brief description of the server. Displayed on the UI to provide users more information."},"icon":{"type":"string","description":"A URL to the server's icon. Will be displayed on the UI."},"address":{"type":"string","description":"The server's IP address."},"discord":{"type":"object","properties":{"shortId":{"type":"string","description":"Short ID for the server. Displayed on the second status line as Server: shortId."},"largeImageText":{"type":"string","description":"Tooltip for the largeImageKey."},"largeImageKey":{"type":"string","description":"Name of the uploaded image for the large profile artwork."}},"required":["shortId","largeImageText","largeImageKey"],"additionalProperties":false,"description":"Server specific settings used for Discord Rich Presence."},"mainServer":{"type":"boolean","description":"Only one server in the array should have the mainServer property enabled. This will tell the launcher that this is the default server to select if either the previously selected server is invalid, or there is no previously selected server. If this field is not defined by any server (avoid this), the first server will be selected as the default. If multiple servers have mainServer enabled, the first one the launcher finds will be the effective value. Servers which are not the default may omit this property rather than explicitly setting it to false."},"autoconnect":{"type":"boolean","description":"Whether or not the server can be autoconnected to. If false, the server will not be autoconnected to even when the user has the autoconnect setting enabled."},"javaOptions":{"$ref":"#/definitions/JavaOptions","description":"Java options."}},"required":["version","name","description","address","autoconnect"],"additionalProperties":false,"description":"Server metadata to be forwarded to the distribution file."},"forge":{"type":"object","properties":{"version":{"type":"string","description":"The forge version. This does NOT include the minecraft version. Ex. 14.23.5.2854"}},"required":["version"],"additionalProperties":false,"description":"Properties related to Forge."},"fabric":{"type":"object","properties":{"version":{"type":"string","description":"The fabric loader version. This does NOT include the minecraft version. Ex. 0.14.18"}},"required":["version"],"additionalProperties":false,"description":"Properties related to Fabric."},"untrackedFiles":{"type":"array","items":{"$ref":"#/definitions/UntrackedFilesOption"},"description":"A list of option objects defining patterns for untracked files."}},"required":["meta"]},"JavaOptions":{"type":"object","properties":{"distribution":{"$ref":"#/definitions/JdkDistribution","description":"Preferred JDK distribution to download if no applicable installation could be found. If omitted, the client will decide (decision may be platform-specific)."},"supported":{"type":"string","description":"A semver range of supported JDK versions.\n\nJava version syntax is platform dependent.\n\nJDK 8 and prior 1.{major}.{minor}_{patch}-b{build} Ex. 1.8.0_152-b16\n\nJDK 9+ {major}.{minor}.{patch}+{build} Ex. 11.0.12+7\n\nFor processing, all versions will be translated into a semver compliant string. JDK 9+ is already semver. For versions 8 and below, 1.{major}.{minor}_{patch}-b{build} will be translated to {major}.{minor}.{patch}+{build}.\n\nIf specified, you must also specify suggestedMajor.\n\nIf omitted, the client will decide based on the game version."},"suggestedMajor":{"type":"number","description":"The suggested major Java version. The suggested major should comply with the version range specified by supported, if defined. This will be used in messages displayed to the end user, and to automatically fetch a Java version.\n\nNOTE If supported is specified, suggestedMajor must be set. The launcher's default value may not comply with your custom major supported range.\n\nCommon use case: supported: >=17.x suggestedMajor: 17\n\nMore involved: supported: >=16 <20 suggestedMajor: 17\n\nGiven a wider support range, it becomes necessary to specify which major version in the range is the suggested."},"platformOptions":{"type":"array","items":{"$ref":"#/definitions/JavaPlatformOptions"},"description":"Platform-specific java rules for this server configuration. Validation rules will be delegated to the client for any undefined properties. Java validation can be configured for specific platforms and architectures. The most specific ruleset will be applied.\n\nMaxtrix Precedence (Highest - Lowest)\n- Current platform, current architecture (ex. win32 x64)\n- Current platform, any architecture (ex. win32)\n- Java Options base properties (defined on this object)\n- Client logic (default logic in the client)"},"ram":{"type":"object","properties":{"recommended":{"type":"number","description":"The recommended amount of RAM in megabytes. Must be an interval of 512."},"minimum":{"type":"number","description":"The absolute minimum amount of RAM in megabytes. Must be an interval of 512."}},"required":["recommended","minimum"],"additionalProperties":false,"description":"RAM settings. If omitted, legacy client logic will be used to determine these values."}},"additionalProperties":false,"description":"Java options."},"JdkDistribution":{"type":"string","enum":["CORRETTO","TEMURIN"],"description":"Supported JDK Distributions"},"JavaPlatformOptions":{"type":"object","properties":{"distribution":{"$ref":"#/definitions/JdkDistribution","description":"Preferred JDK distribution to download if no applicable installation could be found. If omitted, the client will decide (decision may be platform-specific)."},"supported":{"type":"string","description":"A semver range of supported JDK versions.\n\nJava version syntax is platform dependent.\n\nJDK 8 and prior 1.{major}.{minor}_{patch}-b{build} Ex. 1.8.0_152-b16\n\nJDK 9+ {major}.{minor}.{patch}+{build} Ex. 11.0.12+7\n\nFor processing, all versions will be translated into a semver compliant string. JDK 9+ is already semver. For versions 8 and below, 1.{major}.{minor}_{patch}-b{build} will be translated to {major}.{minor}.{patch}+{build}.\n\nIf specified, you must also specify suggestedMajor.\n\nIf omitted, the client will decide based on the game version."},"suggestedMajor":{"type":"number","description":"The suggested major Java version. The suggested major should comply with the version range specified by supported, if defined. This will be used in messages displayed to the end user, and to automatically fetch a Java version.\n\nNOTE If supported is specified, suggestedMajor must be set. The launcher's default value may not comply with your custom major supported range.\n\nCommon use case: supported: >=17.x suggestedMajor: 17\n\nMore involved: supported: >=16 <20 suggestedMajor: 17\n\nGiven a wider support range, it becomes necessary to specify which major version in the range is the suggested."},"platform":{"$ref":"#/definitions/Platform","description":"The platform that this validation matrix applies to."},"architecture":{"$ref":"#/definitions/Architecture","description":"The architecture that this validation matrix applies to. If omitted, applies to all architectures."}},"required":["platform"],"additionalProperties":false,"description":"Java validation rules for a specific platform."},"Platform":{"type":"string","enum":["darwin","linux","win32"],"description":"Supported platforms."},"Architecture":{"type":"string","enum":["arm64","x64"],"description":"Supported architectures."},"UntrackedFilesOption":{"type":"object","properties":{"appliesTo":{"type":"array","items":{"type":"string"},"description":"The subdirectory these patterns will be applied to. Ex. [ \"files\", \"forgegemods\" ]"},"patterns":{"type":"array","items":{"type":"string"},"description":"Glob patterns to match against the file."}},"required":["appliesTo","patterns"],"additionalProperties":false}}} \ No newline at end of file diff --git a/atlas/root/servers/SurviePote-26.3/SurviePote-26.3.png b/atlas/root/servers/SurviePote-26.3/SurviePote-26.3.png new file mode 100644 index 0000000000000000000000000000000000000000..1e2a22eee4ff1737ee770dc63f591faac9604ecb GIT binary patch literal 6784 zcmeAS@N?(olHy`uVBq!ia0y~yU}OMc4mJh`hM1xiX&_#HkzWYo`9CMOLjP-W4ze>LU|F?WfkeB9CDiUQ;{I*{%Y&iR{-f<~QLy#AP22P5}fk8u+fu$h` zli?ot{eJ!b4-cC8`S|Vs6qp;FWahUqkgEUp^SN8$vokY$lV067Q~$%ZVy~E5#gmEd z|Aci}jyWhWOu3bm_+hfT|2%P9fgVQ(292#ptG--xZ$Bc#kuXbG-A~7fL1XLFsy7>t zw{bNZ3WRX5FdUJu|D(t-S{MSdPJj9FxS#)skw6GXf=n2H2UkNW z$apIk+mA(LzMPppPqIUdW$#Kyg$cHdj1Ip4t4kRe7hG}Gc@#J^ zJ*6+PI<4;8&2(|M^^2|7|NZrP{qv*w`~Pk;D6RYVbb36W94M?GE}vf~RrlxP@rS?P z@0Z{A``zw`z2^5M?o~dY`}xt@?f0Vg%(JNd_2t92+xhdw_qgTkn)%{h_4~UTf7n*8 zWq0T*PJX5ySM_q~!=KOR+w*hi@A+`(&<7Wu`HSM>{23Y?9<;?aAo=`+N_z$cZUyEa-)`rNtMM^>I&mWI^8d1~fW`^4 z9M^YxFlcObS@K`x*|BH;4QXHic9PshW(OmNGd&-(Ll_c1$uS58Gt6LS zWL)qqYKHyi|C$U5|D+fidlt-^AF`iG;n^C71q>&YOBom%9^SA2|F?Ms!-}ts8#59& zzgp)6(xY?Ikaxw^zZA<4t zg=;qLjbK=@SFHN;=CJ)b+%Fs#=>1IG#GoL+!LUhaf{%hWW2x^xRtIa_2gYwd#~s^v zN|Kq$;q6uphHL-+hAwAfXsE0WV_5aEa-LE`tggZjV}}K~PZ$}_{GR;px+c^7v!83W zrAcp{ydhRpqPc+~AdO*ZbpB0~1x^jm85*|iV^UDdW<2nmi6M*8fgxC(iP4B*+W%Ku z8m^z8XIuSY=79xsi@q#l3f>A13K52tj5?>8q8Of3o@EGPIdiL?)nQfxQ^OTzEe58> zl7FdF`XAc=`EZz(MM#w4?Uu!9FpVaRtPGhPuN)VgWfGFS$=q=3&t)ctfGe5|>l5E( zW!3o=2h5d9_}6duE92wLh6U^m+ZcAfnFF@+3nK%AQ6pFLy;TnU45rK+EJBTp>z6Zc zcqhmpbM+4+gQUZ=Kb&X&e>^U)FUP6CAW_ATVcfWqX(2;EweB>=&A!w1OSZi`ca|yX z**3)$O%3yBeinWyk;2+wFa9|BMbDcQhO2+}wEeXI`D8LP#~DTk{V0Yz#pi9iAJkXN zu{PXVt;w+1{`cqidp@62oZDEcWw2EzVmU)F!-7ZtD$EPm8+sP>{hKjs;`~?jruOUW zf4yAJ&cMOKaKQfmpUpxnw{#R3RwxKCWaS^3SGJ9Lv-$sTxAWzTZl?2nJfmrMlIxZ& zv**)j1{S8se}_NlGE8Dj`Eq{6xj*}x7!NZyI5aRYFfcLrI4)ps)?`@2#kk+`RY2+E zUUR-9yykZdo>V?sHajos$XWCIHg~FCukGIPbXv4n|BSiE&)faZ`SEJ?da?eVJ@f1T z{e1L%{r-Qy6!!gmHoJMn`pq-jzy1o3ul>3pegE}!r(eI3PGA@|;RhfV-cJ(!1U7_qiU~l44bUr|`HExHw>V7SG1O^!YjilcCLG z?Zvu_wG9%k{QY!VpTEcN!tVLUk7%#oW3=bruh-p`)zjW=v8Sshv# z&U?1bEH}QCUfHW8v;L}5%-rS7%_+PM4HspcH`_`SAO9fA;3KRdv8j5(`45qN5)aSo zoo0B&cbKb@LE$^^vr`*c?5}VB9mR2{^m;6lL+<`b|MX8YR7E-%HHb01+>^cdkb-w% z&nt;pf^x;h1s>IH^1&=;&ZjrBK6_s5!s_71n|1kroalZgg`=lZnD@9T7H4@)$ZWW; zuvI*`Ax^A;(ZJ}XZT}TUriNR^_nRG>L>srTKc65V_j+e-&zCh442>z=Gv+%ba5tDK zXUwh)I(q)Mgza{IRpAq&DKmw>7%^^QU|g^*?s`n|*@CxQuk&4xEx)T&b<=^n;YjZG zyW0$EuU)GKg$fq~(_~@IAA&tUD)m=0iZK4%+4tjX-vBy@o_a&80Q`crFI+3|v zFJLpnGKae6D`Br!tzIY9v3mW!Uk?lm|NQvq%FxNhv0%bdN1ypq=Dn(a|K^V-!v*;Z z?}T_*Bp%KbV`$jJZ(vZrV8XWtOTLuqJKDx8C@3heU^wuGq2c%!{SW!4RF?z`BuG6l z_jt>~5cQdzVTL`!f&dS-11ILkF)&O>wU@ixvzdpf(}dmEQQ3U%UThyY{cF;X8T)%hc(es> z@ho6(h*6%#?Z)wnVb8QWT)iF%CSkJ`}$+$w`gw_EWuv3^Bp8YI7<1fr}@N>@Q z-k-^zd{}?XJT~7@LFMfFhPS ztHu5@9N2Wl{+arUrsQ`U7uYXoQjBfbGwpcK3zlc=#5-6Rc6v`Z@PpHCwkAW``r94B zA70OWWIUCzhjBr;umbl7=_O@i49U-q|LB}y*(Tq;XZ|mTO>JAgUQ1|s{xyZ|tIAJS?mI7?&wHh&?dRDCTXKiPq)uDn2(g zJkCXYswj9;m-7ap=cP{+Pav+SG*`VnA8GBZSe>}amCAS14 z^V3t_zCE4adp^5H?dZmyMV7)FdP{CEeQFkv5U$|%Y@-`@%de=vGPXbe#4r-WEv}QwT;?!gRo$}>I**TD zXk<+1*%I)=)lWI@_#Y+*hMd3R4ZSw2m;B>g#*@Op&>(2vYs%tq;)VR@>(6B4PW$cs zaz1(40S4n`%k2VAw9Ve~cT(mDYs=^Q*J}jl{P9k;SGINF;EB@sU|Dy3)5ZE%wQtoK zwne?}`FE1x#p~loBxYGT_}zV2X=?9lZ79oX=P;q${E~bBLCsqqjjtBQdEd_Y^>$`j z$}9$!AF-Bq#UEcjb2!;SKqKML6{dS0E0DqL#bPR(-}qvm9zee z6DC@2e|7B7j6M_D%|bnUB*!684k#Np8W6fx6{+&&h%e2)N0GQ{BOqo-UAB9*0WcH z9Nzxyyiwe?FKWyuGVOLUsO(qnZ)kWvlZ}DpUyM{yRa^RIzgm9=j)u8x2Vy>NXIQ}3 zAX&UL%&l(P-K0ZVf4CZE*r)%P$a^JFZgFv!Ki_|!vx@0|LqC64igf6i-=;LHzGCm& zGuh>@XEJg$%w<1tr{gDQ7CR#YTPoLt%QX?j{u6VvzVI_-Jvn2?)V#gD=Gueu=L-E* z&*P06#rP!TK3`wy)N{A*-<&(A>;9%NtYhM6*vsZ%THh4faOQULTr=s4OY!&n{u$c8 zf4=9xj`*=BC)HMbxW3ZS_Nn=^^BeE&?RfdRxPf7Y(XW0+hR4heR;&UHOb!etk_=}Z zR{og$DL6IX{qH@I2Gw0}UL1aL=H=vD$$6Ll@0w#VjnTlic_TxQJa^_{IT3N4fKy7}^{j0`x*7wyn@l5wNV{V9h z;$xq1;8%TD(@dieGcMOu37ptjYjgZfq}-X}hEVSMm@}O}J^v?H$St4MxMe=WjYx(s zKIfHv9eSpiyf;{MUh|tO!?73!0h8Y>Oglwq=sC)rl#sZ%d8zq!mhDF!76iNhPf_`^ zJGtl^%Le_X|FL_*{NG7>|G#@qo1sjM!S?g=|MT_h&7NQR{p%TnLiMj`1_M?P{ezp< zC|L9V{Co4|^wi^b%;s1*pX2pNSNOlqq2a(0k-tTu;w$S-EAQ^V&vxNL9aF+{h6~2_ z?X?LLzPw2P|9i&I;|vE%7G7hJIC}ZcNM#pokE0%LTXIUsBWmq$f@v`9z1_qJ8 zGtL>X&e*Ma!r8W*h2bkhcj==Ij72w7r}G|pIz9d#uT)J@sra+J`}w;c7~S`k>N%;g zjkZ$Uk7&va?`I`QM0X--?X$nGR>m zGj6!dknm+$`g~REgbVxP^Eam^^GH{1(s5s?yZozK6+^~Zd)5Y%Ur#2Si(BR}Z9++% zBuCM!fOiTfRvy_|V$}4tE3F-_6Y>W&JYzO}A{ciW{e4@;k562Izd7m8i z>u0vrU8aQ4&+0`JpWg^+yuP`4{a%YDZnjA_>b$}Y3Yk@=_KWT2GYg1)WI5o+xPYCp z>f7;4^-WXeo6P4?U{6i6m0SGm)H-fnhNk<0yV9zsnKMCgyMA@7 z`DpBVVv56WP-bK}Yjq@|wxQBOwANvQ>h_!?2hYUZGMvVkV*P#>gTk>h_n95J>b_Lz|MlipiSabX z7%he^1}82lvmV)%7GM71>zVWQ>u1cr{{Qd$dCnnejt|Awya}Eg z(iq(t87%LuKKFC;Om*ohErvuN7n796x{A0y&0j znJl+N7)t*$F0Q-w&G^#wP)4IB2c?D+zd^ZHm!U|}<2Kuz*;TJh6e1m}n4U2t=&@Xr z{F+>6_Q#yES)nw{B`#(}HMe2g}FRSqUtC|9P$|q zw~9Y-Vq)05`APf3&E*R2HQ|@9JKF^DpZHwDwM^S!W)sMHdwDFj{b6UQ*uPy;!@J3H z`u;S=FRWMm_H^?$-1#(_+cv3go^T{%hU2tH34T94FRq_;Ka_!S!{y+HjR{#3t~PEw zt-r}~g&yZKA%4IAXG0ViB2wKSEbDbB-Cg+mz5lEEsRt$Yrq?klFnkkY2>HL+GDC;i zL$~4Y|F@EVP1sqPf+Y{gaKv=jHGFfqpTc5TbC*Ztar#z=-)}4#E@|)2OW}UCYvarP z^YzR1FNxo0J9gmAnU6m$|65ypP(Jvjf!C!|XLe{YBip9tTOwDwzs-7QmUN`_@cR8i z4BAW$J8O15JH{~KUM0hU&)Y5>`_jZcq{Z;#bBe$p*`9x^84`GyR?o6;uVZ7X`#Y6k1}EdQcWcfDACNxHV4*Xa%l=^I zp&5GBuYT^nBrL$NH0A=1e692CgQ~ zo*6m}3`>j)-mKWLQ$gbOhMu3vKg^^b*d77}yfNc}JM$)5`#R_{7+8I-b*sFm%)CNC zLHfb{Gu1kbpUQpbid(QV72jHWdyT?Ftp;#R1zNKnh+}h@<Gne+bubtSH6K0 z_Ul(O70Jizea<+0Ps;xM@t%D=9N!o<7&jyd8pJl}GGy^H9D7#$XJ-mSX+1;#41@ao zSB!H+3!a^s$tuv`c#h{mmSLXwBU3(+mEye&jo1F{-TF^;_Qoj;0u03r4u$=xd*avx z1U~%C5$XDQTSsAb^N;Eq;S4M{Zca}=vN?6eW@ZM4XYYOaE8?DiX4&7IB=2g^xPavV zYYyu?CWp67K5PMu0g4ku8UNWFV`oZX=y{jII7f+xV@Ifj#D^2V_xLfeZDN`i9Nux$ zu&yqBnbc_`Mo{RlnZS6Z*I}>Jfe5w$#tECh{>b6uJb2;jEj_m%b5v@+y}9X{(&fOw zD);lZ#T=E(+;Xpr=1VqMU1B)-a@Xr)%l7Y=+A?cP?~-?4J{rGTwjlg=3nTN@T0aK0 zwdY@2Rq8K)b3x_E)q5$zGTPIu*|N?wKHvZU_f#PUaV~~nfrL(eh6zRA85|PcTwd<4 z&sDuMdp)az8FQh62g8KqW6x&UKfLbG+2GB>ne;0axYKX^eG$r!eerVK@;Mf9*psdqy>@fkpOjh6cg9EUp8al2{$G7}G%RabyVi zdwhqT>>f0H?%Y?WhJ@h_PD#9u~T2Bt zzOt~eB{Q5TQn*_u%CMF1f*$j_%Y1J@!R^d+AcAc|ro%dx=au@*D;QS1+z%>QIv5fp zS&lI{h#hE?f5l*_w1$=8J##~uenT*WDnl6q!%oi`JL|X^mT{EoJDmShtnlEv|65ZB z2}|=%-}?Wo97|am7y`T)I2b5kylU$1)-mg}WH0hLx{U44xBC6}9dHczV@?6!7P)jl z{4m4pfB(2Y)E{(XvnhLX!wod-(CgHo!KlE%(jWwCHxtQZ05!5X85k89K$)7!0iAi} z;+37n&)a+-Jvlj9`lz76hs5j~p8}X17&sU}K4G91vsAF;(UH!?$rh!ruDCs{XT&*a kf^R~F*qNN^4*%KbO*%biz1Z4w3=9kmp00i_>zopr09%=F$N&HU literal 0 HcmV?d00001 diff --git a/atlas/root/servers/SurviePote-26.3/servermeta.json b/atlas/root/servers/SurviePote-26.3/servermeta.json new file mode 100644 index 0000000..40bc527 --- /dev/null +++ b/atlas/root/servers/SurviePote-26.3/servermeta.json @@ -0,0 +1,16 @@ +{ + "$schema": "file:///home/kisakay/Documents/Code/HomeLabGitea/mc/atlas/root/schemas/ServerMetaSchema.schema.json", + "meta": { + "version": "1.0.0", + "name": "SurviePote", + "description": "Serveur survie modd\u00e9e entre potes (Minecraft 26.3, Fabric).", + "icon": "How to set the server icon: https://github.com/dscalzi/Nebula#setting-the-server-icon", + "address": "mcpv.ihorizon.org:25565", + "mainServer": true, + "autoconnect": true + }, + "fabric": { + "version": "0.20.0" + }, + "untrackedFiles": [] +} \ No newline at end of file diff --git a/atlas/src/index.ts b/atlas/src/index.ts new file mode 100644 index 0000000..04d297c --- /dev/null +++ b/atlas/src/index.ts @@ -0,0 +1,453 @@ +import dotenv from 'dotenv' +import { writeFile } from 'fs/promises' +import { resolve as resolvePath } from 'path' +import { URL } from 'url' +import { inspect } from 'util' +import yargs from 'yargs/yargs' +import { Argv, CommandModule } from 'yargs' +import { hideBin } from 'yargs/helpers' +import { DistributionStructure } from './structure/spec_model/Distribution.struct.js' +import { ServerStructure } from './structure/spec_model/Server.struct.js' +import { VersionSegmentedRegistry } from './util/VersionSegmentedRegistry.js' +import { VersionUtil } from './util/VersionUtil.js' +import { MinecraftVersion } from './util/MinecraftVersion.js' +import { LoggerUtil } from './util/LoggerUtil.js' +import { generateSchemas } from './util/SchemaUtil.js' +import { CurseForgeParser } from './parser/CurseForgeParser.js' + +dotenv.config() + +const logger = LoggerUtil.getLogger('Index') + +function getRoot(): string { + return resolvePath(process.env.ROOT!) +} + +function getHeliosDataFolder(): string | null { + if(process.env.HELIOS_DATA_FOLDER) { + return resolvePath(process.env.HELIOS_DATA_FOLDER) + } + return null +} + +function getBaseURL(): string { + let baseUrl = process.env.BASE_URL! + // Users must provide protocol in all other instances. + if (!baseUrl.includes('//')) { + if (baseUrl.toLowerCase().startsWith('localhost')) { + baseUrl = 'http://' + baseUrl + } else { + throw new TypeError('Please provide a URL protocol (ex. http:// or https://)') + } + } + return (new URL(baseUrl)).toString() +} + +function installLocalOption(yargs: Argv): Argv { + return yargs.option('installLocal', { + describe: 'Install the generated distribution to your local Helios data folder.', + type: 'boolean', + demandOption: false, + global: false, + default: false + }) +} + +function discardOutputOption(yargs: Argv): Argv { + return yargs.option('discardOutput', { + describe: 'Delete cached output after it is no longer required. May be useful if disk space is limited.', + type: 'boolean', + demandOption: false, + global: false, + default: false + }) +} + +function invalidateCacheOption(yargs: Argv): Argv { + return yargs.option('invalidateCache', { + describe: 'Invalidate and delete existing caches as they are encountered. Requires fresh cache generation.', + type: 'boolean', + demandOption: false, + global: false, + default: false + }) +} + +// function rootOption(yargs: Argv) { +// return yargs.option('root', { +// describe: 'File structure root.', +// type: 'string', +// demandOption: true, +// global: true +// }) +// .coerce({ +// root: resolvePath +// }) +// } + +// function baseUrlOption(yargs: Argv) { +// return yargs.option('baseUrl', { +// describe: 'Base url of your file host.', +// type: 'string', +// demandOption: true, +// global: true +// }) +// .coerce({ +// baseUrl: (arg: string) => { +// // Users must provide protocol in all other instances. +// if (arg.indexOf('//') === -1) { +// if (arg.toLowerCase().startsWith('localhost')) { +// arg = 'http://' + arg +// } else { +// throw new TypeError('Please provide a URL protocol (ex. http:// or https://)') +// } +// } +// return (new URL(arg)).toString() +// } +// }) +// } + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function namePositional(yargs: Argv) { + return yargs.option('name', { + describe: 'Distribution index file name.', + type: 'string', + default: 'distribution' + }) +} + +// ------------- +// Init Commands + +const initRootCommand: CommandModule = { + command: 'root', + describe: 'Generate an empty standard file structure.', + builder: (yargs) => { + // yargs = rootOption(yargs) + return yargs + }, + handler: async (argv) => { + argv.root = getRoot() + + logger.debug(`Root set to ${argv.root}`) + logger.debug('Invoked init root.') + try { + await generateSchemas(argv.root as string) + await new DistributionStructure(argv.root as string, '', false, false).init() + await new CurseForgeParser(argv.root as string, '').init() + logger.info(`Successfully created new root at ${argv.root}`) + } catch (error) { + logger.error(`Failed to init new root at ${argv.root}`, error) + } + } +} + +const initCommand: CommandModule = { + command: 'init', + aliases: ['i'], + describe: 'Base init command.', + builder: (yargs) => { + return yargs + .command(initRootCommand) + }, + handler: (argv) => { + argv._handled = true + } +} + +// ----------------- +// Generate Commands + +const generateServerCommand: CommandModule = { + command: 'server ', + describe: 'Generate a new server configuration.', + builder: (yargs) => { + // yargs = rootOption(yargs) + return yargs + .positional('id', { + describe: 'Server id.', + type: 'string' + }) + .positional('version', { + describe: 'Minecraft version.', + type: 'string' + }) + .option('forge', { + describe: 'Forge version.', + type: 'string' + }) + .option('fabric', { + describe: 'Fabric version.', + type: 'string' + }) + .conflicts('forge', 'fabric') + }, + handler: async (argv) => { + argv.root = getRoot() + + logger.debug(`Root set to ${argv.root}`) + logger.debug(`Generating server ${argv.id} for Minecraft ${argv.version}.`, + `\n\t└ Forge version: ${argv.forge}`, + `\n\t└ Fabric version: ${argv.fabric}` + ) + + const minecraftVersion = new MinecraftVersion(argv.version as string) + + if(argv.forge != null) { + if (VersionUtil.isPromotionVersion(argv.forge as string)) { + logger.debug(`Resolving ${argv.forge as string} Forge Version..`) + const version = await VersionUtil.getPromotedForgeVersion(minecraftVersion, argv.forge as string) + logger.debug(`Forge version set to ${version}`) + argv.forge = version + } + if(minecraftVersion.isGreaterThanOrEqualTo(new MinecraftVersion('1.20.3'))) { + logger.error('┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓') + logger.error('┃ !! WARNING !! ┃') + logger.error('┃ ┃') + logger.error('┃ Forge 1.20.3+ removed support for --fml.modLists. ┃') + logger.error('┃ Helios Launcher can no longer load mods through Forge. ┃') + logger.error('┃ Please use Fabric or await NeoForged Support. ┃') + logger.error('┃ ┃') + logger.error('┃ !! WARNING !! ┃') + logger.error('┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛') + } + } + + if(argv.fabric != null) { + if (VersionUtil.isPromotionVersion(argv.fabric as string)) { + logger.debug(`Resolving ${argv.fabric as string} Fabric Version..`) + const version = await VersionUtil.getPromotedFabricVersion(argv.fabric as string) + logger.debug(`Fabric version set to ${version}`) + argv.fabric = version + } + } + + const serverStruct = new ServerStructure(argv.root as string, getBaseURL(), false, false) + await serverStruct.createServer( + argv.id as string, + minecraftVersion, + { + forgeVersion: argv.forge as string, + fabricVersion: argv.fabric as string + } + ) + } +} + +const generateServerCurseForgeCommand: CommandModule = { + command: 'server-curseforge ', + describe: 'Generate a new server configuration from a CurseForge modpack.', + builder: (yargs) => { + // yargs = rootOption(yargs) + return yargs + .positional('id', { + describe: 'Server id.', + type: 'string' + }) + .positional('zipName', { + describe: 'The name of the modpack zip file.', + type: 'string' + }) + }, + handler: async (argv) => { + argv.root = getRoot() + + logger.debug(`Root set to ${argv.root}`) + logger.debug(`Generating server ${argv.id} using CurseForge modpack ${argv.zipName} as a template.`) + + const parser = new CurseForgeParser(argv.root as string, argv.zipName as string) + const modpackManifest = await parser.getModpackManifest() + + const minecraftVersion = new MinecraftVersion(modpackManifest.minecraft.version) + + // Extract forge version + // TODO Support fabric + const forgeModLoader = modpackManifest.minecraft.modLoaders.find(({ id }) => id.toLowerCase().startsWith('forge-')) + const forgeVersion = forgeModLoader != null ? forgeModLoader.id.substring('forge-'.length) : undefined + logger.debug(`Forge version set to ${forgeVersion}`) + + const serverStruct = new ServerStructure(argv.root as string, getBaseURL(), false, false) + const createServerResult = await serverStruct.createServer( + argv.id as string, + minecraftVersion, + { + version: modpackManifest.version, + forgeVersion + } + ) + + if(createServerResult) { + await parser.enrichServer(createServerResult, modpackManifest) + } + } +} + +const generateDistroCommand: CommandModule = { + command: 'distro [name]', + describe: 'Generate a distribution index from the root file structure.', + builder: (yargs) => { + yargs = installLocalOption(yargs) + yargs = discardOutputOption(yargs) + yargs = invalidateCacheOption(yargs) + yargs = namePositional(yargs) + return yargs + }, + handler: async (argv) => { + argv.root = getRoot() + argv.baseUrl = getBaseURL() + + const finalName = `${argv.name}.json` + + logger.debug(`Root set to ${argv.root}`) + logger.debug(`Base Url set to ${argv.baseUrl}`) + logger.debug(`Install option set to ${argv.installLocal}`) + logger.debug(`Discard Output option set to ${argv.discardOutput}`) + logger.debug(`Invalidate Cache option set to ${argv.invalidateCache}`) + logger.debug(`Invoked generate distro name ${finalName}.`) + + const doLocalInstall = argv.installLocal as boolean + const discardOutput = argv.discardOutput as boolean ?? false + const invalidateCache = argv.invalidateCache as boolean ?? false + const heliosDataFolder = getHeliosDataFolder() + if(doLocalInstall && heliosDataFolder == null) { + logger.error('You MUST specify HELIOS_DATA_FOLDER in your .env when using the --installLocal option.') + return + } + + try { + const distributionStruct = new DistributionStructure(argv.root as string, argv.baseUrl as string, discardOutput, invalidateCache) + const distro = await distributionStruct.getSpecModel() + const distroOut = JSON.stringify(distro, null, 2) + const distroPath = resolvePath(argv.root as string, finalName) + await writeFile(distroPath, distroOut) + logger.info(`Successfully generated ${finalName}`) + logger.info(`Saved to ${distroPath}`) + logger.debug('Preview:\n', distro) + if(doLocalInstall) { + const finalDestination = resolvePath(heliosDataFolder!, finalName) + logger.info(`Installing distribution to ${finalDestination}`) + await writeFile(finalDestination, distroOut) + logger.info('Success!') + } + + } catch (error) { + logger.error(`Failed to generate distribution with root ${argv.root}.`, error) + } + } +} + +const generateSchemasCommand: CommandModule = { + command: 'schemas', + describe: 'Generate json schemas.', + handler: async (argv) => { + argv.root = getRoot() + + logger.debug(`Root set to ${argv.root}`) + logger.debug('Invoked generate schemas.') + + try { + await generateSchemas(argv.root as string) + logger.info('Successfully generated schemas') + + } catch (error) { + logger.error(`Failed to generate schemas with root ${argv.root}.`, error) + } + } +} + +const generateCommand: CommandModule = { + command: 'generate', + aliases: ['g'], + describe: 'Base generate command.', + builder: (yargs) => { + return yargs + .command(generateServerCurseForgeCommand) + .command(generateServerCommand) + .command(generateDistroCommand) + .command(generateSchemasCommand) + }, + handler: (argv) => { + argv._handled = true + } +} + +const validateCommand: CommandModule = { + command: 'validate [name]', + describe: 'Validate a distribution.json against the spec.', + builder: (yargs) => { + return namePositional(yargs) + }, + handler: (argv) => { + logger.debug(`Invoked validate with name ${argv.name}.json`) + } +} + +const latestForgeCommand: CommandModule = { + command: 'latest-forge ', + describe: 'Get the latest version of forge.', + handler: async (argv) => { + logger.debug(`Invoked latest-forge with version ${argv.version}.`) + + const minecraftVersion = new MinecraftVersion(argv.version as string) + const forgeVer = await VersionUtil.getPromotedForgeVersion(minecraftVersion, 'latest') + logger.info(`Latest version: Forge ${forgeVer} (${argv.version})`) + } +} + +const recommendedForgeCommand: CommandModule = { + command: 'recommended-forge ', + describe: 'Get the recommended version of forge. Returns latest if there is no recommended build.', + handler: async (argv) => { + logger.debug(`Invoked recommended-forge with version ${argv.version}.`) + + const index = await VersionUtil.getPromotionIndex() + const minecraftVersion = new MinecraftVersion(argv.version as string) + + let forgeVer = VersionUtil.getPromotedVersionStrict(index, minecraftVersion, 'recommended') + if (forgeVer != null) { + logger.info(`Recommended version: Forge ${forgeVer} (${minecraftVersion})`) + } else { + logger.info(`No recommended build for ${minecraftVersion}. Checking for latest version..`) + forgeVer = VersionUtil.getPromotedVersionStrict(index, minecraftVersion, 'latest') + if (forgeVer != null) { + logger.info(`Latest version: Forge ${forgeVer} (${minecraftVersion})`) + } else { + logger.info(`No build available for ${minecraftVersion}.`) + } + } + + } +} + +const testCommand: CommandModule = { + command: 'test ', + describe: 'Validate a distribution.json against the spec.', + builder: (yargs) => { + return namePositional(yargs) + }, + handler: async (argv) => { + logger.debug(`Invoked test with mcVer ${argv.mcVer} forgeVer ${argv.forgeVer}`) + logger.info(process.cwd()) + const mcVer = new MinecraftVersion(argv.mcVer as string) + const resolver = VersionSegmentedRegistry.getForgeResolver(mcVer, + argv.forgeVer as string, getRoot(), '', getBaseURL(), false, false) + if (resolver != null) { + const mdl = await resolver.getModule() + logger.info(inspect(mdl, false, null, true)) + } + } +} + +// Registering yargs configuration. +await yargs(hideBin(process.argv)) + .version(false) + .scriptName('') + .command(initCommand) + .command(generateCommand) + .command(validateCommand) + .command(latestForgeCommand) + .command(recommendedForgeCommand) + .command(testCommand) + .demandCommand() + .help() + .argv diff --git a/atlas/src/model/claritas/ClaritasLibraryType.ts b/atlas/src/model/claritas/ClaritasLibraryType.ts new file mode 100644 index 0000000..08257ec --- /dev/null +++ b/atlas/src/model/claritas/ClaritasLibraryType.ts @@ -0,0 +1,5 @@ +export enum LibraryType { + + FORGE = 'FORGE' + +} \ No newline at end of file diff --git a/atlas/src/model/claritas/ClaritasResult.ts b/atlas/src/model/claritas/ClaritasResult.ts new file mode 100644 index 0000000..a915808 --- /dev/null +++ b/atlas/src/model/claritas/ClaritasResult.ts @@ -0,0 +1,44 @@ +export interface ClaritasModuleMetadata { + + /** + * Present on ForgeMods (1.7-1.12) of type MOD. + * Possibly present on ForgeMods (1.7-1.12) of type CORE_MOD and TWEAKER. + * Always present on ForgeMods 1.13+. + * Never present in all other circumstances. + */ + id?: string + /** + * Never present on ForgeMods (1.7-1.12) of type UNKNOWN. + * Present in all other circumstances. + */ + group?: string + /** + * Possibly present on ForgeMods (1.7-1.12). + * Never present in all other circumstances. + */ + version?: string + /** + * Possibly present on ForgeMods (1.7-1.12). + * Never present in all other circumstances. + */ + name?: string + /** + * Always present on ForgeMods (1.7-1.12). + * Never present in all other circumstances. + */ + modType?: ForgeModType_1_7 + +} + +export enum ForgeModType_1_7 { + MOD = 'MOD', + CORE_MOD = 'CORE_MOD', + TWEAKER = 'TWEAKER', + UNKNOWN = 'UNKNOWN' +} + +export interface ClaritasResult { + + [jarPath: string]: ClaritasModuleMetadata | undefined + +} \ No newline at end of file diff --git a/atlas/src/model/fabric/FabricMeta.ts b/atlas/src/model/fabric/FabricMeta.ts new file mode 100644 index 0000000..b646ba2 --- /dev/null +++ b/atlas/src/model/fabric/FabricMeta.ts @@ -0,0 +1,49 @@ +export interface FabricVersionMeta { + version: string + stable: boolean +} + +export interface FabricLoaderMeta extends FabricVersionMeta { + separator: string + build: number + maven: string +} + +export interface FabricInstallerMeta extends FabricVersionMeta { + url: string + maven: string +} + +export interface Rule { + action: string + os?: { + name: string + version?: string + } + features?: { + [key: string]: boolean + } +} + +export interface RuleBasedArgument { + rules: Rule[] + value: string | string[] +} + +// This is really a mojang format, but it's currently only used here for Fabric. +export interface FabricProfileJson { + id: string + inheritsFrom: string + releaseTime: string + time: string + type: string + mainClass: string + arguments: { + game: (string | RuleBasedArgument)[] + jvm: (string | RuleBasedArgument)[] + } + libraries: { + name: string // Maven identifier + url: string + }[] +} \ No newline at end of file diff --git a/atlas/src/model/fabric/FabricModJson.ts b/atlas/src/model/fabric/FabricModJson.ts new file mode 100644 index 0000000..2f795df --- /dev/null +++ b/atlas/src/model/fabric/FabricModJson.ts @@ -0,0 +1,13 @@ +// https://fabricmc.net/wiki/documentation:fabric_mod_json_spec +// https://github.com/FabricMC/fabric-loader/blob/master/src/main/java/net/fabricmc/loader/impl/metadata/V1ModMetadataParser.java + +type FabricEntryPoint = string | { value: string } + +export interface FabricModJson { + + id: string + version: string + name?: string + entrypoints?: { [key: string]: FabricEntryPoint[] } + +} diff --git a/atlas/src/model/forge/McModInfo.ts b/atlas/src/model/forge/McModInfo.ts new file mode 100644 index 0000000..665b7a8 --- /dev/null +++ b/atlas/src/model/forge/McModInfo.ts @@ -0,0 +1,22 @@ +// https://mcforge.readthedocs.io/en/latest/gettingstarted/structuring/#the-mcmodinfo-file +export interface McModInfo { + + modid: string + name: string + description: string + version: string + mcversion: string + url: string + updateUrl?: string + updateJSON: string + authorList: string[] + credits: string + logoFile: string + screenshots: string[] + parent: string + useDependencyInformation: boolean + requiredMods: string[] + dependencies: string[] + dependants: string[] // Spelled as dependants on forge's wiki. + +} diff --git a/atlas/src/model/forge/McModInfoList.ts b/atlas/src/model/forge/McModInfoList.ts new file mode 100644 index 0000000..f4472af --- /dev/null +++ b/atlas/src/model/forge/McModInfoList.ts @@ -0,0 +1,7 @@ +import { McModInfo } from './McModInfo.js' + +export interface McModInfoList { + + modListVersion: number + modList: McModInfo[] +} diff --git a/atlas/src/model/forge/ModsToml.ts b/atlas/src/model/forge/ModsToml.ts new file mode 100644 index 0000000..50291b7 --- /dev/null +++ b/atlas/src/model/forge/ModsToml.ts @@ -0,0 +1,29 @@ +// https://github.com/MinecraftForge/MinecraftForge/blob/1.15.x/mdk/src/main/resources/META-INF/mods.toml + +export interface ModsToml { + + modLoader: string + loaderVersion: string + issueTrackerURL?: string + + mods: { + modId: string + version: string + displayName: string + updateJSONURL?: string + displayURL?: string + logoFile?: string + credits?: string + authors?: string + description: string + }[] + + dependencies?: {[modId: string]: { + modId: string + mandatory: boolean + versionRange: string + ordering?: 'NONE' | 'BEFORE' | 'AFTER' + side: 'BOTH' | 'CLIENT' | 'SERVER' + }} + +} diff --git a/atlas/src/model/forge/PromotionsSlim.ts b/atlas/src/model/forge/PromotionsSlim.ts new file mode 100644 index 0000000..7a835b6 --- /dev/null +++ b/atlas/src/model/forge/PromotionsSlim.ts @@ -0,0 +1,8 @@ +export interface PromotionsSlim { + + homepage: string + promos: { + [id: string]: string + } + +} diff --git a/atlas/src/model/forge/VersionManifestFG2.ts b/atlas/src/model/forge/VersionManifestFG2.ts new file mode 100644 index 0000000..40362bc --- /dev/null +++ b/atlas/src/model/forge/VersionManifestFG2.ts @@ -0,0 +1,21 @@ +export interface VersionManifestFG2 { + + id: string + time: string + releaseTime: string + type: string + minecraftArguments: string + mainClass: string + inheritsFrom: string + jar: string + logging: Record + libraries: { + name: string + url?: string + checksums?: string[] + serverreq?: boolean + clientreq?: boolean + comment?: string + }[] + +} diff --git a/atlas/src/model/forge/VersionManifestFG3.ts b/atlas/src/model/forge/VersionManifestFG3.ts new file mode 100644 index 0000000..60e0797 --- /dev/null +++ b/atlas/src/model/forge/VersionManifestFG3.ts @@ -0,0 +1,25 @@ +export interface VersionManifestFG3 { + + id: string + time: string + releaseTime: string + type: string + mainClass: string + inheritsFrom: string + logging: Record + arguments: { + game: string[] + } + libraries: { + name: string + downloads: { + artifact: { + path: string + url: string + sha1: string + size: number + } + } + }[] + +} diff --git a/atlas/src/model/nebula/DistroMeta.ts b/atlas/src/model/nebula/DistroMeta.ts new file mode 100644 index 0000000..0817fc3 --- /dev/null +++ b/atlas/src/model/nebula/DistroMeta.ts @@ -0,0 +1,30 @@ +import { Distribution } from 'helios-distribution-types' + +export interface DistroMeta { + + /** + * Distribution metadata to be forwarded to the distribution file. + */ + meta: { + rss: Distribution['rss'] + discord?: Distribution['discord'] + } + +} + +export function getDefaultDistroMeta(): DistroMeta { + + return { + + meta: { + rss: '', + discord: { + clientId: '', + smallImageText: '', + smallImageKey: '' + } + } + + } + +} \ No newline at end of file diff --git a/atlas/src/model/nebula/ServerMeta.ts b/atlas/src/model/nebula/ServerMeta.ts new file mode 100644 index 0000000..16afdc1 --- /dev/null +++ b/atlas/src/model/nebula/ServerMeta.ts @@ -0,0 +1,104 @@ +import { Server } from 'helios-distribution-types' + +export interface UntrackedFilesOption { + /** + * The subdirectory these patterns will be applied to. Ex. + * [ "files", "forgegemods" ] + */ + appliesTo: string[] + /** + * Glob patterns to match against the file. + */ + patterns: string[] +} + +export interface ServerMetaOptions { + version?: string + forgeVersion?: string + fabricVersion?: string +} + +export function getDefaultServerMeta(id: string, version: string, options?: ServerMetaOptions): ServerMeta { + + const servMeta: ServerMeta = { + meta: { + version: options?.version ?? '1.0.0', + name: `${id} (Minecraft ${version})`, + description: `${id} Running Minecraft ${version}`, + icon: 'How to set the server icon: https://github.com/dscalzi/Nebula#setting-the-server-icon', + address: 'localhost:25565', + discord: { + shortId: '', + largeImageText: '', + largeImageKey: '' + }, + mainServer: false, + autoconnect: false + } + } + + if(options?.forgeVersion) { + servMeta.meta.description = `${servMeta.meta.description} (Forge v${options.forgeVersion})` + servMeta.forge = { + version: options.forgeVersion + } + } + + if(options?.fabricVersion) { + servMeta.meta.description = `${servMeta.meta.description} (Fabric v${options.fabricVersion})` + servMeta.fabric = { + version: options.fabricVersion + } + } + + // Add empty untracked files. + servMeta.untrackedFiles = [] + + return servMeta +} + +export interface ServerMeta { + + /** + * Server metadata to be forwarded to the distribution file. + */ + meta: { + version: Server['version'] + name: Server['name'] + description: Server['description'] + icon?: Server['icon'] + address: Server['address'] + discord?: Server['discord'] + mainServer: Server['mainServer'] + autoconnect: Server['autoconnect'] + javaOptions?: Server['javaOptions'] + } + + /** + * Properties related to Forge. + */ + forge?: { + /** + * The forge version. This does NOT include the minecraft version. + * Ex. 14.23.5.2854 + */ + version: string + } + + /** + * Properties related to Fabric. + */ + fabric?: { + /** + * The fabric loader version. This does NOT include the minecraft version. + * Ex. 0.14.18 + */ + version: string + } + + /** + * A list of option objects defining patterns for untracked files. + */ + untrackedFiles?: UntrackedFilesOption[] + +} diff --git a/atlas/src/model/resolver/Asset.ts b/atlas/src/model/resolver/Asset.ts new file mode 100644 index 0000000..ed236cf --- /dev/null +++ b/atlas/src/model/resolver/Asset.ts @@ -0,0 +1,9 @@ +export interface Asset { + + url: string + path: string + size: number + hashType: string + hash: string + +} diff --git a/atlas/src/parser/CurseForgeParser.ts b/atlas/src/parser/CurseForgeParser.ts new file mode 100644 index 0000000..9e9cd19 --- /dev/null +++ b/atlas/src/parser/CurseForgeParser.ts @@ -0,0 +1,171 @@ +import { createWriteStream } from 'fs' +import { mkdirs } from 'fs-extra/esm' +import got from 'got' +import StreamZip from 'node-stream-zip' +import { join, resolve } from 'path' +import { pipeline } from 'stream/promises' +import { ToggleableNamespace } from '../structure/spec_model/module/ToggleableModule.struct.js' +import { CreateServerResult } from '../structure/spec_model/Server.struct.js' +import { LoggerUtil } from '../util/LoggerUtil.js' + +const log = LoggerUtil.getLogger('CurseForgeParser') + +// No idea if this is right +export interface CurseForgeManifest { + minecraft: { + version: string + modLoaders: { + id: string + primary: boolean + }[] + } + manifestType: string + manifestVersion: number + name: string + version: string + author: string + files: { + projectID: number + fileID: number + required: boolean + }[] + overrides: string +} + +export interface CurseForgeModResponse { + data: { + id: number + gameId: number + name: string + slug: string + // There are more fields that we don't use right now. + } +} + +export interface CurseForgeModFileResponse { + data: { + id: number + gameId: number + isAvailable: boolean + displayName: string + fileName: string + downloadUrl: string + // There are more fields that we don't use right now. + } +} + + +export class CurseForgeParser { + + private static cfClient = got.extend({ + prefixUrl: 'https://api.curseforge.com/v1', + responseType: 'json', + headers: { + 'X-API-KEY': '$2a$10$JL4kTO/N/oXIM6o3uTYC3eLxGrOI4BIAqpX4vAFeIPoXiTtagidkK' + } + }) + + private modpackDir: string + private zipPath: string + + constructor( + private absoluteRoot: string, + private zipFileName: string + ) { + this.modpackDir = join(absoluteRoot, 'modpacks', 'curseforge') + this.zipPath = join(this.modpackDir, zipFileName) + } + + public async init(): Promise { + await mkdirs(this.modpackDir) + } + + public async getModpackManifest(): Promise { + + const zip = new StreamZip.async({ file: this.zipPath }) + return JSON.parse((await zip.entryData('manifest.json')).toString('utf8')) as CurseForgeManifest + } + + public async enrichServer(createServerResult: CreateServerResult, manifest: CurseForgeManifest): Promise { + log.debug('Enriching server.') + + // Extract overrides + const zip = new StreamZip.async({ file: this.zipPath }) + try { + if(manifest.overrides) { + await zip.extract(manifest.overrides, createServerResult.miscFileContainer) + } + } + finally { + await zip.close() + } + + if(createServerResult.modContainer) { + const requiredPath = resolve(createServerResult.modContainer, ToggleableNamespace.REQUIRED) + const optionalPath = resolve(createServerResult.modContainer, ToggleableNamespace.OPTIONAL_ON) + + const disallowedFiles: { name: string, fileName: string, url: string }[] = [] + + // Download mods + for(const file of manifest.files) { + log.debug(`Processing - Mod: ${file.projectID}, File: ${file.fileID}`) + const fileInfo = (await CurseForgeParser.cfClient.get(`mods/${file.projectID}/files/${file.fileID}`)).body + log.debug(`Downloading ${fileInfo.data.fileName}`) + + let dir: string + const fileNameLower = fileInfo.data.fileName.toLowerCase() + if(fileNameLower.endsWith('jar')) { + dir = file.required ? requiredPath : optionalPath + } + else if(fileNameLower.endsWith('zip')) { + // Assume it's a resource pack. + dir = join(createServerResult.miscFileContainer, 'resourcepacks') + await mkdirs(dir) + } + else { + dir = createServerResult.miscFileContainer + } + + const thirdPartyDisallowed = fileInfo.data.downloadUrl == null + if(thirdPartyDisallowed) { + + log.warn(`${fileInfo.data.fileName} is not available for 3rd-party download through the curseforge API!`) + const modInfo = (await CurseForgeParser.cfClient.get(`mods/${file.projectID}`)).body + disallowedFiles.push({ + name: modInfo.data.name, + fileName: fileInfo.data.fileName, + url: `https://www.curseforge.com/minecraft/mc-mods/${modInfo.data.slug}/download/${file.fileID}` + }) + + } else { + const downloadStream = got.stream(fileInfo.data.downloadUrl) + const fileWriterStream = createWriteStream(join(dir, fileInfo.data.fileName)) + + await pipeline(downloadStream, fileWriterStream) + } + + } + + if(disallowedFiles.length > 0) { + + log.error('============================================') + log.error('\x1b[41mWARNING\x1b[0m') + log.error(`${disallowedFiles.length} files declared by this modpack do not permit 3rd-party downloads.`) + log.error('They MUST be downloaded manually.') + log.error('The mods and their download urls will be listed below.') + log.error('============================================') + + for(const file of disallowedFiles) { + log.error(`${file.name} (${file.fileName}) - \x1b[32m${file.url}\x1b[0m`) + } + + log.error('============================================') + log.error('YOUR MODPACK IS NOT FULLY GENERATED!') + log.error('MANUAL ACTION REQUIRED! SCROLL UP AND READ THE WARNING!') + log.error('============================================') + } + + } + } + +} \ No newline at end of file diff --git a/atlas/src/resolver/BaseResolver.ts b/atlas/src/resolver/BaseResolver.ts new file mode 100644 index 0000000..68e7584 --- /dev/null +++ b/atlas/src/resolver/BaseResolver.ts @@ -0,0 +1,27 @@ +import { Artifact, Module } from 'helios-distribution-types' +import { VersionSegmented } from '../util/VersionSegmented.js' +import { Resolver } from './Resolver.js' +import { MinecraftVersion } from '../util/MinecraftVersion.js' +import { Stats } from 'fs' +import { createHash } from 'crypto' + +export abstract class BaseResolver implements Resolver, VersionSegmented { + + constructor( + protected absoluteRoot: string, + protected relativeRoot: string, + protected baseUrl: string + ) {} + + public abstract getModule(): Promise + public abstract isForVersion(version: MinecraftVersion, libraryVersion: string): boolean + + protected generateArtifact(buf: Buffer, stats: Stats, url: string): Artifact { + return { + size: stats.size, + MD5: createHash('md5').update(buf).digest('hex'), + url + } + } + +} diff --git a/atlas/src/resolver/Resolver.ts b/atlas/src/resolver/Resolver.ts new file mode 100644 index 0000000..99551c6 --- /dev/null +++ b/atlas/src/resolver/Resolver.ts @@ -0,0 +1,7 @@ +import { Module } from 'helios-distribution-types' + +export interface Resolver { + + getModule(): Promise + +} diff --git a/atlas/src/resolver/fabric/Fabric.resolver.ts b/atlas/src/resolver/fabric/Fabric.resolver.ts new file mode 100644 index 0000000..2e089a0 --- /dev/null +++ b/atlas/src/resolver/fabric/Fabric.resolver.ts @@ -0,0 +1,122 @@ +import { mkdirs, pathExists } from 'fs-extra/esm' +import { lstat, readFile, writeFile } from 'fs/promises' +import { Module, Type } from 'helios-distribution-types' +import { dirname } from 'path' +import { FabricProfileJson } from '../../model/fabric/FabricMeta.js' +import { RepoStructure } from '../../structure/repo/Repo.struct.js' +import { LoggerUtil } from '../../util/LoggerUtil.js' +import { MavenUtil } from '../../util/MavenUtil.js' +import { MinecraftVersion } from '../../util/MinecraftVersion.js' +import { VersionUtil } from '../../util/VersionUtil.js' +import { BaseResolver } from '../BaseResolver.js' + +export class FabricResolver extends BaseResolver { + + private static readonly log = LoggerUtil.getLogger('FabricResolver') + + protected repoStructure: RepoStructure + + public static isForVersion(_version: MinecraftVersion, _libraryVersion: string): boolean { + // --fabric.addMods support was added in https://github.com/FabricMC/fabric-loader/commit/ce8405c22166ef850ae73c09ab513c17d121df5a + return VersionUtil.versionGte(_libraryVersion, '0.12.3') + } + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string, + protected loaderVersion: string, + protected minecraftVersion: MinecraftVersion + ) { + super(absoluteRoot, relativeRoot, baseUrl) + this.repoStructure = new RepoStructure(absoluteRoot, relativeRoot, 'fabric') + } + + public async getModule(): Promise { + return this.getFabricModule() + } + + public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { + return FabricResolver.isForVersion(version, libraryVersion) + } + + public async getFabricModule(): Promise { + + const versionRepo = this.repoStructure.getVersionRepoStruct() + const versionManifest = versionRepo.getVersionManifest(this.minecraftVersion, this.loaderVersion) + + FabricResolver.log.debug(`Checking for fabric profile json at ${versionManifest}..`) + if(!await pathExists(versionManifest)) { + FabricResolver.log.debug('Fabric profile not found locally, initializing download..') + await mkdirs(dirname(versionManifest)) + const manifest = await VersionUtil.getFabricProfileJson(this.minecraftVersion.toString(), this.loaderVersion) + await writeFile(versionManifest, JSON.stringify(manifest)) + } + const profileJsonBuf = await readFile(versionManifest) + const profileJson = JSON.parse(profileJsonBuf.toString()) as FabricProfileJson + + const libRepo = this.repoStructure.getLibRepoStruct() + + const modules: Module[] = [{ + id: versionRepo.getFileName(this.minecraftVersion, this.loaderVersion), + name: 'Fabric (version.json)', + type: Type.VersionManifest, + artifact: this.generateArtifact( + profileJsonBuf, + await lstat(versionManifest), + versionRepo.getVersionManifestURL(this.baseUrl, this.minecraftVersion, this.loaderVersion) + ) + }] + for (const lib of profileJson.libraries) { + FabricResolver.log.debug(`Processing ${lib.name}..`) + + const localPath = libRepo.getArtifactById(lib.name) + + if (!await libRepo.artifactExists(localPath)) { + FabricResolver.log.debug('Not found locally, downloading..') + await libRepo.downloadArtifactById(lib.url, lib.name) + } else { + FabricResolver.log.debug('Using local copy.') + } + + const libBuf = await readFile(localPath) + const stats = await lstat(localPath) + + const mavenComponents = MavenUtil.getMavenComponents(lib.name) + + modules.push({ + id: lib.name, + name: `Fabric (${mavenComponents.artifact})`, + type: Type.Library, + artifact: this.generateArtifact( + libBuf, + stats, + libRepo.getArtifactUrlByComponents( + this.baseUrl, + mavenComponents.group, mavenComponents.artifact, + mavenComponents.version, mavenComponents.classifier + ) + ) + }) + } + + // TODO Rework this + let index = -1 + for(let i=0; i= 12) + const hasPatch = major == 1 && minor >= 12 + let unsafe + + if(isVulnerable) { + if(hasPatch) { + unsafe = !VersionUtil.versionGte(this.forgeVersion, patchMatrix[minor]) + } else { + unsafe = true + } + } + + if(unsafe) { + + const logger = LoggerUtil.getLogger('ForgeSecurity') + + logger.error('==================================================================') + logger.error(' WARNING ') + logger.error(' This version of Forge is vulnerable to a CRITICAL RCE exploit. ') + logger.error(' DO NOT USE THIS VERSION! ') + if(hasPatch) { + logger.error(` A patch is available as of Minecraft Forge v${patchMatrix[minor]} `) + } + else { + logger.error(' There is no patch available for this version. ') + } + logger.error('==================================================================') + + logger.error('To abort, use CTRL + C.') + logger.error('Nebula will proceed in 15 seconds..') + const target = new Date().getTime() + (15*1000) + while(new Date().getTime() <= target) { + // Wait + } + + } + + } + + // Coverage is not 100% but that doesnt matter. + // It's enough and you should always use the latest version anyway. + public inferArtifactVersion(): string { + const version = `${this.minecraftVersion}-${this.forgeVersion}` + + const ver = this.forgeVersion.split('.') + const major = Number(ver[0]) + if ([12, 11, 10].includes(major)) { + const minor = Number(ver[1]) + const revision = Number(ver[2]) + const extra = Number(ver[3]) + + if (major === 10) { + if (minor === 13 && revision >= 2 && extra >= 1300) { + return `${version}-1.7.10` + } + } + else if (major === 11) { + if (minor === 15) { + if (revision === 1 && extra >= 1890) { + return `${version}-1.8.9` + } else if (revision === 0 && extra <= 1654) { + return `${version}-1.8.8` + } + } else if (minor === 14 && revision === 0 && extra <= 1295) { + return `${version}-1.8` + } + } else if (major === 12) { + if (minor === 17 && revision === 0 && extra <= 1936) { + return `${version}-1.9.4` + } else if (minor === 16) { + if (revision === 0 && extra <= 1885) { + return `${version}-1.9` + } else if (revision === 1 && extra === 1938) { + return `${version}-1.9.0` + } + } + } + + } + return version + } + + protected async getVersionManifestFromJar(jarPath: string): Promise{ + return new Promise((resolve, reject) => { + const zip = new StreamZip({ + file: jarPath, + storeEntries: true + }) + zip.on('ready', () => { + try { + const data = zip.entryDataSync('version.json') + zip.close() + resolve(data) + } catch(err) { + reject(err) + } + + }) + zip.on('error', err => reject(err)) + }) + } + +} diff --git a/atlas/src/resolver/forge/adapter/ForgeGradle2.resolver.ts b/atlas/src/resolver/forge/adapter/ForgeGradle2.resolver.ts new file mode 100644 index 0000000..cef2508 --- /dev/null +++ b/atlas/src/resolver/forge/adapter/ForgeGradle2.resolver.ts @@ -0,0 +1,255 @@ +import { createHash } from 'crypto' +import { copy, mkdirs, pathExists, remove } from 'fs-extra/esm' +import { lstat, readFile } from 'fs/promises' +import { Module, Type } from 'helios-distribution-types' +import { basename, join } from 'path' +import { VersionManifestFG2 } from '../../../model/forge/VersionManifestFG2.js' +import { LibRepoStructure } from '../../../structure/repo/LibRepo.struct.js' +import { MavenUtil } from '../../../util/MavenUtil.js' +import { PackXZExtractWrapper } from '../../../util/java/PackXZExtractWrapper.js' +import { VersionUtil } from '../../../util/VersionUtil.js' +import { ForgeResolver } from '../Forge.resolver.js' +import { MinecraftVersion } from '../../../util/MinecraftVersion.js' +import { LoggerUtil } from '../../../util/LoggerUtil.js' + +type ArrayElement = A extends readonly (infer T)[] ? T : never + +export class ForgeGradle2Adapter extends ForgeResolver { + + private static readonly logger = LoggerUtil.getLogger('FG2 Adapter') + + public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { + if(version.getMinor() === 12 && !VersionUtil.isOneDotTwelveFG2(libraryVersion)) { + return false + } + return VersionUtil.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12]) + } + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string, + minecraftVersion: MinecraftVersion, + forgeVersion: string, + discardOutput: boolean, + invalidateCache: boolean + ) { + super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, forgeVersion, discardOutput, invalidateCache) + } + + public async getModule(): Promise { + return this.getForgeByVersion() + } + + public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { + return ForgeGradle2Adapter.isForVersion(version, libraryVersion) + } + + public async getForgeByVersion(): Promise { + const libRepo = this.repoStructure.getLibRepoStruct() + const targetLocalPath = libRepo.getLocalForge(this.artifactVersion, 'universal') + ForgeGradle2Adapter.logger.debug(`Checking for forge version at ${targetLocalPath}..`) + if (!await libRepo.artifactExists(targetLocalPath)) { + ForgeGradle2Adapter.logger.debug('Forge not found locally, initializing download..') + await libRepo.downloadArtifactByComponents( + this.REMOTE_REPOSITORY, + LibRepoStructure.FORGE_GROUP, + LibRepoStructure.FORGE_ARTIFACT, + this.artifactVersion, 'universal', 'jar') + } else { + ForgeGradle2Adapter.logger.debug('Using locally discovered forge.') + } + ForgeGradle2Adapter.logger.debug(`Beginning processing of Forge v${this.forgeVersion} (Minecraft ${this.minecraftVersion})`) + + let versionManifestBuf: Buffer + try { + versionManifestBuf = await this.getVersionManifestFromJar(targetLocalPath) + } catch(err) { + throw new Error('Failed to find version.json in forge universal jar.') + } + + const versionManifest = JSON.parse(versionManifestBuf.toString()) as VersionManifestFG2 + + const forgeModule: Module = { + id: MavenUtil.mavenComponentsToIdentifier( + LibRepoStructure.FORGE_GROUP, + LibRepoStructure.FORGE_ARTIFACT, + this.artifactVersion, 'universal' + ), + name: 'Minecraft Forge', + type: Type.ForgeHosted, + artifact: this.generateArtifact( + await readFile(targetLocalPath), + await lstat(targetLocalPath), + libRepo.getArtifactUrlByComponents( + this.baseUrl, + LibRepoStructure.FORGE_GROUP, + LibRepoStructure.FORGE_ARTIFACT, + this.artifactVersion, 'universal' + ) + ), + subModules: [] + } + + const postProcessQueue = [] + + for (const lib of versionManifest.libraries) { + if (lib.name.startsWith('net.minecraftforge:forge:')) { + // We've already processed forge. + continue + } + ForgeGradle2Adapter.logger.debug(`Processing ${lib.name}..`) + + const extension = await this.determineExtension(lib, libRepo) + const localPath = libRepo.getArtifactById(lib.name, extension) + + const postProcess = extension === 'jar.pack.xz' + + let queueDownload = !await libRepo.artifactExists(localPath) + let libBuf + + if (!queueDownload) { + libBuf = await readFile(localPath) + // VERIFY HASH + if (!postProcess) { // Checksums for .pack.xz in the version.json are completely useless. + if (lib.checksums?.length == 1) { + const sha1 = createHash('sha1').update(libBuf).digest('hex') + if (sha1 !== lib.checksums[0]) { + ForgeGradle2Adapter.logger.debug('Hashes do not match, redownloading..') + queueDownload = true + } + } + } + } else { + ForgeGradle2Adapter.logger.debug('Not found locally, downloading..') + queueDownload = true + } + + if (queueDownload) { + await libRepo.downloadArtifactById(lib.url || this.MOJANG_REMOTE_REPOSITORY, lib.name, extension) + libBuf = await readFile(localPath) + } else { + ForgeGradle2Adapter.logger.debug('Using local copy.') + } + + const stats = await lstat(localPath) + + const mavenComponents = MavenUtil.getMavenComponents(lib.name) + const properId = MavenUtil.mavenComponentsToIdentifier( + mavenComponents.group, mavenComponents.artifact, mavenComponents.version, + mavenComponents.classifier, extension + ) + + forgeModule.subModules?.push({ + id: properId, + name: `Minecraft Forge (${mavenComponents?.artifact})`, + type: Type.Library, + artifact: this.generateArtifact( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + libBuf!, + stats, + libRepo.getArtifactUrlByComponents( + this.baseUrl, + mavenComponents.group, mavenComponents.artifact, + mavenComponents.version, mavenComponents.classifier, extension + ) + ) + }) + + if (postProcess) { + postProcessQueue.push({ + id: properId, + localPath + }) + } + + } + + for (const entry of await this.processPackXZFiles(postProcessQueue)) { + const el = forgeModule.subModules?.find((element) => element.id === entry.id) + if (el != null) { + el.artifact.MD5 = entry.MD5 + } else { + ForgeGradle2Adapter.logger.error(`Error during post processing, could not update ${entry.id}`) + } + } + + return forgeModule + } + + private async determineExtension(lib: ArrayElement, libRepo: LibRepoStructure): Promise { + if(lib.url == null) { + return 'jar' + } + ForgeGradle2Adapter.logger.debug('Determing extension..') + const possibleExt = [ + 'jar.pack.xz', + 'jar' + ] + // Check locally. + for(const ext of possibleExt) { + const localPath = libRepo.getArtifactById(lib.name, ext) + const exists = await libRepo.artifactExists(localPath) + if(exists) { + return ext + } + } + // Check remote. + for(const ext of possibleExt) { + const exists = await libRepo.headArtifactById(this.REMOTE_REPOSITORY, lib.name, ext) + if(exists) { + return ext + } + } + // Default to jar. + return 'jar' + } + + private async processPackXZFiles( + processingQueue: {id: string, localPath: string}[] + ): Promise<{id: string, MD5: string}[]> { + + if(processingQueue.length == 0) { + return [] + } + + const accumulator = [] + + const tempDir = this.repoStructure.getTempDirectory() + + if (await pathExists(tempDir)) { + await remove(tempDir) + } + + await mkdirs(tempDir) + + const files = [] + for (const entry of processingQueue) { + const tmpFile = join(tempDir, basename(entry.localPath)) + await copy(entry.localPath, tmpFile) + files.push(tmpFile) + } + + ForgeGradle2Adapter.logger.debug('Spawning PackXZExtract.') + const packXZExecutor = new PackXZExtractWrapper() + await packXZExecutor.extractUnpack(files) + ForgeGradle2Adapter.logger.debug('All files extracted, calculating hashes..') + + for (const entry of processingQueue) { + const tmpFileName = basename(entry.localPath) + const tmpFile = join(tempDir, tmpFileName.substring(0, tmpFileName.indexOf('.pack.xz'))) + const buf = await readFile(tmpFile) + accumulator.push({ + id: entry.id, + MD5: createHash('md5').update(buf).digest('hex') + }) + } + + ForgeGradle2Adapter.logger.debug('Complete, removing temp directory..') + + await remove(tempDir) + + return accumulator + } + +} diff --git a/atlas/src/resolver/forge/adapter/ForgeGradle3.resolver.ts b/atlas/src/resolver/forge/adapter/ForgeGradle3.resolver.ts new file mode 100644 index 0000000..831b745 --- /dev/null +++ b/atlas/src/resolver/forge/adapter/ForgeGradle3.resolver.ts @@ -0,0 +1,778 @@ +import { ForgeResolver } from '../Forge.resolver.js' +import { MinecraftVersion } from '../../../util/MinecraftVersion.js' +import { LoggerUtil } from '../../../util/LoggerUtil.js' +import { VersionUtil } from '../../../util/VersionUtil.js' +import { Module, Type } from 'helios-distribution-types' +import { LibRepoStructure } from '../../../structure/repo/LibRepo.struct.js' +import { pathExists, remove, mkdirs, copy, writeJson } from 'fs-extra/esm' +import { lstat, readFile, writeFile } from 'fs/promises' +import { join, basename, dirname } from 'path' +import { spawn } from 'child_process' +import { JavaUtil } from '../../../util/java/JavaUtil.js' +import { VersionManifestFG3 } from '../../../model/forge/VersionManifestFG3.js' +import { MavenUtil } from '../../../util/MavenUtil.js' +import { createHash } from 'crypto' + +interface GeneratedFile { + name: string + group: string + artifact: string + version: string + classifiers: string[] | [undefined] + skipIfNotPresent?: boolean + classpath?: boolean +} + +export class ForgeGradle3Adapter extends ForgeResolver { + + private static readonly logger = LoggerUtil.getLogger('FG3 Adapter') + + private static readonly WILDCARD_MCP_VERSION = '${mcpVersion}' + + public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { + if(version.getMinor() === 12 && VersionUtil.isOneDotTwelveFG2(libraryVersion)) { + return false + } + return VersionUtil.isVersionAcceptable(version, [12, 13, 14, 15, 16, 17, 18, 19, 20, 21]) + } + + public static isExecutableJar(version: MinecraftVersion): boolean { + return version.isGreaterThanOrEqualTo(new MinecraftVersion('1.20.3')) + } + + private generatedFiles: GeneratedFile[] | undefined + private wildcardsInUse: string[] | undefined + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string, + minecraftVersion: MinecraftVersion, + forgeVersion: string, + discardOutput: boolean, + invalidateCache: boolean + ) { + super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, forgeVersion, discardOutput, invalidateCache) + this.configure() + } + + private configure(): void { + + if(ForgeGradle3Adapter.isExecutableJar(this.minecraftVersion)) { + + // Separate block for 1.20.4+ + + this.generatedFiles = [ + { + name: 'universal jar', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.FORGE_ARTIFACT, + version: this.artifactVersion, + classifiers: ['universal'], + }, + { + name: 'client jar', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.FORGE_ARTIFACT, + version: this.artifactVersion, + classifiers: ['client'] + }, + { + name: 'client shim', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.FORGE_ARTIFACT, + version: this.artifactVersion, + classifiers: ['shim'], + classpath: false + }, + { + name: 'fmlcore', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.FMLCORE_ARTIFACT, + version: this.artifactVersion, + classifiers: [undefined] + }, + { + name: 'javafmllanguage', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.JAVAFMLLANGUAGE_ARTIFACT, + version: this.artifactVersion, + classifiers: [undefined] + }, + { + name: 'mclanguage', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.MCLANGUAGE_ARTIFACT, + version: this.artifactVersion, + classifiers: [undefined] + }, + { + name: 'lowcodelanguage', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.LOWCODELANGUAGE_ARTIFACT, + version: this.artifactVersion, + classifiers: [undefined] + } + ] + + return + } + + // Configure for 13, 14, 15, 16, 17, 18, 19 + if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [13, 14, 15, 16, 17, 18, 19, 20])) { + + // https://github.com/MinecraftForge/MinecraftForge/commit/97d4652f5fe15931b980117efabdff332f9f6428 + const mcpUnifiedVersion = `${this.minecraftVersion}-${ForgeGradle3Adapter.WILDCARD_MCP_VERSION}` + + this.generatedFiles = [ + { + name: 'universal jar', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.FORGE_ARTIFACT, + version: this.artifactVersion, + classifiers: ['universal'], + classpath: false + }, + { + name: 'client jar', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.FORGE_ARTIFACT, + version: this.artifactVersion, + classifiers: ['client'], + classpath: false + }, + { + name: 'client data', + group: LibRepoStructure.MINECRAFT_GROUP, + artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT, + version: this.minecraftVersion.toString(), + classifiers: ['data'], + skipIfNotPresent: true, + classpath: false + }, + { + name: 'client srg', + group: LibRepoStructure.MINECRAFT_GROUP, + artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT, + version: mcpUnifiedVersion, + classifiers: ['srg'], + classpath: false + } + ] + this.wildcardsInUse = [ + ForgeGradle3Adapter.WILDCARD_MCP_VERSION + ] + + if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [13, 14, 15, 16])) { + + // Base jar present for 1.13-1.16 + + this.generatedFiles.unshift( + { + name: 'base jar', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.FORGE_ARTIFACT, + version: this.artifactVersion, + classifiers: [undefined] + } + ) + } + + if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [17, 18, 19, 20])) { + + // Added in 1.17+ + + this.generatedFiles.unshift( + { + name: 'fmlcore', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.FMLCORE_ARTIFACT, + version: this.artifactVersion, + classifiers: [undefined] + }, + { + name: 'javafmllanguage', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.JAVAFMLLANGUAGE_ARTIFACT, + version: this.artifactVersion, + classifiers: [undefined] + }, + { + name: 'mclanguage', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.MCLANGUAGE_ARTIFACT, + version: this.artifactVersion, + classifiers: [undefined] + } + ) + } + + if (VersionUtil.isVersionAcceptable(this.minecraftVersion, [18, 19, 20])) { + + // Added in 1.18+ + + this.generatedFiles.unshift( + { + name: 'lowcodelanguage', + group: LibRepoStructure.FORGE_GROUP, + artifact: LibRepoStructure.LOWCODELANGUAGE_ARTIFACT, + version: this.artifactVersion, + classifiers: [undefined] + } + ) + } + + if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [13, 14, 15])) { + + // 13, 14, 15 use just the MC version. + + this.generatedFiles.push( + { + name: 'client slim', + group: LibRepoStructure.MINECRAFT_GROUP, + artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT, + version: this.minecraftVersion.toString(), + classifiers: [ + 'slim', + 'slim-stable' + ] + }, + { + name: 'client extra', + group: LibRepoStructure.MINECRAFT_GROUP, + artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT, + version: this.minecraftVersion.toString(), + classifiers: [ + 'extra', + 'extra-stable' + ] + } + ) + } else { + + // 16+ uses the mcp unified version. + + this.generatedFiles.push( + { + name: 'client slim', + group: LibRepoStructure.MINECRAFT_GROUP, + artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT, + version: mcpUnifiedVersion, + classifiers: [ + 'slim', + 'slim-stable' + ], + classpath: false + }, + { + name: 'client extra', + group: LibRepoStructure.MINECRAFT_GROUP, + artifact: LibRepoStructure.MINECRAFT_CLIENT_ARTIFACT, + version: mcpUnifiedVersion, + classifiers: [ + 'extra', + 'extra-stable' + ], + classpath: false + } + ) + + } + + + return + } + + // Configure for 12 + if(VersionUtil.isVersionAcceptable(this.minecraftVersion, [12])) { + // NOTHING TO CONFIGURE + return + } + } + + public async getModule(): Promise { + return this.process() + } + + public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { + return ForgeGradle3Adapter.isForVersion(version, libraryVersion) + } + + private async process(): Promise { + const libRepo = this.repoStructure.getLibRepoStruct() + + // Get Installer + const installerPath = libRepo.getLocalForge(this.artifactVersion, 'installer') + ForgeGradle3Adapter.logger.debug(`Checking for forge installer at ${installerPath}..`) + if (!await libRepo.artifactExists(installerPath)) { + ForgeGradle3Adapter.logger.debug('Forge installer not found locally, initializing download..') + await libRepo.downloadArtifactByComponents( + this.REMOTE_REPOSITORY, + LibRepoStructure.FORGE_GROUP, + LibRepoStructure.FORGE_ARTIFACT, + this.artifactVersion, 'installer', 'jar' + ) + } else { + ForgeGradle3Adapter.logger.debug('Using locally discovered forge installer.') + } + ForgeGradle3Adapter.logger.debug(`Beginning processing of Forge v${this.forgeVersion} (Minecraft ${this.minecraftVersion})`) + + if(this.generatedFiles != null && this.generatedFiles.length > 0) { + // Run installer + return this.processWithInstaller(installerPath) + } else { + // Installer not required + return this.processWithoutInstaller(installerPath) + } + + } + + private async processWithInstaller(installerPath: string): Promise { + + let doInstall = true + // Check cache. + const cacheDir = this.repoStructure.getForgeCacheDirectory(this.artifactVersion) + if (await pathExists(cacheDir)) { + if(this.invalidateCache) { + ForgeGradle3Adapter.logger.info(`Removing existing cache ${cacheDir}..`) + await remove(cacheDir) + } else { + // Use cache. + doInstall = false + ForgeGradle3Adapter.logger.info(`Using cached results at ${cacheDir}.`) + } + } else { + await mkdirs(cacheDir) + } + const installerOutputDir = cacheDir + + if(doInstall) { + const workingInstaller = join(installerOutputDir, basename(installerPath)) + + await copy(installerPath, workingInstaller) + + // Required for the installer to function. + await writeFile(join(installerOutputDir, 'launcher_profiles.json'), JSON.stringify({})) + + ForgeGradle3Adapter.logger.debug('Spawning forge installer') + + ForgeGradle3Adapter.logger.info('============== [ IMPORTANT ] ==============') + ForgeGradle3Adapter.logger.info('When the installer opens please set the client installation directory to:') + ForgeGradle3Adapter.logger.info(installerOutputDir) + ForgeGradle3Adapter.logger.info('===========================================') + + await this.executeInstaller(workingInstaller) + + ForgeGradle3Adapter.logger.debug('Installer finished, beginning processing..') + } + + await this.verifyInstallerRan(installerOutputDir) + + ForgeGradle3Adapter.logger.debug('Processing Version Manifest') + const versionManifestTuple = await this.processVersionManifest(installerOutputDir) + const versionManifest = versionManifestTuple[0] + + ForgeGradle3Adapter.logger.debug('Processing generated forge files.') + const forgeModule = await this.processForgeModule(versionManifest, installerOutputDir) + + // Attach version.json module. + forgeModule.subModules?.unshift(versionManifestTuple[1]) + + ForgeGradle3Adapter.logger.debug('Processing Libraries') + const libs = await this.processLibraries(versionManifest, installerOutputDir) + + forgeModule.subModules = forgeModule.subModules?.concat(libs) + + if(this.discardOutput) { + ForgeGradle3Adapter.logger.info(`Removing installer output at ${installerOutputDir}..`) + await remove(installerOutputDir) + ForgeGradle3Adapter.logger.info('Removed successfully.') + } + + return forgeModule + + } + + private getVersionManifestPath(installerOutputDir: string): string { + const versionRepo = this.repoStructure.getVersionRepoStruct() + const versionName = versionRepo.getFileName(this.minecraftVersion, this.forgeVersion) + return join(installerOutputDir, 'versions', versionName, `${versionName}.json`) + } + + private async verifyInstallerRan(installerOutputDir: string): Promise { + const versionManifestPath = this.getVersionManifestPath(installerOutputDir) + + if(!await pathExists(versionManifestPath)) { + await remove(installerOutputDir) + throw new Error(`Forge was either not installed or installed to the wrong location. When the forge installer opens, you MUST set the installation directory to ${installerOutputDir}`) + } + } + + private async processVersionManifest(installerOutputDir: string): Promise<[VersionManifestFG3, Module]> { + const versionRepo = this.repoStructure.getVersionRepoStruct() + const versionManifestPath = this.getVersionManifestPath(installerOutputDir) + + const versionManifestBuf = await readFile(versionManifestPath) + const versionManifest = JSON.parse(versionManifestBuf.toString()) as VersionManifestFG3 + + const versionManifestModule: Module = { + id: this.artifactVersion, + name: 'Minecraft Forge (version.json)', + type: Type.VersionManifest, + artifact: this.generateArtifact( + versionManifestBuf, + await lstat(versionManifestPath), + versionRepo.getVersionManifestURL(this.baseUrl, this.minecraftVersion, this.forgeVersion) + ) + } + + const destination = versionRepo.getVersionManifest( + this.minecraftVersion, + this.forgeVersion + ) + + await copy(versionManifestPath, destination, {overwrite: true}) + + return [versionManifest, versionManifestModule] + } + + private async processForgeModule(versionManifest: VersionManifestFG3, installerOutputDir: string): Promise { + + const libDir = join(installerOutputDir, 'libraries') + + if(this.wildcardsInUse) { + if(this.wildcardsInUse.includes(ForgeGradle3Adapter.WILDCARD_MCP_VERSION)) { + + const mcpVersion = this.getMCPVersion(versionManifest.arguments.game) + if(mcpVersion == null) { + throw new Error('MCP Version not found.. did forge change their format?') + } + + this.generatedFiles = this.generatedFiles!.map(f => { + if(f.version.includes(ForgeGradle3Adapter.WILDCARD_MCP_VERSION)) { + return { + ...f, + version: f.version.replace(ForgeGradle3Adapter.WILDCARD_MCP_VERSION, mcpVersion) + } + } + return f + }) + + } + } + + const mdls: Module[] = [] + + for (const entry of this.generatedFiles!) { + + const targetLocations: string[] = [] + let located = false + + classifierLoop: + for (const _classifier of entry.classifiers) { + + const targetLocalPath = join( + libDir, + MavenUtil.mavenComponentsAsNormalizedPath(entry.group, entry.artifact, entry.version, _classifier) + ) + + targetLocations.push(targetLocalPath) + + const exists = await pathExists(targetLocalPath) + if (exists) { + + mdls.push({ + id: MavenUtil.mavenComponentsToIdentifier( + entry.group, + entry.artifact, + entry.version, + _classifier + ), + name: `Minecraft Forge (${entry.name})`, + type: Type.Library, + classpath: entry.classpath ?? true, + artifact: this.generateArtifact( + await readFile(targetLocalPath), + await lstat(targetLocalPath), + this.repoStructure.getLibRepoStruct().getArtifactUrlByComponents( + this.baseUrl, + entry.group, + entry.artifact, + entry.version, + _classifier + ) + ), + subModules: [] + }) + + const destination = this.repoStructure.getLibRepoStruct().getArtifactByComponents( + entry.group, + entry.artifact, + entry.version, + _classifier + ) + + await copy(targetLocalPath, destination, {overwrite: true}) + + located = true + break classifierLoop + + } + + } + + if (!entry.skipIfNotPresent && !located) { + throw new Error(`Required file ${entry.name} not found at any expected location:\n\t${targetLocations.join('\n\t')}`) + } + + } + + const forgeModule = mdls.shift()! + forgeModule.type = Type.ForgeHosted + forgeModule.subModules = mdls + + return forgeModule + } + + private async processLibraries(manifest: VersionManifestFG3, installerOutputDir: string): Promise { + + const libDir = join(installerOutputDir, 'libraries') + const libRepo = this.repoStructure.getLibRepoStruct() + + const mdls: Module[] = [] + + for (const entry of manifest.libraries) { + const artifact = entry.downloads.artifact + if (artifact.url) { + + const targetLocalPath = join(libDir, artifact.path) + + if (!await pathExists(targetLocalPath)) { + throw new Error(`Expected library ${entry.name} not found!`) + } + + const components = MavenUtil.getMavenComponents(entry.name) + + mdls.push({ + id: entry.name, + name: `Minecraft Forge (${components.artifact})`, + type: Type.Library, + artifact: this.generateArtifact( + await readFile(targetLocalPath), + await lstat(targetLocalPath), + libRepo.getArtifactUrlByComponents( + this.baseUrl, + components.group, + components.artifact, + components.version, + components.classifier, + components.extension + ) + ) + }) + const destination = libRepo.getArtifactByComponents( + components.group, + components.artifact, + components.version, + components.classifier, + components.extension + ) + + await copy(targetLocalPath, destination, {overwrite: true}) + + } + } + + return mdls + + } + + private executeInstaller(installerExec: string): Promise { + return new Promise(resolve => { + const fiLogger = LoggerUtil.getLogger('Forge Installer') + const child = spawn(JavaUtil.getJavaExecutable(), [ + '-jar', + installerExec + ], { + cwd: dirname(installerExec) + }) + child.stdout.on('data', (data) => fiLogger.info(data.toString('utf8').trim())) + child.stderr.on('data', (data) => fiLogger.error(data.toString('utf8').trim())) + child.on('close', code => { + if(code === 0) { + fiLogger.info('Exited with code', code) + } else { + fiLogger.error('Exited with code', code) + } + + resolve() + }) + }) + } + + private getMCPVersion(args: string[]): string | null { + for (let i = 0; i < args.length; i++) { + if (args[i] === '--fml.mcpVersion') { + return args[i + 1] + } + } + return null + } + + private async processWithoutInstaller(installerPath: string): Promise { + + // Extract version.json from installer. + + let versionManifestBuf: Buffer + try { + versionManifestBuf = await this.getVersionManifestFromJar(installerPath) + } catch(err) { + throw new Error('Failed to find version.json in forge installer jar.') + } + + const versionManifest = JSON.parse(versionManifestBuf.toString()) as VersionManifestFG3 + + // Save Version Manifest + const versionManifestDest = this.repoStructure.getVersionRepoStruct().getVersionManifest( + this.minecraftVersion, + this.forgeVersion + ) + await mkdirs(dirname(versionManifestDest)) + await writeJson(versionManifestDest, versionManifest, { spaces: 4 }) + + const libRepo = this.repoStructure.getLibRepoStruct() + const universalLocalPath = libRepo.getLocalForge(this.artifactVersion, 'universal') + ForgeGradle3Adapter.logger.debug(`Checking for Forge Universal jar at ${universalLocalPath}..`) + + const forgeMdl = versionManifest.libraries.find(val => val.name.startsWith('net.minecraftforge:forge:')) + + if(forgeMdl == null) { + throw new Error('Forge entry not found in version.json!') + } + + let forgeUniversalBuffer + + // Check for local universal jar. + if (await libRepo.artifactExists(universalLocalPath)) { + const localUniBuf = await readFile(universalLocalPath) + const sha1 = createHash('sha1').update(localUniBuf).digest('hex') + if(sha1 !== forgeMdl.downloads.artifact.sha1) { + ForgeGradle3Adapter.logger.debug('SHA-1 of local universal jar does not match version.json entry.') + ForgeGradle3Adapter.logger.debug('Redownloading Forge Universal jar..') + } else { + ForgeGradle3Adapter.logger.debug('Using locally discovered forge.') + forgeUniversalBuffer = localUniBuf + } + } else { + ForgeGradle3Adapter.logger.debug('Forge Universal jar not found locally, initializing download..') + } + + // Download if local is missing or corrupt + if(!forgeUniversalBuffer) { + await libRepo.downloadArtifactByComponents( + this.REMOTE_REPOSITORY, + LibRepoStructure.FORGE_GROUP, + LibRepoStructure.FORGE_ARTIFACT, + this.artifactVersion, 'universal', 'jar') + forgeUniversalBuffer = await readFile(universalLocalPath) + } + + ForgeGradle3Adapter.logger.debug(`Beginning processing of Forge v${this.forgeVersion} (Minecraft ${this.minecraftVersion})`) + + const forgeModule: Module = { + id: MavenUtil.mavenComponentsToIdentifier( + LibRepoStructure.FORGE_GROUP, + LibRepoStructure.FORGE_ARTIFACT, + this.artifactVersion, 'universal' + ), + name: 'Minecraft Forge', + type: Type.ForgeHosted, + artifact: this.generateArtifact( + forgeUniversalBuffer, + await lstat(universalLocalPath), + libRepo.getArtifactUrlByComponents( + this.baseUrl, + LibRepoStructure.FORGE_GROUP, + LibRepoStructure.FORGE_ARTIFACT, + this.artifactVersion, 'universal' + ) + ), + subModules: [] + } + + // Attach Version Manifest module. + forgeModule.subModules?.push({ + id: this.artifactVersion, + name: 'Minecraft Forge (version.json)', + type: Type.VersionManifest, + artifact: this.generateArtifact( + await readFile(versionManifestDest), + await lstat(versionManifestDest), + this.repoStructure.getVersionRepoStruct().getVersionManifestURL( + this.baseUrl, this.minecraftVersion, this.forgeVersion) + ) + }) + + for(const lib of versionManifest.libraries) { + if (lib.name.startsWith('net.minecraftforge:forge:')) { + // We've already processed forge. + continue + } + ForgeGradle3Adapter.logger.debug(`Processing ${lib.name}..`) + + const extension = 'jar' + const localPath = libRepo.getArtifactById(lib.name, extension) + + let queueDownload = !await libRepo.artifactExists(localPath) + let libBuf + + if (!queueDownload) { + libBuf = await readFile(localPath) + const sha1 = createHash('sha1').update(libBuf).digest('hex') + if (sha1 !== lib.downloads.artifact.sha1) { + ForgeGradle3Adapter.logger.debug('Hashes do not match, redownloading..') + queueDownload = true + } + } else { + ForgeGradle3Adapter.logger.debug('Not found locally, downloading..') + queueDownload = true + } + + if (queueDownload) { + await libRepo.downloadArtifactDirect(lib.downloads.artifact.url, lib.downloads.artifact.path) + libBuf = await readFile(localPath) + } else { + ForgeGradle3Adapter.logger.debug('Using local copy.') + } + + const stats = await lstat(localPath) + + const mavenComponents = MavenUtil.getMavenComponents(lib.name) + const properId = MavenUtil.mavenComponentsToIdentifier( + mavenComponents.group, mavenComponents.artifact, mavenComponents.version, + mavenComponents.classifier, extension + ) + + forgeModule.subModules?.push({ + id: properId, + name: `Minecraft Forge (${mavenComponents?.artifact})`, + type: Type.Library, + artifact: this.generateArtifact( + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + libBuf!, + stats, + libRepo.getArtifactUrlByComponents( + this.baseUrl, + mavenComponents.group, mavenComponents.artifact, + mavenComponents.version, mavenComponents.classifier, extension + ) + ) + }) + + } + + return forgeModule + + } + +} \ No newline at end of file diff --git a/atlas/src/structure/BaseFileStructure.ts b/atlas/src/structure/BaseFileStructure.ts new file mode 100644 index 0000000..ba8b3c1 --- /dev/null +++ b/atlas/src/structure/BaseFileStructure.ts @@ -0,0 +1,32 @@ +import { mkdirs } from 'fs-extra/esm' +import { join, resolve } from 'path' +import { FileStructure } from './FileStructure.js' +import { Logger } from 'winston' +import { LoggerUtil } from '../util/LoggerUtil.js' + +export abstract class BaseFileStructure implements FileStructure { + + protected logger: Logger + protected containerDirectory: string + + constructor( + protected absoluteRoot: string, + protected relativeRoot: string, + protected structRoot: string + ) { + this.relativeRoot = join(relativeRoot, structRoot) + this.containerDirectory = resolve(absoluteRoot, structRoot) + this.logger = LoggerUtil.getLogger(this.getLoggerName()) + } + + public async init(): Promise { + await mkdirs(this.containerDirectory) + } + + public getContainerDirectory(): string { + return this.containerDirectory + } + + public abstract getLoggerName(): string + +} diff --git a/atlas/src/structure/FileStructure.ts b/atlas/src/structure/FileStructure.ts new file mode 100644 index 0000000..6ce1d6c --- /dev/null +++ b/atlas/src/structure/FileStructure.ts @@ -0,0 +1,5 @@ +export interface FileStructure { + + init(): Promise + +} diff --git a/atlas/src/structure/repo/BaseMavenRepo.ts b/atlas/src/structure/repo/BaseMavenRepo.ts new file mode 100644 index 0000000..5e15611 --- /dev/null +++ b/atlas/src/structure/repo/BaseMavenRepo.ts @@ -0,0 +1,99 @@ +import got from 'got' +import { mkdirs, pathExists } from 'fs-extra/esm' +import { createWriteStream } from 'fs' +import { dirname, join, resolve } from 'path' +import { URL } from 'url' +import { MavenUtil } from '../../util/MavenUtil.js' +import { BaseFileStructure } from '../BaseFileStructure.js' +import { LoggerUtil } from '../../util/LoggerUtil.js' + +export abstract class BaseMavenRepo extends BaseFileStructure { + + private static readonly logger = LoggerUtil.getLogger('BaseMavenRepo') + + constructor( + absoluteRoot: string, + relativeRoot: string, + structRoot: string + ) { + super(absoluteRoot, relativeRoot, structRoot) + } + + public getArtifactById(mavenIdentifier: string, extension?: string): string { + return resolve(this.containerDirectory, MavenUtil.mavenIdentifierAsPath(mavenIdentifier, extension)) + } + + public getArtifactByComponents( + group: string, artifact: string, version: string, classifier?: string, extension = 'jar' + ): string { + return resolve(this.containerDirectory, + MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension)) + } + + public getArtifactUrlByComponents( + baseURL: string, group: string, artifact: string, version: string, classifier?: string, extension = 'jar' + ): string { + return new URL(join(this.relativeRoot, + MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension)), baseURL).toString() + } + + public async artifactExists(path: string): Promise { + return pathExists(path) + } + + public async downloadArtifactById(url: string, mavenIdentifier: string, extension?: string): Promise { + return this.downloadArtifactBase(url, MavenUtil.mavenIdentifierAsPath(mavenIdentifier, extension)) + } + + public async downloadArtifactByComponents( + url: string, group: string, artifact: string, version: string, classifier?: string, extension?: string + ): Promise { + return this.downloadArtifactBase(url, + MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension)) + } + + private async downloadArtifactBase(url: string, relative: string): Promise { + const resolvedURL = new URL(relative, url).toString() + return this.downloadArtifactDirect(resolvedURL, relative) + } + + public async downloadArtifactDirect(url: string, path: string): Promise { + BaseMavenRepo.logger.debug(`Downloading ${url}..`) + const request = got.stream.get({ url }) + const localPath = resolve(this.containerDirectory, path) + await mkdirs(dirname(localPath)) + const writer = createWriteStream(localPath) + request.pipe(writer) + return new Promise((resolve, reject) => { + writer.on('finish', () => { + BaseMavenRepo.logger.debug(`Completed download of ${url}.`) + resolve() + }) + writer.on('error', reject) + }) + } + + public async headArtifactById(url: string, mavenIdentifier: string, extension?: string): Promise { + return this.headArtifactBase(url, MavenUtil.mavenIdentifierAsPath(mavenIdentifier, extension)) + } + + public async headArtifactByComponents( + url: string, group: string, artifact: string, version: string, classifier?: string, extension?: string + ): Promise { + return this.headArtifactBase(url, + MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension)) + } + + private async headArtifactBase(url: string, relative: string): Promise { + const resolvedURL = new URL(relative, url).toString() + try { + const response = await got.head({ + url: resolvedURL + }) + return response.statusCode === 200 + } catch (ignored) { + return false + } + } + +} diff --git a/atlas/src/structure/repo/LibRepo.struct.ts b/atlas/src/structure/repo/LibRepo.struct.ts new file mode 100644 index 0000000..3c2e33c --- /dev/null +++ b/atlas/src/structure/repo/LibRepo.struct.ts @@ -0,0 +1,33 @@ +import { BaseMavenRepo } from './BaseMavenRepo.js' + +export class LibRepoStructure extends BaseMavenRepo { + + public static readonly MINECRAFT_GROUP = 'net.minecraft' + public static readonly MINECRAFT_CLIENT_ARTIFACT = 'client' + + public static readonly FORGE_GROUP = 'net.minecraftforge' + public static readonly FORGE_ARTIFACT = 'forge' + public static readonly FMLCORE_ARTIFACT = 'fmlcore' + public static readonly JAVAFMLLANGUAGE_ARTIFACT = 'javafmllanguage' + public static readonly MCLANGUAGE_ARTIFACT = 'mclanguage' + public static readonly LOWCODELANGUAGE_ARTIFACT = 'lowcodelanguage' + + constructor( + absoluteRoot: string, + relativeRoot: string + ) { + super(absoluteRoot, relativeRoot, 'lib') + } + + public getLoggerName(): string { + return 'LibRepoStructure' + } + + public getLocalForge(version: string, classifier?: string): string { + return this.getArtifactByComponents( + LibRepoStructure.FORGE_GROUP, + LibRepoStructure.FORGE_ARTIFACT, + version, classifier, 'jar') + } + +} diff --git a/atlas/src/structure/repo/Repo.struct.ts b/atlas/src/structure/repo/Repo.struct.ts new file mode 100644 index 0000000..69f7aae --- /dev/null +++ b/atlas/src/structure/repo/Repo.struct.ts @@ -0,0 +1,57 @@ +import { mkdirs } from 'fs-extra/esm' +import { join } from 'path' +import { BaseFileStructure } from '../BaseFileStructure.js' +import { LibRepoStructure } from './LibRepo.struct.js' +import { VersionRepoStructure } from './VersionRepo.struct.js' + +export class RepoStructure extends BaseFileStructure { + + private libRepoStruct: LibRepoStructure + private versionRepoStruct: VersionRepoStructure + + constructor( + absoluteRoot: string, + relativeRoot: string, + name: string + ) { + super(absoluteRoot, relativeRoot, 'repo') + this.libRepoStruct = new LibRepoStructure(this.containerDirectory, this.relativeRoot) + this.versionRepoStruct = new VersionRepoStructure(this.containerDirectory, this.relativeRoot, name) + } + + public getLoggerName(): string { + return 'RepoStructure' + } + + public async init(): Promise { + await super.init() + await this.libRepoStruct.init() + await this.versionRepoStruct.init() + await mkdirs(this.getCacheDirectory()) + } + + public getLibRepoStruct(): LibRepoStructure { + return this.libRepoStruct + } + + public getVersionRepoStruct(): VersionRepoStructure { + return this.versionRepoStruct + } + + public getTempDirectory(): string { + return join(this.absoluteRoot, 'temp') + } + + public getWorkDirectory(): string { + return join(this.absoluteRoot, 'work') + } + + public getCacheDirectory(): string { + return join(this.absoluteRoot, 'cache') + } + + public getForgeCacheDirectory(artifactVersion: string): string { + return join(this.getCacheDirectory(), 'forge', artifactVersion) + } + +} diff --git a/atlas/src/structure/repo/VersionRepo.struct.ts b/atlas/src/structure/repo/VersionRepo.struct.ts new file mode 100644 index 0000000..fec3b5e --- /dev/null +++ b/atlas/src/structure/repo/VersionRepo.struct.ts @@ -0,0 +1,37 @@ +import { join } from 'path' +import { URL } from 'url' +import { BaseFileStructure } from '../BaseFileStructure.js' +import { MinecraftVersion } from '../../util/MinecraftVersion.js' + +export class VersionRepoStructure extends BaseFileStructure { + + private name: string + + constructor( + absoluteRoot: string, + relativeRoot: string, + name: string + ) { + super(absoluteRoot, relativeRoot, 'versions') + this.name = name + } + + public getLoggerName(): string { + return 'VersionRepoStructure' + } + + public getFileName(minecraftVersion: MinecraftVersion, loaderVersion: string): string { + return `${minecraftVersion}-${this.name}-${loaderVersion}` + } + + public getVersionManifest(minecraftVersion: MinecraftVersion, loaderVersion: string): string { + const fileName = this.getFileName(minecraftVersion, loaderVersion) + return join(this.containerDirectory, fileName, `${fileName}.json`) + } + + public getVersionManifestURL(url: string, minecraftVersion: MinecraftVersion, loaderVersion: string): string { + const fileName = this.getFileName(minecraftVersion, loaderVersion) + return new URL(join(this.relativeRoot, fileName, `${fileName}.json`), url).toString() + } + +} diff --git a/atlas/src/structure/spec_model/BaseModel.struct.ts b/atlas/src/structure/spec_model/BaseModel.struct.ts new file mode 100644 index 0000000..a480bad --- /dev/null +++ b/atlas/src/structure/spec_model/BaseModel.struct.ts @@ -0,0 +1,19 @@ +import { BaseFileStructure } from '../BaseFileStructure.js' +import { SpecModelStructure } from './SpecModelStructure.js' + +export abstract class BaseModelStructure extends BaseFileStructure implements SpecModelStructure { + + protected resolvedModels: T[] | undefined + + constructor( + absoluteRoot: string, + relativeRoot: string, + structRoot: string, + protected baseUrl: string + ) { + super(absoluteRoot, relativeRoot, structRoot) + } + + public abstract getSpecModel(): Promise + +} diff --git a/atlas/src/structure/spec_model/Distribution.struct.ts b/atlas/src/structure/spec_model/Distribution.struct.ts new file mode 100644 index 0000000..97f692e --- /dev/null +++ b/atlas/src/structure/spec_model/Distribution.struct.ts @@ -0,0 +1,62 @@ +import { mkdirs, pathExists } from 'fs-extra/esm' +import { readFile, writeFile } from 'fs/promises' +import { Distribution } from 'helios-distribution-types' +import { SpecModelStructure } from './SpecModelStructure.js' +import { ServerStructure } from './Server.struct.js' +import { join, resolve } from 'path' +import { DistroMeta, getDefaultDistroMeta } from '../../model/nebula/DistroMeta.js' +import { addSchemaToObject, SchemaTypes } from '../../util/SchemaUtil.js' +import { LoggerUtil } from '../../util/LoggerUtil.js' + +const logger = LoggerUtil.getLogger('DistributionStructure') + +export class DistributionStructure implements SpecModelStructure { + + private readonly DISTRO_META_FILE = 'distrometa.json' + + private serverStruct: ServerStructure + private metaPath: string + + constructor( + private absoluteRoot: string, + private baseUrl: string, + discardOutput: boolean, + invalidateCache: boolean + ) { + this.serverStruct = new ServerStructure(this.absoluteRoot, this.baseUrl, discardOutput, invalidateCache) + this.metaPath = join(this.absoluteRoot, 'meta') + } + + public async init(): Promise { + await mkdirs(this.absoluteRoot) + await mkdirs(this.metaPath) + + const distroMetaFile = resolve(this.metaPath, this.DISTRO_META_FILE) + if(await pathExists(distroMetaFile)) { + logger.warn(`Distro Meta file already exists at ${distroMetaFile}!`) + logger.warn('If you wish to regenerate this file, you must delete the existing one!') + } else { + const distroMeta: DistroMeta = addSchemaToObject( + getDefaultDistroMeta(), + SchemaTypes.DistroMetaSchema, + this.absoluteRoot + ) + await writeFile(distroMetaFile, JSON.stringify(distroMeta, null, 2)) + } + + await this.serverStruct.init() + } + + public async getSpecModel(): Promise { + + const distroMeta = JSON.parse(await readFile(resolve(this.metaPath, this.DISTRO_META_FILE), 'utf-8')) as DistroMeta + + return { + version: '1.0.0', + rss: distroMeta.meta.rss, + ...(distroMeta.meta.discord ? {discord: distroMeta.meta.discord} : {}), + servers: await this.serverStruct.getSpecModel() + } + } + +} diff --git a/atlas/src/structure/spec_model/Server.struct.ts b/atlas/src/structure/spec_model/Server.struct.ts new file mode 100644 index 0000000..c5ca866 --- /dev/null +++ b/atlas/src/structure/spec_model/Server.struct.ts @@ -0,0 +1,254 @@ +import { mkdirs, pathExists } from 'fs-extra/esm' +import { lstat, readdir, readFile, writeFile } from 'fs/promises' +import { Server, Module } from 'helios-distribution-types' +import { dirname, join, resolve as resolvePath } from 'path' +import { URL } from 'url' +import { VersionSegmentedRegistry } from '../../util/VersionSegmentedRegistry.js' +import { ServerMeta, getDefaultServerMeta, ServerMetaOptions, UntrackedFilesOption } from '../../model/nebula/ServerMeta.js' +import { BaseModelStructure } from './BaseModel.struct.js' +import { FabricModStructure } from './module/FabricMod.struct.js' +import { MiscFileStructure } from './module/File.struct.js' +import { LibraryStructure } from './module/Library.struct.js' +import { MinecraftVersion } from '../../util/MinecraftVersion.js' +import { addSchemaToObject, SchemaTypes } from '../../util/SchemaUtil.js' +import { isValidUrl } from '../../util/StringUtils.js' +import { FabricResolver } from '../../resolver/fabric/Fabric.resolver.js' + +export interface CreateServerResult { + modContainer?: string + libraryContainer: string + miscFileContainer: string +} + +export class ServerStructure extends BaseModelStructure { + + private readonly ID_REGEX = /(.+-(.+)$)/ + private readonly SERVER_META_FILE = 'servermeta.json' + + constructor( + absoluteRoot: string, + baseUrl: string, + private discardOutput: boolean, + private invalidateCache: boolean + ) { + super(absoluteRoot, '', 'servers', baseUrl) + } + + public getLoggerName(): string { + return 'ServerStructure' + } + + public async getSpecModel(): Promise { + if (this.resolvedModels == null) { + this.resolvedModels = await this._doSeverRetrieval() + } + return this.resolvedModels + } + + public static getEffectiveId(id: string, minecraftVersion: MinecraftVersion): string { + return `${id}-${minecraftVersion}` + } + + public async createServer( + id: string, + minecraftVersion: MinecraftVersion, + options: { + version?: string + forgeVersion?: string + fabricVersion?: string + } + ): Promise { + const effectiveId = ServerStructure.getEffectiveId(id, minecraftVersion) + const absoluteServerRoot = resolvePath(this.containerDirectory, effectiveId) + const relativeServerRoot = join(this.relativeRoot, effectiveId) + + if (await pathExists(absoluteServerRoot)) { + this.logger.error('Server already exists! Aborting.') + return null + } + + await mkdirs(absoluteServerRoot) + + const serverMetaOpts: ServerMetaOptions = { + version: options.version + } + let modContainer: string | undefined = undefined + + if (options.forgeVersion != null) { + const fms = VersionSegmentedRegistry.getForgeModStruct( + minecraftVersion, + options.forgeVersion, + absoluteServerRoot, + relativeServerRoot, + this.baseUrl, + [] + ) + await fms.init() + modContainer = fms.getContainerDirectory() + serverMetaOpts.forgeVersion = options.forgeVersion + } + + if (options.fabricVersion != null) { + const fms = new FabricModStructure( + absoluteServerRoot, + relativeServerRoot, + this.baseUrl, + minecraftVersion, + [] + ) + await fms.init() + modContainer = fms.getContainerDirectory() + serverMetaOpts.fabricVersion = options.fabricVersion + } + + const serverMeta: ServerMeta = addSchemaToObject( + getDefaultServerMeta(id, minecraftVersion.toString(), serverMetaOpts), + SchemaTypes.ServerMetaSchema, + this.absoluteRoot + ) + await writeFile(resolvePath(absoluteServerRoot, this.SERVER_META_FILE), JSON.stringify(serverMeta, null, 2)) + + const libS = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, []) + await libS.init() + + const mfs = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, []) + await mfs.init() + + return { + modContainer, + libraryContainer: libS.getContainerDirectory(), + miscFileContainer: mfs.getContainerDirectory() + } + + } + + private async _doSeverRetrieval(): Promise { + + const accumulator: Server[] = [] + const files = await readdir(this.containerDirectory) + for (const file of files) { + const absoluteServerRoot = resolvePath(this.containerDirectory, file) + const relativeServerRoot = join(this.relativeRoot, file) + if ((await lstat(absoluteServerRoot)).isDirectory()) { + + this.logger.info(`Beginning processing of ${file}.`) + + const match = this.ID_REGEX.exec(file) + if (match == null) { + this.logger.warn(`Server directory ${file} does not match the defined standard.`) + this.logger.warn('All server ids must end with - (ex. -1.12.2)') + continue + } + + // Read server meta + const serverMeta = JSON.parse(await readFile(resolvePath(absoluteServerRoot, this.SERVER_META_FILE), 'utf-8')) as ServerMeta + const minecraftVersion = new MinecraftVersion(match[2]) + const untrackedFiles: UntrackedFilesOption[] = serverMeta.untrackedFiles || [] + + let iconUrl: string = null! + + // Resolve server icon + + if(serverMeta.meta.icon && isValidUrl(serverMeta.meta.icon)) { + // Use the url they gave us. + iconUrl = serverMeta.meta.icon + } else { + + this.logger.info('Server icon is either not set or not a valid URL.') + this.logger.info(`Looking for an icon file at ${absoluteServerRoot}`) + + const subFiles = await readdir(absoluteServerRoot) + for (const subFile of subFiles) { + const caseInsensitive = subFile.toLowerCase() + if (caseInsensitive.endsWith('.jpg') || caseInsensitive.endsWith('.png')) { + iconUrl = new URL(join(relativeServerRoot, subFile), this.baseUrl).toString() + } + } + + if (!iconUrl) { + this.logger.warn(`No icon file found for server ${file}.`) + } + } + + const modules: Module[] = [] + + if(serverMeta.forge) { + const forgeResolver = VersionSegmentedRegistry.getForgeResolver( + minecraftVersion, + serverMeta.forge.version, + dirname(this.containerDirectory), + '', + this.baseUrl, + this.discardOutput, + this.invalidateCache + ) + + // Resolve forge + const forgeItselfModule = await forgeResolver.getModule() + modules.push(forgeItselfModule) + + const forgeModStruct = VersionSegmentedRegistry.getForgeModStruct( + minecraftVersion, + serverMeta.forge.version, + absoluteServerRoot, + relativeServerRoot, + this.baseUrl, + untrackedFiles + ) + + const forgeModModules = await forgeModStruct.getSpecModel() + modules.push(...forgeModModules) + } + + if(serverMeta.fabric) { + const fabricResolver = new FabricResolver(dirname(this.containerDirectory), '', this.baseUrl, serverMeta.fabric.version, minecraftVersion) + if (!fabricResolver.isForVersion(minecraftVersion, serverMeta.fabric.version)) { + throw new Error(`Fabric resolver does not support Fabric ${serverMeta.fabric.version}!`) + } + + const fabricModule = await fabricResolver.getModule() + modules.push(fabricModule) + + const fabricModStruct = new FabricModStructure( + absoluteServerRoot, + relativeServerRoot, + this.baseUrl, + minecraftVersion, + untrackedFiles + ) + + const fabricModModules = await fabricModStruct.getSpecModel() + modules.push(...fabricModModules) + } + + const libraryStruct = new LibraryStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, untrackedFiles) + const libraryModules = await libraryStruct.getSpecModel() + modules.push(...libraryModules) + + const fileStruct = new MiscFileStructure(absoluteServerRoot, relativeServerRoot, this.baseUrl, minecraftVersion, untrackedFiles) + const fileModules = await fileStruct.getSpecModel() + modules.push(...fileModules) + + accumulator.push({ + id: match[1], + name: serverMeta.meta.name, + description: serverMeta.meta.description, + icon: iconUrl, + version: serverMeta.meta.version, + address: serverMeta.meta.address, + minecraftVersion: match[2], + ...(serverMeta.meta.discord ? {discord: serverMeta.meta.discord} : {}), + mainServer: serverMeta.meta.mainServer, + autoconnect: serverMeta.meta.autoconnect, + javaOptions: serverMeta.meta.javaOptions, + modules + }) + + } else { + this.logger.warn(`Path ${file} in server directory is not a directory!`) + } + } + return accumulator + } + +} diff --git a/atlas/src/structure/spec_model/SpecModelStructure.ts b/atlas/src/structure/spec_model/SpecModelStructure.ts new file mode 100644 index 0000000..018f30c --- /dev/null +++ b/atlas/src/structure/spec_model/SpecModelStructure.ts @@ -0,0 +1,7 @@ +import { FileStructure } from '../FileStructure.js' + +export interface SpecModelStructure extends FileStructure { + + getSpecModel(): Promise + +} diff --git a/atlas/src/structure/spec_model/module/FabricMod.struct.ts b/atlas/src/structure/spec_model/module/FabricMod.struct.ts new file mode 100644 index 0000000..56504df --- /dev/null +++ b/atlas/src/structure/spec_model/module/FabricMod.struct.ts @@ -0,0 +1,91 @@ +import StreamZip from 'node-stream-zip' +import { Type } from 'helios-distribution-types' +import { capitalize } from '../../../util/StringUtils.js' +import { FabricModJson } from '../../../model/fabric/FabricModJson.js' +import { MinecraftVersion } from '../../../util/MinecraftVersion.js' +import { BaseModStructure } from './Mod.struct.js' +import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js' + +export class FabricModStructure extends BaseModStructure { + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string, + minecraftVersion: MinecraftVersion, + untrackedFiles: UntrackedFilesOption[] + ) { + super(absoluteRoot, relativeRoot, 'fabricmods', baseUrl, minecraftVersion, Type.FabricMod, untrackedFiles) + } + + public getLoggerName(): string { + return 'FabricModStructure' + } + + protected async getModuleId(name: string, path: string): Promise { + const fmData = await this.getModMetadata(name, path) + let group + if (fmData.entrypoints != null) { + for (const t of ['main', 'client', 'server']) { + if (fmData.entrypoints[t] != null && fmData.entrypoints[t].length > 0) { + const entrypoint = fmData.entrypoints[t][0] + group = typeof entrypoint === 'string' ? entrypoint : entrypoint.value + break + } + } + // adapted from https://github.com/dscalzi/Claritas/blob/master/src/main/java/com/dscalzi/claritas/util/DataUtil.java + if (group != null) { + const packageBits = group.split('.') + const blacklist = ['common', 'util', 'internal', 'tweaker', 'tweak', 'client', ...['forge', 'fabric', 'bukkit', 'sponge'].filter(t => t !== fmData.id)] + // Note: Entry point is a fully qualified class name, hence why this adaptation pops immediately (drop class name). + while (packageBits.length > 0) { + packageBits.pop() + const term = packageBits[packageBits.length - 1] + if ((term !== fmData.id && !blacklist.includes(term)) || packageBits.length === 1 || (packageBits.length === 2 && term === fmData.id)) { + break + } + } + group = packageBits.join('.') + } + } + return this.generateMavenIdentifier(group || this.getDefaultGroup(), fmData.id, fmData.version) + } + protected async getModuleName(name: string, path: string): Promise { + const fmData = await this.getModMetadata(name, path) + return capitalize(fmData.name || fmData.id) + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected processZip(zip: StreamZip, name: string, path: string): FabricModJson { + + let raw: Buffer | undefined + try { + raw = zip.entryDataSync('fabric.mod.json') + } catch(err) { + // ignored + } + + if (raw) { + try { + const parsed = JSON.parse(raw.toString()) as FabricModJson + this.modMetadata[name] = parsed + } catch (err) { + this.logger.error(`FabricMod ${name} contains an invalid fabric.mod.json file.`) + } + } else { + this.logger.error(`FabricMod ${name} does not contain fabric.mod.json file.`) + } + + const crudeInference = this.attemptCrudeInference(name) + + if(this.modMetadata[name] == null) { + this.modMetadata[name] = ({ + id: crudeInference.name.toLowerCase(), + name: crudeInference.name, + version: crudeInference.version + }) + } + + return this.modMetadata[name] + } +} diff --git a/atlas/src/structure/spec_model/module/File.struct.ts b/atlas/src/structure/spec_model/module/File.struct.ts new file mode 100644 index 0000000..45ed481 --- /dev/null +++ b/atlas/src/structure/spec_model/module/File.struct.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Stats } from 'fs' +import { Type, Module } from 'helios-distribution-types' +import { URL } from 'url' +import { ModuleStructure } from './Module.struct.js' +import { readdir, stat } from 'fs/promises' +import { join, resolve, sep } from 'path' +import { MinecraftVersion } from '../../../util/MinecraftVersion.js' +import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js' + +export class MiscFileStructure extends ModuleStructure { + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string, + minecraftVersion: MinecraftVersion, + untrackedFiles: UntrackedFilesOption[] + ) { + super(absoluteRoot, relativeRoot, 'files', baseUrl, minecraftVersion, Type.File, untrackedFiles) + } + + public getLoggerName(): string { + return 'MiscFileStructure' + } + + public async getSpecModel(): Promise { + if (this.resolvedModels == null) { + this.resolvedModels = await this.recursiveModuleScan(this.containerDirectory) + } + + return this.resolvedModels + } + + protected async recursiveModuleScan(dir: string): Promise { + let acc: Module[] = [] + const subdirs = await readdir(dir) + for (const file of subdirs) { + const filePath = resolve(dir, file) + const stats = await stat(filePath) + if (stats.isDirectory()) { + acc = acc.concat(await this.recursiveModuleScan(filePath)) + } else { + if(!this.FILE_NAME_BLACKLIST.includes(file)) { + acc.push(await this.parseModule(file, filePath, stats)) + } + } + } + return acc + } + + protected async getModuleId(name: string, path: string): Promise { + return name + } + protected async getModuleName(name: string, path: string): Promise { + return name + } + protected async getModuleUrl(name: string, path: string, stats: Stats): Promise { + return new URL(join(this.relativeRoot, ...path.substr(this.containerDirectory.length+1).split(sep)), this.baseUrl).toString() + } + protected async getModulePath(name: string, path: string, stats: Stats): Promise { + return path.substr(this.containerDirectory.length+1).replace(/\\/g, '/') + } + +} diff --git a/atlas/src/structure/spec_model/module/ForgeMod.struct.ts b/atlas/src/structure/spec_model/module/ForgeMod.struct.ts new file mode 100644 index 0000000..3c5a117 --- /dev/null +++ b/atlas/src/structure/spec_model/module/ForgeMod.struct.ts @@ -0,0 +1,42 @@ +import { Type } from 'helios-distribution-types' +import { VersionSegmented } from '../../../util/VersionSegmented.js' +import { MinecraftVersion } from '../../../util/MinecraftVersion.js' +import { BaseModStructure } from './Mod.struct.js' +import { LibraryType } from '../../../model/claritas/ClaritasLibraryType.js' +import { ClaritasException } from './Module.struct.js' +import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js' + +export abstract class BaseForgeModStructure extends BaseModStructure implements VersionSegmented { + + protected readonly EXAMPLE_MOD_ID = 'examplemod' + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string, + minecraftVersion: MinecraftVersion, + untrackedFiles: UntrackedFilesOption[] + ) { + super(absoluteRoot, relativeRoot, 'forgemods', baseUrl, minecraftVersion, Type.ForgeMod, untrackedFiles) + } + + public abstract isForVersion(version: MinecraftVersion, libraryVersion: string): boolean + + protected getClaritasExceptions(): ClaritasException[] { + return [{ + exceptionName: 'optifine', + proxyMetadata: { + group: 'net.optifine' + } + }] + } + + protected getClaritasType(): LibraryType { + return LibraryType.FORGE + } + + protected discernResult(claritasValue: string | undefined, crudeInference: string): string { + return (claritasValue == null || claritasValue == '') ? crudeInference : claritasValue + } + +} diff --git a/atlas/src/structure/spec_model/module/Library.struct.ts b/atlas/src/structure/spec_model/module/Library.struct.ts new file mode 100644 index 0000000..e9fa1fc --- /dev/null +++ b/atlas/src/structure/spec_model/module/Library.struct.ts @@ -0,0 +1,46 @@ +import { ModuleStructure } from './Module.struct.js' +import { Type, TypeMetadata } from 'helios-distribution-types' +import { Stats } from 'fs' +import { join } from 'path' +import { URL } from 'url' +import { MinecraftVersion } from '../../../util/MinecraftVersion.js' +import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js' + +export class LibraryStructure extends ModuleStructure { + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string, + minecraftVersion: MinecraftVersion, + untrackedFiles: UntrackedFilesOption[] + ) { + super(absoluteRoot, relativeRoot, 'libraries', baseUrl, minecraftVersion, Type.Library, untrackedFiles, (name: string) => { + return name.toLowerCase().endsWith(TypeMetadata[this.type].defaultExtension!) + }) + } + + public getLoggerName(): string { + return 'LibraryStructure' + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async getModuleId(name: string, path: string): Promise { + const inference = this.attemptCrudeInference(name) + return this.generateMavenIdentifier(this.getDefaultGroup(), inference.name, inference.version) + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async getModuleName(name: string, path: string): Promise { + const inference = this.attemptCrudeInference(name) + return inference.name + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async getModuleUrl(name: string, path: string, stats: Stats): Promise { + return new URL(join(this.relativeRoot, name), this.baseUrl).toString() + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async getModulePath(name: string, path: string, stats: Stats): Promise { + return null + } + +} \ No newline at end of file diff --git a/atlas/src/structure/spec_model/module/Mod.struct.ts b/atlas/src/structure/spec_model/module/Mod.struct.ts new file mode 100644 index 0000000..20b214b --- /dev/null +++ b/atlas/src/structure/spec_model/module/Mod.struct.ts @@ -0,0 +1,66 @@ +import { Stats } from 'fs' +import { Module } from 'helios-distribution-types' +import StreamZip from 'node-stream-zip' +import { join } from 'path' +import { URL } from 'url' +import { ToggleableModuleStructure } from './ToggleableModule.struct.js' + +export abstract class BaseModStructure extends ToggleableModuleStructure { + + protected modMetadata: {[property: string]: T | undefined} = {} + + public async getSpecModel(): Promise { + // Sort by file name to allow control of load order. + return (await super.getSpecModel()).sort((a, b) => { + const aFileName = a.artifact.url.substring(a.artifact.url.lastIndexOf('/')+1) + const bFileName = b.artifact.url.substring(b.artifact.url.lastIndexOf('/')+1) + return aFileName.localeCompare(bFileName) + }) + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async getModuleUrl(name: string, path: string, stats: Stats): Promise { + return new URL(join(this.relativeRoot, this.getActiveNamespace(), name), this.baseUrl).toString() + } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async getModulePath(name: string, path: string, stats: Stats): Promise { + return null + } + + protected getModMetadata(name: string, path: string): Promise { + return new Promise((resolve, reject) => { + if (!Object.prototype.hasOwnProperty.call(this.modMetadata, name)) { + + const zip = new StreamZip({ + file: path, + storeEntries: true + }) + + zip.on('error', err => { + this.logger.error(`Failure while processing ${path}`) + reject(err) + }) + zip.on('ready', () => { + try { + const res = this.processZip(zip, name, path) + zip.close() + resolve(res) + return + } catch(err) { + zip.close() + reject(err) + return + } + }) + + } else { + resolve(this.modMetadata[name]!) + return + } + + }) + } + + protected abstract processZip(zip: StreamZip, name: string, path: string): T + +} \ No newline at end of file diff --git a/atlas/src/structure/spec_model/module/Module.struct.ts b/atlas/src/structure/spec_model/module/Module.struct.ts new file mode 100644 index 0000000..e6004e5 --- /dev/null +++ b/atlas/src/structure/spec_model/module/Module.struct.ts @@ -0,0 +1,221 @@ +import { minimatch } from 'minimatch' +import { createHash } from 'crypto' +import { pathExists } from 'fs-extra/esm' +import { Stats } from 'fs' +import { lstat, readdir, readFile } from 'fs/promises' +import { Artifact, Module, Type, TypeMetadata } from 'helios-distribution-types' +import { resolve } from 'path' +import { BaseModelStructure } from '../BaseModel.struct.js' +import { LibraryType } from '../../../model/claritas/ClaritasLibraryType.js' +import { ClaritasResult, ClaritasModuleMetadata } from '../../../model/claritas/ClaritasResult.js' +import { ClaritasWrapper } from '../../../util/java/ClaritasWrapper.js' +import { MinecraftVersion } from '../../../util/MinecraftVersion.js' +import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js' + +export interface ModuleCandidate { + file: string + filePath: string + stats: Stats +} + +export interface ClaritasException { + exceptionName: string + proxyMetadata: ClaritasModuleMetadata +} + +export abstract class ModuleStructure extends BaseModelStructure { + + private readonly crudeRegex = /(.+?)-(.+).[jJ][aA][rR]/ + protected readonly DEFAULT_VERSION = '0.0.0' + protected readonly FILE_NAME_BLACKLIST = [ + '.gitkeep' + ] + + protected untrackedFilePatterns: string[] // List of glob patterns. + protected claritasResult!: ClaritasResult + + constructor( + absoluteRoot: string, + relativeRoot: string, + structRoot: string, + baseUrl: string, + protected minecraftVersion: MinecraftVersion, + protected type: Type, + untrackedFiles: UntrackedFilesOption[], + protected filter?: ((name: string, path: string, stats: Stats) => boolean) + ) { + super(absoluteRoot, relativeRoot, structRoot, baseUrl) + this.untrackedFilePatterns = this.determineUntrackedFiles(structRoot, untrackedFiles) + } + + public async getSpecModel(): Promise { + if (this.resolvedModels == null) { + this.resolvedModels = await this._doModuleRetrieval(await this._doModuleDiscovery(this.containerDirectory)) + } + + return this.resolvedModels + } + + protected getDefaultGroup(): string { + return `generated.${this.type.toLowerCase()}` + } + + protected generateMavenIdentifier(group: string, id: string, version: string): string { + return `${group}:${id}:${version}@${TypeMetadata[this.type].defaultExtension}` + } + + protected attemptCrudeInference(name: string): { name: string, version: string } { + const result = this.crudeRegex.exec(name) + if(result != null) { + return { + name: result[1], + version: result[2] + } + } else { + return { + name: name.substring(0, name.lastIndexOf('.')), + version: this.DEFAULT_VERSION + } + } + } + + protected getClaritasGroup(path: string): string { + return this.claritasResult[path]?.group || this.getDefaultGroup() + } + + protected getClaritasExceptions(): ClaritasException[] { + return [] + } + + protected getClaritasType(): LibraryType | null { + return null + } + + protected abstract getModuleId(name: string, path: string): Promise + protected abstract getModuleName(name: string, path: string): Promise + protected abstract getModuleUrl(name: string, path: string, stats: Stats): Promise + protected abstract getModulePath(name: string, path: string, stats: Stats): Promise + + protected async parseModule(file: string, filePath: string, stats: Stats): Promise { + + const artifact: Artifact = { + size: stats.size, + url: await this.getModuleUrl(file, filePath, stats) + } + + const relativeToContainer = filePath.substr(this.containerDirectory.length+1) + const untrackedByPattern = this.isFileUntracked(relativeToContainer) + if(!untrackedByPattern) { + const buf = await readFile(filePath) + artifact.MD5 = createHash('md5').update(buf).digest('hex') + } else { + this.logger.debug(`File ${relativeToContainer} is untracked. Matching pattern: ${untrackedByPattern}`) + } + + const mdl: Module = { + id: await this.getModuleId(file, filePath), + name: await this.getModuleName(file, filePath), + type: this.type, + artifact + } + const pth = await this.getModulePath(file, filePath, stats) + if (pth) { + mdl.artifact.path = pth + } + return mdl + } + + protected async _doModuleDiscovery(scanDirectory: string): Promise { + + const moduleCandidates: ModuleCandidate[] = [] + + if (await pathExists(scanDirectory)) { + const files = await readdir(scanDirectory) + for (const file of files) { + const filePath = resolve(scanDirectory, file) + const stats = await lstat(filePath) + if (stats.isFile()) { + if(!this.FILE_NAME_BLACKLIST.includes(file)) { + if(this.filter == null || this.filter(file, filePath, stats)) { + moduleCandidates.push({file, filePath, stats}) + } + } + } + } + } + + return moduleCandidates + + } + + protected async invokeClaritas(moduleCandidates: ModuleCandidate[]): Promise { + if(this.getClaritasType() != null) { + const claritasExecutor = new ClaritasWrapper(this.absoluteRoot) + + let claritasCandidates = moduleCandidates + const exceptionCandidates: [ModuleCandidate, ClaritasException][] = [] + for(const exception of this.getClaritasExceptions()) { + const exceptionCandidate = moduleCandidates.find((value) => value.file.toLowerCase().includes(exception.exceptionName)) + if(exceptionCandidate != null) { + exceptionCandidates.push([exceptionCandidate, exception]) + claritasCandidates = claritasCandidates.filter((value) => !value.file.toLowerCase().includes(exception.exceptionName)) + } + } + + this.claritasResult = await claritasExecutor.execute( + this.getClaritasType()!, + this.minecraftVersion, + claritasCandidates.map(entry => entry.filePath) + ) + + if(this.claritasResult == null) { + this.logger.error('Failed to process Claritas result!') + } else { + for(const [candidate, exception] of exceptionCandidates) { + this.claritasResult[candidate.filePath] = exception.proxyMetadata + } + } + } + } + + protected async _doModuleRetrieval(moduleCandidates: ModuleCandidate[], options?: { + preProcess?: (candidate: ModuleCandidate) => void + postProcess?: (module: Module) => void + }): Promise { + + const accumulator: Module[] = [] + + if(moduleCandidates.length > 0) { + + // Invoke Claritas and attach result to class. + await this.invokeClaritas(moduleCandidates) + + // Process Modules + for(const candidate of moduleCandidates) { + options?.preProcess?.(candidate) + const mdl = await this.parseModule(candidate.file, candidate.filePath, candidate.stats) + options?.postProcess?.(mdl) + accumulator.push(mdl) + } + + } + + return accumulator + + } + + protected determineUntrackedFiles(targetStructRoot: string, untrackedFileOptions?: UntrackedFilesOption[]): string[] { + if(untrackedFileOptions) { + return untrackedFileOptions + .filter(x => x.appliesTo.includes(targetStructRoot)) + .reduce((acc, cur) => acc.concat(cur.patterns), [] as string[]) + } + return [] + } + + // Will return the matching pattern, undefined if no match. + protected isFileUntracked(pathRelativeToContainer: string): string | undefined { + return this.untrackedFilePatterns.find(pattern => minimatch(pathRelativeToContainer, pattern)) + } + +} diff --git a/atlas/src/structure/spec_model/module/ToggleableModule.struct.ts b/atlas/src/structure/spec_model/module/ToggleableModule.struct.ts new file mode 100644 index 0000000..ef8c895 --- /dev/null +++ b/atlas/src/structure/spec_model/module/ToggleableModule.struct.ts @@ -0,0 +1,85 @@ +import { ModuleStructure, ModuleCandidate } from './Module.struct.js' +import { Type, Module } from 'helios-distribution-types' +import { mkdirs } from 'fs-extra/esm' +import { Stats } from 'fs' +import { resolve } from 'path' +import { MinecraftVersion } from '../../../util/MinecraftVersion.js' +import { UntrackedFilesOption } from '../../../model/nebula/ServerMeta.js' + +export enum ToggleableNamespace { + + REQUIRED = 'required', + OPTIONAL_ON = 'optionalon', + OPTIONAL_OFF = 'optionaloff' + +} + +export interface ToggleableModuleCandidate extends ModuleCandidate { + namespace: ToggleableNamespace +} + +export abstract class ToggleableModuleStructure extends ModuleStructure { + + private activeNamespace: string | undefined + + constructor( + absoluteRoot: string, + relativeRoot: string, + structRoot: string, + baseUrl: string, + minecraftVersion: MinecraftVersion, + type: Type, + untrackedFiles: UntrackedFilesOption[], + filter?: ((name: string, path: string, stats: Stats) => boolean) + ) { + super(absoluteRoot, relativeRoot, structRoot, baseUrl, minecraftVersion, type, untrackedFiles, filter) + } + + public async init(): Promise { + await super.init() + for(const namespace of Object.values(ToggleableNamespace)) { + await mkdirs(resolve(this.containerDirectory, namespace)) + } + } + + public async getSpecModel(): Promise { + if (this.resolvedModels == null) { + + const moduleCandidates: ToggleableModuleCandidate[] = [] + for(const value of Object.values(ToggleableNamespace)) { + moduleCandidates.push(...(await super._doModuleDiscovery(resolve(this.containerDirectory, value))).map(val => ({...val, namespace: value}))) + } + + this.resolvedModels = await this._doModuleRetrieval(moduleCandidates, { + preProcess: (candidate) => { + this.activeNamespace = (candidate as ToggleableModuleCandidate).namespace + }, + postProcess: (module) => { + this.getNamespaceMapper(this.activeNamespace as ToggleableNamespace)(module) + } + }) + + // Cleanup + this.activeNamespace = undefined + + } + + return this.resolvedModels + } + + protected getActiveNamespace(): string { + return this.activeNamespace || '' + } + + protected getNamespaceMapper(namespace: ToggleableNamespace): (x: Module) => void { + switch(namespace) { + case ToggleableNamespace.REQUIRED: + return (): void => { /* do nothing */ } + case ToggleableNamespace.OPTIONAL_ON: + return (x): void => { x.required = { value: false } } + case ToggleableNamespace.OPTIONAL_OFF: + return (x): void => { x.required = { value: false, def: false } } + } + } + +} \ No newline at end of file diff --git a/atlas/src/structure/spec_model/module/forgemod/ForgeMod113.struct.ts b/atlas/src/structure/spec_model/module/forgemod/ForgeMod113.struct.ts new file mode 100644 index 0000000..a74ac32 --- /dev/null +++ b/atlas/src/structure/spec_model/module/forgemod/ForgeMod113.struct.ts @@ -0,0 +1,153 @@ +import StreamZip from 'node-stream-zip' +import toml from 'toml' +import { capitalize } from '../../../../util/StringUtils.js' +import { VersionUtil } from '../../../../util/VersionUtil.js' +import { ModsToml } from '../../../../model/forge/ModsToml.js' +import { BaseForgeModStructure } from '../ForgeMod.struct.js' +import { MinecraftVersion } from '../../../../util/MinecraftVersion.js' +import { UntrackedFilesOption } from '../../../../model/nebula/ServerMeta.js' + +export class ForgeModStructure113 extends BaseForgeModStructure { + + public static readonly IMPLEMENTATION_VERSION_REGEX = /^Implementation-Version: (.+)[\r\n]/ + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { + return VersionUtil.isVersionAcceptable(version, [13, 14, 15, 16, 17, 18, 19, 20, 21]) + } + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string, + minecraftVersion: MinecraftVersion, + untrackedFiles: UntrackedFilesOption[] + ) { + super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, untrackedFiles) + } + + public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { + return ForgeModStructure113.isForVersion(version, libraryVersion) + } + + public getLoggerName(): string { + return 'ForgeModStructure (1.13)' + } + + protected async getModuleId(name: string, path: string): Promise { + const fmData = await this.getModMetadata(name, path) + return this.generateMavenIdentifier(this.getClaritasGroup(path), fmData.mods[0].modId, fmData.mods[0].version) + } + protected async getModuleName(name: string, path: string): Promise { + return capitalize((await this.getModMetadata(name, path)).mods[0].displayName) + } + + protected processZip(zip: StreamZip, name: string, path: string): ModsToml { + + // Optifine is a tweak that can be loaded as a forge mod. It does not + // appear to contain a mcmod.info class. This a special case we will + // account for. + if (name.toLowerCase().includes('optifine')) { + + // Read zip for changelog.txt + let changelogBuf: Buffer + try { + changelogBuf = zip.entryDataSync('changelog.txt') + } catch(err) { + throw new Error('Failed to read OptiFine changelog.') + } + + const info = changelogBuf.toString().split('\n')[0].trim() + const version = info.split(' ')[1] + + this.modMetadata[name] = ({ + modLoader: 'javafml', + loaderVersion: '', + mods: [{ + modId: 'optifine', + version, + displayName: 'OptiFine', + description: `OptiFine is a Minecraft optimization mod. + It allows Minecraft to run faster and look better with full support for shaders, HD textures and many configuration options.` + }] + }) + + return this.modMetadata[name] + } + + let raw: Buffer | undefined + try { + raw = zip.entryDataSync('META-INF/mods.toml') + } catch(err) { + // ignored + } + + if (raw) { + try { + const parsed = toml.parse(raw.toString()) as ModsToml + this.modMetadata[name] = parsed + } catch (err) { + this.logger.error(`ForgeMod ${name} contains an invalid mods.toml file.`) + } + } else { + this.logger.error(`ForgeMod ${name} does not contain mods.toml file.`) + } + + const cRes = this.claritasResult?.[path] + + if(cRes == null) { + this.logger.error(`Claritas failed to yield metadata for ForgeMod ${name}!`) + this.logger.error('Is this mod malformatted or does Claritas need an update?') + } + + const claritasId = cRes?.id + + const crudeInference = this.attemptCrudeInference(name) + + if(this.modMetadata[name] != null) { + + const x = this.modMetadata[name] + for(const entry of x.mods) { + + if(entry.modId === this.EXAMPLE_MOD_ID) { + entry.modId = this.discernResult(claritasId, crudeInference.name.toLowerCase()) + entry.displayName = crudeInference.name + } + + if (entry.version === '${file.jarVersion}') { + let version = crudeInference.version + try { + const manifest = zip.entryDataSync('META-INF/MANIFEST.MF') + const keys = manifest.toString().split('\n') + // this.logger.debug(keys) + for (const key of keys) { + const match = ForgeModStructure113.IMPLEMENTATION_VERSION_REGEX.exec(key) + if (match != null) { + version = match[1] + } + } + this.logger.debug(`ForgeMod ${name} contains a version wildcard, inferring ${version}`) + } catch { + this.logger.debug(`ForgeMod ${name} contains a version wildcard yet no MANIFEST.MF.. Defaulting to ${version}`) + } + entry.version = version + } + } + + } else { + this.modMetadata[name] = ({ + modLoader: 'javafml', + loaderVersion: '', + mods: [{ + modId: this.discernResult(claritasId, crudeInference.name.toLowerCase()), + version: crudeInference.version, + displayName: crudeInference.name, + description: '' + }] + }) + } + + return this.modMetadata[name] + } + +} diff --git a/atlas/src/structure/spec_model/module/forgemod/ForgeMod17.struct.ts b/atlas/src/structure/spec_model/module/forgemod/ForgeMod17.struct.ts new file mode 100644 index 0000000..485ee8c --- /dev/null +++ b/atlas/src/structure/spec_model/module/forgemod/ForgeMod17.struct.ts @@ -0,0 +1,154 @@ +import StreamZip from 'node-stream-zip' +import { capitalize } from '../../../../util/StringUtils.js' +import { VersionUtil } from '../../../../util/VersionUtil.js' +import { McModInfo } from '../../../../model/forge/McModInfo.js' +import { McModInfoList } from '../../../../model/forge/McModInfoList.js' +import { BaseForgeModStructure } from '../ForgeMod.struct.js' +import { MinecraftVersion } from '../../../../util/MinecraftVersion.js' +import { ForgeModType_1_7 } from '../../../../model/claritas/ClaritasResult.js' +import { UntrackedFilesOption } from '../../../../model/nebula/ServerMeta.js' + +export class ForgeModStructure17 extends BaseForgeModStructure { + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public static isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { + return VersionUtil.isVersionAcceptable(version, [7, 8, 9, 10, 11, 12]) + } + + constructor( + absoluteRoot: string, + relativeRoot: string, + baseUrl: string, + minecraftVersion: MinecraftVersion, + untrackedFiles: UntrackedFilesOption[] + ) { + super(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, untrackedFiles) + } + + public isForVersion(version: MinecraftVersion, libraryVersion: string): boolean { + return ForgeModStructure17.isForVersion(version, libraryVersion) + } + + public getLoggerName(): string { + return 'ForgeModStructure (1.7)' + } + + protected async getModuleId(name: string, path: string): Promise { + const fmData = await this.getModMetadata(name, path) + return this.generateMavenIdentifier(this.getClaritasGroup(path), fmData.modid, fmData.version) + } + protected async getModuleName(name: string, path: string): Promise { + return capitalize((await this.getModMetadata(name, path)).name) + } + + private isMalformedVersion(version: string): boolean { + // Ex. empty, @VERSION@, ${version} + return version.trim().length === 0 || version.includes('@') || version.includes('$') + } + + protected processZip(zip: StreamZip, name: string, path: string): McModInfo { + // Optifine is a tweak that can be loaded as a forge mod. It does not + // appear to contain a mcmod.info class. This a special case we will + // account for. + if (name.toLowerCase().includes('optifine')) { + + // Read zip for changelog.txt + let changelogBuf: Buffer + try { + changelogBuf = zip.entryDataSync('changelog.txt') + } catch(err) { + throw new Error('Failed to read OptiFine changelog.') + } + + const info = changelogBuf.toString().split('\n')[0].trim() + const version = info.split(' ')[1] + this.modMetadata[name] = ({ + modid: 'optifine', + name: info, + version, + mcversion: version.substring(0, version.indexOf('_')) + }) as McModInfo + return this.modMetadata[name] + } + + let raw: Buffer | undefined + try { + raw = zip.entryDataSync('mcmod.info') + } catch(err) { + // ignored + } + + if (raw) { + // Assuming the main mod will be the first entry in this file. + try { + const resolved = JSON.parse(raw.toString()) as (McModInfoList | McModInfo[]) + + if (Object.prototype.hasOwnProperty.call(resolved, 'modListVersion')) { + this.modMetadata[name] = (resolved as McModInfoList).modList[0] + } else { + this.modMetadata[name] = (resolved as McModInfo[])[0] + } + + } catch (err) { + this.logger.error(`ForgeMod ${name} contains an invalid mcmod.info file.`) + } + } else { + this.logger.warn(`ForgeMod ${name} does not contain mcmod.info file.`) + } + + const cRes = this.claritasResult[path] + + if(cRes == null) { + this.logger.error(`Claritas failed to yield metadata for ForgeMod ${name}!`) + this.logger.error('Is this mod malformated or does Claritas need an update?') + } else { + switch(cRes.modType!) { + case ForgeModType_1_7.CORE_MOD: + this.logger.info(`CORE_MOD Discovered: ForgeMod ${name} has no @Mod annotation. Metadata inference capabilities are limited.`) + break + case ForgeModType_1_7.TWEAKER: + this.logger.info(`TWEAKER Discovered: ForgeMod ${name} has no @Mod annotation. Metadata inference capabilities may be limited.`) + break + case ForgeModType_1_7.UNKNOWN: + this.logger.error(`Jar file ${name} is not a ForgeMod. Is it a library?`) + break + } + } + + const claritasId = cRes?.id + const claritasVersion = cRes?.version + const claritasName = cRes?.name + + + // Validate + const crudeInference = this.attemptCrudeInference(name) + if(this.modMetadata[name] != null) { + + const x = this.modMetadata[name] + if(x.modid == null || x.modid === '' || x.modid === this.EXAMPLE_MOD_ID) { + x.modid = this.discernResult(claritasId, crudeInference.name.toLowerCase()) + x.name = this.discernResult(claritasName, crudeInference.name) + } + + if(this.modMetadata[name].version != null) { + const isMalformedVersion = this.isMalformedVersion(this.modMetadata[name].version) + if(isMalformedVersion) { + x.version = this.discernResult(claritasVersion, crudeInference.version) + } + } else { + x.version = this.discernResult(claritasVersion, crudeInference.version) + } + + + } else { + this.modMetadata[name] = ({ + modid: this.discernResult(claritasId, crudeInference.name.toLowerCase()), + name: this.discernResult(claritasName, crudeInference.name), + version: this.discernResult(claritasVersion, crudeInference.version) + }) as McModInfo + } + + return this.modMetadata[name] + } + +} diff --git a/atlas/src/util/LoggerUtil.ts b/atlas/src/util/LoggerUtil.ts new file mode 100644 index 0000000..3eac9ce --- /dev/null +++ b/atlas/src/util/LoggerUtil.ts @@ -0,0 +1,48 @@ +import { createLogger, format, transports, Logger } from 'winston' +import { SPLAT } from 'triple-beam' +import { DateTime } from 'luxon' +import { inspect } from 'util' + +export class LoggerUtil { + + public static getLogger(label: string): Logger { + return createLogger({ + format: format.combine( + format.label(), + format.colorize(), + format.label({ label }), + format.printf(infoUnknown => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const info = infoUnknown as {[key: string | symbol]: any} + if(info[SPLAT]) { + if(info[SPLAT].length === 1 && info[SPLAT][0] instanceof Error) { + const err: Error = info[SPLAT][0] + if(info.message.length > err.message.length && info.message.endsWith(err.message)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + info.message = info.message.substring(0, info.message.length-err.message.length) + } + } else if(info[SPLAT].length > 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + info.message += ' ' + info[SPLAT].map((it: any) => { + if(typeof it === 'object' && it != null) { + return inspect(it, false, 4, true) + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return it + }).join(' ') + } + } + if(typeof info.message === 'object') { + info.message = inspect(info.message, false, 4, true) + } + return `[${DateTime.local().toFormat('yyyy-MM-dd TT').trim()}] [${info.level}] [${info.label}]: ${info.message}${info.stack ? `\n${info.stack}` : ''}` + }) + ), + level: 'debug', + transports: [ + new transports.Console() + ] + }) + } + +} \ No newline at end of file diff --git a/atlas/src/util/MavenUtil.ts b/atlas/src/util/MavenUtil.ts new file mode 100644 index 0000000..c116447 --- /dev/null +++ b/atlas/src/util/MavenUtil.ts @@ -0,0 +1,100 @@ +import { normalize } from 'path' +import { URL } from 'url' + +export interface MavenComponents { + group: string + artifact: string + version: string + classifier?: string + extension: string +} + +export class MavenUtil { + + public static readonly ID_REGEX = /([^@:]+):([^@:]+):?([^@:]+)?:?(?:([^@:]+))?:?(?:@{1}([^@:]+))?/ + + public static mavenComponentsToIdentifier( + group: string, + artifact: string, + version: string, + classifier?: string, + extension?: string + ): string { + return `${group}:${artifact}:${version}${classifier != null ? `:${classifier}` : ''}${extension != null ? `@${extension}` : ''}` + } + + public static mavenComponentsToExtensionlessIdentifier( + group: string, + artifact: string, + version: string, + classifier?: string + ): string { + return MavenUtil.mavenComponentsToIdentifier(group, artifact, version, classifier) + } + + public static mavenComponentsToVersionlessIdentifier( + group: string, + artifact: string + ): string { + return `${group}:${artifact}` + } + + public static isMavenIdentifier(id: string): boolean { + return MavenUtil.ID_REGEX.test(id) + } + + public static getMavenComponents(id: string, extension = 'jar'): MavenComponents { + if (!MavenUtil.isMavenIdentifier(id)) { + throw new Error('Id is not a maven identifier.') + } + + const result = MavenUtil.ID_REGEX.exec(id) + + if (result != null) { + return { + group: result[1], + artifact: result[2], + version: result[3], + classifier: result[4], + extension: result[5] || extension + } + } + + throw new Error('Failed to process maven data.') + } + + public static mavenIdentifierAsPath(id: string, extension = 'jar'): string { + const tmp = MavenUtil.getMavenComponents(id, extension) + + return MavenUtil.mavenComponentsAsPath( + tmp.group, tmp.artifact, tmp.version, tmp.classifier, tmp.extension + ) + } + + public static mavenComponentsAsPath( + group: string, artifact: string, version: string, classifier?: string, extension = 'jar' + ): string { + return `${group.replace(/\./g, '/')}/${artifact}/${version}/${artifact}-${version}${classifier != null ? `-${classifier}` : ''}.${extension}` + } + + public static mavenIdentifierToUrl(id: string, extension = 'jar'): URL { + return new URL(MavenUtil.mavenIdentifierAsPath(id, extension)) + } + + public static mavenComponentsToUrl( + group: string, artifact: string, version: string, classifier?: string, extension = 'jar' + ): URL { + return new URL(MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension)) + } + + public static mavenIdentifierToPath(id: string, extension = 'jar'): string { + return normalize(MavenUtil.mavenIdentifierAsPath(id, extension)) + } + + public static mavenComponentsAsNormalizedPath( + group: string, artifact: string, version: string, classifier?: string, extension = 'jar' + ): string { + return normalize(MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension)) + } + +} \ No newline at end of file diff --git a/atlas/src/util/MetadataUtil.ts b/atlas/src/util/MetadataUtil.ts new file mode 100644 index 0000000..a94af45 --- /dev/null +++ b/atlas/src/util/MetadataUtil.ts @@ -0,0 +1,21 @@ +export class MetadataUtil { + + public static completeGroupInference(partial: string, id: string): string { + + const bits = partial.split('.') + + let isBadTerm = true + while(isBadTerm && bits.length > 2) { + const term = bits[bits.length-1] + if(term !== id) { + isBadTerm = false + } else { + bits.pop() + } + } + + return bits.join('.') + + } + +} \ No newline at end of file diff --git a/atlas/src/util/MinecraftVersion.ts b/atlas/src/util/MinecraftVersion.ts new file mode 100644 index 0000000..c5b23d7 --- /dev/null +++ b/atlas/src/util/MinecraftVersion.ts @@ -0,0 +1,51 @@ +export class MinecraftVersion { + + private static readonly MINECRAFT_VERSION_REGEX = /(\d+).(\d+).?(\d+)?/ + + private readonly major: number + private readonly minor: number + private readonly revision: number | undefined + + constructor(version: string) { + const res = MinecraftVersion.MINECRAFT_VERSION_REGEX.exec(version) + if(res != null) { + this.major = Number(res[1]) + this.minor = Number(res[2]) + this.revision = res[3] != null ? Number(res[3]) : undefined + } else { + throw new Error(`${version} is not a valid minecraft version!`) + } + } + + public static isMinecraftVersion(version: string): boolean { + return MinecraftVersion.MINECRAFT_VERSION_REGEX.test(version) + } + + public getMajor(): number { return this.major } + public getMinor(): number { return this.minor } + public getRevision(): number | undefined { return this.revision } + + public toString(): string { return `${this.major}.${this.minor}${this.revision != null ? '.' + this.revision : ''}`} + + public compareTo(other: MinecraftVersion): number { + // Compare major + if (this.major !== other.major) { + return this.major - other.major + } + + // Compare minor + if (this.minor !== other.minor) { + return this.minor - other.minor + } + + // Compare revision (null as 0) + const thisRevision = this.revision ?? 0 + const otherRevision = other.revision ?? 0 + return thisRevision - otherRevision + } + + public isGreaterThanOrEqualTo(other: MinecraftVersion): boolean { + return this.compareTo(other) >= 0 + } + +} \ No newline at end of file diff --git a/atlas/src/util/SchemaUtil.ts b/atlas/src/util/SchemaUtil.ts new file mode 100644 index 0000000..a1639eb --- /dev/null +++ b/atlas/src/util/SchemaUtil.ts @@ -0,0 +1,77 @@ +import { mkdirs, pathExists, remove } from 'fs-extra/esm' +import { writeFile } from 'fs/promises' +import { join, resolve } from 'path' +import { createGenerator } from 'ts-json-schema-generator' +import { URL, fileURLToPath } from 'url' +import { DistroMeta } from '../model/nebula/DistroMeta.js' +import { ServerMeta } from '../model/nebula/ServerMeta.js' +import { LoggerUtil } from './LoggerUtil.js' + +const logger = LoggerUtil.getLogger('SchemaUtil') + +const __filename = fileURLToPath(import.meta.url) +const __dirname = fileURLToPath(new URL('.', import.meta.url)) + +interface SchemaType { + /** + * URL to the JSON schema for this type of file. + * This is used by editors to validate and annotate the data. + */ + $schema?: string +} + +export type DistroMetaSchema = DistroMeta & SchemaType +export type ServerMetaSchema = ServerMeta & SchemaType + +export enum SchemaTypes { + DistroMetaSchema = 'DistroMetaSchema', + ServerMetaSchema = 'ServerMetaSchema' +} + +function getSchemaFileName(typeName: string): string { + return `${typeName}.schema.json` +} + +function getSchemaDirectory(absoluteRoot: string): string { + return resolve(absoluteRoot, 'schemas') +} + +function getSchemaLocation(typeName: string, absoluteRoot: string): string { + return resolve(getSchemaDirectory(absoluteRoot), getSchemaFileName(typeName)) +} + +export function addSchemaToObject(obj: T, typeName: string, absoluteRoot: string): T { + return { + $schema: new URL(`file:${getSchemaLocation(typeName, absoluteRoot)}`).href, + ...obj + } +} + +export async function generateSchemas(absoluteRoot: string): Promise { + + const selfPath = __filename.replace('dist', 'src').replace('.js', '.ts') + + const schemaDir = getSchemaDirectory(absoluteRoot) + if(await pathExists(schemaDir)) { + await remove(schemaDir) + } + await mkdirs(schemaDir) + + for(const typeName of Object.values(SchemaTypes)) { + + logger.info(`Generating schema for ${typeName}`) + + const schema = createGenerator({ + tsconfig: join(__dirname, '..', '..', 'tsconfig.json'), + path: selfPath, + type: typeName + }).createSchema(typeName) + + const schemaString = JSON.stringify(schema) + const schemaLoc = getSchemaLocation(typeName, absoluteRoot) + await writeFile(schemaLoc, schemaString) + + logger.info(`Schema for ${typeName} saved to ${schemaLoc}`) + } + +} \ No newline at end of file diff --git a/atlas/src/util/StringUtils.ts b/atlas/src/util/StringUtils.ts new file mode 100644 index 0000000..0536c03 --- /dev/null +++ b/atlas/src/util/StringUtils.ts @@ -0,0 +1,15 @@ +export function capitalize(str: string): string { + if (!str) { + return str + } + return str.charAt(0).toUpperCase() + str.slice(1) +} + +export function isValidUrl(candidate: string): boolean { + try { + new URL(candidate) + return true + } catch (err) { + return false + } +} diff --git a/atlas/src/util/VersionSegmented.ts b/atlas/src/util/VersionSegmented.ts new file mode 100644 index 0000000..a785311 --- /dev/null +++ b/atlas/src/util/VersionSegmented.ts @@ -0,0 +1,7 @@ +import { MinecraftVersion } from './MinecraftVersion.js' + +export interface VersionSegmented { + + isForVersion(version: MinecraftVersion, libraryVersion: string): boolean + +} diff --git a/atlas/src/util/VersionSegmentedRegistry.ts b/atlas/src/util/VersionSegmentedRegistry.ts new file mode 100644 index 0000000..3415306 --- /dev/null +++ b/atlas/src/util/VersionSegmentedRegistry.ts @@ -0,0 +1,55 @@ +import { ForgeModStructure113 } from '../structure/spec_model/module/forgemod/ForgeMod113.struct.js' +import { ForgeModStructure17 } from '../structure/spec_model/module/forgemod/ForgeMod17.struct.js' +import { ForgeGradle3Adapter } from '../resolver/forge/adapter/ForgeGradle3.resolver.js' +import { ForgeGradle2Adapter } from '../resolver/forge/adapter/ForgeGradle2.resolver.js' +import { ForgeResolver } from '../resolver/forge/Forge.resolver.js' +import { BaseForgeModStructure } from '../structure/spec_model/module/ForgeMod.struct.js' +import { MinecraftVersion } from './MinecraftVersion.js' +import { UntrackedFilesOption } from '../model/nebula/ServerMeta.js' + +export class VersionSegmentedRegistry { + + public static readonly FORGE_ADAPTER_IMPL = [ + ForgeGradle2Adapter, + ForgeGradle3Adapter + ] + + public static readonly FORGEMOD_STRUCT_IMPL = [ + ForgeModStructure17, + ForgeModStructure113 + ] + + public static getForgeResolver( + minecraftVersion: MinecraftVersion, + forgeVersion: string, + absoluteRoot: string, + relativeRoot: string, + baseURL: string, + discardOutput: boolean, + invalidateCache: boolean + ): ForgeResolver { + for (const impl of VersionSegmentedRegistry.FORGE_ADAPTER_IMPL) { + if (impl.isForVersion(minecraftVersion, forgeVersion)) { + return new impl(absoluteRoot, relativeRoot, baseURL, minecraftVersion, forgeVersion, discardOutput, invalidateCache) + } + } + throw new Error(`No forge resolver found for Minecraft ${minecraftVersion}!`) + } + + public static getForgeModStruct( + minecraftVersion: MinecraftVersion, + forgeVersion: string, + absoluteRoot: string, + relativeRoot: string, + baseUrl: string, + untrackedFiles: UntrackedFilesOption[] + ): BaseForgeModStructure { + for (const impl of VersionSegmentedRegistry.FORGEMOD_STRUCT_IMPL) { + if (impl.isForVersion(minecraftVersion, forgeVersion)) { + return new impl(absoluteRoot, relativeRoot, baseUrl, minecraftVersion, untrackedFiles) + } + } + throw new Error(`No forge mod structure found for Minecraft ${minecraftVersion}!`) + } + +} diff --git a/atlas/src/util/VersionUtil.ts b/atlas/src/util/VersionUtil.ts new file mode 100644 index 0000000..c22ec03 --- /dev/null +++ b/atlas/src/util/VersionUtil.ts @@ -0,0 +1,139 @@ +import got from 'got' +import { PromotionsSlim } from '../model/forge/PromotionsSlim.js' +import { MinecraftVersion } from './MinecraftVersion.js' +import { LoggerUtil } from './LoggerUtil.js' +import { FabricInstallerMeta, FabricLoaderMeta, FabricProfileJson, FabricVersionMeta } from '../model/fabric/FabricMeta.js' + +export class VersionUtil { + + private static readonly logger = LoggerUtil.getLogger('VersionUtil') + + public static readonly PROMOTION_TYPE = [ + 'recommended', + 'latest' + ] + + public static isVersionAcceptable(version: MinecraftVersion, acceptable: number[]): boolean { + if (version.getMajor() === 1) { + return acceptable.find((element) => version.getMinor() === element) != null + } + return false + } + + public static versionGte(version: string, min: string): boolean { + + if(version === min) { + return true + } + + const left = version.split('.').map(x => Number(x)) + const right = min.split('.').map(x => Number(x)) + + if(left.length != right.length) { + throw new Error('Cannot compare mismatched versions.') + } + + for(let i=0; i right[i]) { + return true + } + } + + return false + } + + public static isPromotionVersion(version: string): boolean { + return VersionUtil.PROMOTION_TYPE.includes(version.toLowerCase()) + } + + // ------------------------------- + // Forge + + public static isOneDotTwelveFG2(libraryVersion: string): boolean { + const maxFG2 = [14, 23, 5, 2847] + const verSplit = libraryVersion.split('.').map(v => Number(v)) + + for(let i=0; i maxFG2[i]) { + return false + } + } + + return true + } + + public static async getPromotionIndex(): Promise { + const response = await got.get({ + method: 'get', + url: 'https://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json', + responseType: 'json' + }) + return response.body + } + + public static getPromotedVersionStrict(index: PromotionsSlim, minecraftVersion: MinecraftVersion, promotion: string): string { + const workingPromotion = promotion.toLowerCase() + return index.promos[`${minecraftVersion}-${workingPromotion}`] + } + + public static async getPromotedForgeVersion(minecraftVersion: MinecraftVersion, promotion: string): Promise { + const workingPromotion = promotion.toLowerCase() + const res = await VersionUtil.getPromotionIndex() + let version = res.promos[`${minecraftVersion}-${workingPromotion}`] + if (version == null) { + VersionUtil.logger.warn(`No ${workingPromotion} version found for Forge ${minecraftVersion}.`) + VersionUtil.logger.warn('Attempting to pull latest version instead.') + version = res.promos[`${minecraftVersion}-latest`] + if (version == null) { + throw new Error(`No latest version found for Forge ${minecraftVersion}.`) + } + } + return version + } + + // ------------------------------- + // Fabric + + public static async getFabricInstallerMeta(): Promise { + const response = await got.get({ + method: 'get', + url: 'https://meta.fabricmc.net/v2/versions/installer', + responseType: 'json' + }) + return response.body + } + + public static async getFabricLoaderMeta(): Promise { + const response = await got.get({ + method: 'get', + url: 'https://meta.fabricmc.net/v2/versions/loader', + responseType: 'json' + }) + return response.body + } + + public static async getFabricGameMeta(): Promise { + const response = await got.get({ + method: 'get', + url: 'https://meta.fabricmc.net/v2/versions/game', + responseType: 'json' + }) + return response.body + } + + public static async getFabricProfileJson(gameVersion: string, loaderVersion: string): Promise { + const response = await got.get({ + method: 'get', + url: `https://meta.fabricmc.net/v2/versions/loader/${gameVersion}/${loaderVersion}/profile/json`, + responseType: 'json' + }) + return response.body + } + + public static async getPromotedFabricVersion(promotion: string): Promise { + const stable = promotion.toLowerCase() === 'recommended' + const fabricLoaderMeta = await this.getFabricLoaderMeta() + return !stable ? fabricLoaderMeta[0].version : fabricLoaderMeta.find(({ stable }) => stable)!.version + } + +} diff --git a/atlas/src/util/java/ClaritasWrapper.ts b/atlas/src/util/java/ClaritasWrapper.ts new file mode 100644 index 0000000..a5607e6 --- /dev/null +++ b/atlas/src/util/java/ClaritasWrapper.ts @@ -0,0 +1,70 @@ +import { JarExecutor } from './JarExecutor.js' +import { join, resolve } from 'path' +import { ClaritasResult } from '../../model/claritas/ClaritasResult.js' +import { MinecraftVersion } from '../MinecraftVersion.js' +import { LibraryType } from '../../model/claritas/ClaritasLibraryType.js' +import { pathExists, remove, mkdirs } from 'fs-extra/esm' +import { readFile, writeFile } from 'fs/promises' + +export class ClaritasWrapper extends JarExecutor { + + private readonly WORK_DIR: string + private readonly ARG_FILE: string + private readonly OUTPUT_FILE: string + + constructor(cwd: string) { + super('Claritas') + + this.WORK_DIR = resolve(cwd, 'claritasWork') + this.ARG_FILE = resolve(this.WORK_DIR, 'claritasArgFile.txt') + this.OUTPUT_FILE = resolve(this.WORK_DIR, 'claritasOutput.json') + + this.onCloseListeners.push(async (code) => { + if(code !== 0) { + this.logger.error('Claritas finished with non-zero exit code, ', code) + this.lastExecutionResult = undefined! + } else { + if(await pathExists(this.OUTPUT_FILE)) { + this.lastExecutionResult = JSON.parse((await readFile(this.OUTPUT_FILE)).toString('utf8')) as ClaritasResult + } else { + this.logger.error('Claritas output file not found when exit code is 0, is this a bug?') + this.lastExecutionResult = undefined! + } + } + await this.cleanOutput() + }) + + } + + protected getJarPath(): string { + return join(process.cwd(), 'libraries', 'java', 'Claritas.jar') + } + + private async writeArgFile(...programArgs: string[]): Promise { + await mkdirs(this.WORK_DIR) + await writeFile( + this.ARG_FILE, + programArgs.join('\n') + ) + } + + public async execute(libraryType: LibraryType, mcVersion: MinecraftVersion, absoluteJarPaths: string[]): Promise { + await this.writeArgFile( + '--absoluteJarPaths', absoluteJarPaths.join(','), + '--libraryType', libraryType, + '--mcVersion', mcVersion.toString(), + '--outputFile', this.OUTPUT_FILE, + '--previewOutput', 'true' + ) + return await super.executeJar( + [`-Dclaritas.argFile=${this.ARG_FILE}`] + ) + } + + private async cleanOutput(): Promise { + if(await pathExists(this.WORK_DIR)) { + await remove(this.WORK_DIR) + } + } + +} \ No newline at end of file diff --git a/atlas/src/util/java/JarExecutor.ts b/atlas/src/util/java/JarExecutor.ts new file mode 100644 index 0000000..1f913ed --- /dev/null +++ b/atlas/src/util/java/JarExecutor.ts @@ -0,0 +1,55 @@ +import { JavaUtil } from './JavaUtil.js' +import { Logger } from 'winston' +import { spawn } from 'child_process' +import { LoggerUtil } from '../LoggerUtil.js' + +export abstract class JarExecutor { + + protected readonly logger: Logger + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected stdoutListeners: ((chunk: any) => void)[] = [] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected stderrListeners: ((chunk: any) => void)[] = [] + protected onCloseListeners: ((code: number | null) => Promise)[] = [] + + protected lastExecutionResult!: T + + protected constructor(loggerName: string) { + this.logger = LoggerUtil.getLogger(loggerName) + } + + protected abstract getJarPath(): string + + protected executeJar(vmOptions: string[], ...args: string[]): Promise { + this.lastExecutionResult = undefined! + return new Promise((resolve, reject) => { + const child = spawn(JavaUtil.getJavaExecutable(), [ + ...vmOptions, + '-jar', + this.getJarPath(), + ...args + ]) + child.stdout.on('data', (data) => this.logger.info(data.toString('utf8').trim())) + this.stdoutListeners.forEach(l => child.stdout.on('data', l)) + + child.stderr.on('data', (data) => this.logger.error(data.toString('utf8').trim())) + this.stderrListeners.forEach(l => child.stderr.on('data', l)) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + child.on('close', async code => { + this.logger.info('Exited with code', code) + for(const l of this.onCloseListeners) { + await l(code) + } + resolve(this.lastExecutionResult) + }) + + child.on('error', (err) => { + this.logger.info('Error during process execution', err) + reject(err) + }) + }) + } + +} \ No newline at end of file diff --git a/atlas/src/util/java/JavaUtil.ts b/atlas/src/util/java/JavaUtil.ts new file mode 100644 index 0000000..20e6f3c --- /dev/null +++ b/atlas/src/util/java/JavaUtil.ts @@ -0,0 +1,7 @@ +export class JavaUtil { + + public static getJavaExecutable(): string { + return process.env.JAVA_EXECUTABLE! + } + +} diff --git a/atlas/src/util/java/PackXZExtractWrapper.ts b/atlas/src/util/java/PackXZExtractWrapper.ts new file mode 100644 index 0000000..afb4ba3 --- /dev/null +++ b/atlas/src/util/java/PackXZExtractWrapper.ts @@ -0,0 +1,30 @@ +import { join } from 'path' +import { JarExecutor } from './JarExecutor.js' + +export class PackXZExtractWrapper extends JarExecutor { + + constructor() { + super('PackXZExtract') + } + + protected getJarPath(): string { + return join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar') + } + + protected execute(command: string, paths: string[]): Promise { + return super.executeJar([], command, paths.join(',')) + } + + public extractUnpack(paths: string[]): Promise { + return this.execute('-packxz', paths) + } + + public extract(paths: string[]): Promise { + return this.execute('-xz', paths) + } + + public unpack(paths: string[]): Promise { + return this.execute('-pack', paths) + } + +} diff --git a/atlas/tsconfig.json b/atlas/tsconfig.json new file mode 100644 index 0000000..a89c181 --- /dev/null +++ b/atlas/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2024", + "module": "Node16", + "lib": ["ES2024", "DOM"], + "sourceMap": true, + "outDir": "dist", + "strict": true, + "moduleResolution": "node16", + "esModuleInterop": true + } +}