From 765920f80ce6922cca63cb5d9917b662bdcd2b85 Mon Sep 17 00:00:00 2001 From: Kallum Jones Date: Wed, 3 Aug 2022 15:00:36 +0100 Subject: [PATCH] Added downloading mods from Modrinth --- package-lock.json | 335 +++++++++++++++++++++++++++- package.json | 5 +- src/commands/install_command.ts | 19 ++ src/commands/subcommand.ts | 5 +- src/errors/download_error.ts | 7 + src/errors/mod_not_found_error.ts | 7 + src/io/download_task.d.ts | 8 + src/io/file_downloder.ts | 21 ++ src/mod-manager.ts | 17 +- src/mods/mods.ts | 51 +++++ src/mods/sources/mod_source.ts | 5 + src/mods/sources/modrinth_source.ts | 163 ++++++++++++++ src/util/minecraft_utils.ts | 44 ++++ src/util/util.ts | 5 + 14 files changed, 681 insertions(+), 11 deletions(-) create mode 100644 src/commands/install_command.ts create mode 100644 src/errors/download_error.ts create mode 100644 src/errors/mod_not_found_error.ts create mode 100644 src/io/download_task.d.ts create mode 100644 src/io/file_downloder.ts create mode 100644 src/mods/mods.ts create mode 100644 src/mods/sources/mod_source.ts create mode 100644 src/mods/sources/modrinth_source.ts create mode 100644 src/util/minecraft_utils.ts create mode 100644 src/util/util.ts diff --git a/package-lock.json b/package-lock.json index e882b3a..86bc149 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,10 @@ "axios": "^0.27.2", "chalk": "^5.0.1", "commander": "^9.4.0", - "ora": "^6.1.2" + "error": "^10.4.0", + "ora": "^6.1.2", + "pretty-error": "^4.0.0", + "string-format": "^2.0.0" }, "devDependencies": { "@types/node": "^18.6.3", @@ -79,6 +82,11 @@ "readable-stream": "^3.4.0" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -165,6 +173,32 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/defaults": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", @@ -181,6 +215,78 @@ "node": ">=0.4.0" } }, + "node_modules/dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "dependencies": { + "utila": "~0.4" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/error/-/error-10.4.0.tgz", + "integrity": "sha512-YxIFEJuhgcICugOUvRx5th0UM+ActZ9sjY0QJmeVwsQdvosZ7kYzc9QqS0Da3R5iUmgU5meGIxh0xBeZpMVeLw==" + }, "node_modules/follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", @@ -213,6 +319,24 @@ "node": ">= 6" } }, + "node_modules/htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -259,6 +383,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/log-symbols": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", @@ -301,6 +430,17 @@ "node": ">=6" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -337,6 +477,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "dependencies": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -350,6 +499,37 @@ "node": ">= 6" } }, + "node_modules/renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "dependencies": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + } + }, + "node_modules/renderkid/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/renderkid/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -397,6 +577,11 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==" + }, "node_modules/strip-ansi": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", @@ -429,6 +614,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -479,6 +669,11 @@ "readable-stream": "^3.4.0" } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", @@ -524,6 +719,23 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-9.4.0.tgz", "integrity": "sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==" }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "defaults": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", @@ -537,6 +749,57 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, + "dom-converter": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", + "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", + "requires": { + "utila": "~0.4" + } + }, + "dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, + "error": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/error/-/error-10.4.0.tgz", + "integrity": "sha512-YxIFEJuhgcICugOUvRx5th0UM+ActZ9sjY0QJmeVwsQdvosZ7kYzc9QqS0Da3R5iUmgU5meGIxh0xBeZpMVeLw==" + }, "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", @@ -552,6 +815,17 @@ "mime-types": "^2.1.12" } }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -572,6 +846,11 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.2.0.tgz", "integrity": "sha512-wH+U77omcRzevfIG8dDhTS0V9zZyweakfD01FULl97+0EHiJTTZtJqxPSkIIo/SDPv/i07k/C9jAPY+jwLLeUQ==" }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "log-symbols": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", @@ -599,6 +878,14 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -623,6 +910,15 @@ "wcwidth": "^1.0.1" } }, + "pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", + "requires": { + "lodash": "^4.17.20", + "renderkid": "^3.0.0" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -633,6 +929,33 @@ "util-deprecate": "^1.0.1" } }, + "renderkid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", + "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", + "requires": { + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, "restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -660,6 +983,11 @@ "safe-buffer": "~5.2.0" } }, + "string-format": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz", + "integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==" + }, "strip-ansi": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", @@ -679,6 +1007,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "utila": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", + "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==" + }, "wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index 59fd242..a9e5980 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,10 @@ "axios": "^0.27.2", "chalk": "^5.0.1", "commander": "^9.4.0", - "ora": "^6.1.2" + "error": "^10.4.0", + "ora": "^6.1.2", + "pretty-error": "^4.0.0", + "string-format": "^2.0.0" }, "devDependencies": { "@types/node": "^18.6.3", diff --git a/src/commands/install_command.ts b/src/commands/install_command.ts new file mode 100644 index 0000000..109196f --- /dev/null +++ b/src/commands/install_command.ts @@ -0,0 +1,19 @@ +import { Command } from "commander"; +import Subcommand from "./subcommand.js" +import ModManager from "../mod-manager.js"; +import Mods from "../mods/mods.js"; + +export default class InstallCommand implements Subcommand { + registerCommand(program: Command): void { + program.command("install") + .description("Installs the provided mods") + .argument("", "The mods to install") + .action((mods) => { + ModManager.execute(async () => { + for (const mod of mods) { + await Mods.install(mod); + } + }) + }); + } +} \ No newline at end of file diff --git a/src/commands/subcommand.ts b/src/commands/subcommand.ts index 1f2a9a6..a5808ed 100644 --- a/src/commands/subcommand.ts +++ b/src/commands/subcommand.ts @@ -1,6 +1,5 @@ import { Command } from "commander"; -export default abstract class Subcommand { - abstract registerCommand(program: Command): void; - abstract execute(): void; +export default interface Subcommand { + registerCommand(program: Command): void; } \ No newline at end of file diff --git a/src/errors/download_error.ts b/src/errors/download_error.ts new file mode 100644 index 0000000..b233fa4 --- /dev/null +++ b/src/errors/download_error.ts @@ -0,0 +1,7 @@ +export default class DownloadError extends Error { + constructor(message: string | undefined) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} \ No newline at end of file diff --git a/src/errors/mod_not_found_error.ts b/src/errors/mod_not_found_error.ts new file mode 100644 index 0000000..ef3aacf --- /dev/null +++ b/src/errors/mod_not_found_error.ts @@ -0,0 +1,7 @@ +export default class ModNotFoundError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} \ No newline at end of file diff --git a/src/io/download_task.d.ts b/src/io/download_task.d.ts new file mode 100644 index 0000000..074148e --- /dev/null +++ b/src/io/download_task.d.ts @@ -0,0 +1,8 @@ +declare global { + type DownloadTask = { + fileName: string, + url: string + } +} + +export {} \ No newline at end of file diff --git a/src/io/file_downloder.ts b/src/io/file_downloder.ts new file mode 100644 index 0000000..26d9b69 --- /dev/null +++ b/src/io/file_downloder.ts @@ -0,0 +1,21 @@ +import path from "path"; +import * as https from "https"; +import {createWriteStream} from "fs"; +import DownloadError from "../errors/download_error.js"; + + +export default class FileDownloader { + private static readonly MODS_FOLDER_PATH: string = path.join("mods"); + + static downloadMod(task: DownloadTask): void { + https.get(task.url, res => { + const filePath = path.join(this.MODS_FOLDER_PATH, task.fileName); + const writeStream = createWriteStream(filePath); + res.pipe(writeStream); + writeStream.on("finish", () => writeStream.close()); + writeStream.on('error', () => { + throw new DownloadError(`Failed to download ${task.fileName} from ${task.url}`) + }) + }) + } +} \ No newline at end of file diff --git a/src/mod-manager.ts b/src/mod-manager.ts index b35f247..dc6cb24 100644 --- a/src/mod-manager.ts +++ b/src/mod-manager.ts @@ -1,25 +1,31 @@ #!/usr/bin/env node import { Command } from "commander"; import InitCommand from "./commands/init_command.js"; +import InstallCommand from "./commands/install_command.js"; import Subcommand from "./commands/subcommand.js"; import Initialiser from "./util/initialiser.js"; import PrintUtils from "./util/print_utils.js"; +//import PrettyError from "pretty-error"; export default class ModManager { private static program: Command = new Command(); private static subcommands: Array = [ - new InitCommand() + new InitCommand(), + new InstallCommand() ]; static init() { + //const pe = new PrettyError(); + //pe.start(); + this.program .name('mod-manager') .description('A package (mod) manager for Fabric Minecraft Servers'); - this.subcommands.forEach(command => { - command.registerCommand(ModManager.program); - }); + for (const command of this.subcommands) { + command.registerCommand(this.program); + } this.program.parse(); } @@ -30,10 +36,9 @@ export default class ModManager { } else { PrintUtils.error("Mod Manager is not initialised"); } - } + } } - ModManager.init(); diff --git a/src/mods/mods.ts b/src/mods/mods.ts new file mode 100644 index 0000000..62bfd1d --- /dev/null +++ b/src/mods/mods.ts @@ -0,0 +1,51 @@ + +import path from "path"; +import Initialiser from "../util/initialiser.js"; +import PrintUtils from "../util/print_utils.js"; +import ModrinthSource from "./sources/modrinth_source.js"; +import ModSource from "./sources/mod_source.js"; +import ModNotFoundError from "../errors/mod_not_found_error.js"; + + +export default class Mods { + public static readonly MOD_FILE = "mods.json"; + private static readonly MOD_SOURCES: Array = [ + new ModrinthSource() + ]; + + public static async install(mod: string): Promise { + let success: boolean = false; + + // Go through each mod source + for (const source of this.MOD_SOURCES) { + // If we have not yet successfully installed the queried mod + if (!success) { + PrintUtils.info(`Searching for ${mod}...`); + // Search for the mod + let id; + try { + id = await source.search(mod); + } catch (e) { + if (e instanceof ModNotFoundError) { + PrintUtils.info(`Mod not found on ${source.getName()}`); + } + } + + // If a mod is found, install it + if (id != undefined) { + PrintUtils.info(`Installing ${mod}...`); + try { + await source.install(id); + PrintUtils.success(`Successfully installed ${mod}`); + } catch (e) { + PrintUtils.error(`An error occurred downloading ${mod} from ${source.getName()}`); + } + } + } + } + } + + public static getModFilePath(): string { + return path.join(Initialiser.getModManagerFolderPath(), this.MOD_FILE); + } +} \ No newline at end of file diff --git a/src/mods/sources/mod_source.ts b/src/mods/sources/mod_source.ts new file mode 100644 index 0000000..3ec1a64 --- /dev/null +++ b/src/mods/sources/mod_source.ts @@ -0,0 +1,5 @@ +export default interface ModSource { + search(query: string): Promise; + install(id: string): Promise; + getName(): string; +} \ No newline at end of file diff --git a/src/mods/sources/modrinth_source.ts b/src/mods/sources/modrinth_source.ts new file mode 100644 index 0000000..d4d647e --- /dev/null +++ b/src/mods/sources/modrinth_source.ts @@ -0,0 +1,163 @@ +import ModSource from "./mod_source.js"; +import "string-format" +import {format} from "util"; +import MinecraftUtils from "../../util/minecraft_utils.js"; +import axios from "axios"; +import ModNotFoundError from "../../errors/mod_not_found_error.js"; +import Util from "../../util/util.js"; +import FileDownloader from "../../io/file_downloder.js"; +import DownloadError from "../../errors/download_error.js"; + +export default class ModrinthSource implements ModSource { + private static readonly BASE_URL: string = "https://api.modrinth.com/v2"; + private static readonly SEARCH_URL: string = ModrinthSource.BASE_URL + "/search"; + private static readonly INSTALL_URL: string = ModrinthSource.BASE_URL + "/project/%s/version"; + + /** + * Searches Modrinth for the specified query + * Example shape of data returned by query: + * { + * "hits": [ + * { + * "project_id": "gvQqBUqZ", + * "project_type": "mod", + * "slug": "lithium", + * "author": "jellysquid3", + * "title": "Lithium", + * "description": "No-compromises game logic/server optimization mod", + * "categories": [ + * "optimization", + * "fabric" + * ], + * "display_categories": [ + * "optimization", + * "fabric" + * ], + * "versions": [ + * "1.16.2", + * "1.16.3", + * "1.16.4", + * "1.16.5", + * "1.17", + * "1.17.1", + * "1.18", + * "1.18.1", + * "1.18.2", + * "1.19", + * "1.19.1" + * ], + * "downloads": 223103, + * "follows": 1866, + * "icon_url": "https://cdn.modrinth.com/data/gvQqBUqZ/icon.png", + * "date_created": "2021-01-03T00:56:52.292581Z", + * "date_modified": "2022-07-29T22:18:05.703354Z", + * "latest_version": "1.19.1", + * "license": "lgpl-3", + * "client_side": "optional", + * "server_side": "optional", + * "gallery": [] + * } + * ], + * "offset": 0, + * "limit": 1, + * "total_hits": 1 + * } + * @param query the query to search for + * @throws ModNotFoundError if the query returns no results. + * @returns The mod id of the found mod + */ + async search(query: string): Promise | never { + const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion(); + + const params = { + query: query, + limit: 1, + facets: format('[["categories:fabric"],["versions:%s"]]', mcVersion) + } + + const response = await axios.get(ModrinthSource.SEARCH_URL, {params}); + const results = await response.data.hits; + + if (Util.isArrayEmpty(results)) { + throw new ModNotFoundError(`Mod ${query} could not be found on ${this.getName()}`); + } + + return results[0].project_id; + } + + /** + * Installs the mod with the provided mod id + * Example shape of data returned by query: + * [ + * { + * "id": "ZRR9yqHD", + * "project_id": "gvQqBUqZ", + * "author_id": "uhPSqlnd", + * "featured": false, + * "name": "Lithium 0.8.3", + * "version_number": "mc1.19.1-0.8.3", + * "changelog": "Lithium 0.8.3 is the second release for 1.19.1! It includes a bugfix too!\n\n## Fixes\n- fix: update chunk serialization patch to new mappings\n\nYou can donate on patreon: https://www.patreon.com/2No2Name\n", + * "changelog_url": null, + * "date_published": "2022-07-29T22:18:09.072973Z", + * "downloads": 3592, + * "version_type": "release", + * "files": [ + * { + * "hashes": { + * "sha1": "9ef9f10f62d4c19b736fe493f2a11d737fbe3d7c", + * "sha512": "a3b623b4c14f6ba46d1486ffb3d1ba3174e3317b419b2ddfdf7bb572244e706d2e0a37bdce169c94455bec00fd107530ba78d7e611162a632cc6950e6a625433" + * }, + * "url": "https://cdn.modrinth.com/data/gvQqBUqZ/versions/mc1.19.1-0.8.3/lithium-fabric-mc1.19.1-0.8.3.jar", + * "filename": "lithium-fabric-mc1.19.1-0.8.3.jar", + * "primary": true, + * "size": 476619 + * } + * ], + * "dependencies": [], + * "game_versions": [ + * "1.19.1" + * ], + * "loaders": [ + * "fabric" + * ] + * } + * ] + * @param id the id of the mod + * @throws DownloadError if an error occurs when downloading + * @throws ModNotFoundError if there are no versions available for the current Minecraft Version + */ + async install(id: string): Promise { + const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion(); + + const params = { + loaders: '["fabric"]', + game_versions: format('["%s"]', mcVersion) + } + + const response = await axios.get(format(ModrinthSource.INSTALL_URL, id), {params}); + const results = await response.data; + + if (Util.isArrayEmpty(results)) { + throw new ModNotFoundError(`Mod with id ${id} has no available versions on ${this.getName()} for Minecraft version ${mcVersion}`); + } + + const latestFile = results[0].files[0]; + + const task: DownloadTask = { + fileName: latestFile.filename, + url: latestFile.url + } + + try { + FileDownloader.downloadMod(task) + } catch (e) { + throw new DownloadError(`An error occurred downloading mod with id ${id} from ${this.getName()}`) + } + } + + getName(): string { + return "Modrinth"; + } + + +} \ No newline at end of file diff --git a/src/util/minecraft_utils.ts b/src/util/minecraft_utils.ts new file mode 100644 index 0000000..2b58c2b --- /dev/null +++ b/src/util/minecraft_utils.ts @@ -0,0 +1,44 @@ +import {readdirSync} from "fs"; +import path from "path"; +import axios from "axios"; + +export default class MinecraftUtils { + static async getCurrentMinecraftVersion(): Promise { + // Get installed versions as strings + const installedVersions: Array = readdirSync(this.getVersionsFolderPath(), {withFileTypes: true}) + .filter(entry => entry.isDirectory()) + .map(entry => entry.name); + + // Get a list of Minecraft Versions + const response = await axios.get("https://meta.fabricmc.net/v2/versions/game"); + const data = await response.data; + + const minecraftVersions: Array = []; + for (const ele of data) { + minecraftVersions.push(ele.version); + } + + // Find the latest version that is currently installed + let index = Number.MAX_VALUE; + for (let version of installedVersions) { + let currentIndex = minecraftVersions.indexOf(version); + + // If this version, is newer than the previous newest, save it's index + if (currentIndex < index) { + index = currentIndex; + } + } + + const latestVersion = minecraftVersions[index]; + if (latestVersion == undefined) { + throw new Error("There are no Minecraft versions available in this server. Is this a valid server installation?"); + } + + return latestVersion; + } + + static getVersionsFolderPath(): string { + return path.join("versions") + } +} + diff --git a/src/util/util.ts b/src/util/util.ts new file mode 100644 index 0000000..1869f76 --- /dev/null +++ b/src/util/util.ts @@ -0,0 +1,5 @@ +export default class Util { + static isArrayEmpty(array: Array | undefined): boolean { + return array === undefined || array.length == 0; + } +}