start over
This commit is contained in:
Generated
+60
-25
@@ -9,9 +9,10 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"license": "LGPL-3.0-or-later",
|
"license": "LGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth-ts/lib": "^2.1.0",
|
|
||||||
"@sugoidogo/js-util": "^0.3.1",
|
"@sugoidogo/js-util": "^0.3.1",
|
||||||
"@sugoidogo/node-file-system-adapter": "^1.0.3",
|
"@sugoidogo/node-file-system-adapter": "^1.0.3",
|
||||||
|
"@xmcl/curseforge": "^2.2.0",
|
||||||
|
"@xmcl/modrinth": "^2.5.0",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
"confbox": "^0.2.4",
|
"confbox": "^0.2.4",
|
||||||
"curseforge-v2": "^1.5.0",
|
"curseforge-v2": "^1.5.0",
|
||||||
@@ -22,9 +23,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sugoidogo/importable-types-web": "^1.0.0",
|
"@sugoidogo/importable-types-web": "^1.0.0",
|
||||||
|
"@sugoidogo/node-web-globals": "^2.0.0",
|
||||||
"@types/md5": "^2.3.6",
|
"@types/md5": "^2.3.6",
|
||||||
"@types/node": "^25.2.2",
|
"@types/node": "^25.2.2",
|
||||||
"@types/web": "^0.0.329",
|
|
||||||
"deno": "^2.6.8",
|
"deno": "^2.6.8",
|
||||||
"typedoc": "^0.28.16",
|
"typedoc": "^0.28.16",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
@@ -1224,18 +1225,6 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modrinth-ts/lib": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@modrinth-ts/lib/-/lib-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-W+HGQgQSmK/bmQO2hjEQROwMRZUSH+uigke4Zat1DzeBjsKGIotEy2inuIXX+b0uh6SqRwb4grpbJKk/VPMT8Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "^1.13.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": "^5.9.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@poppinss/colors": {
|
"node_modules/@poppinss/colors": {
|
||||||
"version": "4.1.6",
|
"version": "4.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
|
||||||
@@ -1372,6 +1361,16 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@sugoidogo/node-web-globals": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://gitea.sugoidogo.com/api/packages/sugoidogo/npm/%40sugoidogo%2Fnode-web-globals/-/2.0.0/node-web-globals-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-bPFW3kBVj5pmshivjLQE/ZWKs4+6hx2rQc7YSgzK9RX48pjAeJ9/9p5/45M3Wg4KIxNi+8Jz7WpIntcDYovuHA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "^7.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/hast": {
|
"node_modules/@types/hast": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||||
@@ -1413,12 +1412,47 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/web": {
|
"node_modules/@xmcl/curseforge": {
|
||||||
"version": "0.0.329",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.329.tgz",
|
"resolved": "https://registry.npmjs.org/@xmcl/curseforge/-/curseforge-2.2.0.tgz",
|
||||||
"integrity": "sha512-bN6arw3cJ1P607brIP16VVP7TA+jrMA5RdV1bEbi9UshLSzLQ8EVM4AsZo5/hhGRbB3n9eExbCem+GUohXv+Ng==",
|
"integrity": "sha512-W1b92Z1IYItIyplq2poWP1NL47buE4LOHWEU+duHCj0xWHI5y4ya2pIyXQhNzsBCSJ+ZZBXzPx/27hBl2PUwcA==",
|
||||||
"dev": true,
|
"license": "MIT",
|
||||||
"license": "Apache-2.0"
|
"dependencies": {
|
||||||
|
"undici": "7.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xmcl/curseforge/node_modules/undici": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-WZkQ6eH9f5ZT93gaIffsbUaDpBwjbpvmMbfaEhOnbdUneurTESeRxwPGwjI28mRFESH3W3e8Togijh37ptOQqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xmcl/modrinth": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@xmcl/modrinth/-/modrinth-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-OpPpBemZ/yYvphgLVXGV+jESR9n8OceEsMaMvxuFZrIpi9LdYTa2oyVlnZHD7Dte+S+ttD8Y2AUncSJUNFRgNg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici": "7.2.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xmcl/modrinth/node_modules/undici": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-2oSLHaDalSt2/O/wHA9M+/ZPAOcU2yrSP/cdBYJ+YxZskiPYDSqHbysLSlD7gq3JMqOoJI5O31RVU3BxX/MnAA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.18.1"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
@@ -1434,13 +1468,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.13.2",
|
"version": "1.13.5",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.15.6",
|
"follow-redirects": "^1.15.11",
|
||||||
"form-data": "^4.0.4",
|
"form-data": "^4.0.5",
|
||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2150,6 +2184,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+3
-2
@@ -29,9 +29,10 @@
|
|||||||
"docs": "typedoc src/lib/packwiz.ts --tsconfig src/lib/tsconfig.json"
|
"docs": "typedoc src/lib/packwiz.ts --tsconfig src/lib/tsconfig.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modrinth-ts/lib": "^2.1.0",
|
|
||||||
"@sugoidogo/js-util": "^0.3.1",
|
"@sugoidogo/js-util": "^0.3.1",
|
||||||
"@sugoidogo/node-file-system-adapter": "^1.0.3",
|
"@sugoidogo/node-file-system-adapter": "^1.0.3",
|
||||||
|
"@xmcl/curseforge": "^2.2.0",
|
||||||
|
"@xmcl/modrinth": "^2.5.0",
|
||||||
"commander": "^14.0.3",
|
"commander": "^14.0.3",
|
||||||
"confbox": "^0.2.4",
|
"confbox": "^0.2.4",
|
||||||
"curseforge-v2": "^1.5.0",
|
"curseforge-v2": "^1.5.0",
|
||||||
@@ -42,9 +43,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sugoidogo/importable-types-web": "^1.0.0",
|
"@sugoidogo/importable-types-web": "^1.0.0",
|
||||||
|
"@sugoidogo/node-web-globals": "^2.0.0",
|
||||||
"@types/md5": "^2.3.6",
|
"@types/md5": "^2.3.6",
|
||||||
"@types/node": "^25.2.2",
|
"@types/node": "^25.2.2",
|
||||||
"@types/web": "^0.0.329",
|
|
||||||
"deno": "^2.6.8",
|
"deno": "^2.6.8",
|
||||||
"typedoc": "^0.28.16",
|
"typedoc": "^0.28.16",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
|
|||||||
+31
-816
@@ -1,365 +1,8 @@
|
|||||||
/**
|
import type { FileSystemDirectoryHandle, FileSystemGetDirectoryOptions, FileSystemGetFileOptions } from "@sugoidogo/importable-types-web"
|
||||||
* The hash algorithm used to determine if a file is valid.
|
import type { PackFileMetaData, PackIndex, PackMetaData, Path, Url } from "./types"
|
||||||
* All functions listed must be supported by tools implementing the packwiz pack format.
|
import { parseTOML } from "confbox"
|
||||||
*/
|
|
||||||
export type HashFormat = "sha256" | "sha512" | "sha1" | "md5" | "murmur2"
|
|
||||||
/**
|
|
||||||
* A relative path using forward slashes.
|
|
||||||
* Must not reference a file outside the pack root,
|
|
||||||
* and should not include characters or filenames restricted on common operating systems.
|
|
||||||
*
|
|
||||||
* Implementations must support special characters including those that are percent-encoded in URLs,
|
|
||||||
* such as spaces and square brackets.
|
|
||||||
* Implementations must guard against path traversal attacks and manually validate paths.
|
|
||||||
*/
|
|
||||||
export type Path = string
|
|
||||||
/**
|
|
||||||
* Binary hashes should be stored as hexadecimal,
|
|
||||||
* and case should be ignored during parsing.
|
|
||||||
* Numeric hashes (e.g. Murmur2) should still be stored as a string,
|
|
||||||
* to ensure the value is preserved correctly.
|
|
||||||
*/
|
|
||||||
export type Hash = string
|
|
||||||
/**
|
|
||||||
* A physical Minecraft side.
|
|
||||||
* Server applies to the dedicated server,
|
|
||||||
* client applies to the client (and integrated server),
|
|
||||||
* and both applies to every installation.
|
|
||||||
*/
|
|
||||||
export type Side = "both" | "client" | "server"
|
|
||||||
/**
|
|
||||||
* An absolute URI compliant with RFC 3986.
|
|
||||||
* Implementations may need to be more lenient in accepting reserved characters in paths
|
|
||||||
* due to historical implementation bugs.
|
|
||||||
* Only the HTTP/HTTPS protocols must be supported,
|
|
||||||
* other protocols should not be used.
|
|
||||||
*/
|
|
||||||
export type Url = string
|
|
||||||
/**
|
|
||||||
* The main modpack file for a packwiz modpack.
|
|
||||||
* This is the first file loaded,
|
|
||||||
* to allow the modpack downloader to download all the files in the modpack.
|
|
||||||
*/
|
|
||||||
export interface PackMetaData {
|
|
||||||
/**
|
|
||||||
* The name of the modpack.
|
|
||||||
* This can be displayed in user interfaces to identify the pack,
|
|
||||||
* and it does not need to be unique between packs.
|
|
||||||
*/
|
|
||||||
"name": string
|
|
||||||
/**
|
|
||||||
* The author(s) of the modpack.
|
|
||||||
* This is output when exporting to the CurseForge pack format,
|
|
||||||
* and can be displayed in user interfaces.
|
|
||||||
*/
|
|
||||||
"author"?: string
|
|
||||||
/**
|
|
||||||
* The version of the modpack.
|
|
||||||
* This is output when exporting to the CurseForge pack format,
|
|
||||||
* but is not currently used elsewhere by the tools or installer.
|
|
||||||
* It must not be used for determining if the modpack is outdated.
|
|
||||||
*/
|
|
||||||
"version"?: string
|
|
||||||
/**
|
|
||||||
* A short description of the modpack.
|
|
||||||
* This is output when exporting to the Modrinth pack format,
|
|
||||||
* but is not currently used elsewhere by the tools or installer.
|
|
||||||
*/
|
|
||||||
"description"?: string
|
|
||||||
/**
|
|
||||||
* A version string identifying the pack format and version of it.
|
|
||||||
* Currently, this pack format uses version 1.1.0.
|
|
||||||
*
|
|
||||||
* If it is not defined, default to "packwiz:1.0.0"
|
|
||||||
* for backwards-compatibility with packs created before this field was added.
|
|
||||||
*
|
|
||||||
* If it is defined:
|
|
||||||
*
|
|
||||||
* - All consumers should fail to load the modpack if it does not begin with "packwiz:"
|
|
||||||
*
|
|
||||||
* - All consumers should fail to load the modpack if the latter section is not valid semver
|
|
||||||
* as defined in https://semver.org/spec/v2.0.0.html
|
|
||||||
*
|
|
||||||
* - All consumers should fail to load the modpack if the major version is greater than the version they support
|
|
||||||
*
|
|
||||||
* - Consumers can suggest updating themselves if the minor version is greater than the version they implement
|
|
||||||
*
|
|
||||||
* - Pack tools should suggest and support migration when they support a version newer than this field
|
|
||||||
*/
|
|
||||||
"pack-format": string
|
|
||||||
/** Information about the index file in this modpack. */
|
|
||||||
"index": {
|
|
||||||
/** The path to the file that contains the index. @see {@link Path} */
|
|
||||||
"file": Path
|
|
||||||
/** The hash format for the hash of the index file. @see {@link HashFormat} */
|
|
||||||
"hash-format"?: HashFormat
|
|
||||||
/** The hash of the index file, as a string. @see {@link Hash} */
|
|
||||||
"hash"?: Hash
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The versions of components used by this modpack - usually Minecraft and the mod loader this pack uses.
|
|
||||||
* The existence of a component implies that it should be installed.
|
|
||||||
* These values can also be used by tools to determine which versions of mods should be installed.
|
|
||||||
*/
|
|
||||||
"versions": {
|
|
||||||
/**
|
|
||||||
* The version of Minecraft used by this modpack.
|
|
||||||
* This should be in the format used by the version.json files.
|
|
||||||
*/
|
|
||||||
"minecraft": string
|
|
||||||
/**
|
|
||||||
* The version of Forge used by this modpack.
|
|
||||||
* This version must not include the Minecraft version as a prefix.
|
|
||||||
*/
|
|
||||||
"forge"?: string
|
|
||||||
/** The version of Fabric loader used by this modpack. */
|
|
||||||
"fabric"?: string
|
|
||||||
/** The version of Quilt loader used by this modpack. */
|
|
||||||
"quilt"?: string
|
|
||||||
/** The version of Liteloader used by this modpack. */
|
|
||||||
"liteloader"?: string
|
|
||||||
"neoforge"?: string
|
|
||||||
},
|
|
||||||
/** Additional options */
|
|
||||||
"options"?: {
|
|
||||||
/**
|
|
||||||
* A list of additional Minecraft versions to accept when installing or updating mods.
|
|
||||||
* @see https://packwiz.infra.link/tutorials/creating/adding-mods/
|
|
||||||
*/
|
|
||||||
"acceptable-game-versions"?: string[]
|
|
||||||
/**
|
|
||||||
* The folder in which new metadata files will be added,
|
|
||||||
* defaulting to a folder based on the category
|
|
||||||
* (mods, resourcepacks, etc; if the category is unknown the current directory is used)
|
|
||||||
*/
|
|
||||||
"meta-folder"?: Path
|
|
||||||
/** deprecated; aliassed to meta-folder */
|
|
||||||
"mods-folder"?: Path
|
|
||||||
/**
|
|
||||||
* The base folder from which meta-folder will be resolved,
|
|
||||||
* defaulting to the current directory
|
|
||||||
* (so you can put all mods/etc in a subfolder while still using the default behaviour)
|
|
||||||
*/
|
|
||||||
"meta-folder-base"?: Path
|
|
||||||
/**
|
|
||||||
* If this is set to true,
|
|
||||||
* packwiz will not generate hashes of local files,
|
|
||||||
* to prevent merge conflicts and inconsistent hashes when using git/etc.
|
|
||||||
*
|
|
||||||
* - `packwiz refresh --build` can be used in this mode to generate internal hashes
|
|
||||||
* for distributing the pack with packwiz-installer
|
|
||||||
*/
|
|
||||||
"no-internal-hashes"?: boolean
|
|
||||||
/**
|
|
||||||
* The folder in which datapacks are to be added;
|
|
||||||
* specific to the datapack loader mod you use,
|
|
||||||
* and must be set to add datapacks
|
|
||||||
* (that are not bundled as mods)
|
|
||||||
*/
|
|
||||||
"datapack-folder"?: Path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* The index file of the modpack,
|
|
||||||
* storing references to every file to be downloaded in the pack.
|
|
||||||
*/
|
|
||||||
export interface PackIndex {
|
|
||||||
/** The default hash format for every file in the index. @see {@link HashFormat} */
|
|
||||||
"hash-format"?: HashFormat
|
|
||||||
/**
|
|
||||||
* The files listed in this index.
|
|
||||||
* If it is not defined, defaults to an empty list.
|
|
||||||
*/
|
|
||||||
"files"?: [{
|
|
||||||
/**
|
|
||||||
* The path to the file to be downloaded,
|
|
||||||
* relative to this index file.
|
|
||||||
* @see {@link Path}
|
|
||||||
*/
|
|
||||||
"file": Path
|
|
||||||
/** The hash of the specified file, as a string. @see {@link Hash} */
|
|
||||||
"hash"?: Hash
|
|
||||||
/**
|
|
||||||
* The name with which this file should be downloaded,
|
|
||||||
* instead of the filename specified in the path.
|
|
||||||
* Not compatible with metafile,
|
|
||||||
* and may not be very well supported.
|
|
||||||
*/
|
|
||||||
"alias"?: Path
|
|
||||||
/**
|
|
||||||
* The hash format for the hash of the specified file.
|
|
||||||
* Defaults to the hash format specified in the index -
|
|
||||||
* ideally remove this value if it is equal to the hash format for the index to save space.
|
|
||||||
* @see {@link HashFormat}
|
|
||||||
*/
|
|
||||||
"hash-format"?: HashFormat
|
|
||||||
/**
|
|
||||||
* True when this entry points to a .toml metadata file,
|
|
||||||
* which references a file outside the pack.
|
|
||||||
*/
|
|
||||||
"metafile"?: boolean
|
|
||||||
/**
|
|
||||||
* When this is set to true,
|
|
||||||
* the file is not overwritten if it already exists,
|
|
||||||
* to preserve changes made by a user.
|
|
||||||
*/
|
|
||||||
"preserve"?: boolean
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* A metadata file which references an external file from a URL.
|
|
||||||
* This allows for side-only mods,
|
|
||||||
* optional mods and stores metadata to allow finding updates from Modrinth and CurseForge.
|
|
||||||
* The "mod" terminology is used a lot here,
|
|
||||||
* but this should work for any file.
|
|
||||||
*/
|
|
||||||
export interface PackFileMetaData {
|
|
||||||
/**
|
|
||||||
* The destination path of the mod file,
|
|
||||||
* relative to this file.
|
|
||||||
* @see {@link Path}
|
|
||||||
*/
|
|
||||||
"filename": Path
|
|
||||||
/**
|
|
||||||
* The name of the mod,
|
|
||||||
* which can be displayed in user interfaces to identify the mod.
|
|
||||||
* It does not need to be unique between mods,
|
|
||||||
* although this may cause confusion.
|
|
||||||
*/
|
|
||||||
"name": string
|
|
||||||
/** The side on which this mod should be installed. @see {@link Side} */
|
|
||||||
"side"?: Side
|
|
||||||
/** Information about how to download this mod. */
|
|
||||||
"download": {
|
|
||||||
/** The hash format for the hash of the specified file. @see {@link HashFormat} */
|
|
||||||
"hash-format": HashFormat
|
|
||||||
/** The hash of the specified file, as a string. @see {@link Hash} */
|
|
||||||
"hash": Hash
|
|
||||||
/** The URL to download the mod from. @see {@link Url} */
|
|
||||||
"url"?: Url
|
|
||||||
"mode"?: string
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Information about the optional state of this mod.
|
|
||||||
* When excluded, this indicates that the mod is not optional.
|
|
||||||
*/
|
|
||||||
"option"?: {
|
|
||||||
/**
|
|
||||||
* Whether or not the mod is optional.
|
|
||||||
* This can be set to false if you want to keep the description but make the mod required.
|
|
||||||
*/
|
|
||||||
"optional": boolean
|
|
||||||
/**
|
|
||||||
* If true, the mod will be enabled by default.
|
|
||||||
* If false, the mod will be disabled by default.
|
|
||||||
* If a pack format does not support optional mods but it does support disabling mods,
|
|
||||||
* the mod will be disabled if it defaults to being disabled.
|
|
||||||
*/
|
|
||||||
"default"?: boolean
|
|
||||||
/**
|
|
||||||
* A description displayed to the user when they select optional mods.
|
|
||||||
* This should explain why or why not the user should enable the mod.
|
|
||||||
*/
|
|
||||||
"description"?: string
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Information about how to update the download details of this mod with tools.
|
|
||||||
*
|
|
||||||
* If this object does not exist or there are no defined update sources,
|
|
||||||
* the mod will not be automatically updated.
|
|
||||||
*
|
|
||||||
* If there are multiple defined update sources,
|
|
||||||
* one of them will be chosen.
|
|
||||||
* The source that is chosen is not defined,
|
|
||||||
* so it is therefore dependent on the implementation of the tool
|
|
||||||
* (may not be deterministic, so do not rely on one source being chosen over another).
|
|
||||||
*/
|
|
||||||
"update"?: {
|
|
||||||
/** An update source for updating mods downloaded from CurseForge. */
|
|
||||||
"curseforge"?: {
|
|
||||||
/**
|
|
||||||
* An integer representing the unique file ID of this mod file.
|
|
||||||
* This can be used if more metadata needs to be obtained relating to the mod.
|
|
||||||
*/
|
|
||||||
"file-id": number
|
|
||||||
/**
|
|
||||||
* An integer representing the unique project ID of this mod.
|
|
||||||
* Updating will retrieve the latest file for this project ID that is valid
|
|
||||||
* (correct Minecraft version, release channel, modloader, etc.).
|
|
||||||
*/
|
|
||||||
"project-id": number
|
|
||||||
}
|
|
||||||
/** An update source for updating mods downloaded from Modrinth. */
|
|
||||||
"modrinth"?: {
|
|
||||||
/**
|
|
||||||
* A string representing the unique mod ID of this mod.
|
|
||||||
* Updating will retrieve the latest file for this project ID that is valid
|
|
||||||
* (correct Minecraft version, release channel, modloader, etc.).
|
|
||||||
*/
|
|
||||||
"mod-id": string
|
|
||||||
/**
|
|
||||||
* A string representing the unique version ID of this file.
|
|
||||||
* This can be used if more metadata needs to be obtained relating to the mod.
|
|
||||||
*/
|
|
||||||
"version": string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
import WorkerlessPool from 'workerless'
|
const pathSeperatorRegex = /[\\\/]/
|
||||||
import { parseTOML, stringifyTOML } from 'confbox'
|
|
||||||
import { CFV2Client } from 'curseforge-v2'
|
|
||||||
import { forAsync } from '@sugoidogo/js-util'
|
|
||||||
import * as modrinth from '@modrinth-ts/lib/dist/index'
|
|
||||||
|
|
||||||
import type {
|
|
||||||
Crypto,
|
|
||||||
FileSystemDirectoryHandle,
|
|
||||||
FileSystemGetDirectoryOptions,
|
|
||||||
FileSystemGetFileOptions,
|
|
||||||
TextDecoder,
|
|
||||||
TextEncoder,
|
|
||||||
Console
|
|
||||||
} from '@sugoidogo/importable-types-web'
|
|
||||||
|
|
||||||
declare var TextEncoder: {
|
|
||||||
prototype: TextEncoder;
|
|
||||||
new(): TextEncoder;
|
|
||||||
};
|
|
||||||
|
|
||||||
declare var TextDecoder: {
|
|
||||||
prototype: TextDecoder
|
|
||||||
new(): TextDecoder
|
|
||||||
}
|
|
||||||
|
|
||||||
declare var crypto: Crypto
|
|
||||||
declare var console: Console
|
|
||||||
|
|
||||||
export async function getHash(data: Uint8Array<ArrayBuffer> | string, format: HashFormat): Promise<string | number> {
|
|
||||||
if (typeof data === 'string') data = new TextEncoder().encode(data)
|
|
||||||
if (format.startsWith('sha')) { // copied from https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API/Non-cryptographic_uses_of_subtle_crypto#hashing_a_file
|
|
||||||
const hashAsArrayBuffer = await crypto.subtle.digest("SHA-" + format.substring(3), data);
|
|
||||||
const uint8ViewOfHash = new Uint8Array(hashAsArrayBuffer);
|
|
||||||
// @ts-ignore
|
|
||||||
if (uint8ViewOfHash.toHex) {
|
|
||||||
// @ts-ignore
|
|
||||||
return uint8ViewOfHash.toHex() as string;
|
|
||||||
}
|
|
||||||
const hashAsString = Array.from(uint8ViewOfHash)
|
|
||||||
.map((b) => b.toString(16).padStart(2, "0"))
|
|
||||||
.join("");
|
|
||||||
return hashAsString;
|
|
||||||
}
|
|
||||||
if (format === "murmur2") {
|
|
||||||
const { default: murmurHash } = await import('murmur2')
|
|
||||||
return murmurHash(data, 1, true) as number
|
|
||||||
}
|
|
||||||
if (format === "md5") {
|
|
||||||
const md5 = await import('md5')
|
|
||||||
return md5(data)
|
|
||||||
}
|
|
||||||
throw new Error('unknown hash format: ' + format)
|
|
||||||
}
|
|
||||||
|
|
||||||
function splitPath(path: string) {
|
function splitPath(path: string) {
|
||||||
path = path.replaceAll('\\', '/')
|
path = path.replaceAll('\\', '/')
|
||||||
@@ -387,464 +30,36 @@ async function getFileHandle(dir: FileSystemDirectoryHandle, path: string, optio
|
|||||||
return dir.getFileHandle(basename, options)
|
return dir.getFileHandle(basename, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeEntry(dir: FileSystemDirectoryHandle, path: string) {
|
|
||||||
const { dirname, basename } = splitPath(path)
|
async function getBytes(origin: FileSystemDirectoryHandle | Url, filePath: Path) {
|
||||||
dir = await getDirectoryHandle(dir, dirname)
|
if (typeof origin === "string") {
|
||||||
return dir.removeEntry(basename)
|
const url=new URL(origin)
|
||||||
|
if (!url.pathname.endsWith(filePath)) {
|
||||||
|
const pathSegments = url.pathname.split(pathSeperatorRegex)
|
||||||
|
if (pathSegments.at(-1) === "") pathSegments.pop()
|
||||||
|
pathSegments.push(filePath)
|
||||||
|
url.pathname=pathSegments.join("/")
|
||||||
|
}
|
||||||
|
return fetch(url).then(response=>response.bytes())
|
||||||
|
}
|
||||||
|
return getFileHandle(origin, filePath)
|
||||||
|
.then(fileHandle => fileHandle.getFile())
|
||||||
|
.then(file=>file.bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
class Modpack {
|
export interface PackwizPack {
|
||||||
pack_directory_handle: FileSystemDirectoryHandle
|
packOrigin: FileSystemDirectoryHandle | Url
|
||||||
pack_file_name: string
|
packFileName: Path
|
||||||
pack: PackMetaData
|
packMetaData: PackMetaData
|
||||||
index: PackIndex
|
packIndex: PackIndex
|
||||||
files: { [key: Path]: PackFileMetaData }
|
packMetaFiles: { [key: Path]: PackFileMetaData | undefined }
|
||||||
constructor({ pack_file_name = 'pack.toml', pack_directory_handle }: {
|
|
||||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle
|
|
||||||
}) {
|
|
||||||
this.pack_file_name = pack_file_name
|
|
||||||
this.pack_directory_handle = pack_directory_handle
|
|
||||||
}
|
|
||||||
// initialize this modpack with default values
|
|
||||||
async init() {
|
|
||||||
this.index = {
|
|
||||||
'hash-format': 'sha256'
|
|
||||||
}
|
|
||||||
this.pack = {
|
|
||||||
'name': 'modpack',
|
|
||||||
'pack-format': 'packwiz:1.1.0',
|
|
||||||
'index': {
|
|
||||||
'file': 'index.toml',
|
|
||||||
'hash-format': 'sha256',
|
|
||||||
'hash': await getHash((stringifyTOML(this.index)), 'sha256') as string
|
|
||||||
},
|
|
||||||
'versions': {
|
|
||||||
'minecraft': '1.21.11' // TODO get latest minecraft version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.files = {}
|
|
||||||
}
|
|
||||||
// load modpack, rebuilding index
|
|
||||||
async refresh({ pack_file_name, pack_directory_handle, useHashes = false }: {
|
|
||||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle, useHashes?: boolean
|
|
||||||
}) {
|
|
||||||
this.init()
|
|
||||||
pack_directory_handle = pack_directory_handle || this.pack_directory_handle
|
|
||||||
pack_file_name = pack_file_name || this.pack_file_name
|
|
||||||
const textDecoder = new TextDecoder()
|
|
||||||
const pack_file_handle = await pack_directory_handle.getFileHandle(pack_file_name)
|
|
||||||
{
|
|
||||||
const file = await pack_file_handle.getFile()
|
|
||||||
const text = await file.text()
|
|
||||||
this.pack = parseTOML(text)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
useHashes = useHashes || !this.pack.options || !this.pack.options['no-internal-hashes']
|
|
||||||
this.index = useHashes ? { 'hash-format': this.pack.index['hash-format'] } : {}
|
|
||||||
const { dirname, basename } = splitPath(this.pack.index.file)
|
|
||||||
const index_directory_handle = await getDirectoryHandle(pack_directory_handle, dirname)
|
|
||||||
const index_file_handle = await index_directory_handle.getFileHandle(basename)
|
|
||||||
const directories = [index_directory_handle]
|
|
||||||
while (directories.length !== 0) {
|
|
||||||
await forAsync(directories.pop().values(), async handle => {
|
|
||||||
if (handle.kind === 'directory') return directories.push(handle)
|
|
||||||
if (handle.isSameEntry(index_file_handle) || handle.isSameEntry(pack_file_handle)) return
|
|
||||||
const file = await handle.getFile()
|
|
||||||
const bytes = await file.bytes()
|
|
||||||
const path = (await index_directory_handle.resolve(handle)).join('/')
|
|
||||||
const isMetaFile = handle.name.endsWith('.pw.toml')
|
|
||||||
this.index.files.push({
|
|
||||||
'file': path,
|
|
||||||
'metafile': isMetaFile,
|
|
||||||
'hash': useHashes ? await getHash(bytes, this.index['hash-format']) as string : undefined
|
|
||||||
})
|
|
||||||
if (isMetaFile) this.files[path] = parseTOML(textDecoder.decode(bytes))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// load modpack from storage
|
|
||||||
async load({ pack_file_name, pack_directory_handle }: {
|
|
||||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle
|
|
||||||
}) {
|
|
||||||
this.init()
|
|
||||||
pack_file_name = pack_file_name || this.pack_file_name
|
|
||||||
pack_directory_handle = pack_directory_handle || this.pack_directory_handle
|
|
||||||
const textDecoder = new TextDecoder()
|
|
||||||
{
|
|
||||||
const pack_file_handle = await pack_directory_handle.getFileHandle(pack_file_name)
|
|
||||||
const file = await pack_file_handle.getFile()
|
|
||||||
const text = await file.text()
|
|
||||||
this.pack = parseTOML(text)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const { dirname, basename } = splitPath(this.pack.index.file)
|
|
||||||
const index_directory_handle = await getDirectoryHandle(pack_directory_handle, dirname)
|
|
||||||
const index_file_handle = await index_directory_handle.getFileHandle(basename)
|
|
||||||
const file = await index_file_handle.getFile()
|
|
||||||
const bytes = await file.bytes()
|
|
||||||
if (
|
|
||||||
this.pack.options &&
|
|
||||||
this.pack.options['no-internal-hashes'] &&
|
|
||||||
await getHash(bytes, this.pack.index['hash-format']) !== this.pack.index.hash
|
|
||||||
) throw new Error('hash validation failed on ' + file.name)
|
|
||||||
const text = textDecoder.decode(bytes)
|
|
||||||
this.index = parseTOML(text)
|
|
||||||
await forAsync(this.index.files, async entry => {
|
|
||||||
if (!entry.metafile) return
|
|
||||||
const file_handle = await getFileHandle(index_directory_handle, entry.file)
|
|
||||||
const file = await file_handle.getFile()
|
|
||||||
const bytes = await file.bytes()
|
|
||||||
if (
|
|
||||||
this.pack.options &&
|
|
||||||
this.pack.options['no-internal-hashes'] &&
|
|
||||||
await getHash(bytes, entry['hash-format']) !== entry.hash
|
|
||||||
) throw new Error('hash validation failed on ' + file.name)
|
|
||||||
const text = textDecoder.decode(bytes)
|
|
||||||
this.files[entry.file] = parseTOML(text)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// save modpack to storage
|
|
||||||
async save({ pack_file_name, pack_directory_handle }: {
|
|
||||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle
|
|
||||||
}) {
|
|
||||||
pack_file_name = pack_file_name || this.pack_file_name
|
|
||||||
pack_directory_handle = pack_directory_handle || this.pack_directory_handle
|
|
||||||
{
|
|
||||||
const pack_file_handle = await pack_directory_handle.getFileHandle(pack_file_name, { create: true })
|
|
||||||
const writeable = await pack_file_handle.createWritable()
|
|
||||||
await writeable.write(stringifyTOML(this.pack))
|
|
||||||
await writeable.close()
|
|
||||||
}
|
|
||||||
{
|
|
||||||
const { dirname, basename } = splitPath(this.pack.index.file)
|
|
||||||
const index_directory_handle = await getDirectoryHandle(pack_directory_handle, dirname, { create: true })
|
|
||||||
const index_file_handle = await index_directory_handle.getFileHandle(basename)
|
|
||||||
const writeable = await index_file_handle.createWritable()
|
|
||||||
writeable.write(stringifyTOML(this.index))
|
|
||||||
writeable.close()
|
|
||||||
await forAsync(Object.entries(this.files), async ([path, mod]) => {
|
|
||||||
const mod_file_handle = await getFileHandle(index_directory_handle, path, { create: true })
|
|
||||||
const writeable = await mod_file_handle.createWritable()
|
|
||||||
await writeable.write(stringifyTOML(mod))
|
|
||||||
await writeable.close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// find and replace files availible on curseforge
|
|
||||||
async cfDetect({ pack_file_name, pack_directory_handle, api_key, size_min = 4096 }: {
|
|
||||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle,
|
|
||||||
api_key: string, size_min?: number
|
|
||||||
}) {
|
|
||||||
pack_file_name = pack_file_name || this.pack_file_name
|
|
||||||
pack_directory_handle = pack_directory_handle || this.pack_directory_handle
|
|
||||||
const { dirname, basename } = splitPath(this.pack.index.file)
|
|
||||||
const index_directory_handle = await getDirectoryHandle(pack_directory_handle, dirname)
|
|
||||||
const workerlessPool = new WorkerlessPool()
|
|
||||||
const file_hash_map = new Map<number, Path>()
|
|
||||||
await forAsync(this.index.files, async indexEntry => {
|
|
||||||
if (indexEntry.metafile) return
|
|
||||||
const file_handle = await getFileHandle(index_directory_handle, indexEntry.file)
|
|
||||||
const file = await file_handle.getFile()
|
|
||||||
if (file.size < size_min) return
|
|
||||||
const bytes = file.bytes()
|
|
||||||
const hash = Number(await workerlessPool.run(getHash, bytes, 'murmur2'))
|
|
||||||
|
|
||||||
})
|
|
||||||
workerlessPool.terminate()
|
|
||||||
console.log('requesting matches from curseforge')
|
|
||||||
const response = await new CFV2Client({ apiKey: api_key }).getFingerprintMatches({ 'fingerprints': Array.from(file_hash_map.keys()) })
|
|
||||||
const textEncoder = new TextEncoder()
|
|
||||||
let resultCount = 0
|
|
||||||
await forAsync(Object.values(response.data.data.exactMatches), async function (match) {
|
|
||||||
let file_path = file_hash_map.get(match.file.fileFingerprint)
|
|
||||||
if (!file_path) {
|
|
||||||
for (const module of match.file.modules) {
|
|
||||||
file_path = file_hash_map.get(module.fingerprint)
|
|
||||||
}
|
|
||||||
if (file_path) {
|
|
||||||
console.debug("skipping module match, this is probably a false positive", file_path, match.file)
|
|
||||||
} else {
|
|
||||||
console.debug("curseforge returned a result we didn't ask for, skipping", match.file)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const { dirname: file_dir_name, basename: file_name } = splitPath(file_path)
|
|
||||||
for (const hash of match.file.hashes) {
|
|
||||||
if (hash.algo === 1) {
|
|
||||||
const mod: PackFileMetaData = {
|
|
||||||
"name": match.file.displayName,
|
|
||||||
"filename": match.file.fileName,
|
|
||||||
"download": {
|
|
||||||
"hash-format": "sha1",
|
|
||||||
"hash": hash.value,
|
|
||||||
"mode": "metadata:curseforge"
|
|
||||||
},
|
|
||||||
"update": {
|
|
||||||
"curseforge": {
|
|
||||||
"file-id": match.file.id,
|
|
||||||
"project-id": match.file.modId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const new_file_path = file_dir_name + '/' + encodeURI(match.file.displayName.toLowerCase().replaceAll(' ', '-') + '.pw.toml')
|
|
||||||
const new_file_data = textEncoder.encode(stringifyTOML(mod))
|
|
||||||
const new_file_hash = (await getHash(new_file_data, this.index['hash-format'])).toString()
|
|
||||||
await getFileHandle(index_directory_handle, new_file_path, { 'create': true })
|
|
||||||
.then(handle => handle.createWritable())
|
|
||||||
.then(stream => {
|
|
||||||
stream.write(new_file_data)
|
|
||||||
stream.close()
|
|
||||||
this.index.files.push({
|
|
||||||
"file": new_file_path,
|
|
||||||
"hash": new_file_hash.toString(),
|
|
||||||
"metafile": true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
await removeEntry(index_directory_handle, file_path)
|
|
||||||
.then(() => {
|
|
||||||
for (let i = 0; i < this.index.files.length; i++) {
|
|
||||||
if (this.index.files[i].file != file_path) continue
|
|
||||||
this.index.files.splice(i, 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
throw new Error("can't find removed file in index")
|
|
||||||
})
|
|
||||||
resultCount++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const index_file_data = textEncoder.encode(stringifyTOML(this.index))
|
|
||||||
const index_file_hash = await getHash(index_file_data, this.pack.index['hash-format'])
|
|
||||||
const index_file_handle = await getFileHandle(index_directory_handle, basename)
|
|
||||||
const pack_file_handle = await getFileHandle(pack_directory_handle, this.pack_file_name)
|
|
||||||
await index_file_handle.createWritable()
|
|
||||||
.then(async stream => {
|
|
||||||
await stream.write(index_file_data)
|
|
||||||
await stream.close()
|
|
||||||
this.pack.index.hash = index_file_hash.toString()
|
|
||||||
return pack_file_handle.createWritable()
|
|
||||||
}).then(async stream => {
|
|
||||||
await stream.write(stringifyTOML(this.pack))
|
|
||||||
await stream.close()
|
|
||||||
})
|
|
||||||
console.log(`found ${resultCount} matching files`)
|
|
||||||
}
|
|
||||||
// detect and replace files availible on modrinth
|
|
||||||
async mrDetect({ pack_file_name, pack_directory_handle, api_key, size_min = 4096 }: {
|
|
||||||
pack_file_name?: string, pack_directory_handle?: FileSystemDirectoryHandle,
|
|
||||||
api_key: string, size_min?: number
|
|
||||||
}) {
|
|
||||||
pack_file_name = pack_file_name || this.pack_file_name
|
|
||||||
pack_directory_handle = pack_directory_handle || this.pack_directory_handle
|
|
||||||
const { dirname, basename } = splitPath(this.pack.index.file)
|
|
||||||
const index_directory_handle = await getDirectoryHandle(pack_directory_handle, dirname)
|
|
||||||
const file_hash_map = new Map<Hash, Path>()
|
|
||||||
console.log('hashing files')
|
|
||||||
await forAsync(this.index.files, async function (entry) {
|
|
||||||
if (entry.metafile) return
|
|
||||||
const file = await getFileHandle(index_directory_handle, entry.file)
|
|
||||||
.then(handle => handle.getFile())
|
|
||||||
if (file.size < size_min) return
|
|
||||||
const file_data = await file.bytes()
|
|
||||||
const hash = await getHash(file_data, "sha1") as string
|
|
||||||
file_hash_map.set(hash, entry.file)
|
|
||||||
})
|
|
||||||
console.log('requesting matches from modrinth')
|
|
||||||
const matches = await modrinth.getLatestVersionsFromHashes({
|
|
||||||
'auth': api_key,
|
|
||||||
'body': {
|
|
||||||
"algorithm": "sha1",
|
|
||||||
"hashes": Array.from(file_hash_map.keys()),
|
|
||||||
"game_versions": [],
|
|
||||||
"loaders": []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const version_matches = new Map<string, modrinth.Version>()
|
|
||||||
for (const match of Object.values(matches.data)) version_matches.set(match.project_id, match)
|
|
||||||
const projects = await modrinth.getProjects({
|
|
||||||
'auth': api_key,
|
|
||||||
query: {
|
|
||||||
ids: JSON.stringify(Array.from(version_matches.keys()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const textEncoder = new TextEncoder()
|
|
||||||
let resultCount = 0
|
|
||||||
await forAsync(projects.data, async function (project) {
|
|
||||||
const version = version_matches.get(project.id)
|
|
||||||
if (!version) {
|
|
||||||
console.debug("modrinth returned a project result we didn't ask for, skipping", project)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const file_path = file_hash_map.get(version.files[0].hashes.sha1)
|
|
||||||
if (!file_path) {
|
|
||||||
console.debug("modrinth returned a file result we didn't ask for, skipping", version)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let side: Side = "both"
|
|
||||||
if (project.client_side === "unsupported") side = "server"
|
|
||||||
if (project.server_side === "unsupported") side = "client"
|
|
||||||
const mod: PackFileMetaData = {
|
|
||||||
"name": project.title,
|
|
||||||
"filename": version.files[0].filename,
|
|
||||||
"side": side,
|
|
||||||
"download": {
|
|
||||||
"hash-format": "sha1",
|
|
||||||
"hash": version.files[0].hashes.sha1,
|
|
||||||
"url": version.files[0].url
|
|
||||||
},
|
|
||||||
"update": {
|
|
||||||
"modrinth": {
|
|
||||||
"version": version.id,
|
|
||||||
"mod-id": version.project_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const new_file_path = splitPath(file_path).dirname + '/' + encodeURI(project.title.toLowerCase().replaceAll(' ', '-') + '.pw.toml')
|
|
||||||
const new_file_data = textEncoder.encode(stringifyTOML(mod))
|
|
||||||
const new_file_hash = await getHash(new_file_data, this.index['hash-format'])
|
|
||||||
await getFileHandle(index_directory_handle, new_file_path, { 'create': true })
|
|
||||||
.then(handle => handle.createWritable())
|
|
||||||
.then(stream => {
|
|
||||||
stream.write(new_file_data)
|
|
||||||
stream.close()
|
|
||||||
this.index.files.push({
|
|
||||||
"file": new_file_path,
|
|
||||||
"hash": new_file_hash,
|
|
||||||
"metafile": true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
await removeEntry(index_directory_handle, file_path)
|
|
||||||
.then(() => {
|
|
||||||
for (let i = 0; i < this.index.files.length; i++) {
|
|
||||||
if (this.index.files[i].file != file_path) continue
|
|
||||||
this.index.files.splice(i, 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
throw new Error("can't find removed file in index")
|
|
||||||
})
|
|
||||||
resultCount++
|
|
||||||
})
|
|
||||||
const index_file_data = textEncoder.encode(stringifyTOML(this.index))
|
|
||||||
const index_file_hash = await getHash(index_file_data, this.pack.index['hash-format'])
|
|
||||||
const index_file_handle = await getFileHandle(index_directory_handle, basename)
|
|
||||||
const pack_file_handle = await getFileHandle(pack_directory_handle, pack_file_name)
|
|
||||||
await index_file_handle.createWritable()
|
|
||||||
.then(async stream => {
|
|
||||||
await stream.write(index_file_data)
|
|
||||||
await stream.close()
|
|
||||||
this.pack.index.hash = index_file_hash.toString()
|
|
||||||
return pack_file_handle.createWritable()
|
|
||||||
}).then(async stream => {
|
|
||||||
await stream.write(stringifyTOML(this.pack))
|
|
||||||
await stream.close()
|
|
||||||
})
|
|
||||||
console.log(`found ${resultCount} matching files`)
|
|
||||||
}
|
|
||||||
// cache download urls for curseforge files
|
|
||||||
async cfUrl({ api_key }: { api_key: string }) {
|
|
||||||
const mod_id_paths: Map<number,Path>=new Map()
|
|
||||||
for (const [path, mod] of Object.entries(this.files)) {
|
|
||||||
if (mod.download.mode !== 'metadata:curseforge') continue
|
|
||||||
mod_id_paths.set(Number(mod.update.curseforge['file-id']),path)
|
|
||||||
}
|
|
||||||
console.log(`requesting ${mod_id_paths.size} download urls`)
|
|
||||||
const result = await new CFV2Client({ 'apiKey': api_key }).getFiles({ 'fileIds': Array.from(mod_id_paths.keys()) })
|
|
||||||
if (result.statusCode != 200) {
|
|
||||||
throw new Error(result.message)
|
|
||||||
}
|
|
||||||
const files = result.data.data
|
|
||||||
let blockedFileCount=0
|
|
||||||
for (const file of files) {
|
|
||||||
if (!file.downloadUrl) {
|
|
||||||
console.warn('third party downloads disabled for ' + file.displayName)
|
|
||||||
blockedFileCount++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
const path = mod_id_paths.get(file.id)!
|
|
||||||
delete this.files[path].download.mode
|
|
||||||
this.files[path].download.url = file.downloadUrl
|
|
||||||
}
|
|
||||||
console.log(`found ${files.length-blockedFileCount} download urls`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function loadPack(origin: FileSystemDirectoryHandle | Url, packFileName = 'pack.toml') {
|
||||||
* find curseforge files that are availible on modrinth and merge their metadata
|
const packMetaData:PackMetaData = await getBytes(origin, packFileName).then(bytes => parseTOML(bytes.toString()))
|
||||||
* @param pack_url url or path to pack.toml
|
const packIndex: PackIndex = await getBytes(origin, packMetaData.index.file).then(bytes => parseTOML(bytes.toString()))
|
||||||
* @param mrApiKey modrinth api key
|
const {dirname:indexDirPath}=splitPath(packMetaData.index.file)
|
||||||
*/
|
for (const entry of packIndex.files) {
|
||||||
export async function mrMerge(pack_url: string, mrApiKey?: string): Promise<Result> {
|
// TODO
|
||||||
console.log('loading pack')
|
|
||||||
const pack = await fetch_toml(pack_url) as PackMetaData
|
|
||||||
const pack_dir = dirname(pack_url)
|
|
||||||
const index_url = `${pack_dir}/${pack.index.file}`
|
|
||||||
console.log('loading index')
|
|
||||||
const index = await fetch_toml(index_url) as PackIndex
|
|
||||||
const index_dir = dirname(index_url)
|
|
||||||
if (!index.files) {
|
|
||||||
console.warn(`${index_url} has no files indexed`)
|
|
||||||
return {}
|
|
||||||
}
|
}
|
||||||
console.log('loading curseforge files')
|
|
||||||
const file_hash_map: any = {}
|
|
||||||
await Promise.all(index.files.map(async function (file) {
|
|
||||||
if (!file.metafile) return
|
|
||||||
const file_url = `${index_dir}/${file.file}`
|
|
||||||
const mod = await fetch_toml(file_url) as PackFileMetaData
|
|
||||||
if (mod.update && mod.update.modrinth) return
|
|
||||||
if (!file_hash_map[mod.download['hash-format']]) file_hash_map[mod.download['hash-format']] = {}
|
|
||||||
file_hash_map[mod.download['hash-format']][mod.download.hash] = { file_url, mod }
|
|
||||||
}))
|
|
||||||
console.log('requesting matches from modrinth')
|
|
||||||
const project_matches = new Map<string, any>()
|
|
||||||
for (const hash_format in file_hash_map) {
|
|
||||||
const matches = await fetch_json('https://api.modrinth.com/v2/version_files', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
'accept': 'application/json',
|
|
||||||
'authorization': mrApiKey
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
"algorithm": hash_format,
|
|
||||||
"hashes": Array.from(Object.keys(file_hash_map[hash_format]))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
for (const match of Object.values(matches) as any[]) project_matches.set(match.project_id, match)
|
|
||||||
}
|
|
||||||
const projects = await fetch_json(`https://api.modrinth.com/v2/projects?ids=["${Array.from(project_matches.keys()).join('","')}"]`)
|
|
||||||
const result: Result = { write: {} }
|
|
||||||
for (const project of projects) {
|
|
||||||
const match = project_matches.get(project.id)
|
|
||||||
if (!match) {
|
|
||||||
console.debug("modrinth returned a project result we didn't ask for, skipping", project)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
let file_url: Path | undefined
|
|
||||||
let mod: PackFileMetaData | undefined
|
|
||||||
for (const hash_format in file_hash_map) {
|
|
||||||
({ file_url, mod } = file_hash_map[hash_format][match.files[0].hashes[hash_format].toLowerCase().replace('-', '')])
|
|
||||||
}
|
|
||||||
if (!file_url || !mod) {
|
|
||||||
console.debug("modrinth returned a file result we didn't ask for, skipping", match)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
delete mod.download.mode
|
|
||||||
mod.download.url = match.files[0].url
|
|
||||||
let side: Side = "both"
|
|
||||||
if (project.client_side === "unsupported") side = "server"
|
|
||||||
if (project.server_side === "unsupported") side = "client"
|
|
||||||
mod.side = side
|
|
||||||
if (!mod.update) mod.update = {}
|
|
||||||
mod.update.modrinth = {
|
|
||||||
"version": match.id,
|
|
||||||
"mod-id": match.project_id
|
|
||||||
}
|
|
||||||
result.write[file_url] = mod
|
|
||||||
}
|
|
||||||
console.log(`found ${Object.keys(result.write).length} matching files`)
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
@@ -8,12 +8,9 @@
|
|||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "../../dist",
|
"outDir": "../../dist",
|
||||||
"types": [],
|
"types": ["@sugoidogo/node-web-globals"],
|
||||||
"rewriteRelativeImportExtensions": true,
|
"rewriteRelativeImportExtensions": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true
|
||||||
},
|
}
|
||||||
"include": [
|
|
||||||
"packwiz.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
Vendored
+309
@@ -0,0 +1,309 @@
|
|||||||
|
/**
|
||||||
|
* The hash algorithm used to determine if a file is valid.
|
||||||
|
* All functions listed must be supported by tools implementing the packwiz pack format.
|
||||||
|
*/
|
||||||
|
export type HashFormat = "sha256" | "sha512" | "sha1" | "md5" | "murmur2"
|
||||||
|
/**
|
||||||
|
* A relative path using forward slashes.
|
||||||
|
* Must not reference a file outside the pack root,
|
||||||
|
* and should not include characters or filenames restricted on common operating systems.
|
||||||
|
*
|
||||||
|
* Implementations must support special characters including those that are percent-encoded in URLs,
|
||||||
|
* such as spaces and square brackets.
|
||||||
|
* Implementations must guard against path traversal attacks and manually validate paths.
|
||||||
|
*/
|
||||||
|
export type Path = string
|
||||||
|
/**
|
||||||
|
* Binary hashes should be stored as hexadecimal,
|
||||||
|
* and case should be ignored during parsing.
|
||||||
|
* Numeric hashes (e.g. Murmur2) should still be stored as a string,
|
||||||
|
* to ensure the value is preserved correctly.
|
||||||
|
*/
|
||||||
|
export type Hash = string
|
||||||
|
/**
|
||||||
|
* A physical Minecraft side.
|
||||||
|
* Server applies to the dedicated server,
|
||||||
|
* client applies to the client (and integrated server),
|
||||||
|
* and both applies to every installation.
|
||||||
|
*/
|
||||||
|
export type Side = "both" | "client" | "server"
|
||||||
|
/**
|
||||||
|
* An absolute URI compliant with RFC 3986.
|
||||||
|
* Implementations may need to be more lenient in accepting reserved characters in paths
|
||||||
|
* due to historical implementation bugs.
|
||||||
|
* Only the HTTP/HTTPS protocols must be supported,
|
||||||
|
* other protocols should not be used.
|
||||||
|
*/
|
||||||
|
export type Url = string
|
||||||
|
/**
|
||||||
|
* The main modpack file for a packwiz modpack.
|
||||||
|
* This is the first file loaded,
|
||||||
|
* to allow the modpack downloader to download all the files in the modpack.
|
||||||
|
*/
|
||||||
|
export interface PackMetaData {
|
||||||
|
/**
|
||||||
|
* The name of the modpack.
|
||||||
|
* This can be displayed in user interfaces to identify the pack,
|
||||||
|
* and it does not need to be unique between packs.
|
||||||
|
*/
|
||||||
|
"name": string
|
||||||
|
/**
|
||||||
|
* The author(s) of the modpack.
|
||||||
|
* This is output when exporting to the CurseForge pack format,
|
||||||
|
* and can be displayed in user interfaces.
|
||||||
|
*/
|
||||||
|
"author"?: string
|
||||||
|
/**
|
||||||
|
* The version of the modpack.
|
||||||
|
* This is output when exporting to the CurseForge pack format,
|
||||||
|
* but is not currently used elsewhere by the tools or installer.
|
||||||
|
* It must not be used for determining if the modpack is outdated.
|
||||||
|
*/
|
||||||
|
"version"?: string
|
||||||
|
/**
|
||||||
|
* A short description of the modpack.
|
||||||
|
* This is output when exporting to the Modrinth pack format,
|
||||||
|
* but is not currently used elsewhere by the tools or installer.
|
||||||
|
*/
|
||||||
|
"description"?: string
|
||||||
|
/**
|
||||||
|
* A version string identifying the pack format and version of it.
|
||||||
|
* Currently, this pack format uses version 1.1.0.
|
||||||
|
*
|
||||||
|
* If it is not defined, default to "packwiz:1.0.0"
|
||||||
|
* for backwards-compatibility with packs created before this field was added.
|
||||||
|
*
|
||||||
|
* If it is defined:
|
||||||
|
*
|
||||||
|
* - All consumers should fail to load the modpack if it does not begin with "packwiz:"
|
||||||
|
*
|
||||||
|
* - All consumers should fail to load the modpack if the latter section is not valid semver
|
||||||
|
* as defined in https://semver.org/spec/v2.0.0.html
|
||||||
|
*
|
||||||
|
* - All consumers should fail to load the modpack if the major version is greater than the version they support
|
||||||
|
*
|
||||||
|
* - Consumers can suggest updating themselves if the minor version is greater than the version they implement
|
||||||
|
*
|
||||||
|
* - Pack tools should suggest and support migration when they support a version newer than this field
|
||||||
|
*/
|
||||||
|
"pack-format": string
|
||||||
|
/** Information about the index file in this modpack. */
|
||||||
|
"index": {
|
||||||
|
/** The path to the file that contains the index. @see {@link Path} */
|
||||||
|
"file": Path
|
||||||
|
/** The hash format for the hash of the index file. @see {@link HashFormat} */
|
||||||
|
"hash-format"?: HashFormat
|
||||||
|
/** The hash of the index file, as a string. @see {@link Hash} */
|
||||||
|
"hash"?: Hash
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The versions of components used by this modpack - usually Minecraft and the mod loader this pack uses.
|
||||||
|
* The existence of a component implies that it should be installed.
|
||||||
|
* These values can also be used by tools to determine which versions of mods should be installed.
|
||||||
|
*/
|
||||||
|
"versions": {
|
||||||
|
/**
|
||||||
|
* The version of Minecraft used by this modpack.
|
||||||
|
* This should be in the format used by the version.json files.
|
||||||
|
*/
|
||||||
|
"minecraft": string
|
||||||
|
/**
|
||||||
|
* The version of Forge used by this modpack.
|
||||||
|
* This version must not include the Minecraft version as a prefix.
|
||||||
|
*/
|
||||||
|
"forge"?: string
|
||||||
|
/** The version of Fabric loader used by this modpack. */
|
||||||
|
"fabric"?: string
|
||||||
|
/** The version of Quilt loader used by this modpack. */
|
||||||
|
"quilt"?: string
|
||||||
|
/** The version of Liteloader used by this modpack. */
|
||||||
|
"liteloader"?: string
|
||||||
|
"neoforge"?: string
|
||||||
|
},
|
||||||
|
/** Additional options */
|
||||||
|
"options"?: {
|
||||||
|
/**
|
||||||
|
* A list of additional Minecraft versions to accept when installing or updating mods.
|
||||||
|
* @see https://packwiz.infra.link/tutorials/creating/adding-mods/
|
||||||
|
*/
|
||||||
|
"acceptable-game-versions"?: string[]
|
||||||
|
/**
|
||||||
|
* The folder in which new metadata files will be added,
|
||||||
|
* defaulting to a folder based on the category
|
||||||
|
* (mods, resourcepacks, etc; if the category is unknown the current directory is used)
|
||||||
|
*/
|
||||||
|
"meta-folder"?: Path
|
||||||
|
/** deprecated; aliassed to meta-folder */
|
||||||
|
"mods-folder"?: Path
|
||||||
|
/**
|
||||||
|
* The base folder from which meta-folder will be resolved,
|
||||||
|
* defaulting to the current directory
|
||||||
|
* (so you can put all mods/etc in a subfolder while still using the default behaviour)
|
||||||
|
* @see {@link Path}
|
||||||
|
*/
|
||||||
|
"meta-folder-base"?: Path
|
||||||
|
/**
|
||||||
|
* If this is set to true,
|
||||||
|
* packwiz will not generate hashes of local files,
|
||||||
|
* to prevent merge conflicts and inconsistent hashes when using git/etc.
|
||||||
|
*
|
||||||
|
* - `packwiz refresh --build` can be used in this mode to generate internal hashes
|
||||||
|
* for distributing the pack with packwiz-installer
|
||||||
|
*/
|
||||||
|
"no-internal-hashes"?: boolean
|
||||||
|
/**
|
||||||
|
* The folder in which datapacks are to be added;
|
||||||
|
* specific to the datapack loader mod you use,
|
||||||
|
* and must be set to add datapacks
|
||||||
|
* (that are not bundled as mods)
|
||||||
|
* @see {@link Path}
|
||||||
|
*/
|
||||||
|
"datapack-folder"?: Path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The index file of the modpack,
|
||||||
|
* storing references to every file to be downloaded in the pack.
|
||||||
|
*/
|
||||||
|
export interface PackIndex {
|
||||||
|
/** The default hash format for every file in the index. @see {@link HashFormat} */
|
||||||
|
"hash-format"?: HashFormat
|
||||||
|
/**
|
||||||
|
* The files listed in this index.
|
||||||
|
* If it is not defined, defaults to an empty list.
|
||||||
|
*/
|
||||||
|
"files"?: [{
|
||||||
|
/**
|
||||||
|
* The path to the file to be downloaded,
|
||||||
|
* relative to this index file.
|
||||||
|
* @see {@link Path}
|
||||||
|
*/
|
||||||
|
"file": Path
|
||||||
|
/** The hash of the specified file, as a string. @see {@link Hash} */
|
||||||
|
"hash"?: Hash
|
||||||
|
/**
|
||||||
|
* The name with which this file should be downloaded,
|
||||||
|
* instead of the filename specified in the path.
|
||||||
|
* Not compatible with metafile,
|
||||||
|
* and may not be very well supported.
|
||||||
|
*/
|
||||||
|
"alias"?: Path
|
||||||
|
/**
|
||||||
|
* The hash format for the hash of the specified file.
|
||||||
|
* Defaults to the hash format specified in the index -
|
||||||
|
* ideally remove this value if it is equal to the hash format for the index to save space.
|
||||||
|
* @see {@link HashFormat}
|
||||||
|
*/
|
||||||
|
"hash-format"?: HashFormat
|
||||||
|
/**
|
||||||
|
* True when this entry points to a .toml metadata file,
|
||||||
|
* which references a file outside the pack.
|
||||||
|
*/
|
||||||
|
"metafile"?: boolean
|
||||||
|
/**
|
||||||
|
* When this is set to true,
|
||||||
|
* the file is not overwritten if it already exists,
|
||||||
|
* to preserve changes made by a user.
|
||||||
|
*/
|
||||||
|
"preserve"?: boolean
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* A metadata file which references an external file from a URL.
|
||||||
|
* This allows for side-only mods,
|
||||||
|
* optional mods and stores metadata to allow finding updates from Modrinth and CurseForge.
|
||||||
|
* The "mod" terminology is used a lot here,
|
||||||
|
* but this should work for any file.
|
||||||
|
*/
|
||||||
|
export interface PackFileMetaData {
|
||||||
|
/**
|
||||||
|
* The destination path of the mod file,
|
||||||
|
* relative to this file.
|
||||||
|
* @see {@link Path}
|
||||||
|
*/
|
||||||
|
"filename": Path
|
||||||
|
/**
|
||||||
|
* The name of the mod,
|
||||||
|
* which can be displayed in user interfaces to identify the mod.
|
||||||
|
* It does not need to be unique between mods,
|
||||||
|
* although this may cause confusion.
|
||||||
|
*/
|
||||||
|
"name": string
|
||||||
|
/** The side on which this mod should be installed. @see {@link Side} */
|
||||||
|
"side"?: Side
|
||||||
|
/** Information about how to download this mod. */
|
||||||
|
"download": {
|
||||||
|
/** The hash format for the hash of the specified file. @see {@link HashFormat} */
|
||||||
|
"hash-format": HashFormat
|
||||||
|
/** The hash of the specified file, as a string. @see {@link Hash} */
|
||||||
|
"hash": Hash
|
||||||
|
/** The URL to download the mod from. @see {@link Url} */
|
||||||
|
"url"?: Url
|
||||||
|
"mode"?: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Information about the optional state of this mod.
|
||||||
|
* When excluded, this indicates that the mod is not optional.
|
||||||
|
*/
|
||||||
|
"option"?: {
|
||||||
|
/**
|
||||||
|
* Whether or not the mod is optional.
|
||||||
|
* This can be set to false if you want to keep the description but make the mod required.
|
||||||
|
*/
|
||||||
|
"optional": boolean
|
||||||
|
/**
|
||||||
|
* If true, the mod will be enabled by default.
|
||||||
|
* If false, the mod will be disabled by default.
|
||||||
|
* If a pack format does not support optional mods but it does support disabling mods,
|
||||||
|
* the mod will be disabled if it defaults to being disabled.
|
||||||
|
*/
|
||||||
|
"default"?: boolean
|
||||||
|
/**
|
||||||
|
* A description displayed to the user when they select optional mods.
|
||||||
|
* This should explain why or why not the user should enable the mod.
|
||||||
|
*/
|
||||||
|
"description"?: string
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Information about how to update the download details of this mod with tools.
|
||||||
|
*
|
||||||
|
* If this object does not exist or there are no defined update sources,
|
||||||
|
* the mod will not be automatically updated.
|
||||||
|
*
|
||||||
|
* If there are multiple defined update sources,
|
||||||
|
* one of them will be chosen.
|
||||||
|
* The source that is chosen is not defined,
|
||||||
|
* so it is therefore dependent on the implementation of the tool
|
||||||
|
* (may not be deterministic, so do not rely on one source being chosen over another).
|
||||||
|
*/
|
||||||
|
"update"?: {
|
||||||
|
/** An update source for updating mods downloaded from CurseForge. */
|
||||||
|
"curseforge"?: {
|
||||||
|
/**
|
||||||
|
* An integer representing the unique file ID of this mod file.
|
||||||
|
* This can be used if more metadata needs to be obtained relating to the mod.
|
||||||
|
*/
|
||||||
|
"file-id": number
|
||||||
|
/**
|
||||||
|
* An integer representing the unique project ID of this mod.
|
||||||
|
* Updating will retrieve the latest file for this project ID that is valid
|
||||||
|
* (correct Minecraft version, release channel, modloader, etc.).
|
||||||
|
*/
|
||||||
|
"project-id": number
|
||||||
|
}
|
||||||
|
/** An update source for updating mods downloaded from Modrinth. */
|
||||||
|
"modrinth"?: {
|
||||||
|
/**
|
||||||
|
* A string representing the unique mod ID of this mod.
|
||||||
|
* Updating will retrieve the latest file for this project ID that is valid
|
||||||
|
* (correct Minecraft version, release channel, modloader, etc.).
|
||||||
|
*/
|
||||||
|
"mod-id": string
|
||||||
|
/**
|
||||||
|
* A string representing the unique version ID of this file.
|
||||||
|
* This can be used if more metadata needs to be obtained relating to the mod.
|
||||||
|
*/
|
||||||
|
"version": string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user