diff --git a/package-lock.json b/package-lock.json index a8120be..25cc4f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,8 @@ "figlet": "^1.7.0", "fs-extra": "^11.2.0", "gradient-string": "^2.0.2", + "inquirer": "^9.2.22", + "joi": "^17.13.3", "log-symbols": "^6.0.0", "mongodb": "^6.3.0", "node-emoji": "^2.1.3", @@ -1409,6 +1411,19 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -1457,6 +1472,14 @@ "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", "dev": true }, + "node_modules/@inquirer/figures": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.3.tgz", + "integrity": "sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==", + "engines": { + "node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1602,12 +1625,11 @@ } }, "node_modules/@ljharb/through": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.11.tgz", - "integrity": "sha512-ccfcIDlogiXNq5KcbAwbaO7lMh3Tm1i3khMPYpxlK8hH/W53zN81KM9coerRLOnTGu3nfXIniAmQbRI9OxbC0w==", - "dev": true, + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" }, "engines": { "node": ">= 0.4" @@ -2093,6 +2115,24 @@ } } }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -2635,7 +2675,6 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, "dependencies": { "type-fest": "^0.21.3" }, @@ -2854,7 +2893,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -2889,7 +2927,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -3104,7 +3141,6 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, "funding": [ { "type": "github", @@ -3194,14 +3230,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", - "dev": true, + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3405,8 +3445,7 @@ "node_modules/chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "dev": true + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "node_modules/ci-info": { "version": "3.9.0", @@ -3563,7 +3602,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "dev": true, "engines": { "node": ">= 12" } @@ -3603,7 +3641,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, "engines": { "node": ">=0.8" } @@ -5044,7 +5081,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, "dependencies": { "clone": "^1.0.2" }, @@ -5062,17 +5098,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", - "dev": true, + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -5363,6 +5401,25 @@ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", "dev": true }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -5953,7 +6010,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dev": true, "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", @@ -6330,16 +6386,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", - "dev": true, + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6975,7 +7034,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -7091,12 +7149,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", - "dev": true, + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7106,7 +7163,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7118,7 +7174,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -7259,7 +7314,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -7271,7 +7325,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -7367,18 +7420,17 @@ "dev": true }, "node_modules/inquirer": { - "version": "9.2.12", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", - "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", - "dev": true, + "version": "9.2.23", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.23.tgz", + "integrity": "sha512-kod5s+FBPIDM2xiy9fu+6wdU/SkK5le5GS9lh4FEBjBHqiMgD9lLFbCbuqFNAjNL2ZOy9Wd9F694IOzN9pZHBA==", "dependencies": { - "@ljharb/through": "^2.3.11", + "@inquirer/figures": "^1.0.3", + "@ljharb/through": "^2.3.13", "ansi-escapes": "^4.3.2", "chalk": "^5.3.0", "cli-cursor": "^3.1.0", "cli-width": "^4.1.0", "external-editor": "^3.1.0", - "figures": "^5.0.0", "lodash": "^4.17.21", "mute-stream": "1.0.0", "ora": "^5.4.1", @@ -7389,14 +7441,13 @@ "wrap-ansi": "^6.2.0" }, "engines": { - "node": ">=14.18.0" + "node": ">=18" } }, "node_modules/inquirer/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -7408,7 +7459,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, "dependencies": { "restore-cursor": "^3.1.0" }, @@ -7420,7 +7470,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, "engines": { "node": ">=8" } @@ -7429,7 +7478,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, "engines": { "node": ">=10" }, @@ -7441,7 +7489,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -7457,7 +7504,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7473,7 +7519,6 @@ "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", @@ -7496,7 +7541,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -7512,7 +7556,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -7525,7 +7568,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8181,6 +8223,18 @@ "jiti": "bin/jiti.js" } }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9135,7 +9189,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", - "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -9565,7 +9618,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -10645,7 +10697,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -10785,6 +10836,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/release-it/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/release-it/node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -10811,6 +10874,151 @@ } } }, + "node_modules/release-it/node_modules/inquirer": { + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", + "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", + "dev": true, + "dependencies": { + "@ljharb/through": "^2.3.11", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^5.0.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/release-it/node_modules/inquirer/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/release-it/node_modules/inquirer/node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/release-it/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/release-it/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/release-it/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/release-it/node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/release-it/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/release-it/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -11069,7 +11277,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -11101,7 +11308,6 @@ "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", - "dev": true, "dependencies": { "tslib": "^2.1.0" } @@ -11128,7 +11334,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -11164,8 +11369,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/scule": { "version": "1.3.0", @@ -11222,16 +11426,16 @@ "dev": true }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", - "dev": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -11611,7 +11815,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -11911,7 +12114,6 @@ "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, "dependencies": { "os-tmpdir": "~1.0.2" }, @@ -11982,8 +12184,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/type": { "version": "2.7.2", @@ -12006,7 +12207,6 @@ "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, "engines": { "node": ">=10" }, @@ -12386,8 +12586,7 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", @@ -12402,7 +12601,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, "dependencies": { "defaults": "^1.0.3" } diff --git a/package.json b/package.json index bc87aba..022cf78 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,8 @@ "figlet": "^1.7.0", "fs-extra": "^11.2.0", "gradient-string": "^2.0.2", + "inquirer": "^9.2.22", + "joi": "^17.13.3", "log-symbols": "^6.0.0", "mongodb": "^6.3.0", "node-emoji": "^2.1.3", diff --git a/src/actions/abstract.action.ts b/src/actions/abstract.action.ts index e773995..c545b0f 100644 --- a/src/actions/abstract.action.ts +++ b/src/actions/abstract.action.ts @@ -1,3 +1,4 @@ +import { verifyMantisProject } from '../utils/verify-mantis-project'; import { OrbitLogger } from '../utils/orbitLogger.helper'; /** @@ -7,8 +8,16 @@ import { OrbitLogger } from '../utils/orbitLogger.helper'; export abstract class Action { protected logger: OrbitLogger; - constructor(loggerContext: string) { + constructor( + loggerContext: string, + options: { checkMantisProject: boolean } = { + checkMantisProject: true, + }, + ) { this.logger = new OrbitLogger(loggerContext); + if (options?.checkMantisProject) { + verifyMantisProject(); + } } /** diff --git a/src/actions/generate/generate.actions.ts b/src/actions/generate/generate.actions.ts new file mode 100644 index 0000000..fae6def --- /dev/null +++ b/src/actions/generate/generate.actions.ts @@ -0,0 +1,86 @@ +import ora from 'ora'; +import { Action } from '../abstract.action'; +import { GenerateActionOptions } from './generate.types'; +import { execa } from 'execa'; + +import fs from 'fs'; +import path from 'path'; + +export class GenerateAction extends Action { + private options: GenerateActionOptions; + + constructor(options: GenerateActionOptions) { + super('[GENERATE-ACTION]'); + this.options = options; + } + + updateIndexTs = async (name: string) => { + const filePath = path.join('shared-ui/src', 'index.ts'); + let content = await fs.promises.readFile(filePath, 'utf8'); + // add new line to the end of the file + content += `\nexport * from './lib/${name}/${name}.component';`; + await fs.promises.writeFile(filePath, content, 'utf8'); + }; + + addNewComponentToTsConfig = async (name: string) => { + const filePath = path.join('tsconfig.base.json'); + const content = await fs.promises.readFile(filePath, 'utf8'); + // Update tsconfig.base.json to add the new component in compilerOptions.paths array + const json = JSON.parse(content); + const COMPONENT_PATH = `shared-ui/src/lib/${name}/${name}.component`; + json.compilerOptions.paths[`@todo/${name}`] = [COMPONENT_PATH]; + await fs.promises.writeFile( + filePath, + JSON.stringify(json, null, 2), + 'utf8', + ); + }; + + generateComponent = async (name: string) => { + const spinner = ora(); + spinner.start('Generating component'); + // generate new shared ui component + await execa('npx', [ + 'nx', + 'generate', + '@nx/angular:component', + name, + '--directory', + `shared-ui/src/lib/${name}`, + '--nameAndDirectoryFormat', + 'as-provided', + ]); + // export the component from the index.ts file + await this.updateIndexTs(name); + // add new component to tsconfig.base.json + await this.addNewComponentToTsConfig(name); + spinner.succeed(); + }; + + async execute() { + try { + this.logger.info('Generating...'); + const { type, name, filePath } = this.options; + switch (type) { + case 'component': + await this.generateComponent(name); + break; + case 'service': + this.logger.warning('Unimplemented'); + break; + case 'mongo-schema': + if (!filePath) { + throw new Error('File path is required'); + } + this.logger.warning('Unimplemented'); + // await generateMongoSchema({ + // name, + // filePath, + // }); + break; + } + } catch (error) { + this.logger.error(error); + } + } +} diff --git a/src/actions/generate/generate.types.ts b/src/actions/generate/generate.types.ts new file mode 100644 index 0000000..bf82ad9 --- /dev/null +++ b/src/actions/generate/generate.types.ts @@ -0,0 +1,7 @@ +export type GeneratorType = 'component' | 'service' | 'mongo-schema'; + +export type GenerateActionOptions = { + type: GeneratorType; + name: string; + filePath?: string; +}; diff --git a/src/actions/init/init.actions.ts b/src/actions/init/init.actions.ts new file mode 100644 index 0000000..6ce2682 --- /dev/null +++ b/src/actions/init/init.actions.ts @@ -0,0 +1,212 @@ +import { checkNodeVersion } from '../../utils/utilities.helper'; +import { Action } from '../abstract.action'; +import { InitActionOptions } from './init.types'; +import { MongoClient } from 'mongodb'; +import { PackageManager, PackageManagerName, installDependencies } from 'nypm'; + +import fsExtra from 'fs-extra'; +import { $, execaCommand } from 'execa'; +import fs from 'fs'; +import inquirer from 'inquirer'; +import ora from 'ora'; +import path from 'path'; +import { changeDirectory } from '../../utils/files.helper'; +import { printWithMantisGradient } from '../../utils/prettyPrint.helper'; + +type PMTypePromptResult = Pick; + +export default class InitAction extends Action { + private options: InitActionOptions; + + constructor(options: InitActionOptions) { + super('[INIT-ACTION]', { checkMantisProject: false }); + this.options = options; + } + + promptForPackageManagerSelect = async (): Promise => { + const temp = await inquirer.prompt({ + type: 'list', + name: 'name' satisfies keyof PMTypePromptResult, + message: "Select the package manager you'd like to use:", + choices: ['npm', 'bun', 'pnpm', 'yarn'] satisfies PackageManagerName[], + }); + return temp; + }; + + installDependenciesWithMessage = async (workspacePath: string) => { + const pm = await this.promptForPackageManagerSelect(); + const spinner = ora('Installing dependencies').start(); + await installDependencies({ + packageManager: pm.name, + cwd: workspacePath, + silent: true, + }); + spinner.succeed('Installed dependencies'); + return pm; + }; + + async promptForMissingValues() { + type PromptResult = { + workspaceName: string; + mongoUrl: string; + createMobile: boolean; + }; + const answers = await inquirer.prompt([ + { + type: 'input', + name: 'workspaceName', + message: 'Enter the name of the workspace to create:', + default: 'mantis', + when: () => !this.options.workspaceName, + validate: async (value) => { + if (value.trim().length <= 0) + return 'Workspace name must not be empty'; + if (await fsExtra.pathExists(value)) { + return `Workspace already exists: ${value}`; + } + + return true; + }, + }, + { + type: 'input', + name: 'mongoUrl', + message: + "Enter the MongoDB URI (default is 'mongodb://localhost:27017/mantis'):", + default: 'mongodb://localhost:27017/mantis', + when: () => !this.options.mongodbUri, + validate: async (value) => { + if (value.trim().length <= 0) return 'Db url must not be empty'; + try { + await MongoClient.connect(value); + return true; + } catch (error) { + return `Db url seems to be invalid: ${error.message}`; + } + }, + }, + { + type: 'confirm', + name: 'createMobile', + message: 'Create mobile app?', + when: () => ![true, false].includes(this.options.createMobile), + }, + ]); + + return { + workspaceName: answers.workspaceName || this.options.workspaceName, + mongoUrl: answers.mongoUrl || this.options.mongodbUri, + createMobile: [true, false].includes(answers.createMobile) + ? answers.createMobile + : this.options.createMobile, + }; + } + + async createWorkspace(workspaceName: string) { + const spinner = ora(`Creating ${workspaceName} workspace...`).start(); + try { + const { stdout } = await execaCommand( + `yes | npx create-nx-workspace ${workspaceName} --preset=ts --nxCloud=skip --interactive=false`, + { shell: true }, + ); + console.log(stdout); + spinner.succeed(`Workspace ${workspaceName} created`); + } catch (error) { + spinner.fail(`Failed to create ${workspaceName} workspace`); + throw error; + } + } + + async setupEnvironment( + workspacePath: string, + mongoUrl: string, + frontendUrl: string = 'http://localhost:4200', + ) { + const spinner = ora('Setting up environment variables...').start(); + const envPath = path.join(workspacePath, '.env'); + const envContent = `MONGODB_URI=${mongoUrl}\nFRONTEND_URLS=${frontendUrl}`; + fs.writeFileSync(envPath, envContent); + spinner.succeed('Environment variables set'); + } + + async cloneAndSetupTemplate(params: { + workspaceName: string; + workspacePath: string; + createMobile: boolean; + }) { + const { workspaceName, workspacePath } = params; + const spinner = ora('Setting up project template...').start(); + try { + await $`git clone --no-checkout https://github.com/mantis-apps/mantis-templates.git`; + changeDirectory('mantis-templates'); + await $`git sparse-checkout init`; + await $`git sparse-checkout set todo`; + await $`git checkout`; + + await $`mv todo/app-web ${workspacePath}/app-web`; + if (params.createMobile) { + await $`mv todo/app-mobile ${workspacePath}/app-mobile`; + } + await $`mv todo/shared-ui ${workspacePath}/shared-ui`; + await $`mv todo/tsconfig.base.json ${workspacePath}/tsconfig.base.json`; + await $`mv todo/jest.config.ts ${workspacePath}/jest.config.ts`; + await $`mv todo/jest.preset.js ${workspacePath}/jest.preset.js`; + await $`mv todo/package.json ${workspacePath}/package.json`; + // await $`mv todo/package-lock.json ${workspacePath}/package-lock.json`; + await $`mv todo/nx.json ${workspacePath}/nx.json`; + + changeDirectory(workspacePath); + await $`npm pkg set name=@${workspaceName}/source`; + await $`rm -rf mantis-templates`; + spinner.succeed('Project template setup complete.'); + } catch (error) { + spinner.fail('Failed to set up project template.'); + throw error; + } + } + + async execute() { + try { + printWithMantisGradient(`🛖 WORKSPACE CREATION 🛠️`); + await checkNodeVersion(); + const { workspaceName, mongoUrl, createMobile } = + await this.promptForMissingValues(); + + const workspacePath = path.join(process.cwd(), workspaceName); + this.logger.info('Node version is valid'); + await this.createWorkspace(workspaceName); + await this.setupEnvironment(workspacePath, mongoUrl); + + // navigate to the workspace + changeDirectory(workspacePath); + + // remove node_modules, package-lock.json & package.json + await $`rm -rf node_modules package-lock.json package.json`; + + // clone and setup template + await this.cloneAndSetupTemplate({ + workspaceName, + workspacePath, + createMobile, + }); + + // install dependencies + const pm = await this.installDependenciesWithMessage(workspacePath); + + // add mantis json + const mantisJsonPath = path.join(workspacePath, 'mantis.json'); + const mantisJsonContent = `{ + "name": "${workspaceName}", + "version": "0.0.1", + "description": "Mantis Workspace", + "workspace": ["@${workspaceName}/source"], + "packageManager": "${pm.name}" + }`; + fs.writeFileSync(mantisJsonPath, mantisJsonContent); + fs.writeFileSync(mantisJsonPath, mantisJsonContent); + this.logger.info('Workspace initialized'); + } catch (error) { + this.logger.error(error); + } + } +} diff --git a/src/actions/init/init.types.ts b/src/actions/init/init.types.ts new file mode 100644 index 0000000..a0e3024 --- /dev/null +++ b/src/actions/init/init.types.ts @@ -0,0 +1,5 @@ +export type InitActionOptions = { + workspaceName: string; + mongodbUri: string; + createMobile: boolean; +}; diff --git a/src/actions/start/start.action.ts b/src/actions/start/start.action.ts index d28eb32..14224a0 100644 --- a/src/actions/start/start.action.ts +++ b/src/actions/start/start.action.ts @@ -1,295 +1,27 @@ -import Enquirer from 'enquirer'; -import { isValidVariableName } from '../../utils/globalValidators.helper'; -import { - printWithBadge, - printWithMantisGradient, -} from '../../utils/prettyPrint.helper'; +import { execa } from 'execa'; import { Action } from '../abstract.action'; -import { StartCommandOptions } from './start.types'; -import { - changeDirectory, - createDirectory, - moveFile, - removeFile, - replaceInFile, - replaceInFiles, -} from '../../utils/files.helper'; -import { - checkPortAvailability, - execCommand, - findAvailablePort, -} from '../../utils/process.helper'; -import fs from 'fs'; -import path from 'path'; export default class StartAction extends Action { - private options: StartCommandOptions; - private createMobileApp: boolean = false; - - constructor(options: StartCommandOptions) { + constructor() { super('[START-ACTION]'); - this.options = options; } async execute() { - printWithMantisGradient(`🛖 NX WORKSPACE CREATION 🛠️`); - - this.logger.debug(`Started [START] Action...`, this.options); - - console.log(printWithBadge({ text: 'Hello World !' })); - - const { workspace, createMobileApp } = - this.options && this.options.workspace - ? this.options - : await this.getWorkspaceInfo(); - - this.createMobileApp = createMobileApp || false; - - this.logger.debug(`Prompts Answers: ${workspace} - ${createMobileApp}`); - + this.logger.debug(`Started [START] Action...`); try { - this.createNxWorkspace(workspace); - changeDirectory(path.resolve(workspace)); - this.installDependencies(); - this.createApplications(workspace); - this.createLibraryDirectories(); - this.installAngularPackage(); - this.generateFeatureLibrary(); - this.moveContent(workspace); - this.cleanupDirectories(workspace); - this.replaceHomePage(); - this.updateImportPaths(workspace); - // this.installConcurrentlyPackage(); - // await this.checkPorts(); - this.launchApplications(); - - this.logger.info('Workspace setup complete!'); - } catch (error) { - this.logger.error( - `Error occurred during workspace setup: ${error.message}`, + await execa( + 'npx', + ['nx', 'run-many', '--target=serve', '--projects=app-web,app-mobile'], + { + stdout: 'inherit', + stderr: 'inherit', + }, ); - throw new Error(error); - } - } - - private async getWorkspaceInfo(): Promise { - return Enquirer.prompt([ - { - type: 'input', - name: 'workspace', - message: 'Enter the name of the workspace (default is mantis):', - initial: 'mantis', - validate: (value) => isValidVariableName(value), - }, - ]) as Promise; - } - - private createNxWorkspace(workspace: string) { - try { - this.logger.info(`Creating NX workspace in ${process.cwd()}...`); - const command = `npx -y create-nx-workspace ${workspace} --preset nest --name ${workspace} --appName server --nxCloud false --docker true --create`; - - execCommand({ - command, - useSpinner: false, - }); - - this.logger.info(`NX workspace '${workspace}' created successfully.`); + this.logger.info('Workspace started!'); } catch (error) { - this.logger.error(`Failed to create NX workspace: ${error.message}`); - throw new Error(error); - } - } - - private installDependencies() { - this.logger.info('Installing necessary plugins and dependencies...'); - execCommand({ - command: 'npm install --save-dev @nxext/ionic-angular @nxext/capacitor', - useSpinner: false, - }); - } - - private createApplications(workspace: string) { - const slugifiedName = workspace.toLowerCase().replace(/\s+/g, '-'); - this.logger.info(`Creating Ionic applications in ${process.cwd()}...`); - execCommand({ - command: `npx nx generate @nxext/ionic-angular:application ${slugifiedName}-app --template=blank`, - useSpinner: false, - }); - if (this.createMobileApp) { - execCommand({ - command: `npx nx generate @nxext/ionic-angular:application ${slugifiedName}-mobile --template=blank`, - useSpinner: false, - }); - } - } - - private createLibraryDirectories() { - this.logger.info('Creating library directories...'); - const LibsFolders = ['libs/shared/home', 'libs/web']; - - if (this.createMobileApp) LibsFolders.push('libs/mobile'); - - createDirectory('-p', LibsFolders); - } - - private installAngularPackage() { - this.logger.info('Installing @nrwl/angular package...'); - execCommand({ - command: 'npm install --save-dev @nrwl/angular', - useSpinner: false, - }); - } - - private generateFeatureLibrary() { - this.logger.info('Generating new feature library...'); - execCommand({ - command: - 'npx nx generate @nx/angular:library --name=feature --directory=shared/home --inlineStyle=true --inlineTemplate=true --standalone=true --no-interactive', - useSpinner: false, - }); - } - - private moveContent(workspace: string) { - const slugifiedName = workspace.toLowerCase().replace(/\s+/g, '-'); - const workspaceRoot = process.cwd(); - const sourcePath = `${workspaceRoot}/apps/${slugifiedName}-app/src/app/home`; - const destinationPath = `${workspaceRoot}/libs/shared/home/feature/src/lib`; - - if (fs.existsSync(sourcePath)) { - this.logger.info('Moving content...'); - moveFile(`${sourcePath}/*`, destinationPath); - } else { - this.logger.error(`Directory ${sourcePath} not found.`); - } - } - - private cleanupDirectories(workspace: string) { - const slugifiedName = workspace.toLowerCase().replace(/\s+/g, '-'); - const workspaceRoot = process.cwd(); - const webAppPath = `${workspaceRoot}/apps/${slugifiedName}-app/src/app/home`; - const mobileAppPath = `${workspaceRoot}/apps/${slugifiedName}-mobile/src/app/home`; - const featureLibPath = `${workspaceRoot}/libs/shared/home/feature/src/lib/shared-home-feature`; - - const pathsToRemove = [webAppPath, featureLibPath]; - if (this.createMobileApp) pathsToRemove.push(mobileAppPath); - - this.logger.info('Cleaning up unnecessary directories...'); - removeFile('-rf', pathsToRemove); - } - - private replaceHomePage() { - const workspaceRoot = process.cwd(); - const featureLibPath = `${workspaceRoot}/libs/shared/home/feature/src/lib`; - - this.logger.info( - 'Replacing instances of HomePage with HomePageComponent...', - ); - replaceInFiles({ - dir: featureLibPath, - searchStr: 'HomePage', - replaceStr: 'HomePageComponent', - }); - } - - private updateImportPaths(workspace: string) { - const workspaceRoot = process.cwd(); - const slugifiedName = workspace.toLowerCase().replace(/\s+/g, '-'); - - const featureLibIndexPath = path.resolve( - `${workspaceRoot}/libs/shared/home/feature/src/index.ts`, - ); - const webAppRoutingModulePath = path.resolve( - `${workspaceRoot}/apps/${slugifiedName}-app/src/app/app-routing.module.ts`, - ); - const webAppGlobalStylesPath = path.resolve( - `${workspaceRoot}/apps/${slugifiedName}-app/src/styles.scss`, - ); - const mobileAppRoutingModulePath = this.createMobileApp - ? path.resolve( - `${workspaceRoot}/apps/${slugifiedName}-mobile/src/app/app-routing.module.ts`, - ) - : ''; - const sharedHomeFeaturePath = `@${workspace}/shared/home/feature`; - - this.logger.info('-> Updating import paths...'); - - replaceInFile(webAppGlobalStylesPath, '~@', '@'); - replaceInFile( - featureLibIndexPath, - 'lib/shared-home-feature/shared-home-feature.component', - 'lib/home.module', - ); - replaceInFile( - webAppRoutingModulePath, - "import\\('./home/home.module'\\)\\.then\\(\\(m\\) => m\\.HomePageModule\\)", - `import('${sharedHomeFeaturePath}').then((m) => m.HomePageComponentModule)`, - ); - if (this.createMobileApp) - replaceInFile( - mobileAppRoutingModulePath, - "import\\('./home/home.module'\\)\\.then\\(\\(m\\) => m\\.HomePageModule\\)", - `import('${sharedHomeFeaturePath}').then((m) => m.HomePageComponentModule)`, + this.logger.error( + `Error occurred during workspace start: ${error.message}`, ); - } - - private installConcurrentlyPackage() { - this.logger.info('Installing concurrently package...'); - execCommand({ - command: 'npm install --save-dev concurrently', - useSpinner: false, - }); - } - - private async checkPorts() { - const webPort = Number(process.env.WebPort) || 4200; - const mobilePort = Number(process.env.MobilePort) || 4300; - const serverPort = Number(process.env.ServerPort) || 3000; - - const isWebPortAvailable = await checkPortAvailability({ port: webPort }); - const isMobilePortAvailable = this.createMobileApp - ? await checkPortAvailability({ port: mobilePort }) - : true; - const isServerPortAvailable = await checkPortAvailability({ - port: serverPort, - }); - - if (!isWebPortAvailable) { - this.logger.warning(`Port ${webPort} (Web) is already in use`); - process.env.WebPort = ( - await findAvailablePort({ startPort: webPort, endPort: webPort + 99 }) - ).toString(); - } - if (!isMobilePortAvailable) { - this.logger.warning(`Port ${mobilePort} (Mobile) is already in use`); - process.env.MobilePort = ( - await findAvailablePort({ - startPort: mobilePort, - endPort: mobilePort + 99, - }) - ).toString(); - } - if (!isServerPortAvailable) { - this.logger.warning(`Port ${serverPort} (Server) is already in use`); - process.env.ServerPort = ( - await findAvailablePort({ - startPort: serverPort, - endPort: serverPort + 99, - }) - ).toString(); - } - } - - private launchApplications() { - const command = 'npx nx run-many --target=serve --all --maxParallel=100'; - - try { - this.logger.info('Launching applications...'); - execCommand({ command, useSpinner: true }); - - this.logger.sponsor('Applications launched successfully.'); - } catch (error) { - this.logger.error(`Error launching applications: ${error.message}`); throw new Error(error); } } diff --git a/src/actions/start/start.types.ts b/src/actions/start/start.types.ts deleted file mode 100644 index feb286b..0000000 --- a/src/actions/start/start.types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface StartCommandOptions { - workspace: string; - workDir?: string; - createMobileApp?: boolean; -} diff --git a/src/commands/generate.command.ts b/src/commands/generate.command.ts new file mode 100644 index 0000000..1c3e4a9 --- /dev/null +++ b/src/commands/generate.command.ts @@ -0,0 +1,11 @@ +import { Command } from 'commander'; +import { GenerateAction } from '../actions/generate/generate.actions'; + +export default new Command('generate') + .description('Generate a new Mantis feature') + .argument('', 'The type of thing to generate') + .argument('', 'The name of the thing to generate') + .option('-p, --path ', 'The path to the file') + .action(async (type, name, options) => { + await new GenerateAction({ type, name, filePath: options.path }).execute(); + }); diff --git a/src/commands/init.command.ts b/src/commands/init.command.ts index dff7898..b4796b6 100644 --- a/src/commands/init.command.ts +++ b/src/commands/init.command.ts @@ -1,71 +1,22 @@ import { Command } from 'commander'; -import fs from 'fs-extra'; -import path from 'path'; -import { PackageManager, PackageManagerName, installDependencies } from 'nypm'; -import { fileURLToPath } from 'url'; -import { execa } from 'execa'; import Enquirer from 'enquirer'; import { MongoClient } from 'mongodb'; -import ora from 'ora'; -import logSymbols from 'log-symbols'; -import { replaceInFiles } from '../utils/files.helper'; -const __dirname = fileURLToPath(new URL('.', import.meta.url)); +import InitAction from '../actions/init/init.actions'; + +// const __dirname = fileURLToPath(new URL('.', import.meta.url)); export default new Command('init') .description('Create a basic mantis app') - .action(async () => { - const templateName = 'mantis-todo'; - const templatePath = path.resolve( - __dirname, - `../templates/${templateName}`, - ); - const workspaceName = await promptForWorkspaceName({ - defaultName: templateName, - }); - const workspacePath = path.join(process.cwd(), workspaceName); - - const { isLocal, dbUrl } = await promptForDbUrl(); - - await copyTemplate({ - template: { name: templateName, path: templatePath }, - workspace: { name: workspaceName, path: workspacePath }, - secrets: { dbUrl }, - }); - await installDependenciesWithMessage(workspacePath); - await startApplications(workspacePath, { - startLocalDb: isLocal, - }); + .option('-m, --mongodb-uri ', 'The MongoDB URI') + .option('-n, --workspace-name ', 'The name of the workspace') + .option('-cm, --create-mobile ', 'Create mobile app') + .action(async (params) => { + await new InitAction(params).execute(); }); -const promptForWorkspaceName = async ({ - defaultName, -}: { - defaultName: string; -}) => { - interface PromptResult { - workspaceName: string; - } - const { workspaceName } = await Enquirer.prompt([ - { - type: 'input', - name: 'workspaceName' satisfies keyof PromptResult, - message: 'Enter the name of the workspace to create:', - initial: defaultName, - validate: async (value) => { - if (value.trim().length <= 0) return 'Workspace name must not be empty'; - if (await fs.pathExists(value)) { - return `Workspace already exists: ${value}`; - } - - return true; - }, - }, - ]); - return workspaceName; -}; - -const promptForDbUrl = async (): Promise<{ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _promptForDbUrl = async (): Promise<{ dbUrl: string; isLocal: boolean; }> => { @@ -116,109 +67,3 @@ const promptForDbUrl = async (): Promise<{ ]); return { isLocal: false, dbUrl }; }; - -const copyTemplate = async ({ - template, - workspace, - secrets: { dbUrl }, -}: { - template: { name: string; path: string }; - workspace: { name: string; path: string }; - secrets: { dbUrl: string }; -}) => { - const spinner = ora('Creating workspace').start(); - await fs.copy(template.path, workspace.path); - await renameGitignoreFiles(workspace.path); - const envPath = path.join(workspace.path, 'apps/web-client/.env.local'); - await fs.ensureFile(envPath); - await fs.appendFile(envPath, `\nMONGODB_URI='${dbUrl}'`); - replaceInFiles({ - dir: workspace.path, - searchStr: template.name, - replaceStr: workspace.name, - }); - spinner.succeed('Created workspace'); -}; - -/** - * This is needed because npm can't publish .gitignore files - * https://github.com/npm/npm/issues/3763 - */ -const renameGitignoreFiles = async (workspacePath: string) => { - const gitignoreFiles = await fs.readdir(workspacePath, { - withFileTypes: true, - recursive: true, - }); - await Promise.all( - gitignoreFiles - .filter((entry) => entry.isFile() && entry.name === '_gitignore') - .map(async (entry) => { - await fs.rename( - path.join(entry.path, entry.name), - path.join(entry.path, '.gitignore'), - ); - }), - ); -}; - -type PMTypePromptResult = Pick; -const promptForPackageManagerSelect = async (): Promise => { - const temp = await Enquirer.prompt({ - type: 'select', - name: 'name' satisfies keyof PMTypePromptResult, - message: "Select the package manager you'd like to use:", - choices: ['npm', 'bun', 'pnpm', 'yarn'] satisfies PackageManagerName[], - // This is to workaround a bug - // https://github.com/enquirer/enquirer/issues/121 - result(choice) { - return (this as any).map(choice)[choice]; - }, - }); - return temp; -}; - -const installDependenciesWithMessage = async (workspacePath: string) => { - const pm = await promptForPackageManagerSelect(); - const spinner = ora('Installing dependencies').start(); - await installDependencies({ - packageManager: pm.name, - cwd: workspacePath, - silent: true, - }); - spinner.succeed('Installed dependencies'); -}; - -const startApplications = async ( - workspacePath: string, - { startLocalDb }: { startLocalDb: boolean }, -) => { - console.log(`${logSymbols.info} Starting applications...`); - try { - await execa( - 'npx', - [ - 'nx', - 'run-many', - startLocalDb ? '--targets=serve,start-local-db' : '--target=serve', - '--projects=web-client,mobile-client', - ], - { cwd: workspacePath, stdio: 'inherit' }, - ); - } catch (err: unknown) { - if ( - err && - typeof err === 'object' && - 'exitCode' in err && - // 130 is the exit code for SIGINT (ctrl + c) - // i.e. the user cancelled the process - err.exitCode === 130 - ) { - return; - } - console.error( - `${logSymbols.error} Failed to start applications: ${ - err instanceof Error ? err.message : 'Unknown cause' - }`, - ); - } -}; diff --git a/src/commands/start.command.ts b/src/commands/start.command.ts index a3502ec..70c4f36 100644 --- a/src/commands/start.command.ts +++ b/src/commands/start.command.ts @@ -2,9 +2,7 @@ import { Command } from 'commander'; import StartAction from '../actions/start/start.action'; export default new Command('start') - .description('Description of someCommand') - .option('-w, --workspace ', 'Specify the name of the workspace') - .option('--createMobileApp', 'Create a Mobile App') + .description('Start the Mantis project') .action(async () => { - await new StartAction({ workspace: '' }).execute(); + await new StartAction().execute(); }); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..8228ecd --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const MIN_NODE_VERSION = 16; diff --git a/src/mantis-app.ts b/src/mantis-app.ts index defdfdf..49c5bb4 100644 --- a/src/mantis-app.ts +++ b/src/mantis-app.ts @@ -4,6 +4,7 @@ import { welcome } from './utils/welcome.helper'; import packageJson from '../package.json'; import initCommand from './commands/init.command'; import startCommand from './commands/start.command'; +import generateCommand from './commands/generate.command'; const logger = new OrbitLogger('[BOOTSTRAP]'); @@ -33,6 +34,7 @@ const bootstrap = async () => { program.addCommand(initCommand); program.addCommand(startCommand); + program.addCommand(generateCommand); await program.parseAsync(process.argv); diff --git a/src/utils/utilities.helper.ts b/src/utils/utilities.helper.ts index e271158..292ea06 100644 --- a/src/utils/utilities.helper.ts +++ b/src/utils/utilities.helper.ts @@ -1,3 +1,7 @@ +import { exec } from 'child_process'; +import { promisify } from 'util'; +import { MIN_NODE_VERSION } from '../constants'; + /** * Clears the console screen. */ @@ -8,3 +12,29 @@ export const clearConsole = () => { process.stdout.write('\x1Bc'); // '\x1Bc' is an escape sequence to clear the screen. console.log(''); }; + +const execAsync = promisify(exec); + +export async function runCommand(command: string) { + const { stdout, stderr } = await execAsync(command); + if (stderr) throw new Error(stderr); + return stdout; +} + +export async function checkNodeVersion( + requiredVersion: number = MIN_NODE_VERSION, +): Promise { + try { + const stdout = await runCommand('node -v'); + const version = parseInt(stdout.slice(1).split('.')[0]); + if (version < requiredVersion) { + console.error( + `Node version must be at least ${requiredVersion}. You are running version ${stdout}. Please update Node.js.`, + ); + process.exit(1); + } + } catch (error) { + console.error('Node is not installed. Please install Node.js.'); + process.exit(1); + } +} diff --git a/src/utils/verify-mantis-project.ts b/src/utils/verify-mantis-project.ts new file mode 100644 index 0000000..ffe7dd7 --- /dev/null +++ b/src/utils/verify-mantis-project.ts @@ -0,0 +1,28 @@ +import fs from 'fs-extra'; +import Joi from 'joi'; + +export const verifyMantisProject = () => { + // check if mantis.json exists + if (!fs.existsSync('mantis.json')) { + throw new Error('Mantis project not found'); + } + // check if mantis.json is valid + const mantisTxt = fs.readFileSync('mantis.json', 'utf8'); + let mantisJson; + try { + mantisJson = JSON.parse(mantisTxt); + } catch (error) { + throw new Error('Mantis project is not valid'); + } + const mantisJsonSchema = Joi.object({ + name: Joi.string().required(), + version: Joi.string().required(), + workspace: Joi.array().items(Joi.string()).required(), + description: Joi.string().optional(), + packageManager: Joi.string().required(), + }); + const result = mantisJsonSchema.validate(mantisJson); + if (result.error) { + throw new Error('Mantis project is not valid'); + } +};