commit fd56aa7aa0bb8dc94f98ca1c15585e152ed1fcf4 Author: Moritz Ruth Date: Tue Aug 16 00:24:29 2022 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67ccce4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.idea/ +dist/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6b5bf98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022 Moritz Ruth + +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. diff --git a/docs/horizr.md b/docs/horizr.md new file mode 100644 index 0000000..8c97364 --- /dev/null +++ b/docs/horizr.md @@ -0,0 +1,40 @@ +# horizr + +## CLI +**Note:** Most commands are interactive and therefore not suitable for usage in scripts. + +Commands expecting a `MOD_ID` will reuse the ID from the last command if none is provided. + +All commands (aside from `init`) expect to find a `horizr.json` file in their current working directory. + +### init +Initialize a new pack in the current working directory. + +### info +Print information about the pack. + +### search NAME +Search for mods by `NAME` and allow selecting one from the results. + +Selecting a mod has the same effect as the `mod MOD_ID` subcommand. + +### add MOD_ID +Adds the mod to the pack. + +### remove MOD_ID +Remove the mod from the pack. + +### refresh +Fetches information about updates. + +### update MOD_ID +Update the mod to a newer version. + +### mod MOD_ID +Print information about the mod. + +### export modrinth +Export the pack into `./NAME.mrpack` for Modrinth. + +### export packwiz +Export the pack into the `./packwiz` directory for packwiz. diff --git a/package.json b/package.json new file mode 100644 index 0000000..1a9ed1d --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "horizr-cli", + "version": "1.0.0", + "private": true, + "main": "./src/main.ts", + "type": "module", + "license": "MIT", + "scripts": { + "start": "tsx .", + "build": "tsc" + }, + "oclif": { + "bin": "horizr", + "dirname": "horizr", + "commands": "./dist/commands", + "plugins": [ + "@oclif/plugin-plugins" + ], + "topicSeparator": " " + }, + "bin": { + "horizr": "./bin/horizr" + }, + "dependencies": { + "commander": "^9.4.0", + "dedent": "^0.7.0", + "env-paths": "^3.0.0", + "figures": "^5.0.0", + "find-up": "^6.3.0", + "fs-extra": "^10.1.0", + "got": "^12.3.1", + "hasha": "^5.2.2", + "keyv-file": "^0.2.0", + "kleur": "^4.1.5", + "lodash-es": "^4.17.21", + "loud-rejection": "^2.2.0", + "nanoid": "^4.0.0", + "ora": "^6.1.2", + "p-limit": "^4.0.0", + "s-ago": "^2.2.0", + "semver": "^7.3.7", + "wrap-ansi": "^8.0.1", + "zod": "^3.18.0" + }, + "devDependencies": { + "@types/dedent": "^0.7.0", + "@types/fs-extra": "^9.0.13", + "@types/lodash-es": "^4.17.6", + "@types/node": "^18.7.3", + "@types/semver": "^7.3.12", + "@types/wrap-ansi": "^8.0.1", + "tsx": "^3.8.2", + "type-fest": "^2.18.0", + "typescript": "^4.7.4" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..c66d42e --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1002 @@ +lockfileVersion: 5.4 + +specifiers: + '@types/dedent': ^0.7.0 + '@types/fs-extra': ^9.0.13 + '@types/lodash-es': ^4.17.6 + '@types/node': ^18.7.3 + '@types/semver': ^7.3.12 + '@types/wrap-ansi': ^8.0.1 + commander: ^9.4.0 + dedent: ^0.7.0 + env-paths: ^3.0.0 + figures: ^5.0.0 + find-up: ^6.3.0 + fs-extra: ^10.1.0 + got: ^12.3.1 + hasha: ^5.2.2 + keyv-file: ^0.2.0 + kleur: ^4.1.5 + lodash-es: ^4.17.21 + loud-rejection: ^2.2.0 + nanoid: ^4.0.0 + ora: ^6.1.2 + p-limit: ^4.0.0 + s-ago: ^2.2.0 + semver: ^7.3.7 + tsx: ^3.8.2 + type-fest: ^2.18.0 + typescript: ^4.7.4 + wrap-ansi: ^8.0.1 + zod: ^3.18.0 + +dependencies: + commander: 9.4.0 + dedent: 0.7.0 + env-paths: 3.0.0 + figures: 5.0.0 + find-up: 6.3.0 + fs-extra: 10.1.0 + got: 12.3.1 + hasha: 5.2.2 + keyv-file: 0.2.0 + kleur: 4.1.5 + lodash-es: 4.17.21 + loud-rejection: 2.2.0 + nanoid: 4.0.0 + ora: 6.1.2 + p-limit: 4.0.0 + s-ago: 2.2.0 + semver: 7.3.7 + wrap-ansi: 8.0.1 + zod: 3.18.0 + +devDependencies: + '@types/dedent': 0.7.0 + '@types/fs-extra': 9.0.13 + '@types/lodash-es': 4.17.6 + '@types/node': 18.7.3 + '@types/semver': 7.3.12 + '@types/wrap-ansi': 8.0.1 + tsx: 3.8.2 + type-fest: 2.18.0 + typescript: 4.7.4 + +packages: + + /@esbuild-kit/cjs-loader/2.3.3: + resolution: {integrity: sha512-Rt4O1mXlPEDVxvjsHLgbtHVdUXYK9C1/6ThpQnt7FaXIjUOsI6qhHYMgALhNnlIMZffag44lXd6Dqgx3xALbpQ==} + dependencies: + '@esbuild-kit/core-utils': 2.1.0 + get-tsconfig: 4.2.0 + dev: true + + /@esbuild-kit/core-utils/2.1.0: + resolution: {integrity: sha512-fZirrc2KjeTumVjE4bpleWOk2gD83b7WuGeQqOceKFQL+heNKKkNB5G5pekOUTLzfSBc0hP7hCSBoD9TuR0hLw==} + dependencies: + esbuild: 0.14.54 + source-map-support: 0.5.21 + dev: true + + /@esbuild-kit/esm-loader/2.4.2: + resolution: {integrity: sha512-N9dPKAj8WOx6djVnStgILWXip4fjDcBk9L7azO0/uQDpu8Ee0eaL78mkN4Acid9BzvNAKWwdYXFJZnsVahNEew==} + dependencies: + '@esbuild-kit/core-utils': 2.1.0 + get-tsconfig: 4.2.0 + dev: true + + /@esbuild/linux-loong64/0.14.54: + resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@sindresorhus/is/5.3.0: + resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==} + engines: {node: '>=14.16'} + dev: false + + /@szmarczak/http-timer/5.0.1: + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + dependencies: + defer-to-connect: 2.0.1 + dev: false + + /@types/cacheable-request/6.0.2: + resolution: {integrity: sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==} + dependencies: + '@types/http-cache-semantics': 4.0.1 + '@types/keyv': 3.1.4 + '@types/node': 18.7.3 + '@types/responselike': 1.0.0 + dev: false + + /@types/dedent/0.7.0: + resolution: {integrity: sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==} + dev: true + + /@types/fs-extra/9.0.13: + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + dependencies: + '@types/node': 18.7.3 + dev: true + + /@types/http-cache-semantics/4.0.1: + resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} + dev: false + + /@types/json-buffer/3.0.0: + resolution: {integrity: sha512-3YP80IxxFJB4b5tYC2SUPwkg0XQLiu0nWvhRgEatgjf+29IcWO9X1k8xRv5DGssJ/lCrjYTjQPcobJr2yWIVuQ==} + dev: false + + /@types/keyv/3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 18.7.3 + dev: false + + /@types/lodash-es/4.17.6: + resolution: {integrity: sha512-R+zTeVUKDdfoRxpAryaQNRKk3105Rrgx2CFRClIgRGaqDTdjsm8h6IYA8ir584W3ePzkZfst5xIgDwYrlh9HLg==} + dependencies: + '@types/lodash': 4.14.182 + dev: true + + /@types/lodash/4.14.182: + resolution: {integrity: sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q==} + dev: true + + /@types/node/18.7.3: + resolution: {integrity: sha512-LJgzOEwWuMTBxHzgBR/fhhBOWrvBjvO+zPteUgbbuQi80rYIZHrk1mNbRUqPZqSLP2H7Rwt1EFLL/tNLD1Xx/w==} + + /@types/responselike/1.0.0: + resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==} + dependencies: + '@types/node': 18.7.3 + dev: false + + /@types/semver/7.3.12: + resolution: {integrity: sha512-WwA1MW0++RfXmCr12xeYOOC5baSC9mSb0ZqCquFzKhcoF4TvHu5MKOuXsncgZcpVFhB1pXd5hZmM0ryAoCp12A==} + dev: true + + /@types/wrap-ansi/8.0.1: + resolution: {integrity: sha512-cjwgM6WWy9YakrQ36Pq0vg5XoNblVEaNq+/pHngKl4GyyDIxTeskPoG+tp4LsRk0lHrA4LaLJqlvYridi7mzlw==} + dev: true + + /ansi-regex/6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + + /ansi-styles/6.1.0: + resolution: {integrity: sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==} + engines: {node: '>=12'} + dev: false + + /array-find-index/1.0.2: + resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} + engines: {node: '>=0.10.0'} + dev: false + + /base64-js/1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + + /bl/5.0.0: + resolution: {integrity: sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==} + dependencies: + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 3.6.0 + dev: false + + /buffer-from/1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer/6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /cacheable-lookup/6.1.0: + resolution: {integrity: sha512-KJ/Dmo1lDDhmW2XDPMo+9oiy/CeqosPguPCrgcVzKyZrL6pM1gU2GmPY/xo6OQPTUaA/c0kwHuywB4E6nmT9ww==} + engines: {node: '>=10.6.0'} + dev: false + + /cacheable-request/7.0.2: + resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.0 + keyv: 4.3.3 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + dev: false + + /chalk/5.0.1: + resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + + /cli-cursor/4.0.0: + resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + restore-cursor: 4.0.0 + dev: false + + /cli-spinners/2.7.0: + resolution: {integrity: sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw==} + engines: {node: '>=6'} + dev: false + + /clone-response/1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + dependencies: + mimic-response: 1.0.1 + dev: false + + /clone/1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + dev: false + + /commander/9.4.0: + resolution: {integrity: sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==} + engines: {node: ^12.20.0 || >=14} + dev: false + + /compress-brotli/1.3.8: + resolution: {integrity: sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==} + engines: {node: '>= 12'} + dependencies: + '@types/json-buffer': 3.0.0 + json-buffer: 3.0.1 + dev: false + + /currently-unhandled/0.4.1: + resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} + engines: {node: '>=0.10.0'} + dependencies: + array-find-index: 1.0.2 + dev: false + + /debug/4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + /decompress-response/6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + dependencies: + mimic-response: 3.1.0 + dev: false + + /dedent/0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + dev: false + + /defaults/1.0.3: + resolution: {integrity: sha512-s82itHOnYrN0Ib8r+z7laQz3sdE+4FP3d9Q7VLO7U+KRT+CR0GsWuyHxzdAY82I7cXv0G/twrqomTJLOssO5HA==} + dependencies: + clone: 1.0.4 + dev: false + + /defer-to-connect/2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + dev: false + + /eastasianwidth/0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: false + + /emoji-regex/9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: false + + /end-of-stream/1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + + /env-paths/3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /esbuild-android-64/0.14.54: + resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64/0.14.54: + resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64/0.14.54: + resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64/0.14.54: + resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64/0.14.54: + resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64/0.14.54: + resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32/0.14.54: + resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64/0.14.54: + resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm/0.14.54: + resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64/0.14.54: + resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le/0.14.54: + resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le/0.14.54: + resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64/0.14.54: + resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x/0.14.54: + resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.14.54: + resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.14.54: + resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.14.54: + resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.14.54: + resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.14.54: + resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.14.54: + resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.14.54: + resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/linux-loong64': 0.14.54 + esbuild-android-64: 0.14.54 + esbuild-android-arm64: 0.14.54 + esbuild-darwin-64: 0.14.54 + esbuild-darwin-arm64: 0.14.54 + esbuild-freebsd-64: 0.14.54 + esbuild-freebsd-arm64: 0.14.54 + esbuild-linux-32: 0.14.54 + esbuild-linux-64: 0.14.54 + esbuild-linux-arm: 0.14.54 + esbuild-linux-arm64: 0.14.54 + esbuild-linux-mips64le: 0.14.54 + esbuild-linux-ppc64le: 0.14.54 + esbuild-linux-riscv64: 0.14.54 + esbuild-linux-s390x: 0.14.54 + esbuild-netbsd-64: 0.14.54 + esbuild-openbsd-64: 0.14.54 + esbuild-sunos-64: 0.14.54 + esbuild-windows-32: 0.14.54 + esbuild-windows-64: 0.14.54 + esbuild-windows-arm64: 0.14.54 + dev: true + + /escape-string-regexp/5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + dev: false + + /figures/5.0.0: + resolution: {integrity: sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==} + engines: {node: '>=14'} + dependencies: + escape-string-regexp: 5.0.0 + is-unicode-supported: 1.2.0 + dev: false + + /find-up/6.3.0: + resolution: {integrity: sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + locate-path: 7.1.1 + path-exists: 5.0.0 + dev: false + + /form-data-encoder/2.0.1: + resolution: {integrity: sha512-Oy+P9w5mnO4TWXVgUiQvggNKPI9/ummcSt5usuIV6HkaLKigwzPpoenhEqmGmx3zHqm6ZLJ+CR/99N8JLinaEw==} + engines: {node: '>= 14.17'} + dev: false + + /fs-extra/10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.10 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: false + + /fs-extra/4.0.3: + resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==} + dependencies: + graceful-fs: 4.2.10 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: false + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /get-stream/5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.0 + dev: false + + /get-stream/6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: false + + /get-tsconfig/4.2.0: + resolution: {integrity: sha512-X8u8fREiYOE6S8hLbq99PeykTDoLVnxvF4DjWKJmz9xy2nNRdUcV8ZN9tniJFeKyTU3qnC9lL8n4Chd6LmVKHg==} + dev: true + + /got/12.3.1: + resolution: {integrity: sha512-tS6+JMhBh4iXMSXF6KkIsRxmloPln31QHDlcb6Ec3bzxjjFJFr/8aXdpyuLmVc9I4i2HyBHYw1QU5K1ruUdpkw==} + engines: {node: '>=14.16'} + dependencies: + '@sindresorhus/is': 5.3.0 + '@szmarczak/http-timer': 5.0.1 + '@types/cacheable-request': 6.0.2 + '@types/responselike': 1.0.0 + cacheable-lookup: 6.1.0 + cacheable-request: 7.0.2 + decompress-response: 6.0.0 + form-data-encoder: 2.0.1 + get-stream: 6.0.1 + http2-wrapper: 2.1.11 + lowercase-keys: 3.0.0 + p-cancelable: 3.0.0 + responselike: 2.0.1 + dev: false + + /graceful-fs/4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: false + + /hasha/5.2.2: + resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} + engines: {node: '>=8'} + dependencies: + is-stream: 2.0.1 + type-fest: 0.8.1 + dev: false + + /http-cache-semantics/4.1.0: + resolution: {integrity: sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==} + dev: false + + /http2-wrapper/2.1.11: + resolution: {integrity: sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ==} + engines: {node: '>=10.19.0'} + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + dev: false + + /ieee754/1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /is-interactive/2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + dev: false + + /is-stream/2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: false + + /is-unicode-supported/1.2.0: + resolution: {integrity: sha512-wH+U77omcRzevfIG8dDhTS0V9zZyweakfD01FULl97+0EHiJTTZtJqxPSkIIo/SDPv/i07k/C9jAPY+jwLLeUQ==} + engines: {node: '>=12'} + dev: false + + /json-buffer/3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: false + + /jsonfile/4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.10 + dev: false + + /jsonfile/6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.10 + dev: false + + /keyv-file/0.2.0: + resolution: {integrity: sha512-zUQ11eZRmilEUpV1gJSj8mBAHjyXpleQo1iCS0khb+GFRhiPfwavWgn4eDUKNlOyMZzmExnISl8HE1hNbim0gw==} + dependencies: + debug: 4.3.4 + fs-extra: 4.0.3 + tslib: 1.14.1 + transitivePeerDependencies: + - supports-color + dev: false + + /keyv/4.3.3: + resolution: {integrity: sha512-AcysI17RvakTh8ir03+a3zJr5r0ovnAH/XTXei/4HIv3bL2K/jzvgivLK9UuI/JbU1aJjM3NSAnVvVVd3n+4DQ==} + dependencies: + compress-brotli: 1.3.8 + json-buffer: 3.0.1 + dev: false + + /kleur/4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: false + + /locate-path/7.1.1: + resolution: {integrity: sha512-vJXaRMJgRVD3+cUZs3Mncj2mxpt5mP0EmNOsxRSZRMlbqjvxzDEOIUWXGmavo0ZC9+tNZCBLQ66reA11nbpHZg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-locate: 6.0.0 + dev: false + + /lodash-es/4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + + /log-symbols/5.1.0: + resolution: {integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==} + engines: {node: '>=12'} + dependencies: + chalk: 5.0.1 + is-unicode-supported: 1.2.0 + dev: false + + /loud-rejection/2.2.0: + resolution: {integrity: sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ==} + engines: {node: '>=8'} + dependencies: + currently-unhandled: 0.4.1 + signal-exit: 3.0.7 + dev: false + + /lowercase-keys/2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: false + + /lowercase-keys/3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /lru-cache/6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: false + + /mimic-fn/2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + dev: false + + /mimic-response/1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: false + + /mimic-response/3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + dev: false + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + + /nanoid/4.0.0: + resolution: {integrity: sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + dev: false + + /normalize-url/6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + dev: false + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: false + + /onetime/5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + dev: false + + /ora/6.1.2: + resolution: {integrity: sha512-EJQ3NiP5Xo94wJXIzAyOtSb0QEIAUu7m8t6UZ9krbz0vAJqr92JpcK/lEXg91q6B9pEGqrykkd2EQplnifDSBw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + bl: 5.0.0 + chalk: 5.0.1 + cli-cursor: 4.0.0 + cli-spinners: 2.7.0 + is-interactive: 2.0.0 + is-unicode-supported: 1.2.0 + log-symbols: 5.1.0 + strip-ansi: 7.0.1 + wcwidth: 1.0.1 + dev: false + + /p-cancelable/3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + dev: false + + /p-limit/4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.0.0 + dev: false + + /p-locate/6.0.0: + resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + p-limit: 4.0.0 + dev: false + + /path-exists/5.0.0: + resolution: {integrity: sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: false + + /pump/3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + + /quick-lru/5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: false + + /readable-stream/3.6.0: + resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: false + + /resolve-alpn/1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + dev: false + + /responselike/2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + dependencies: + lowercase-keys: 2.0.0 + dev: false + + /restore-cursor/4.0.0: + resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + dev: false + + /s-ago/2.2.0: + resolution: {integrity: sha512-t6Q/aFCCJSBf5UUkR/WH0mDHX8EGm2IBQ7nQLobVLsdxOlkryYMbOlwu2D4Cf7jPUp0v1LhfPgvIZNoi9k8lUA==} + dev: false + + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /semver/7.3.7: + resolution: {integrity: sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: false + + /signal-exit/3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false + + /source-map-support/0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /string-width/5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.0.1 + dev: false + + /string_decoder/1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /strip-ansi/7.0.1: + resolution: {integrity: sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false + + /tslib/1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: false + + /tsx/3.8.2: + resolution: {integrity: sha512-Jf9izq3Youry5aEarspf6Gm+v/IE2A2xP7YVhtNH1VSCpM0jjACg7C3oD5rIoLBfXWGJSZj4KKC2bwE0TgLb2Q==} + hasBin: true + dependencies: + '@esbuild-kit/cjs-loader': 2.3.3 + '@esbuild-kit/core-utils': 2.1.0 + '@esbuild-kit/esm-loader': 2.4.2 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /type-fest/0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + dev: false + + /type-fest/2.18.0: + resolution: {integrity: sha512-pRS+/yrW5TjPPHNOvxhbNZexr2bS63WjrMU8a+VzEBhUi9Tz1pZeD+vQz3ut0svZ46P+SRqMEPnJmk2XnvNzTw==} + engines: {node: '>=12.20'} + dev: true + + /typescript/4.7.4: + resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /universalify/0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: false + + /universalify/2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: false + + /util-deprecate/1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false + + /wcwidth/1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + dependencies: + defaults: 1.0.3 + dev: false + + /wrap-ansi/8.0.1: + resolution: {integrity: sha512-QFF+ufAqhoYHvoHdajT/Po7KoXVBPXS2bgjIam5isfWJPfIOnQZ50JtUiVvCv/sjgacf3yRrt2ZKUZ/V4itN4g==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.1.0 + string-width: 5.1.2 + strip-ansi: 7.0.1 + dev: false + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: false + + /yallist/4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: false + + /yocto-queue/1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + dev: false + + /zod/3.18.0: + resolution: {integrity: sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==} + dev: false diff --git a/src/commands/modrinth.ts b/src/commands/modrinth.ts new file mode 100644 index 0000000..e643cb9 --- /dev/null +++ b/src/commands/modrinth.ts @@ -0,0 +1,317 @@ +import { Command } from "commander" +import { take } from "lodash-es" +import { usePack } from "../pack.js" +import kleur from "kleur" +import { optionParsePositiveInteger, truncateWithEllipsis } from "../utils.js" +import { default as wrapAnsi } from "wrap-ansi" +import figures from "figures" +import { + addModrinthMod, + findModForModrinthMod, + getModFileDataForModrinthVersion, isModrinthVersionCompatible, + modrinthApi, + ModrinthMod, + ModrinthVersion, + ModrinthVersionRelation, + sortModrinthVersionsByPreference +} from "../modrinth.js" +import dedent from "dedent" +import ago from "s-ago" +import semver from "semver" +import { output } from "../output.js" + +const modrinthCommand = new Command("modrinth") + .alias("mr") + .option("--clear-cache", "Clear the cache before doing the operation.") + .on("option:clear-cache", () => { + modrinthApi.clearCache() + output.println(kleur.green("Cache was cleared.\n")) + }) + +modrinthCommand.command("search ") + .description("Search for mods.") + .option("-l, --limit ", "Limit the number of results", optionParsePositiveInteger, 8) + .option("-s, --skip ", "Skip results", optionParsePositiveInteger, 0) + .action(async (query, options) => { + const pack = await usePack() + const loader = output.startLoading(`Searching for ${kleur.yellow(query)}`) + const { results } = await modrinthApi.searchMods(pack.horizrFile.loader, pack.horizrFile.versions.minecraft, query, options) + loader.stop() + + output.println( + results.map(result => + `${kleur.blue(result.id)} ${kleur.bold(truncateWithEllipsis(result.title, 30))} ${kleur.gray(`(↓ ${result.downloadsCount})`)}\n` + + wrapAnsi(result.description, process.stdout.columns) + ) + .join("\n\n") + ) + }) + +const colorBySideCompatibility: Record = { + optional: kleur.blue, + required: kleur.green, + unsupported: kleur.red +} + +const modrinthModCommand = modrinthCommand.command("mod") + +modrinthModCommand.command("info ") + .description("Show information about the mod.") + .action(async id => { + const loader = output.startLoading("Fetching mod information") + const modrinthMod = await modrinthApi.getMod(id) + if (modrinthMod === null) return loader.failAndExit("not found") + + loader.stop() + const existingMod = await findModForModrinthMod(modrinthMod) + + output.println(dedent` + ${kleur.bold(modrinthMod.title)} ${kleur.gray(`(↓ ${modrinthMod.downloadsCount})`)} + ${wrapAnsi(modrinthMod.description, process.stdout.columns)} + + Client Server + ${colorBySideCompatibility[modrinthMod.clientSide](modrinthMod.clientSide.padEnd(12, " "))} ${colorBySideCompatibility[modrinthMod.serverSide](modrinthMod.serverSide)} + + License: ${kleur.yellow(modrinthMod.licenseCode.toUpperCase())} + Last update: ${kleur.yellow(ago(modrinthMod.updateDate))}\ + ${existingMod === null ? "" : kleur.green("\n\nThis mod is in the pack.")} + + https://modrinth.com/mod/${modrinthMod.slug} + `) + }) + +modrinthModCommand.command("versions ") + .description("Show a list of compatible versions of the mod.") + .option("-l, --limit ", "Limit the number of versions displayed.", optionParsePositiveInteger, 3) + .action(async (id, options) => { + const pack = await usePack() + + const loader = output.startLoading("Fetching mod information") + const modrinthMod = await modrinthApi.getMod(id) + if (modrinthMod === null) return loader.failAndExit("not found") + + const existingMod = await findModForModrinthMod(modrinthMod) + + loader.setText("Fetching versions") + const modrinthVersions = await modrinthApi.listVersions(id, pack.horizrFile.loader, pack.horizrFile.versions.minecraft) + loader.stop() + + if (modrinthVersions.length === 0) { + const message = dedent` + There are no versions compatible with the pack (Loader: ${kleur.yellow(pack.horizrFile.loader)}, \ + Minecraft ${kleur.yellow(pack.horizrFile.versions.minecraft)}). + ` + + output.println(kleur.red(message)) + } else { + const versions = take(sortModrinthVersionsByPreference(modrinthVersions), options.limit) + .map(modrinthVersion => { + const state = existingMod !== null && existingMod.modFile.source.versionId === modrinthVersion.id + ? kleur.bgGreen().black(" active ") + "\n\n" + : modrinthVersion.isFeatured + ? kleur.green("featured") + "\n\n" + : "" + + return dedent` + ${kleur.blue(modrinthVersion.id)} ${kleur.bold(modrinthVersion.versionString)} ${kleur.gray(`(↓ ${modrinthVersion.downloadsCount})`)} + ${state}\ + ${modrinthVersion.name !== modrinthVersion.versionString ? `Name: ${kleur.yellow(modrinthVersion.name)}\n` : ""}\ + Channel: ${kleur.yellow(modrinthVersion.releaseChannel)} + Minecraft versions: ${kleur.yellow(modrinthVersion.supportedMinecraftVersions.join(", "))} + + Publication: ${kleur.yellow(ago(modrinthVersion.publicationDate))} + + https://modrinth.com/mod/${modrinthVersion.projectId}/version/${modrinthVersion.id} + ` + }) + .join("\n\n") + + output.println(versions) + } + }) + +modrinthModCommand.command("activate ") + .description("Activate the recommended version of the mod.") + .alias("a") + .option("-f, --force", "Replace a different version already active.") + .action(async (id, options) => { + const pack = await usePack() + + const loader = output.startLoading("Fetching mod information") + const modrinthMod = await modrinthApi.getMod(id) + if (modrinthMod === null) return loader.failAndExit("not found") + + loader.setText("Fetching versions") + const modrinthVersions = await modrinthApi.listVersions(id, pack.horizrFile.loader, pack.horizrFile.versions.minecraft) + loader.stop() + + if (modrinthVersions.length === 0) return output.failAndExit("There is no compatible version of this mod.") + + const sortedModrinthVersions = sortModrinthVersionsByPreference(modrinthVersions) + const modrinthVersion = sortedModrinthVersions[0] + + await handleActivate(modrinthMod, modrinthVersion, options.force) + }) + +const colorByRelationType: Record = { + "embedded_dependency": kleur.green, + "soft_dependency": kleur.magenta, + "hard_dependency": kleur.yellow, + "incompatible": kleur.red +} + +const nullVersionStringByRelationType: Record = { + "embedded_dependency": "unknown version", + "soft_dependency": "any version", + "hard_dependency": "any version", + "incompatible": "all versions" +} + +const versionStateStrings = { + "active": kleur.bgGreen().black(" active "), + "compatible": kleur.blue("compatible"), + "incompatible": kleur.red("incompatible"), + "newer_version": `${kleur.bgYellow().black(" older version active ")} ${figures.arrowRight} EXISTING_VERSION`, + "older_version": `${kleur.bgYellow().black(" newer version active ")} ${figures.arrowRight} EXISTING_VERSION`, + "different_version": `${kleur.bgYellow().black(" different version active ")} ${figures.arrowRight} EXISTING_VERSION` +} + +async function getRelationsListLines(relations: ModrinthVersionRelation[]) { + return await Promise.all(relations.map(async relation => { + const color = colorByRelationType[relation.type] + + const relatedVersion = relation.versionId === null ? null : (await modrinthApi.getVersion(relation.versionId)) + const versionString = relatedVersion === null ? nullVersionStringByRelationType[relation.type] : relatedVersion.versionString + const relatedMod = (await modrinthApi.getMod(relation.projectId === null ? relatedVersion!.projectId : relation.projectId))! + + return `${color(figures.circleFilled)} ${relatedMod.title}${relation.projectId ? ` (${kleur.blue(relation.projectId)})` : ""}: ` + + `${versionString}${relation.versionId ? ` (${kleur.blue(relation.versionId)})` + " " : ""}` + })) +} + +const modrinthVersionCommand = modrinthCommand.command("version") + +modrinthVersionCommand.command("info ") + .description("Show information about the version.") + .action(async id => { + const pack = await usePack() + const loader = output.startLoading("Fetching version information") + + const modrinthVersion = await modrinthApi.getVersion(id) + if (modrinthVersion === null) return loader.failAndExit("not found") + + loader.setText("Fetching mod information") + const modrinthMod = (await modrinthApi.getMod(modrinthVersion.projectId))! + + const existingMod = await findModForModrinthMod(modrinthMod) + + let state: keyof typeof versionStateStrings + if (existingMod === null) state = isModrinthVersionCompatible(modrinthVersion, pack) ? "compatible" : "incompatible" + else { + if (existingMod.modFile.source.versionId === modrinthVersion.id) state = "active" + else { + const existingSemver = semver.parse(existingMod.modFile.file.version) + const newSemver = semver.parse(modrinthVersion.versionString) + + if (existingSemver === null || newSemver === null) state = "different_version" + else { + const comparison = newSemver.compare(existingSemver) + + if (comparison === 1) state = "newer_version" + else if (comparison === -1) state = "older_version" + else state = "active" // this should not happen: the versionString is the same but the versionId is different + } + } + } + + loader.setText("Resolving relations") + + const relationsList = modrinthVersion.relations.length !== 0 ? (await getRelationsListLines(modrinthVersion.relations)).join("\n") : kleur.gray("none") + + const relationsColorKey = `${colorByRelationType.hard_dependency("hard dependency")}, ${colorByRelationType.soft_dependency("soft dependency")}, ` + + `${colorByRelationType.embedded_dependency("embedded")}, ${colorByRelationType.incompatible("incompatible")}` + + loader.stop() + + output.println(dedent` + ${kleur.underline(modrinthMod.title)} ${kleur.yellow(`${modrinthVersion.versionString} (${modrinthVersion.releaseChannel})`)} + ${versionStateStrings[state].replace("EXISTING_VERSION", existingMod?.modFile?.file.version ?? "ERROR")} + + Version name: ${kleur.yellow(modrinthVersion.name)} ${kleur.gray(ago(modrinthVersion.publicationDate))} + Minecraft versions: ${modrinthVersion.supportedMinecraftVersions.map(version => version === pack.horizrFile.versions.minecraft ? kleur.green(version) : kleur.red(version)).join(", ")} + Loaders: ${modrinthVersion.supportedLoaders.map(loader => loader === pack.horizrFile.loader ? kleur.green(loader) : kleur.red(loader)).join(", ")} + + Related mods: ${relationsColorKey} + ${relationsList} + + https://modrinth.com/mod/${modrinthMod.slug}/version/${modrinthVersion.versionString} + `) + }) + +modrinthVersionCommand.command("activate ") + .description("Activate the mod version.") + .alias("a") + .option("-f, --force", "Replace a different version already active.") + .action(async (id, options) => { + const pack = await usePack() + const loader = output.startLoading("Fetching version information") + + const modrinthVersion = await modrinthApi.getVersion(id) + if (modrinthVersion === null) return loader.failAndExit("not found") + + loader.setText("Fetching mod information") + const modrinthMod = (await modrinthApi.getMod(modrinthVersion.projectId))! + loader.stop() + + if (!isModrinthVersionCompatible(modrinthVersion, pack)) return output.failAndExit("This version is not compatible with the pack.") + + await handleActivate(modrinthMod, modrinthVersion, options.force) + }) + +modrinthVersionCommand.command("export") + .description("Generate a Modrinth pack file suitable for uploading") + .action(async () => { + // TODO: Implement export + }) + +async function handleActivate(modrinthMod: ModrinthMod, modrinthVersion: ModrinthVersion, force: boolean) { + const existingMod = await findModForModrinthMod(modrinthMod) + + if (existingMod === null) { + await addModrinthMod(modrinthMod, modrinthVersion) + output.println(`${modrinthMod.title} (${modrinthVersion.versionString}) ${kleur.green("was successfully activated.")}\n`) + + await handleDependencies(modrinthVersion.relations) + } else { + const oldVersion = existingMod.modFile.file.version + if (existingMod.modFile.source.versionId === modrinthVersion.id) { + output.println(kleur.green("This version is already installed.")) + } else if (force) { + existingMod.modFile.file = getModFileDataForModrinthVersion(modrinthMod, modrinthVersion) + existingMod.modFile.source.versionId = modrinthVersion.id + await existingMod.saveModFile() + output.println(`${kleur.green("Successfully replaced version")} ${oldVersion} ${kleur.green("of")} ${modrinthMod.title} ${kleur.green("with")} ${modrinthVersion.versionString}${kleur.green(".")}`) + + await handleDependencies(modrinthVersion.relations) + } else { + output.failAndExit(`There is already a different version of this mod installed.\nRun this command again with ${kleur.yellow("-f")} to change the version.`) + } + } +} + +async function handleDependencies(relations: ModrinthVersionRelation[]) { + const loader = output.startLoading("Fetching dependency information") + const lines = await getRelationsListLines(relations.filter(relation => relation.type === "hard_dependency" || relation.type === "soft_dependency")) + + if (lines.length !== 0) { + output.println(dedent` + \n${kleur.underline("Dependencies")} ${colorByRelationType.hard_dependency("hard")}, ${colorByRelationType.soft_dependency("soft")} + + ${lines.join("\n")} + `) + } + + loader.stop() +} + +export { modrinthCommand } diff --git a/src/commands/packwiz.ts b/src/commands/packwiz.ts new file mode 100644 index 0000000..546ee8d --- /dev/null +++ b/src/commands/packwiz.ts @@ -0,0 +1,94 @@ +import { Command } from "commander" +import { usePack } from "../pack.js" +import fs from "fs-extra" +import dedent from "dedent" +import kleur from "kleur" +import { relative } from "path" +import { getSha512HexHash } from "../utils.js" +import { output } from "../output.js" + +const packwizCommand = new Command("packwiz") + +interface IndexedFile { + path: string + sha512HashHex: string + isMeta: boolean +} + +packwizCommand.command("export") + .description("Generates a packwiz pack in the packwiz directory") + .action(async () => { + const pack = await usePack() + + if (pack.horizrFile.loader !== "fabric") + output.println(kleur.yellow(`packwiz does not yet support the ${kleur.reset(pack.horizrFile.loader)} loader. No loader will be specified.`)) + + const loader = output.startLoading("Generating") + + const rootDirectoryPath = pack.resolvePath("packwiz") + await fs.remove(rootDirectoryPath) + await fs.mkdirp(pack.resolvePath("packwiz/mods")) + + const indexedFiles: IndexedFile[] = [] + for (const mod of pack.mods) { + if (!mod.modFile.enabled) output.warn(`${kleur.yellow(mod.modFile.name)} is disabled and will not be included.`) + const innerLoader = output.startLoading(`Generating ${kleur.yellow(mod.id + ".toml")} (${indexedFiles.length + 1}/${pack.mods.length})`) + + const content = dedent` + name = ${JSON.stringify(mod.modFile.name)} + filename = ${JSON.stringify(mod.modFile.file.name)} + side = "${mod.modFile.side.replace("client+server", "both")}" + + [download] + hash-format = "${mod.modFile.file.hashAlgorithm}" + hash = ${JSON.stringify(mod.modFile.file.hash)} + url = ${JSON.stringify(mod.modFile.file.downloadUrl)} + ` + + const path = pack.resolvePath("packwiz/mods", mod.id + ".toml") + await fs.writeFile(path, content) + + indexedFiles.push({ + path: relative(rootDirectoryPath, path), + isMeta: true, + sha512HashHex: await getSha512HexHash(content) + }) + + innerLoader.stop() + } + + const index = dedent` + hash-format = "sha512" + + ${indexedFiles.map(file => dedent` + [[files]] + file = ${JSON.stringify(file.path)} + hash = "${file.sha512HashHex}" + metafile = ${file.isMeta} + `).join("\n\n")} + ` + + await fs.writeFile(pack.resolvePath("packwiz/index.toml"), index) + const indexHash = await getSha512HexHash(index) + + await fs.writeFile(pack.resolvePath("packwiz/pack.toml"), dedent` + name = ${JSON.stringify(pack.horizrFile.meta.name)} + authors = ${JSON.stringify(pack.horizrFile.meta.authors.join(", "))}\ + ${pack.horizrFile.meta.description === undefined ? "" : "\n" + `description = ${JSON.stringify(pack.horizrFile.meta.description)}`} + pack-format = "packwiz:1.0.0" + + [versions] + minecraft = "${pack.horizrFile.versions.minecraft}"\ + ${pack.horizrFile.loader === "fabric" ? "\n" + `fabric = ${JSON.stringify(pack.horizrFile.versions.loader)}` : ""} + + [index] + file = "index.toml" + hash-format = "sha512" + hash = "${indexHash}" + `) + + loader.stop() + output.println(kleur.green("Successfully generated packwiz pack.")) + }) + +export { packwizCommand} diff --git a/src/files.ts b/src/files.ts new file mode 100644 index 0000000..6507c89 --- /dev/null +++ b/src/files.ts @@ -0,0 +1,128 @@ +import { SafeParseError, z, ZodRawShape } from "zod" +import kleur from "kleur" +import fs from "fs-extra" +import * as process from "process" +import { resolve, dirname } from "path" +import { findUp } from "find-up" +import { output } from "./output.js" + +export async function findPackDirectoryPath() { + if (process.argv0.endsWith("/node")) { // run using pnpm + return resolve(process.cwd(), "./test-pack") + } else { + const parent = await findUp("horizr.json") + if (parent === undefined) return output.failAndExit(`${kleur.yellow("horizr.json")} could not be found in the current working directory or any parent.`) + + return dirname(parent) + } +} + +export async function readJsonFileInPack>( + packPath: string, + filePath: string, + schema: S +): Promise | null> { + let data + + try { + data = await fs.readJson(resolve(packPath, filePath)) + } catch (e: unknown) { + if (e instanceof SyntaxError) return output.failAndExit(`${kleur.yellow(filePath)} does not contain valid JSON.`) + else return null + } + + const result = await schema.safeParseAsync(data) + if (!result.success) { + const error = (result as SafeParseError).error + return output.failAndExit(`${kleur.yellow(filePath)} is invalid:\n${error.issues.map(issue => `- ${kleur.yellow(issue.path.join("/"))} — ${kleur.red(issue.message)}`).join("\n")}`) + } + + return result.data +} + +export async function writeJsonFileInPack>(packPath: string, filePath: string, schema: S, data: z.input) { + const absolutePath = resolve(packPath, filePath) + await fs.mkdirp(dirname(absolutePath)) + + await fs.writeJson(absolutePath, schema.parse(data), { spaces: 2 }) +} + +const horizrFileSchema = z.object({ + formatVersion: z.string().or(z.number()), + meta: z.object({ + name: z.string(), + version: z.string(), + authors: z.array(z.string()), + description: z.string().optional(), + license: z.string() + }), + loader: z.enum(["fabric", "quilt"]), + versions: z.object({ + minecraft: z.string(), + loader: z.string() + }) +}) + +export type HorizrFile = z.output + +export async function readHorizrFile(packPath: string) { + const data = await readJsonFileInPack(packPath, "horizr.json", horizrFileSchema) + if (data === null) return output.failAndExit(`${kleur.yellow("horizr.json")} does not exist.`) + if (data.formatVersion !== 1) return output.failAndExit(`${kleur.yellow("horizr.json")} has unsupported format version: ${kleur.yellow(data.formatVersion)}`) + + return data +} + +const modFileModrinthSourceSchema = z.object({ + type: z.literal("modrinth"), + modId: z.string(), + versionId: z.string() +}) + +export type ModFileModrinthSource = z.output + +const modFileDataSchema = z.object({ + version: z.string(), + name: z.string(), + size: z.number().int().min(0).optional(), + downloadUrl: z.string().url(), + hashAlgorithm: z.enum(["sha1", "sha256", "sha512"]), + hash: z.string() +}) + +export type ModFileData = z.output + +const modFileSchema = z.object({ + name: z.string(), + enabled: z.boolean().default(true), + ignoreUpdates: z.boolean().default(false), + side: z.enum(["client", "server", "client+server"]), + comment: z.string().optional(), + file: modFileDataSchema, + source: z.discriminatedUnion("type", [ + modFileModrinthSourceSchema, + z.object({ type: z.literal("raw") }) + ]) +}) + +export type ModFile = z.output + +export async function readModFile(packPath: string, modId: string): Promise { + return await readJsonFileInPack(packPath, `mods/${modId}.json`, modFileSchema) +} + +export async function writeModFile(packPath: string, modId: string, data: z.input): Promise { + await writeJsonFileInPack(packPath, `mods/${modId}.json`, modFileSchema, data) +} + +export async function removeModFile(packPath: string, modId: string): Promise { + await fs.remove(resolve(packPath, `mods/${modId}.json`)) +} + +export async function readModIds(packPath: string) { + const modsPath = resolve(packPath, "./mods") + await fs.mkdirp(modsPath) + const files = await fs.readdir(modsPath, { withFileTypes: true }) + + return files.filter(file => file.isFile() && file.name.endsWith(".json")).map(file => file.name.slice(0, -5)) +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..5a2dbc7 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,95 @@ +import { Command } from "commander" +import kleur from "kleur" +import { usePack } from "./pack.js" +import loudRejection from "loud-rejection" +import { modrinthCommand } from "./commands/modrinth.js" +import { packwizCommand } from "./commands/packwiz.js" +import dedent from "dedent" +import { default as wrapAnsi } from "wrap-ansi" +import { removeModFile } from "./files.js" +import { output } from "./output.js" +import figures from "figures" +import { releaseChannelOrder } from "./modrinth.js" + +const program = new Command("horizr") + +program.command("info", { isDefault: true }) + .description("Print information about the pack.") + .action(async () => { + const pack = await usePack() + const disabledModsCount = pack.mods.filter(mod => !mod.modFile.enabled).length + const { description } = pack.horizrFile.meta + + output.println(dedent` + ${kleur.underline(pack.horizrFile.meta.name)} ${kleur.dim(`(${pack.horizrFile.meta.version})`)} + ${description === undefined ? "" : wrapAnsi(description, process.stdout.columns) + "\n"}\ + + Authors: ${kleur.yellow(pack.horizrFile.meta.authors.join(", "))} + License: ${kleur.yellow(pack.horizrFile.meta.license.toUpperCase())} + Mods: ${kleur.yellow(pack.mods.length.toString())}${disabledModsCount === 0 ? "" : ` (${disabledModsCount} disabled)`} + + Loader: ${kleur.yellow(`${pack.horizrFile.loader} v${pack.horizrFile.versions.loader}`)} + Minecraft version: ${kleur.yellow(pack.horizrFile.versions.minecraft)} + `) + }) + +program.command("remove ") + .description("Remove the mod from the pack.") + .action(async code => { + const pack = await usePack() + const mod = pack.findModByCodeOrFail(code) + + await removeModFile(pack.path, mod.id) + + output.println(`${mod.modFile.name} ${kleur.green("was removed from the pack.")}`) + }) + +program.command("update [code]") + .description("Check for updates of all mods or update a specific mod") + .option("-y, --yes", "Skip confirmations") + .option("-b, --allow-beta", "Allow beta versions") + .option("-a, --allow-alpha", "Allow alpha and beta versions") + .action(async (code, options) => { + const pack = await usePack() + const allowedReleaseChannels = releaseChannelOrder.slice(releaseChannelOrder.indexOf(options.allowAlpha ? "alpha" : options.allowBeta ? "beta" : "release")) + + if (code === undefined) { + const updates = await pack.checkForUpdates(allowedReleaseChannels) + + if (updates.length === 0) output.println(kleur.green("Everything up-to-date.")) + else { + output.println(dedent` + ${kleur.underline("Available updates")} + + ${updates.map(update => `- ${kleur.gray(update.mod.id)} ${update.mod.modFile.name}: ${kleur.red(update.activeVersion)} ${figures.arrowRight} ${kleur.green(update.availableVersion)}`).join("\n")} + `) + } + } else { + const loader = output.startLoading("Checking for an update") + const mod = pack.findModByCodeOrFail(code) + const update = await mod.checkForUpdate(allowedReleaseChannels) + + if (update === null) { + loader.stop() + output.println(kleur.green("No update available.")) + } else { + loader.setText("Updating") + await update.apply() + loader.stop() + output.println(kleur.green(`Successfully updated ${kleur.yellow(update.mod.modFile.name)} to ${kleur.yellow(update.availableVersion)}.`)) + } + } + }) + +loudRejection() + +program + .addCommand(packwizCommand) + .addCommand(modrinthCommand) + .addHelpText("afterAll", "\n" + dedent` + ${kleur.blue("code")} can be one of the following: + - The name of a file in the ${kleur.yellow("mods")} directory, optionally without the ${kleur.yellow(".json")} extension + - The ID of a Modrinth Project, prefixed with ${kleur.yellow("mr:")} + - The ID of a Modrinth Version, prefixed with ${kleur.yellow("mrv:")} + `) + .parse(process.argv) diff --git a/src/modrinth.ts b/src/modrinth.ts new file mode 100644 index 0000000..71d4877 --- /dev/null +++ b/src/modrinth.ts @@ -0,0 +1,284 @@ +import { IterableElement } from "type-fest" +import originalGot, { HTTPError, Response } from "got" +import { sortBy } from "lodash-es" +import { Loader, Mod, Pack, usePack } from "./pack.js" +import { ModFile, ModFileData, ModFileModrinthSource } from "./files.js" +import { pathExists } from "fs-extra" +import kleur from "kleur" +import { nanoid } from "nanoid/non-secure" +import { KeyvFile } from "keyv-file" +import { resolve } from "path" +import { delay, paths } from "./utils.js" +import { output } from "./output.js" + +const keyvCache = new KeyvFile({ + filename: resolve(paths.cache, "http.json"), + writeDelay: 50, + expiredCheckDelay: 24 * 3600 * 1000, + encode: JSON.stringify, + decode: JSON.parse +}) + +const got = originalGot.extend() + +async function getModrinthApiOptional(url: string): Promise { + let response: Response + + while (true) { + response = await got(url, { + prefixUrl: "https://api.modrinth.com", + headers: { + "User-Agent": "moritzruth/horizr/1.0.0 (not yet public)" + }, + cache: keyvCache, + responseType: "json", + throwHttpErrors: false + }) + + if (response.statusCode.toString().startsWith("2")) { + // success + return response.body + } else if (response.statusCode === 404) { + // not found + return null + } else if (response.statusCode === 429) { + // rate limited + const secondsUntilReset = Number(response.headers["x-ratelimit-reset"]) + output.warn(`Rate-limit exceeded. Retrying in ${kleur.yellow(secondsUntilReset)} seconds…`) + await delay(secondsUntilReset * 1000) + } else { + output.failAndExit(`A request to the Modrinth API failed with status code ${kleur.yellow(response.statusCode)}.`) + } + } +} + +async function getModrinthApi(url: string): Promise { + const response = await getModrinthApiOptional(url) + if (response === null) return output.failAndExit("Request failed with status code 404.") + return response +} + +const dependencyToRelatedVersionType: Record["type"]> = { + required: "hard_dependency", + optional: "soft_dependency", + embedded: "embedded_dependency", + incompatible: "incompatible" +} + +export type ReleaseChannel = "alpha" | "beta" | "release" +export const releaseChannelOrder: ReleaseChannel[] = ["alpha", "beta", "release"] + +export const sortModrinthVersionsByPreference = (versions: ModrinthVersion[]) => sortBy(versions, [v => releaseChannelOrder.indexOf(v.releaseChannel), "isFeatured", "publicationDate"]).reverse() + +export async function findModForModrinthMod(modrinthMod: ModrinthMod): Promise<(Mod & { modFile: ModFile & { source: ModFileModrinthSource } }) | null> { + const pack = await usePack() + + return ( + pack.mods.find( + mod => mod.modFile.source.type === "modrinth" && mod.modFile.source.modId === modrinthMod.id + ) as (Mod & { modFile: Mod & { source: ModFileModrinthSource } }) | undefined + ) ?? null +} + +export const isModrinthVersionCompatible = (modrinthVersion: ModrinthVersion, pack: Pack) => + modrinthVersion.supportedMinecraftVersions.includes(pack.horizrFile.versions.minecraft) && modrinthVersion.supportedLoaders.includes(pack.horizrFile.loader) + +export function getModFileDataForModrinthVersion(modrinthMod: ModrinthMod, modrinthModVersion: ModrinthVersion): ModFileData { + const modrinthVersionFile = findCorrectModVersionFile(modrinthModVersion.files) + + return { + version: modrinthModVersion.versionString, + hash: modrinthVersionFile.hashes.sha512, + hashAlgorithm: "sha512", + downloadUrl: modrinthVersionFile.url, + name: modrinthVersionFile.fileName, + size: modrinthVersionFile.sizeInBytes, + } +} + +export async function addModrinthMod(modrinthMod: ModrinthMod, modrinthVersion: ModrinthVersion) { + const pack = await usePack() + let id = modrinthMod.slug + + if (await pathExists(pack.resolvePath("mods", `${id}.json`))) { + const oldId = id + id = `${id}-${nanoid(5)}` + + output.warn( + `There is already a mod file named ${kleur.yellow(`${oldId}.json`)} specifying a non-Modrinth mod.\n` + + `The file for this mod will therefore be named ${kleur.yellow(`${id}.json`)}` + ) + } + + const isClientSupported = modrinthMod.clientSide !== "unsupported" + const isServerSupported = modrinthMod.serverSide !== "unsupported" + + await pack.addMod(id, { + name: modrinthMod.title, + enabled: true, + ignoreUpdates: false, + side: isClientSupported && isServerSupported ? "client+server" : isClientSupported ? "client" : "server", + file: getModFileDataForModrinthVersion(modrinthMod, modrinthVersion), + source: { + type: "modrinth", + modId: modrinthMod.id, + versionId: modrinthVersion.id + } + }) +} + +export function findCorrectModVersionFile(files: ModrinthVersionFile[]) { + const primary = files.find(file => file.isPrimary) + + if (primary !== undefined) return primary + + // shortest file name + return files.sort((a, b) => a.fileName.length - b.fileName.length)[0] +} + +function transformApiModVersion(raw: any): ModrinthVersion { + return { + id: raw.id, + projectId: raw.project_id, + name: raw.name, + versionString: raw.version_number, + releaseChannel: raw.version_type, + isFeatured: raw.featured, + publicationDate: new Date(raw.date_published), + changelog: raw.changelog, + supportedMinecraftVersions: raw.game_versions, + supportedLoaders: raw.loaders, + downloadsCount: raw.downloads, + relations: raw.dependencies.map((dependency: any): ModrinthVersionRelation => ({ + type: dependencyToRelatedVersionType[dependency.dependency_type], + versionId: dependency.version_id, + projectId: dependency.project_id + })), + files: raw.files.map((file: any): ModrinthVersionFile => ({ + isPrimary: file.primary, + hashes: file.hashes, + fileName: file.filename, + url: file.url, + sizeInBytes: file.size + })) + } +} + +export interface PaginationOptions { + limit: number + skip: number +} + +type ProjectOrVersionId = { + versionId: string + projectId: string | null +} | { + versionId: string | null + projectId: string +} + +export interface ModrinthMod { + id: string + slug: string + title: string + description: string + categories: string[] + clientSide: "required" | "optional" | "unsupported" + serverSide: "required" | "optional" | "unsupported" + downloadsCount: number + licenseCode: string + creationDate: Date + updateDate: Date +} + +export type ModrinthVersionRelation = ProjectOrVersionId & { + type: "hard_dependency" | "soft_dependency" | "embedded_dependency" | "incompatible" +} + +export interface ModrinthVersion { + id: string + projectId: string + name: string + versionString: string + releaseChannel: ReleaseChannel + isFeatured: boolean + publicationDate: Date + changelog: string | null + supportedMinecraftVersions: string[] + supportedLoaders: string[] + downloadsCount: number + relations: ModrinthVersionRelation[] + files: ModrinthVersionFile[] +} + +export interface ModrinthVersionFile { + hashes: Record<"sha512" | "sha1", string> + url: string + fileName: string + isPrimary: boolean + sizeInBytes: number +} + +export const modrinthApi = { + clearCache: () => keyvCache.clear(), + async searchMods( + loader: Loader, + minecraftVersion: string, + query: string, + pagination: PaginationOptions + ): Promise<{ total: number; results: ModrinthMod[] }> { + const facets = `[["categories:${loader}"],["versions:${minecraftVersion}"],["project_type:mod"]]` + + const response = await getModrinthApi(`v2/search?query=${encodeURIComponent(query)}&limit=${pagination.limit}&offset=${pagination.skip}&facets=${facets}`) + + return { + total: response.total_hits, + results: response.hits.map((hit: any): ModrinthMod => ({ + id: hit.project_id, + slug: hit.slug, + title: hit.title, + description: hit.description, + categories: hit.categories, + clientSide: hit.client_side, + serverSide: hit.server_side, + downloadsCount: hit.downloads, + licenseCode: hit.license, + creationDate: new Date(hit.date_created), + updateDate: new Date(hit.date_modified) + })) + } + }, + async getMod(idOrSlug: string): Promise { + const response = await getModrinthApiOptional(`v2/project/${idOrSlug}`) + if (response === null) return null + + return { + id: response.id, + slug: response.slug, + title: response.title, + description: response.description, + categories: response.categories, + clientSide: response.client_side, + serverSide: response.server_side, + downloadsCount: response.downloads, + licenseCode: response.license.id, + creationDate: new Date(response.published), + updateDate: new Date(response.updated) + } + }, + async listVersions(idOrSlug: string, loader: Loader, minecraftVersion: string): Promise { + const response = await getModrinthApi(`v2/project/${idOrSlug}/version?loaders=["${loader}"]&game_versions=["${minecraftVersion}"]`) + + return response.map(transformApiModVersion) + }, + async getVersion(id: string): Promise { + try { + const response = await getModrinthApiOptional(`v2/version/${id}`) + + return transformApiModVersion(response) + } catch (e: unknown) { + if (e instanceof HTTPError && e.response.statusCode === 404) return null + throw e + } + } +} diff --git a/src/output.ts b/src/output.ts new file mode 100644 index 0000000..f67f180 --- /dev/null +++ b/src/output.ts @@ -0,0 +1,115 @@ +import ora, { Ora } from "ora" +import kleur from "kleur" +import { default as wrapAnsi } from "wrap-ansi" +import { last, without } from "lodash-es" +import figures from "figures" + +let loadersStack: InternalLoader[] = [] + +export interface Loader { + setText(text: string): void + fail(message?: string): void + failAndExit(message?: string): never + stop(): void +} + +export interface InternalLoader extends Loader { + spinner: Ora + isActive: boolean + state: "running" | "stopped" | "should_fail" + text: string + activate(): void + deactivate(): void +} + +export const output = { + startLoading(text: string): Loader { + const loader: InternalLoader = { + isActive: false, + state: "running", + text, + spinner: ora({ + spinner: "dots4", + color: "blue" + }), + fail(message?: string) { + if (this.state !== "running") throw new Error("state is not 'running'") + + if (message !== undefined) this.text = this.text + " — " + kleur.red(message) + + if (this.isActive) { + this.spinner.fail(this.text) + this.stop() + } else { + this.state = "should_fail" + } + }, + failAndExit(message?: string): never { + this.fail(message) + process.exit(1) + }, + setText(text: string) { + if (this.state !== "running") throw new Error("state is not 'running'") + + this.text = text + }, + stop() { + this.state = "stopped" + + if (this.isActive) this.spinner.stop() + loadersStack = without(loadersStack, this) + last(loadersStack)?.activate() + }, + activate() { + if (!this.isActive) { + this.isActive = true + + if (this.state === "should_fail") { + this.spinner.fail(this.text) + this.stop() + } else if (this.state === "running") this.spinner.start(this.text) + } + }, + deactivate() { + if (this.isActive) { + this.isActive = false + + if (this.state === "running") this.spinner.stop() + } + } + } + + last(loadersStack)?.deactivate() + loadersStack.push(loader) + loader.activate() + + return loader + }, + print(text: string) { + const loader = last(loadersStack) + if (loader === undefined) { + process.stdout.write(text) + } else { + loader.deactivate() + process.stdout.write(text + "\n" + "\n") + loader.activate() + } + }, + println(text: string) { + this.print(text + "\n") + }, + printlnWrapping(text: string) { + this.println(wrapAnsi(text, process.stdout.columns)) + }, + warn(text: string) { + this.printlnWrapping(`${kleur.yellow(figures.pointer)} ${text}`) + }, + fail(text: string) { + last(loadersStack)?.fail() + this.printlnWrapping(`${kleur.red(figures.pointer)} ${text}`) + }, + failAndExit(text: string): never { + this.fail(text) + process.exit(1) + } +} diff --git a/src/pack.ts b/src/pack.ts new file mode 100644 index 0000000..76f56e9 --- /dev/null +++ b/src/pack.ts @@ -0,0 +1,149 @@ +import { findPackDirectoryPath, HorizrFile, ModFile, ModFileModrinthSource, readHorizrFile, readModFile, readModIds, writeModFile } from "./files.js" +import { resolve } from "path" +import { output } from "./output.js" +import pLimit from "p-limit" +import kleur from "kleur" +import { getModFileDataForModrinthVersion, modrinthApi, ReleaseChannel, sortModrinthVersionsByPreference } from "./modrinth.js" +import semver from "semver" + +export type Loader = "fabric" | "quilt" + +export interface Update { + mod: Mod + activeVersion: string + availableVersion: string + apply(): Promise +} + +export interface Pack { + path: string + horizrFile: HorizrFile + mods: Mod[] + + addMod(id: string, file: ModFile): Promise + + findModByCode(code: string): Mod | null + + findModByCodeOrFail(code: string): Mod + + checkForUpdates(allowedReleaseChannels: ReleaseChannel[]): Promise + + resolvePath(...segments: string[]): string +} + +export interface Mod { + id: string + + modFile: ModFile + saveModFile(): Promise + + checkForUpdate(allowedReleaseChannels: ReleaseChannel[]): Promise +} + +let pack: Pack + +export async function usePack(): Promise { + if (pack === undefined) { + const path = await findPackDirectoryPath() + + pack = { + path, + horizrFile: await readHorizrFile(path), + mods: await Promise.all((await readModIds(path)).map(async id => { + const mod: Mod = { + id, + modFile: (await readModFile(path, id))!, + async saveModFile() { + await writeModFile(path, id, this.modFile) + }, + async checkForUpdate(allowedReleaseChannels: ReleaseChannel[]): Promise { + if (mod.modFile.ignoreUpdates) return null + + if (mod.modFile.source.type === "modrinth") { + const activeVersionString = mod.modFile.file.version + const activeSemver = semver.parse(activeVersionString) + if (activeSemver === null) + output.warn(`${kleur.yellow(mod.modFile.name)} has no valid semantic version: ${kleur.yellow(mod.modFile.file.version)}. The publication date will instead be used.`) + + const versions = await modrinthApi.listVersions(mod.modFile.source.modId, pack.horizrFile.loader, pack.horizrFile.versions.minecraft) + const allowedVersions = versions.filter(version => allowedReleaseChannels.includes(version.releaseChannel)) + + const newerVersions = activeSemver === null ? allowedVersions : allowedVersions.filter(version => { + const thisSemver = semver.parse(version.versionString) + if (thisSemver === null) return false + + return thisSemver.compare(activeSemver) === 1 + }) + + if (newerVersions.length === 0) return null + + const sortedNewerVersions = sortModrinthVersionsByPreference(newerVersions) + const newestVersion = sortedNewerVersions[0] + + if (activeSemver === null ? activeVersionString === newestVersion.versionString : semver.eq(activeSemver, newestVersion.versionString)) return null + + return { + mod, + activeVersion: activeVersionString, + availableVersion: newestVersion.versionString, + async apply() { + const modrinthMod = (await modrinthApi.getMod(newestVersion.projectId))! + + mod.modFile.file = getModFileDataForModrinthVersion(modrinthMod, newestVersion) + ;(mod.modFile.source as ModFileModrinthSource).versionId = newestVersion.id + + await mod.saveModFile() + } + } + } else { + output.warn(`${kleur.yellow(mod.modFile.name)} has no source information attached.`) + } + + return null + } + } + + return mod + })), + async addMod(id: string, file: ModFile) { + await writeModFile(path, id, file) + }, + findModByCode(code: string): Mod | null { + if (code.startsWith("mrv:")) { + return this.mods.find(mod => mod.modFile.source.type === "modrinth" && mod.modFile.source.versionId === code.slice(4)) ?? null + } else if (code.startsWith("mr:")) { + return this.mods.find(mod => mod.modFile.source.type === "modrinth" && mod.modFile.source.modId === code.slice(3)) ?? null + } else if (code.endsWith(".json")) { + return this.mods.find(mod => mod.id === code.slice(0, -5)) ?? null + } else { + return this.mods.find(mod => mod.id === code) ?? null + } + }, + findModByCodeOrFail(code: string): Mod { + const mod = this.findModByCode(code) + if (mod === null) return output.failAndExit("The mod could not be found.") + return mod + }, + async checkForUpdates(allowedReleaseChannels: ReleaseChannel[]): Promise { + const limit = pLimit(5) + + const loader = output.startLoading(`Checking for updates (0/${this.mods.length})`) + let finishedCount = 0 + const updates: Array = await Promise.all(this.mods.map(mod => limit(async () => { + const update = await mod.checkForUpdate(allowedReleaseChannels) + finishedCount++ + loader.setText(`Checking for updates (${finishedCount}/${this.mods.length})`) + return update + }))) + + loader.stop() + return updates.filter(info => info !== null) as Update[] + }, + resolvePath(...segments): string { + return resolve(path, ...segments) + } + } + } + + return pack +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..0b02e1c --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,29 @@ +import envPaths from "env-paths" +import { InvalidArgumentError } from "commander" +import hash, { HashaInput } from "hasha" + +export const paths = envPaths("horizr", { suffix: "" }) + +export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +export const getSha512HexHash = (input: HashaInput) => hash.async(input, { algorithm: "sha512", encoding: "hex" }) + +export function truncateWithEllipsis(text: string, maxLength: number) { + if (text.length <= maxLength) return text + + return text.slice(0, maxLength - 1).trimEnd() + "…" +} + +export function optionParseInteger(value: string): number { + const parsed = parseInt(value, 10) + if (isNaN(parsed)) throw new InvalidArgumentError("Must be an integer.") + + return parsed +} + +export function optionParsePositiveInteger(value: string): number { + const parsed = parseInt(value, 10) + if (isNaN(parsed) || parsed < 0) throw new InvalidArgumentError("Must be a positive integer.") + + return parsed +} diff --git a/test-pack/horizr.json b/test-pack/horizr.json new file mode 100644 index 0000000..8396ba3 --- /dev/null +++ b/test-pack/horizr.json @@ -0,0 +1,15 @@ +{ + "formatVersion": 1, + "meta": { + "name": "Test", + "version": "1.0.0", + "authors": ["John Doe"], + "description": "A test pack for testing the horizr CLI. It is not intended for playing.", + "license": "MIT" + }, + "loader": "fabric", + "versions": { + "loader": "0.14.7", + "minecraft": "1.18.2" + } +} diff --git a/test-pack/mods/charm.json b/test-pack/mods/charm.json new file mode 100644 index 0000000..2b1b738 --- /dev/null +++ b/test-pack/mods/charm.json @@ -0,0 +1,19 @@ +{ + "name": "Charm", + "enabled": true, + "ignoreUpdates": true, + "side": "client+server", + "file": { + "version": "4.2.0+1.18.2", + "name": "charm-fabric-1.18.2-4.2.0.jar", + "size": 3413876, + "downloadUrl": "https://cdn.modrinth.com/data/pOQTcQmj/versions/4.2.0+1.18.2/charm-fabric-1.18.2-4.2.0.jar", + "hashAlgorithm": "sha512", + "hash": "3c8cd08ab1e37dcbf0f5a956cd20d84c98e58ab49fdc13faafb9c2af4dbf7fba7c8328cb5365997fe4414cfc5cb554ed13b3056a22df1c6bd335594f380facb6" + }, + "source": { + "type": "modrinth", + "modId": "pOQTcQmj", + "versionId": "BT9G1Jjs" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8e4c35f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "node16", + "moduleResolution": "node16", + "target": "es2022", + "esModuleInterop": true, + "strict": true, + "rootDir": "src", + "outDir": "dist", + "sourceMap": false, + "declaration": false, + "skipLibCheck": true + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules" + ] +}