diff --git a/.github/workflows/on-push-publish-to-npm.yml b/.github/workflows/on-push-publish-to-npm.yml index c857d39..89370ae 100644 --- a/.github/workflows/on-push-publish-to-npm.yml +++ b/.github/workflows/on-push-publish-to-npm.yml @@ -16,7 +16,12 @@ jobs: node-version: 20 - run: npm install - run: npm test - - uses: JS-DevTools/npm-publish@v1 + - name: Publish to NPM + uses: JS-DevTools/npm-publish@v3 + id: publish with: token: ${{ secrets.ADOBE_BOT_NPM_TOKEN }} access: public + - name: Check if version changed + if: ${{ steps.publish.outputs.type }} + run: echo "Version changed!" diff --git a/package-lock.json b/package-lock.json index 4bb760a..ba3b0f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,8 +24,10 @@ "inquirer": "^8.2.6", "inquirer-autocomplete-prompt": "^2.0.1", "jsonwebtoken": "^9.0.2", + "node-notifier": "^10.0.1", "open": "^7.0.0", - "ora": "^5.4.1" + "ora": "^5.4.1", + "spinnies": "^0.5.1" }, "bin": { "adobe-aem-rde-cli": "bin/run" @@ -34,7 +36,7 @@ "@adobe/eslint-config-aio-lib-config": "^4.0.0", "@inquirer/testing": "^2.1.19", "@oclif/dev-cli": "^1.26.10", - "chai": "^5.1.1", + "chai": "^4.5.0", "chai-as-promised": "^8.0.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", @@ -49,7 +51,7 @@ "source-map-support": "^0.5.21" }, "engines": { - "node": "^16.13 || ^18 || ^20", + "node": "^18 || ^20 || >= 22.15 <23", "npm": ">= 8.0.0" } }, @@ -699,89 +701,20 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", @@ -971,19 +904,21 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -998,109 +933,28 @@ } }, "node_modules/@babel/helpers": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.23.9", - "@babel/traverse": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/types": "^7.28.4" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", - "dev": true, "bin": { "parser": "bin/babel-parser.js" }, @@ -1314,14 +1168,15 @@ } }, "node_modules/@babel/template": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/parser": "^7.23.9", - "@babel/types": "^7.23.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1358,14 +1213,14 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1514,6 +1369,27 @@ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.1.tgz", + "integrity": "sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.0", + "iconv-lite": "^0.6.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@inquirer/testing": { "version": "2.1.19", "resolved": "https://registry.npmjs.org/@inquirer/testing/-/testing-2.1.19.tgz", @@ -2981,10 +2857,11 @@ } }, "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3256,12 +3133,13 @@ } }, "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": "*" } }, "node_modules/async": { @@ -3483,9 +3361,10 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3687,6 +3566,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3738,19 +3630,22 @@ } }, "node_modules/chai": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", - "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, + "license": "MIT", "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" }, "engines": { - "node": ">=12" + "node": ">=4" } }, "node_modules/chai-as-promised": { @@ -3765,6 +3660,29 @@ "chai": ">= 2.1.2 < 6" } }, + "node_modules/chai/node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chai/node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3803,9 +3721,10 @@ } }, "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.0.tgz", + "integrity": "sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==", + "license": "MIT" }, "node_modules/check-error": { "version": "2.1.1", @@ -4256,9 +4175,10 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4323,11 +4243,12 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -4364,10 +4285,14 @@ } }, "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, "engines": { "node": ">=6" } @@ -4477,10 +4402,11 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } @@ -4530,6 +4456,20 @@ "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -4669,14 +4609,10 @@ "peer": 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==", - "dev": true, - "peer": true, - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4685,18 +4621,15 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "peer": true, "engines": { "node": ">= 0.4" } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, - "peer": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -4705,15 +4638,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", - "dev": true, - "peer": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -5430,30 +5363,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/external-editor/node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/extract-stack": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/extract-stack/-/extract-stack-2.0.0.tgz", @@ -5589,9 +5498,10 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -5755,12 +5665,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -5830,7 +5743,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5887,22 +5799,27 @@ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "dev": true, - "peer": true, + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5919,6 +5836,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -5988,9 +5918,10 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -6069,13 +6000,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dev": true, - "peer": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6092,6 +6022,12 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==", + "license": "MIT" + }, "node_modules/halfred": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/halfred/-/halfred-2.0.0.tgz", @@ -6142,11 +6078,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "peer": true, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -6158,8 +6093,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "peer": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -6199,7 +6132,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -6354,11 +6286,12 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -6466,15 +6399,16 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/inquirer": { - "version": "8.2.6", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", - "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "version": "8.2.7", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.7.tgz", + "integrity": "sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==", + "license": "MIT", "dependencies": { + "@inquirer/external-editor": "^1.0.0", "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", - "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", @@ -7753,7 +7687,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", @@ -8191,10 +8126,11 @@ } }, "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.1" } @@ -8233,6 +8169,15 @@ "tmpl": "1.0.5" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -8259,11 +8204,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -8325,31 +8271,32 @@ "dev": true }, "node_modules/mocha": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.3.0.tgz", - "integrity": "sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -8360,10 +8307,11 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -8380,10 +8328,11 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -8391,12 +8340,6 @@ "node": ">=10" } }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/mocha/node_modules/yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -8422,9 +8365,10 @@ "dev": true }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", @@ -8505,6 +8449,20 @@ "optional": true, "peer": true }, + "node_modules/node-notifier": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-10.0.1.tgz", + "integrity": "sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==", + "license": "MIT", + "dependencies": { + "growly": "^1.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.5", + "shellwords": "^0.1.1", + "uuid": "^8.3.2", + "which": "^2.0.2" + } + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -8970,14 +8928,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -9131,10 +9081,11 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", - "dev": true + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", @@ -9145,18 +9096,20 @@ } }, "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 14.16" + "node": "*" } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -9484,10 +9437,11 @@ "dev": true }, "node_modules/qqjs/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "dev": true, + "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -9740,6 +9694,7 @@ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -9774,9 +9729,10 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -10103,7 +10059,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/sax": { "version": "1.3.0", @@ -10149,10 +10106,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -10222,6 +10180,12 @@ "node": ">=8" } }, + "node_modules/shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "license": "MIT" + }, "node_modules/side-channel": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", @@ -10286,15 +10250,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/sinon/node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/sinon/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10441,6 +10396,109 @@ "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", "dev": true }, + "node_modules/spinnies": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/spinnies/-/spinnies-0.5.1.tgz", + "integrity": "sha512-WpjSXv9NQz0nU3yCT9TFEOfpFrXADY9C5fG6eAJqixLhvTX1jP3w92Y8IE5oafIe42nlF9otjhllnXN/QCaB3A==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^3.0.0", + "strip-ansi": "^5.2.0" + } + }, + "node_modules/spinnies/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/spinnies/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/spinnies/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/spinnies/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/spinnies/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/spinnies/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/spinnies/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/spinnies/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/spinnies/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -10697,10 +10755,11 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "dev": true, + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -10836,15 +10895,6 @@ "optional": true, "peer": true }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11362,10 +11412,11 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/wrap-ansi": { "version": "7.0.0", @@ -11514,10 +11565,11 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } diff --git a/package.json b/package.json index 46106d2..a635c96 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "Adobe Inc.", "engines": { "npm": ">= 8.0.0", - "node": "^16.13 || ^18 || ^20" + "node": "^18 || ^20 || >= 22.15 <23" }, "dependencies": { "@adobe/aio-lib-cloudmanager": "^3.1.0", @@ -23,14 +23,16 @@ "inquirer": "^8.2.6", "inquirer-autocomplete-prompt": "^2.0.1", "jsonwebtoken": "^9.0.2", + "node-notifier": "^10.0.1", "open": "^7.0.0", - "ora": "^5.4.1" + "ora": "^5.4.1", + "spinnies": "^0.5.1" }, "devDependencies": { "@adobe/eslint-config-aio-lib-config": "^4.0.0", "@inquirer/testing": "^2.1.19", "@oclif/dev-cli": "^1.26.10", - "chai": "^5.1.1", + "chai": "^4.5.0", "chai-as-promised": "^8.0.0", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", @@ -71,7 +73,8 @@ "commands": "./src/commands", "bin": "aio", "experimental-features": [ - "aem:rde:inspect" + "aem:rde:inspect", + "aem:rde:snapshot" ], "hooks": { "init": "./src/lib/hooks/experimental-features-init-hook" @@ -82,7 +85,10 @@ }, "aem:rde:inspect": { "description": "Inspects the RapidDev Environments (experimental)." + }, + "aem:rde:snapshot": { + "description": "Provides functionality for managing the RapidDev Environments snapshots (experimental)." } } } -} +} \ No newline at end of file diff --git a/src/commands/aem/rde/reset.js b/src/commands/aem/rde/reset.js index 80ec661..22d86cc 100644 --- a/src/commands/aem/rde/reset.js +++ b/src/commands/aem/rde/reset.js @@ -24,18 +24,24 @@ class ResetCommand extends BaseCommand { try { const result = this.jsonResult(); this.doLog(`Reset cm-p${this._programId}-e${this._environmentId}`); - this.spinnerStart('resetting environment'); + this.spinnerStart('resetting environment ... '); const status = await this.withCloudSdk((cloudSdkAPI) => - cloudSdkAPI.resetEnv(flags.wait) + cloudSdkAPI.resetEnv( + flags.wait, + flags['keep-mutable-content'], + flags.force + ) ); this.spinnerStop(); if (flags.wait) { if (status === 'ready') { result.status = 'reset'; this.doLog(`Environment reset.`); + this.notify('reset', 'RDE environment is reset.'); } else if (status === 'reset_failed') { result.status = 'reset_failed'; this.doLog(`Failed to reset the environment.`); + this.notify('reset failed', 'RDE environment failed to reset.'); } } else { result.status = 'resetting'; @@ -61,6 +67,20 @@ Object.assign(ResetCommand, { organizationId: commonFlags.organizationId, programId: commonFlags.programId, environmentId: commonFlags.environmentId, + 'keep-mutable-content': Flags.boolean({ + description: 'Reset the RDE but keep mutable content.', + required: false, + default: false, + multiple: false, + }), + force: Flags.boolean({ + char: 'f', + multiple: false, + required: false, + default: false, + description: + 'Force resets the RDE, not re-using a previously generated base repository. Can be used in case of issues but takes longer.', + }), wait: Flags.boolean({ description: 'Do or do not wait for completion of the reset operation. Progress can be manually checked using the "status" command.', diff --git a/src/commands/aem/rde/restart.js b/src/commands/aem/rde/restart.js index 0b95c55..9b4d6d2 100644 --- a/src/commands/aem/rde/restart.js +++ b/src/commands/aem/rde/restart.js @@ -25,6 +25,7 @@ class RestartCommand extends BaseCommand { this.spinnerStop(); result.status = 'restarted'; this.doLog(`Environment restarted.`); + this.notify('restarted', 'RDE environment is restarted.'); return result; } catch (err) { this.spinnerStop(); diff --git a/src/commands/aem/rde/setup.js b/src/commands/aem/rde/setup.js index f21db4f..02d5d23 100644 --- a/src/commands/aem/rde/setup.js +++ b/src/commands/aem/rde/setup.js @@ -265,7 +265,21 @@ class SetupCommand extends BaseCommand { } async runCommand(args, flags) { - if (flags.show) { + if (flags['enable-notifications']) { + Config.set('rde_enableNotifications', true); + this.notify( + 'RDE notifictions enabled', + 'Notifications enabled for long running tasks.' + ); + return; + } else if (flags['disable-notifications']) { + this.notify( + 'RDE notifictions disabled', + 'You will no longer receive notifications for long running tasks.' + ); + Config.set('rde_enableNotifications', false); + return; + } else if (flags.show) { const orgId = Config.get(CONFIG_ORG); const programId = Config.get(CONFIG_PROGRAM); const programName = Config.get(CONFIG_PROGRAM_NAME); @@ -307,6 +321,17 @@ class SetupCommand extends BaseCommand { }, ]); + const { enableNotifications } = await inquirer.prompt([ + { + type: 'confirm', + name: 'enableNotifications', + message: + 'Do you want to enable desktop notifications for long running tasks, such as reset, snapshot handling and more?', + default: true, + }, + ]); + Config.set('rde_enableNotifications', enableNotifications, storeLocal); + const orgId = await this.getOrgId(); const prevOrgId = Config.get(CONFIG_ORG); Config.set(CONFIG_ORG, orgId, storeLocal); @@ -436,6 +461,22 @@ Object.assign(SetupCommand, { required: false, default: false, }), + 'enable-notifications': Flags.boolean({ + description: + 'Enables desktop notifications for long-running tasks (global aio config).', + char: 'e', + multiple: false, + required: false, + default: false, + }), + 'disable-notifications': Flags.boolean({ + description: + 'Disables desktop notifications for long-running tasks (global aio config).', + char: 'd', + multiple: false, + required: false, + default: false, + }), }, }); diff --git a/src/commands/aem/rde/snapshot/create.js b/src/commands/aem/rde/snapshot/create.js new file mode 100644 index 0000000..a6d0009 --- /dev/null +++ b/src/commands/aem/rde/snapshot/create.js @@ -0,0 +1,247 @@ +/* + * Copyright 2022 Adobe Inc. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +'use strict'; + +const { BaseCommand, Flags } = require('../../../../lib/base-command'); +const { codes: snapshotCodes } = require('../../../../lib/snapshot-errors'); +const { codes: internalCodes } = require('../../../../lib/internal-errors'); +const { + codes: configurationCodes, +} = require('../../../../lib/configuration-errors'); +const { throwAioError } = require('../../../../lib/error-helpers'); +const chalk = require('chalk'); + +const { sleepMillis } = require('../../../../lib/utils'); +const { loadAllArtifacts } = require('../../../../lib/rde-utils'); + +const Spinnies = require('spinnies'); + +class CreateSnapshots extends BaseCommand { + constructor(argv, config, sleepTime = 5000) { + super(argv, config); + this.sleepTime = sleepTime; + } + + async runCommand(args, flags) { + const spinnies = this.getSpinnies(flags); + spinnies?.add('spinner-requesting', { + text: `Requesting to create snapshot ${args.name} (<1m) ...`, + }); + spinnies?.add('spinner-backend', { + text: 'Waiting for backend to pick up the job to create the snapshot (<1min) ...', + }); + spinnies?.add('spinner-create', { + text: 'Locking RDE and create the snapshot (2-5m) ...', + }); + spinnies?.add('spinner-restart', { + text: 'Unlocking the RDE (1-2m)...', + }); + + const result = this.jsonResult(); + const startTime = Date.now(); + result.startTime = startTime; + + let response; + try { + response = await this.withCloudSdk((cloudSdkAPI) => + cloudSdkAPI.createSnapshot(args.name, { + description: flags.description, + }) + ); + } catch (err) { + spinnies?.stopAll('fail'); + throwAioError( + err, + new internalCodes.INTERNAL_SNAPSHOT_ERROR({ messageValues: err }) + ); + } + let actionid; + if (response?.status === 451) { + throw new configurationCodes.NON_EAP(); + } else if (response?.status === 200 || response?.status === 201) { + const json = await response.json(); + actionid = json?.actionid; + const took = this.formatElapsedTime(startTime, Date.now()); + spinnies?.succeed('spinner-requesting', { + text: `Requested to create the snapshot successfully. (${took})`, + successColor: 'greenBright', + }); + } else if (response?.status === 400) { + spinnies?.stopAll('fail'); + throw new configurationCodes.DIFFERENT_ENV_TYPE(); + } else if (response?.status === 404) { + spinnies?.stopAll('fail'); + throw new configurationCodes.PROGRAM_OR_ENVIRONMENT_NOT_FOUND(); + } else if (response?.status === 409) { + spinnies?.stopAll('fail'); + throw new snapshotCodes.ALREADY_EXISTS(); + } else if (response?.status === 503) { + spinnies?.stopAll('fail'); + throw new snapshotCodes.INVALID_STATE(); + } else if (response?.status === 507) { + spinnies?.stopAll('fail'); + throw new snapshotCodes.SNAPSHOT_LIMIT(); + } else { + spinnies?.stopAll('fail'); + throw new internalCodes.UNKNOWN(); + } + + let currentProgress = -1; + let lastProgressChange = new Date(); + let lastProgress = -1; + result.waitingforbackend = new Date(); + + while (currentProgress < 100) { + let progressResponse; + try { + progressResponse = await this.withCloudSdk((cloudSdkAPI) => + cloudSdkAPI.getSnapshotProgress( + 'snapshot_create', + args.name, + actionid + ) + ); + } catch (err) { + result.error = err; + spinnies?.stopAll('fail'); + throwAioError( + err, + new internalCodes.INTERNAL_SNAPSHOT_ERROR({ messageValues: err }) + ); + } + + if (progressResponse.status === 200) { + const json = await progressResponse.json(); + lastProgress = currentProgress; + currentProgress = json?.progressPercentage; + } else if (progressResponse.status === 404) { + spinnies?.stopAll('fail'); + throw new snapshotCodes.SNAPSHOT_NOT_FOUND(); + } else { + spinnies?.stopAll('fail'); + this.doLog('Could not get the progress of the snapshot creation.'); + spinnies?.stopAll('fail'); + throw new internalCodes.UNKNOWN(); + } + + if (currentProgress > 0 && !result.processnigsnapshotstarted) { + const took = this.formatElapsedTime( + result.waitingforbackend, + Date.now() + ); + spinnies?.succeed('spinner-backend', { + text: `Backend picked up the job to create the snapshot. (${took})`, + successColor: 'greenBright', + }); + result.processnigsnapshotstarted = new Date(); + } + if (lastProgress === -2) { + spinnies?.stopAll('fail'); + this.doLog(chalk.red('Snapshot creation failed.')); + this.notify('failed', 'Snapshot creation failed.'); + throw new snapshotCodes.SNAPSHOT_CREATION_FAILED(); + } + + if (currentProgress > lastProgress) { + lastProgressChange = new Date(); + } else if (lastProgressChange.getTime() + 1000 * 60 * 15 < Date.now()) { + // If the progress hasn't changed in 15 minutes, we assume the process is stuck. + spinnies?.stopAll('fail'); + this.doLog( + chalk.red( + 'The snapshot creation seems stuck. Either the snapshot is huge and takes a long time, or the backend is not responding. Please monitor snapshot creation using the list of snapshots to check on the state. If the snapshot is stuck for more than an hour, please contact support.' + ) + ); + this.notify('failed', 'Snapshot creation is stuck.'); + throw new snapshotCodes.SNAPSHOT_CREATION_STUCK(); + } + + await sleepMillis(this.sleepTime); + } + + if (currentProgress === 100) { + const took = this.formatElapsedTime( + result.processnigsnapshotstarted, + Date.now() + ); + spinnies?.succeed('spinner-create', { + text: `Created snapshot successfully. (${took})`, + successColor: 'greenBright', + }); + } + result.processnigsnapshotended = new Date(); + + while (true) { + const status = await this.withCloudSdk((cloudSdkAPI) => + loadAllArtifacts(cloudSdkAPI) + ); + if (status.status === 'Ready') { + break; + } + await sleepMillis(this.sleepTime * 2); + } + + const took = this.formatElapsedTime( + result.processnigsnapshotended, + Date.now() + ); + spinnies?.succeed('spinner-restart', { + text: `RDE unlocked successfully. (${took})`, + successColor: 'greenBright', + }); + + this.doLog( + chalk.green( + `Snapshot ${args.name} created successfully. Check the list of snapshots using the command: 'aio aem rde snapshot'` + ) + ); + + result.endTime = new Date(); + this.doLog( + chalk.yellow( + `Total time to create the snapshot: ${this.formatElapsedTime(startTime, Date.now())}` + ) + ); + result.totalseconds = (result.endTime - startTime) / 1000; + result.startTime = new Date(startTime); + this.notify('restored', 'Snapshot created.'); + return result; + } + + getSpinnies(flags) { + return flags.quiet || flags.json ? undefined : new Spinnies(); + } +} + +Object.assign(CreateSnapshots, { + description: + 'Creates a snapshot of the current state of the environment, includes content and deployment.', + args: [ + { + name: 'name', + description: + 'The name of the new snapshot. The name must be unique within the environment.', + required: true, + }, + ], + aliases: [], + flags: { + description: Flags.string({ + description: 'A brief description of the snapshot.', + char: 'd', + multiple: false, + required: false, + }), + }, +}); + +module.exports = CreateSnapshots; diff --git a/src/commands/aem/rde/snapshot/delete.js b/src/commands/aem/rde/snapshot/delete.js new file mode 100644 index 0000000..6c84ace --- /dev/null +++ b/src/commands/aem/rde/snapshot/delete.js @@ -0,0 +1,149 @@ +/* + * Copyright 2022 Adobe Inc. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +'use strict'; + +const { BaseCommand, Flags } = require('../../../../lib/base-command'); +const { codes: snapshotCodes } = require('../../../../lib/snapshot-errors'); +const { codes: internalCodes } = require('../../../../lib/internal-errors'); +const { + codes: configurationCodes, +} = require('../../../../lib/configuration-errors'); +const { throwAioError } = require('../../../../lib/error-helpers'); +const chalk = require('chalk'); + +class DeleteSnapshots extends BaseCommand { + async runCommand(args, flags) { + if (flags.all) { + await this.deleteAllSnapshots(flags.force); + } else { + await this.deleteSnapshot(args.name, flags.force); + } + } + + async deleteAllSnapshots(force) { + let response; + try { + this.spinnerStart('fetching all snapshots'); + response = await this.withCloudSdk((cloudSdkAPI) => + cloudSdkAPI.getSnapshots() + ); + } catch (err) { + throwAioError( + err, + new internalCodes.INTERNAL_SNAPSHOT_ERROR({ messageValues: err }) + ); + } finally { + this.spinnerStop(); + } + + if (response?.status === 451) { + throw new configurationCodes.NON_EAP(); + } else if (response.status === 200) { + const json = await response.json(); + this.spinnerStop(); + if (json?.items?.length === 0) { + this.doLog('There are no snapshots yet.'); + } else { + const promises = + json?.map((e) => this.deleteSnapshot(e.name, force)) || []; + await Promise.all(promises); + } + } else { + throw new internalCodes.UNKNOWN(); + } + } + + async deleteSnapshot(name, force) { + let response; + try { + this.spinnerStart(`Deleting snapshot ${name}...`); + response = await this.withCloudSdk((cloudSdkAPI) => + cloudSdkAPI.deleteSnapshot(name, force) + ); + } catch (err) { + this.spinnerStop(); + throwAioError( + err, + new internalCodes.INTERNAL_SNAPSHOT_ERROR({ messageValues: err }) + ); + } + this.spinnerStop(); + if (response?.status === 451) { + throw new configurationCodes.NON_EAP(); + } else if (response?.status === 200 || response?.status === 201) { + if (force) { + this.doLog( + chalk.green( + `Snapshot ${name} deleted successfully. Use 'aio aem rde snapshot' to validate its removal.` + ) + ); + } else { + this.doLog( + chalk.green( + `Snapshot ${name} deleted successfully. Use 'aio aem rde snapshot' to view its updated state, it will be removed once the retention time has passed. Use 'aio aem rde snapshot undelete ${name}' to undelete it.` + ) + ); + } + } else if (response?.status === 400) { + throw new configurationCodes.DIFFERENT_ENV_TYPE(); + } else if (response?.status === 404) { + const json = await response.json(); + if ( + json.details === 'The requested environment or program does not exist.' + ) { + throw new configurationCodes.PROGRAM_OR_ENVIRONMENT_NOT_FOUND(); + } else if (json.details === 'The requested snapshot does not exist.') { + throw new snapshotCodes.SNAPSHOT_NOT_FOUND(); + } + } else if (response?.status === 403) { + const json = await response.json(); + if ( + json.details === "The snapshot to be wiped is not in state 'removed'." + ) { + throw new snapshotCodes.SNAPSHOT_WRONG_STATE(); + } + } else if (response?.status === 503) { + throw new snapshotCodes.INVALID_STATE(); + } else { + throw new internalCodes.UNKNOWN(); + } + } +} + +Object.assign(DeleteSnapshots, { + description: + 'Marks a snapshot for deletion. The snapshot will be deleted after 7 days. A previously deleted snapshot can be undeleted.', + args: [ + { + name: 'name', + description: 'The name of the snapshot to delete.', + required: true, + }, + ], + aliases: [], + flags: { + all: Flags.boolean({ + description: 'Mark all snapshots as deleted.', + char: 'a', + multiple: false, + required: false, + default: false, + }), + force: Flags.boolean({ + char: 'f', + multiple: false, + required: false, + }), + }, +}); + +module.exports = DeleteSnapshots; diff --git a/src/commands/aem/rde/snapshot/index.js b/src/commands/aem/rde/snapshot/index.js new file mode 100644 index 0000000..a99e226 --- /dev/null +++ b/src/commands/aem/rde/snapshot/index.js @@ -0,0 +1,153 @@ +/* + * Copyright 2022 Adobe Inc. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +'use strict'; + +const { BaseCommand, Flags, cli } = require('../../../../lib/base-command'); +const { codes: internalCodes } = require('../../../../lib/internal-errors'); +const { throwAioError } = require('../../../../lib/error-helpers'); +const { + codes: configurationCodes, +} = require('../../../../lib/configuration-errors'); + +const DATE_FORMATTER = (val) => + new Date(val).toLocaleString(undefined, { + dateStyle: 'medium', + timeStyle: 'short', + }); + +// Helper to format bytes to MB or GB +const BYTE_FORMATTER = (bytes) => { + if (typeof bytes !== 'number' || isNaN(bytes)) return ''; + const kb = 1024; + const mb = 1024 * kb; + const gb = 1024 * mb; + if (bytes >= gb) { + return (bytes / gb).toFixed(2) + ' GB'; + } else if (bytes >= mb) { + return (bytes / mb).toFixed(2) + ' MB'; + } else if (bytes >= kb) { + return (bytes / kb).toFixed(2) + ' KB'; + } + return bytes + ' B'; +}; + +const SIZE_FORMATTER = (size) => { + const bytes = size.total_size ?? size; + return BYTE_FORMATTER(bytes); +}; + +const FORMATTERS = { + created: DATE_FORMATTER, + lastUsed: DATE_FORMATTER, + size: SIZE_FORMATTER, +}; + +const formatItem = (row) => { + const copy = Object.assign({}, row); + const keys = Object.keys(copy); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (Object.hasOwn(FORMATTERS, key)) { + copy[key] = FORMATTERS[key].call(null, copy[key]); + } + } + return copy; +}; + +class ListSnapshots extends BaseCommand { + constructor(argv, config) { + super(argv, config); + this.programsCached = []; + this.environmentsCached = []; + } + + async runCommand(args, flags) { + let response; + const result = this.jsonResult(); + try { + this.spinnerStart('fetching snapshots'); + response = await this.withCloudSdk((cloudSdkAPI) => + cloudSdkAPI.getSnapshots() + ); + } catch (err) { + throwAioError( + err, + new internalCodes.INTERNAL_SNAPSHOT_ERROR({ messageValues: err }) + ); + } finally { + this.spinnerStop(); + } + + if (response?.status === 451) { + throw new configurationCodes.NON_EAP(); + } else if (response.status === 200) { + const json = await response.json(); + result.status = response.status; + this.spinnerStop(); + if (json?.length === 0) { + this.doLog('There are no snapshots yet.'); + } else { + result.snapshots = json; + this.logInTableFormat(json, flags); + } + } else if (response?.status === 400) { + throw new configurationCodes.DIFFERENT_ENV_TYPE(); + } else if (response?.status === 404) { + throw new configurationCodes.PROGRAM_OR_ENVIRONMENT_NOT_FOUND(); + } else { + throw new internalCodes.UNKNOWN(); + } + return result; + } + + logInTableFormat(items, flags) { + cli.table( + items.map(formatItem), + { + name: { + minWidth: 20, + }, + description: { + minWidth: 20, + }, + usage: {}, + size: {}, + state: {}, + created: {}, + lastUsed: { header: 'Last Used' }, + }, + { + sort: flags.sort, + printLine: (s) => this.doLog(s, true), + } + ); + } +} + +Object.assign(ListSnapshots, { + description: + 'Lists all content and deployment snapshots in your organization. Use --help for a list of subcommands.', + args: [], + aliases: [], + flags: { + sort: Flags.string({ + description: + 'Sort the table by a table header, prefixed by a minus symbol for reverse sorting', + char: 's', + multiple: false, + required: false, + default: '-Last Used', + }), + }, +}); + +module.exports = ListSnapshots; diff --git a/src/commands/aem/rde/snapshot/restore.js b/src/commands/aem/rde/snapshot/restore.js new file mode 100644 index 0000000..2fb3483 --- /dev/null +++ b/src/commands/aem/rde/snapshot/restore.js @@ -0,0 +1,237 @@ +/* + * Copyright 2022 Adobe Inc. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +'use strict'; + +const { BaseCommand, Flags } = require('../../../../lib/base-command'); +const { codes: snapshotCodes } = require('../../../../lib/snapshot-errors'); +const { codes: internalCodes } = require('../../../../lib/internal-errors'); +const { codes: validationCodes } = require('../../../../lib/validation-errors'); +const { + codes: configurationCodes, +} = require('../../../../lib/configuration-errors'); +const { throwAioError } = require('../../../../lib/error-helpers'); +const chalk = require('chalk'); + +const { sleepMillis } = require('../../../../lib/utils'); +const { loadAllArtifacts } = require('../../../../lib/rde-utils'); + +const Spinnies = require('spinnies'); + +class RestoreSnapshot extends BaseCommand { + constructor(argv, config, sleepTime = 5000) { + super(argv, config); + this.sleepTime = sleepTime; + } + + async runCommand(args, flags) { + const spinnies = this.getSpinnies(flags); + if (!flags.status) { + spinnies?.add('spinner-requesting', { + text: `Requesting to restore snapshot ${args.name} (<1m) ...`, + }); + } + spinnies?.add('spinner-backend', { + text: 'Waiting for backend to pick up the job to restore the snapshot (<1min) ...', + }); + spinnies?.add('spinner-restore', { + text: 'Restoring snapshot to RDE (2-5m) ...', + }); + spinnies?.add('spinner-restart', { + text: 'Wait for the RDE to restart (5-10m)...', + }); + + const result = this.jsonResult(); + const startTime = Date.now(); + result.startTime = startTime; + let response; + try { + response = await this.withCloudSdk((cloudSdkAPI) => + cloudSdkAPI.restoreSnapshot(args.name, { + 'only-mutable-content': flags['only-mutable-content'], + }) + ); + } catch (err) { + result.error = err; + spinnies?.stopAll('fail'); + throwAioError( + err, + new internalCodes.INTERNAL_SNAPSHOT_ERROR({ messageValues: err }) + ); + } + let actionid; + if (response?.status === 451) { + throw new configurationCodes.NON_EAP(); + } else if (response?.status === 200) { + const json = await response.json(); + actionid = json?.actionid; + const took = this.formatElapsedTime(startTime, Date.now()); + spinnies?.succeed('spinner-requesting', { + text: `Requested to restore the snapshot successfully. (${took})`, + successColor: 'greenBright', + }); + } else if (response?.status === 400) { + spinnies?.stopAll('fail'); + throw new configurationCodes.DIFFERENT_ENV_TYPE(); + } else if (response?.status === 404) { + const json = await response.json(); + if ( + json.details === 'The requested environment or program does not exist.' + ) { + spinnies?.stopAll('fail'); + throw new configurationCodes.PROGRAM_OR_ENVIRONMENT_NOT_FOUND(); + } else if (json.details === 'The requested snapshot does not exist.') { + spinnies?.stopAll('fail'); + throw new snapshotCodes.SNAPSHOT_NOT_FOUND(); + } else if (json.details === 'The snapshot is in deleted state.') { + spinnies?.stopAll('fail'); + throw new snapshotCodes.SNAPSHOT_DELETED(); + } + } else if (response?.status === 406) { + spinnies?.stopAll('fail'); + throw new snapshotCodes.INVALID_STATE(); + } else if (response?.status === 503) { + spinnies?.stopAll('fail'); + throw new validationCodes.DEPLOYMENT_IN_PROGRESS(); + } else { + spinnies?.stopAll('fail'); + throw new internalCodes.UNKNOWN(); + } + + let lastProgress = -1; + result.waitingforbackend = new Date(); + + while (lastProgress < 100) { + let progressResponse; + try { + progressResponse = await this.withCloudSdk((cloudSdkAPI) => + cloudSdkAPI.getSnapshotProgress( + 'snapshot_restore', + args.name, + actionid + ) + ); + } catch (err) { + result.error = err; + spinnies?.stopAll('fail'); + throwAioError( + err, + new internalCodes.INTERNAL_SNAPSHOT_ERROR({ messageValues: err }) + ); + } + + if (progressResponse.status === 200) { + const json = await progressResponse.json(); + lastProgress = json?.progressPercentage; + } else if (progressResponse.status === 404) { + spinnies?.stopAll('fail'); + throw new snapshotCodes.SNAPSHOT_NOT_FOUND(); + } else { + spinnies?.stopAll('fail'); + this.doLog('Could not get the progress of the snapshot application.'); + throw new internalCodes.UNKNOWN(); + } + + if (lastProgress > 0 && !result.processnigsnapshotstarted) { + const took = this.formatElapsedTime( + result.waitingforbackend, + Date.now() + ); + spinnies?.succeed('spinner-backend', { + text: `Backend picked up the job to restore the snapshot. (${took})`, + successColor: 'greenBright', + }); + result.processnigsnapshotstarted = new Date(); + } + if (lastProgress === -2) { + spinnies?.stopAll('fail'); + this.doLog(chalk.red('Snapshot creation failed.')); + this.notify('failed', 'Snapshot creation failed.'); + throw new snapshotCodes.SNAPSHOT_RESTORE_FAILED(); + } + await sleepMillis(this.sleepTime); + } + + if (lastProgress === 100) { + const took = this.formatElapsedTime( + result.processnigsnapshotstarted, + Date.now() + ); + spinnies?.succeed('spinner-restore', { + text: `Restored snapshot to RDE successfully. (${took})`, + successColor: 'greenBright', + }); + } + result.processnigsnapshotended = new Date(); + + while (true) { + const status = await this.withCloudSdk((cloudSdkAPI) => + loadAllArtifacts(cloudSdkAPI) + ); + if (status.status === 'Ready') { + break; + } + await sleepMillis(this.sleepTime * 2); + } + + const took = this.formatElapsedTime( + result.processnigsnapshotended, + Date.now() + ); + spinnies?.succeed('spinner-restart', { + text: `RDE restarted successfully. (${took})`, + successColor: 'greenBright', + }); + + this.doLog( + chalk.green( + `Snapshot ${args.name} restored successfully. Check the deployment using the command: 'aio aem rde status'` + ) + ); + + result.endTime = new Date(); + this.doLog( + chalk.yellow( + `Total time to rebase on snapshot: ${this.formatElapsedTime(startTime, Date.now())}` + ) + ); + result.startTime = new Date(startTime); + result.totalseconds = (result.endTime - startTime) / 1000; + this.notify('restored', 'Snapshot restored.'); + return result; + } + + getSpinnies(flags) { + return flags.quiet || flags.json ? undefined : new Spinnies(); + } +} + +Object.assign(RestoreSnapshot, { + description: 'Restores a snapshot to the environment.', + args: [ + { + name: 'name', + description: 'The name of the snapshot to restore to the current RDE.', + required: true, + }, + ], + aliases: [], + flags: { + 'only-mutable-content': Flags.boolean({ + description: 'Restores the mutable content only.', + multiple: false, + required: false, + default: false, + }), + }, +}); + +module.exports = RestoreSnapshot; diff --git a/src/commands/aem/rde/snapshot/undelete.js b/src/commands/aem/rde/snapshot/undelete.js new file mode 100644 index 0000000..b29df1b --- /dev/null +++ b/src/commands/aem/rde/snapshot/undelete.js @@ -0,0 +1,78 @@ +/* + * Copyright 2022 Adobe Inc. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +'use strict'; + +const { BaseCommand } = require('../../../../lib/base-command'); +const { codes: snapshotCodes } = require('../../../../lib/snapshot-errors'); +const { codes: internalCodes } = require('../../../../lib/internal-errors'); +const { + codes: configurationCodes, +} = require('../../../../lib/configuration-errors'); +const { throwAioError } = require('../../../../lib/error-helpers'); +const chalk = require('chalk'); +class UndeleteSnapshots extends BaseCommand { + async runCommand(args, flags) { + let response; + try { + this.spinnerStart(`Undelete snapshot ${args.name}...`); + response = await this.withCloudSdk((cloudSdkAPI) => + cloudSdkAPI.undeleteSnapshot(args.name) + ); + } catch (err) { + this.spinnerStop(); + throwAioError( + err, + new internalCodes.INTERNAL_SNAPSHOT_ERROR({ messageValues: err }) + ); + } + this.spinnerStop(); + if (response?.status === 451) { + throw new configurationCodes.NON_EAP(); + } else if (response?.status === 200) { + this.doLog( + chalk.green( + `Snapshot ${args.name} undeleted successfully. Use 'aio aem rde snapshot' to view its updated state. Use 'aio aem rde snapshot restore ${args.name}' to restore it on the RDE.` + ) + ); + } else if (response?.status === 400) { + throw new configurationCodes.DIFFERENT_ENV_TYPE(); + } else if (response?.status === 404) { + const json = await response.json(); + if ( + json.details === 'The requested environment or program does not exist.' + ) { + throw new configurationCodes.PROGRAM_OR_ENVIRONMENT_NOT_FOUND(); + } else if (json.details === 'The requested snapshot does not exist.') { + throw new snapshotCodes.SNAPSHOT_NOT_FOUND(); + } + } else if (response?.status === 507) { + throw new snapshotCodes.SNAPSHOT_LIMIT(); + } else { + throw new internalCodes.UNKNOWN(); + } + } +} + +Object.assign(UndeleteSnapshots, { + description: 'Undeletes a snapshot so it will not be deleted any longer.', + args: [ + { + name: 'name', + description: 'The name of the snapshot to undelete.', + required: true, + }, + ], + aliases: [], + flags: {}, +}); + +module.exports = UndeleteSnapshots; diff --git a/src/commands/aem/rde/status.js b/src/commands/aem/rde/status.js index 36c19b3..68bff39 100644 --- a/src/commands/aem/rde/status.js +++ b/src/commands/aem/rde/status.js @@ -11,18 +11,43 @@ */ 'use strict'; -const { BaseCommand, commonFlags } = require('../../../lib/base-command'); +const { + BaseCommand, + commonFlags, + Flags, +} = require('../../../lib/base-command'); const { loadAllArtifacts, groupArtifacts } = require('../../../lib/rde-utils'); const { codes: internalCodes } = require('../../../lib/internal-errors'); const { throwAioError } = require('../../../lib/error-helpers'); +const { sleepMillis } = require('../../../lib/utils'); + class StatusCommand extends BaseCommand { async runCommand(args, flags) { try { this.doLog(`Info for cm-p${this._programId}-e${this._environmentId}`); - this.spinnerStart('retrieving environment status information'); - const status = await this.withCloudSdk((cloudSdkAPI) => - loadAllArtifacts(cloudSdkAPI) - ); + + let status; + if (flags?.wait) { + this.spinnerStart( + 'retrieving environment status information - waiting for ready state' + ); + while (true) { + status = await this.withCloudSdk((cloudSdkAPI) => + loadAllArtifacts(cloudSdkAPI) + ); + if (status.status === 'Ready') { + break; + } + await sleepMillis(10000); + } + this.notify('ready', 'RDE environment is ready'); + } else { + this.spinnerStart('retrieving environment status information'); + status = await this.withCloudSdk((cloudSdkAPI) => + loadAllArtifacts(cloudSdkAPI) + ); + } + this.spinnerStop(); this.doLog(`Environment: ${status.status}`, true); const result = this.jsonResult(status.status); @@ -87,10 +112,18 @@ Object.assign(StatusCommand, { programId: commonFlags.programId, environmentId: commonFlags.environmentId, quiet: commonFlags.quiet, + wait: Flags.boolean({ + description: 'Wait for the environment to be ready', + char: 'w', + multiple: false, + required: false, + default: false, + }), }, usage: [ 'status # output as textual content', 'status --json # output as json object', + 'status --wait # wait for the environment to be ready', ], aliases: [], }); diff --git a/src/lib/base-command.js b/src/lib/base-command.js index a5b5a62..b4a529a 100644 --- a/src/lib/base-command.js +++ b/src/lib/base-command.js @@ -16,6 +16,7 @@ const jwt = require('jsonwebtoken'); const inquirer = require('inquirer'); const spinner = require('ora')(); const chalk = require('chalk'); +const notifier = require('node-notifier'); // Adobe dependencies const { getToken, context } = require('@adobe/aio-lib-ims'); @@ -110,8 +111,12 @@ class BaseCommand extends Command { handleError(err, this.error); } + rdeIdentification() { + return `${concatEnvironemntId(this._programId, this._environmentId)}${this.printNamesWhenAvailable()}`; + } + getLogHeader() { - return `Running ${!this.id ? this.constructor.name : this.id} on ${concatEnvironemntId(this._programId, this._environmentId)}${this.printNamesWhenAvailable()}`; + return `Running ${!this.id ? this.constructor.name : this.id} on ${this.rdeIdentification()}`; } printNamesWhenAvailable() { @@ -141,6 +146,13 @@ class BaseCommand extends Command { spinner.stop(); } + notify(title, message) { + if (Config.get('rde_enableNotifications')) { + title = `${this.rdeIdentification()} - ${title}`; + notifier.notify({ title, message }); + } + } + /** * */ @@ -162,9 +174,10 @@ class BaseCommand extends Command { * */ async getTokenAndKey() { - // TODO - support context flag let contextName = - (await context.getCurrent()) || 'aio-cli-plugin-cloudmanager'; + this.flags?.context || + (await context.getCurrent()) || + 'aio-cli-plugin-cloudmanager'; let contextData = await context.get(contextName); if (!contextData?.data) { @@ -296,11 +309,31 @@ class BaseCommand extends Command { }; return result; } + + formatElapsedTime(startTime, endTime) { + const ms = endTime - startTime; + if (ms < 1000) { + return `${ms}ms`; + } else if (ms < 60000) { + return `${(ms / 1000).toFixed(2)}s`; + } else { + return `${(ms / 60000).toFixed(2)}m`; + } + } } Object.assign(BaseCommand, { description: 'Enable json output for all commands by default.', enableJsonFlag: true, + flags: { + context: Flags.string({ + aliases: ['ctx', 'imsContextName'], + description: 'The IMS context used to retrieve login information', + multiple: false, + required: false, + helpGroup: 'GLOBAL', + }), + }, }); module.exports = { diff --git a/src/lib/cloud-sdk-api.js b/src/lib/cloud-sdk-api.js index cdcca8d..e73c175 100644 --- a/src/lib/cloud-sdk-api.js +++ b/src/lib/cloud-sdk-api.js @@ -62,6 +62,12 @@ class CloudSdkAPI { `${rdeUrl}/program/${programId}/environment/${environmentId}`, authorizationHeaders ); + this._snapshotClient = new DoRequest( + `${rdeUrl}/snapshots`, + authorizationHeaders + ); + this.programId = programId; + this.environmentId = environmentId; this._cmReleaseId = concatEnvironemntId(programId, environmentId); } @@ -181,6 +187,68 @@ class CloudSdkAPI { ); } + async getSnapshotProgress(action, snapshotName, actionid) { + const params = { + programId: this.programId, + environmentId: this.environmentId, + 'snapshot-name': snapshotName, + action, + actionid, + }; + const queryString = this.createUrlQueryStr(params); + return await this._rdeClient.doGet(`/runtime/status${queryString}`); + } + + async getSnapshots() { + const params = { + programId: this.programId, + environmentId: this.environmentId, + }; + const queryString = this.createUrlQueryStr(params); + return await this._snapshotClient.doGet(`${queryString}`); + } + + async deleteSnapshot(name, force) { + const params = { + force, + programId: this.programId, + environmentId: this.environmentId, + }; + const queryString = this.createUrlQueryStr(params); + return await this._snapshotClient.doDelete(`/${name}${queryString}`); + } + + async undeleteSnapshot(name) { + const params = { + programId: this.programId, + environmentId: this.environmentId, + }; + const queryString = this.createUrlQueryStr(params); + return await this._snapshotClient.doPut(`/${name}${queryString}`); + } + + async createSnapshot(name, params) { + params = { + ...params, + 'snapshot-name': name, + programId: this.programId, + environmentId: this.environmentId, + }; + const queryString = this.createUrlQueryStr(params); + return await this._snapshotClient.doPost(`${queryString}`); + } + + async restoreSnapshot(name, params) { + const queryString = this.createUrlQueryStr({ + programId: this.programId, + environmentId: this.environmentId, + }); + return await this._snapshotClient.doPost( + `/${name}/restore${queryString}`, + params + ); + } + async getLogs(id) { return await this._rdeClient.doGet(`/runtime/updates/${id}/logs`); } @@ -516,17 +584,47 @@ class CloudSdkAPI { } } - async resetEnv(wait) { + async resetEnv(wait, keepMutableContent, force) { await this._checkRDE(); - await this._waitForCMStatus(); - await this._resetEnv(); + + // later we should fold everything into the RDE API and not use the CM API + if (keepMutableContent || force) { + const result = await this._rdeClient.doPost(`/runtime/reset`, { + 'keep-mutable-content': keepMutableContent, + force, + }); + + if (result.status !== 201) { + throw await this._createError(result); + } + + const namespace = await this._getNamespace(); + const tries = 3; + for (let i = 0; i < tries; i++) { + await sleepSeconds(5); + await this._waitForEnvRunning(namespace); + } + } else { + await this._waitForCMStatus(); + await this._cloudManagerClient.doPut(`/reset`); + if (wait) { + return await this._waitForCMStatus(); + } + } + } + + async cleanEnv(wait, params) { + await this._checkRDE(); + await this._waitForEnvReady(); + await this._cleanEnv(); if (wait) { - return await this._waitForCMStatus(); + await this._waitForEnvReady(); } } - async _resetEnv() { - await this._cloudManagerClient.doPut(`/reset`); + async _cleanEnv(params) { + const queryString = this.createUrlQueryStr(params); + await this._rdeClient.doPut(`/clean${queryString}`); } async _waitForCMStatus() { diff --git a/src/lib/configuration-errors.js b/src/lib/configuration-errors.js index 8235eab..9fef050 100644 --- a/src/lib/configuration-errors.js +++ b/src/lib/configuration-errors.js @@ -102,3 +102,12 @@ E( 'MISSING_INSPECT_ACCESS_TOKEN', 'The access token for the inspect commands is missing. Please set one up with the `aio aem rde inspect setup` command.' ); +E('DIFFERENT_ENV_TYPE', 'The given environment is not an RDE'); +E( + 'PROGRAM_OR_ENVIRONMENT_NOT_FOUND', + 'The environment or program does not exist' +); +E( + 'NON_EAP', + 'The feature is part of the EAP program and not available for general use. Please contact your Adobe representative for more information on how to join the early access program.' +); diff --git a/src/lib/doRequest.js b/src/lib/doRequest.js index 042a29b..da12589 100644 --- a/src/lib/doRequest.js +++ b/src/lib/doRequest.js @@ -33,7 +33,8 @@ class DoRequest { (response) => response && ((response.status >= 200 && response.status < 300) || - response.status === 404), + response.status === 404 || + response.status === 451), // 451 Unavailable For Legal Reasons, EAP early access), 1, 5 ); @@ -46,27 +47,27 @@ class DoRequest { } async doPost(path, body) { - const ret = this.doRequest('post', path, body); - if (ret) { - return ret; - } - throw new internalCodes.NETWORK_ERROR({ - messageValues: this._baseUrl + path, - }); + return this.do('post', path, body); } async doPut(path, body) { - const ret = this.doRequest('put', path, body); - if (ret) { - return ret; - } - throw new internalCodes.NETWORK_ERROR({ - messageValues: this._baseUrl + path, - }); + return this.do('put', path, body); + } + + async doOptions(path, body) { + return this.do('options', path, body); + } + + async doPatch(path, body) { + return this.do('patch', path, body); } async doDelete(path) { - const ret = this.doRequest('delete', path); + return this.do('delete', path); + } + + async do(method, path, body) { + const ret = this.doRequest(method, path, body); if (ret) { return ret; } diff --git a/src/lib/internal-errors.js b/src/lib/internal-errors.js index 3f2be66..f8fffbd 100644 --- a/src/lib/internal-errors.js +++ b/src/lib/internal-errors.js @@ -112,6 +112,10 @@ E( 'INTERNAL_GET_SLING_REQUESTS_ERROR', 'There was an unexpected error when running get sling requests command. Please, try again later and if the error persists, report it. Error %s' ); +E( + 'INTERNAL_SNAPSHOT_ERROR', + 'There was an unexpected error when running a snapshot command. Please, try again later and if the error persists, report it. Error %s' +); E( 'INTERNAL_DELETE_ERROR', 'There was an unexpected error when running delete command. Please, try again later and if the error persists, report it. Error %s' @@ -128,6 +132,10 @@ E( 'INTERNAL_RESET_ERROR', 'There was an unexpected error when running reset command. Please, try again later and if the error persists, report it. Error %s' ); +E( + 'INTERNAL_CLEAN_ERROR', + 'There was an unexpected error when running clean command. Please, try again later and if the error persists, report it. Error %s' +); E( 'INTERNAL_RESTART_ERROR', 'There was an unexpected error when running restart command. Please, try again later and if the error persists, report it. Error %s' @@ -136,3 +144,8 @@ E( 'INTERNAL_STATUS_ERROR', 'There was an unexpected error when running status command. Please, try again later and if the error persists, report it. Error %s' ); +E('UNKNOWN', 'An unknown error occurred.'); +E( + 'INVALID_STATE', + 'The RDE is not in a state where the command can be executed.' +); diff --git a/src/lib/snapshot-errors.js b/src/lib/snapshot-errors.js new file mode 100644 index 0000000..519c8c9 --- /dev/null +++ b/src/lib/snapshot-errors.js @@ -0,0 +1,81 @@ +/* + * Copyright 2023 Adobe Inc. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +const { ErrorWrapper, createUpdater } = + require('@adobe/aio-lib-core-errors').AioCoreSDKErrorWrapper; + +const codes = {}; +const messages = new Map(); + +/** + * Create an Updater for the Error wrapper + * + * @ignore + */ +const Updater = createUpdater( + // object that stores the error classes (to be exported) + codes, + // Map that stores the error strings (to be exported) + messages +); + +/** + * Provides a wrapper to easily create classes of a certain name, and values + * + * @ignore + */ +const E = ErrorWrapper( + // The class name for your SDK Error. Your Error objects will be these objects + 'RDECLIValidationError', + // The name of your SDK. This will be a property in your Error objects + 'RDECLI', + // the object returned from the CreateUpdater call above + Updater + // the base class that your Error class is extending. AioCoreSDKError is the default + /* AioCoreSDKError, */ +); + +module.exports = { + codes, + messages, +}; + +// Define your error codes with the wrapper +E( + 'INVALID_STATE', + 'The RDE is not in a state where a snapshot can be created or restored.' +); +E('ALREADY_EXISTS', 'A snapshot with the given name already exists'); +E('SNAPSHOT_NOT_FOUND', 'The snapshot does not exist'); +E( + 'SNAPSHOT_LIMIT', + 'Reached the maximum number or diskspace of snapshots. Remove some snapshots and try again' +); +E( + 'SNAPSHOT_WRONG_STATE', + 'Snapshot is in wrong state. Must be in state "REMOVED" to be able to wipe.' +); +E( + 'SNAPSHOT_DELETED', + 'The snapshot is in deleted state, change the state to available before restoring.' +); +E( + 'SNAPSHOT_CREATION_FAILED', + 'The snapshot failed to be created. Please contact support.' +); +E( + 'SNAPSHOT_CREATION_STUCK', + 'The snapshot creation seems stuck. Either the snapshot is huge and takes a long time, or the backend is not responding. Please monitor snapshot creation using the list of snapshots to check on the state. If the snapshot is stuck for some hours, please contact support.' +); +E( + 'SNAPSHOT_RESTORE_FAILED', + 'The snapshot failed to be restored. Please try again. When this still happens, reset the RDE and try again. Otherwise contact support.' +); diff --git a/test/commands/aem/rde/snapshot/create.test.js b/test/commands/aem/rde/snapshot/create.test.js new file mode 100644 index 0000000..e723426 --- /dev/null +++ b/test/commands/aem/rde/snapshot/create.test.js @@ -0,0 +1,311 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const CreateSnapshot = require('../../../../../src/commands/aem/rde/snapshot/create'); +const Spinnies = require('spinnies'); +const assert = require('assert'); + +/** + * + * @param sinon + * @param command + * @param methods + */ +function createCloudSdkAPIStub(sinon, command, methods) { + const cloudSdkApiStub = {}; + Object.keys(methods).forEach((k) => { + cloudSdkApiStub[k] = methods[k]; + }); + sinon + .stub(command, 'withCloudSdk') + .callsFake(async (fn) => fn(cloudSdkApiStub)); + return [command, cloudSdkApiStub]; +} + +/** + * + * @param sinon + * @param command + */ +function createSpinniesStub(sinon, command) { + const spinnies = new Spinnies(); + const addSpy = sinon.spy(spinnies, 'add'); + const stopAllSpy = sinon.spy(spinnies, 'stopAll'); + const succeedSpy = sinon.spy(spinnies, 'succeed'); + sinon.stub(command, 'getSpinnies').callsFake(() => spinnies); + + return { + addSpy, + stopAllSpy, + succeedSpy, + }; +} + +/** + * + * @param sinon + * @param command + */ +function setupLogCapturing(sinon, command) { + const logs = []; + sinon.stub(command, 'doLog').callsFake((msg) => logs.push(msg)); + command.log = { getCapturedLogOutput: () => logs.join('\n') }; +} + +describe('CreateSnapshot', function () { + describe('#runCommand', function () { + let command, + progressCounter, + cloudSdkApiStub, + addSpinniesSpy, + stopAllSpinniesSpy, + succeedSpinniesSpy; + + beforeEach(function () { + progressCounter = 0; + }); + + const stubbedCreateResponseSuccess = { + status: 200, + json: async () => ({ + actionid: 1234123, + success: true, + }), + }; + + const stubbedCreateResponseFailure = (status = 400) => ({ + status, + json: async () => ({ + actionid: 1234123, + success: false, + error: 'Failed to create snapshot', + }), + }); + + const getSnapshotProgressResponse = ( + percentages = [20, 100], + status = 200 + ) => { + const result = { + status, + json: async () => ({ + action: 'create-snapshot', + progressPercentage: percentages[progressCounter], + snapshotName: 'snapshot-001', + }), + }; + progressCounter++; + return result; + }; + + const getArtifactsResponse = { + status: 200, + json: async () => ({ + status: 'Ready', + items: [], + }), + }; + + const stub = (response) => sinon.stub().resolves(response); + const stubReject = (message) => sinon.stub().rejects(message); + + const prepareStubs = (cloudSdkMethods) => { + command = new CreateSnapshot([], {}, 10); + [command, cloudSdkApiStub] = createCloudSdkAPIStub( + sinon, + command, + cloudSdkMethods + ); + const { addSpy, stopAllSpy, succeedSpy } = createSpinniesStub( + sinon, + command + ); + addSpinniesSpy = addSpy; + stopAllSpinniesSpy = stopAllSpy; + succeedSpinniesSpy = succeedSpy; + setupLogCapturing(sinon, command); + }; + + afterEach(() => { + sinon.restore(); + }); + + it('calls the appropriate api and shows success spinners if the result is good.', async function () { + prepareStubs({ + createSnapshot: stub(stubbedCreateResponseSuccess), + getSnapshotProgress: stub(getSnapshotProgressResponse()), + getArtifacts: stub(getArtifactsResponse), + }); + + const result = await command.runCommand([], {}); + + expect(result.totalseconds).to.be.a('number'); + expect(result.waitingforbackend).to.be.a('date'); + expect(result.startTime).to.be.a('date'); + expect(result.processnigsnapshotstarted).to.be.a('date'); + expect(result.processnigsnapshotended).to.be.a('date'); + + assert.equal(cloudSdkApiStub.createSnapshot.calledOnce, true); + assert.equal(cloudSdkApiStub.getSnapshotProgress.called, true); + assert.equal(cloudSdkApiStub.getArtifacts.called, true); + + expect(addSpinniesSpy.callCount).to.equal(4); + + expect(addSpinniesSpy.getCall(0).args[0]).to.equal('spinner-requesting'); + expect(addSpinniesSpy.getCall(1).args[0]).to.equal('spinner-backend'); + expect(addSpinniesSpy.getCall(2).args[0]).to.equal('spinner-create'); + expect(addSpinniesSpy.getCall(3).args[0]).to.equal('spinner-restart'); + + expect(succeedSpinniesSpy.callCount).to.equal(4); + + const verifySpinnySucceeded = ( + callIndex, + expectedSpinnerName, + compareObjectFn + ) => { + const [spinnerName, obj] = succeedSpinniesSpy.getCall(callIndex).args; + expect(spinnerName).to.equal(expectedSpinnerName); + + assert.equal(compareObjectFn(obj), true); + }; + + verifySpinnySucceeded( + 0, + 'spinner-requesting', + (obj) => + obj.text.startsWith( + 'Requested to create the snapshot successfully.' + ) && obj.successColor === 'greenBright' + ); + + verifySpinnySucceeded( + 1, + 'spinner-backend', + (obj) => + obj.text.startsWith( + 'Backend picked up the job to create the snapshot.' + ) && obj.successColor === 'greenBright' + ); + verifySpinnySucceeded( + 2, + 'spinner-create', + (obj) => + obj.text.startsWith('Created snapshot successfully.') && + obj.successColor === 'greenBright' + ); + }); + + it('catches a bad progress result (-2)', async function () { + prepareStubs({ + createSnapshot: stub(stubbedCreateResponseSuccess), + getArtifacts: stub(getArtifactsResponse), + getSnapshotProgress: stub(getSnapshotProgressResponse([20, -2])), + }); + + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.contain( + 'The snapshot failed to be created. Please contact support.' + ); + } + }); + + it('catches 404 on progress response', async function () { + prepareStubs({ + createSnapshot: stub(stubbedCreateResponseSuccess), + getArtifacts: stub(getArtifactsResponse), + getSnapshotProgress: stub(getSnapshotProgressResponse([20, 50], 404)), + }); + + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.contain('The snapshot does not exist'); + } + }); + + it('catches unknown on progress response', async function () { + prepareStubs({ + createSnapshot: stub(stubbedCreateResponseSuccess), + getArtifacts: stub(getArtifactsResponse), + getSnapshotProgress: stub(getSnapshotProgressResponse([20, 50], 500)), + }); + + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.contain('An unknown error occurred.'); + } + }); + + it('catches a withCloudSdk exception - createSnapshot', async function () { + prepareStubs({ + createSnapshot: stubReject('Internal error'), + }); + + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.contain( + 'There was an unexpected error when running a snapshot command.' + ); + } + }); + + it('catches a withCloudSdk exception - getSnapshotProgress', async function () { + prepareStubs({ + createSnapshot: stub(stubbedCreateResponseSuccess), + getSnapshotProgress: stubReject('Internal error'), + }); + + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.contain( + 'There was an unexpected error when running a snapshot command.' + ); + } + }); + + it('calls the appropriate api and shows failures', async function () { + const checkError = async (statusCode, expectedMessage) => { + prepareStubs({ + createSnapshot: stub(stubbedCreateResponseFailure(statusCode)), + getSnapshotProgress: stub(getSnapshotProgressResponse()), + getArtifacts: stub(getArtifactsResponse), + }); + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.include(expectedMessage); + } + expect(stopAllSpinniesSpy.callCount).to.equal(1); + }; + + await checkError(400, 'The given environment is not an RDE'); + await checkError(404, 'The environment or program does not exist'); + await checkError(409, 'A snapshot with the given name already exists'); + await checkError( + 503, + 'The RDE is not in a state where a snapshot can be created or restored.' + ); + await checkError( + 507, + 'Reached the maximum number or diskspace of snapshots. Remove some snapshots and try again' + ); + await checkError(500, 'An unknown error occurred.'); + }); + }); +}); diff --git a/test/commands/aem/rde/snapshot/delete.test.js b/test/commands/aem/rde/snapshot/delete.test.js new file mode 100644 index 0000000..f4efdd3 --- /dev/null +++ b/test/commands/aem/rde/snapshot/delete.test.js @@ -0,0 +1,246 @@ +const assert = require('assert'); +const { expect } = require('chai'); +const sinon = require('sinon'); +const DeleteSnapshot = require('../../../../../src/commands/aem/rde/snapshot/delete'); +const { snapshotsResponse, snapshots } = require('./snapshots.mocks'); + +/** + * + * @param sinon + * @param command + * @param methods + */ +function createCloudSdkAPIStub(sinon, command, methods) { + const cloudSdkApiStub = {}; + Object.keys(methods).forEach((k) => { + cloudSdkApiStub[k] = methods[k]; + }); + sinon + .stub(command, 'withCloudSdk') + .callsFake(async (fn) => fn(cloudSdkApiStub)); + return [command, cloudSdkApiStub]; +} + +/** + * + * @param sinon + * @param command + */ +function setupLogCapturing(sinon, command) { + const logs = []; + sinon.stub(command, 'doLog').callsFake((msg) => logs.push(msg)); + command.log = { getCapturedLogOutput: () => logs.join('\n') }; +} + +describe('DeleteSnapshots', function () { + describe('#run commands', function () { + let command, cloudSdkApiStub; + + const stubbedDeleteResponse = (status, details) => ({ + status, + json: async () => ({ + details, + }), + }); + + const stubCommandResponse = (status, details) => { + [command, cloudSdkApiStub] = createCloudSdkAPIStub(sinon, command, { + getSnapshots: sinon.stub().resolves(snapshotsResponse), + deleteSnapshot: sinon + .stub() + .resolves(stubbedDeleteResponse(status, details)), + }); + }; + + beforeEach(() => { + command = new DeleteSnapshot([], {}); + sinon.stub(command, 'spinnerStart'); + sinon.stub(command, 'spinnerStop'); + setupLogCapturing(sinon, command); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('calls deleteSnapshots successfully', async () => { + stubCommandResponse(200); + + await command.runCommand( + { + name: 'snap1', + }, + { + force: false, + } + ); + expect(cloudSdkApiStub.deleteSnapshot.calledOnce).to.be.true; + const output = command.log.getCapturedLogOutput(); + expect(output).to.include('snap1 deleted successfully'); + + expect(command.spinnerStart.calledOnce).to.be.true; + expect(command.spinnerStop.calledOnce).to.be.true; + expect(cloudSdkApiStub.deleteSnapshot.calledWith('snap1', false)).to.be + .true; + }); + + const executeWithErrorExpected = async ( + snapshotName, + force, + status, + statusMessage, + expectedMessage + ) => { + const isSingle = snapshotName && snapshotName !== 'all'; + stubCommandResponse(status, statusMessage); + try { + const args = {}; + const flags = { + force, + }; + if (isSingle) { + args.name = snapshotName; + } else { + flags.all = true; + } + await command.runCommand(args, flags); + assert.fail('Expected an error to be thrown'); + } catch (err) { + expect(err).to.be.instanceOf(Error); + expect(err.message).to.include(expectedMessage); + } + + if (isSingle) { + assert.equal(cloudSdkApiStub.deleteSnapshot.callCount, 1); + }else{ + assert.equal(cloudSdkApiStub.deleteSnapshot.callCount, snapshots.length); + } + + const output = command.log.getCapturedLogOutput(); + expect(command.spinnerStart.called).to.be.true; + expect(command.spinnerStop.called).to.be.true; + if (isSingle) { + expect(cloudSdkApiStub.deleteSnapshot.calledWith('snap1', false)).to.be + .true; + } else { + for (const snapshot of snapshots) { + expect( + cloudSdkApiStub.deleteSnapshot.calledWith(snapshot.name, false) + ).to.be.true; + } + } + }; + + describe('single snapshot deletion', function () { + it('reacts to error code 404 appropriately - 1', async () => + executeWithErrorExpected( + 'snap1', + false, + 404, + 'The requested environment or program does not exist.', + 'The environment or program does not exist' + )); + + it('reacts to error code 404 appropriately - 2', async () => + executeWithErrorExpected( + 'snap1', + false, + 404, + 'The requested snapshot does not exist.', + 'The snapshot does not exist' + )); + + it('reacts to error code 403 appropriately', async () => + executeWithErrorExpected( + 'snap1', + false, + 403, + "The snapshot to be wiped is not in state 'removed'.", + 'Snapshot is in wrong state. Must be in state "REMOVED" to be able to wipe.' + )); + + it('reacts to error code 400 appropriately', async () => + executeWithErrorExpected( + 'snap1', + false, + 400, + null, + 'The given environment is not an RDE' + )); + + it('reacts to error code 500 appropriately', async () => + executeWithErrorExpected( + 'snap1', + false, + 451, + null, + 'The feature is part of the EAP program and not available for general use.' + )); + + it('reacts to error code 503 appropriately', async () => + executeWithErrorExpected( + 'snap1', + false, + 503, + null, + 'The RDE is not in a state where a snapshot can be created or restored.' + )); + }); + + describe('all snapshot deletion', function () { + it('reacts to error code 404 appropriately - 1', async () => + executeWithErrorExpected( + 'all', + false, + 404, + 'The requested environment or program does not exist.', + 'The environment or program does not exist' + )); + + it('reacts to error code 404 appropriately - 2', async () => + executeWithErrorExpected( + 'all', + false, + 404, + 'The requested snapshot does not exist.', + 'The snapshot does not exist' + )); + + it('reacts to error code 403 appropriately', async () => + executeWithErrorExpected( + 'all', + false, + 403, + "The snapshot to be wiped is not in state 'removed'.", + 'Snapshot is in wrong state. Must be in state "REMOVED" to be able to wipe.' + )); + + it('reacts to error code 400 appropriately', async () => + executeWithErrorExpected( + 'all', + false, + 400, + null, + 'The given environment is not an RDE' + )); + + it('reacts to error code 500 appropriately', async () => + executeWithErrorExpected( + 'all', + false, + 451, + null, + 'The feature is part of the EAP program and not available for general use.' + )); + + it('reacts to error code 503 appropriately', async () => + executeWithErrorExpected( + 'all', + false, + 503, + null, + 'The RDE is not in a state where a snapshot can be created or restored.' + )); + }); + }); +}); diff --git a/test/commands/aem/rde/snapshot/index.test.js b/test/commands/aem/rde/snapshot/index.test.js new file mode 100644 index 0000000..e11ab4e --- /dev/null +++ b/test/commands/aem/rde/snapshot/index.test.js @@ -0,0 +1,138 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const ListSnapshots = require('../../../../../src/commands/aem/rde/snapshot'); +const internalErrors = require('../../../../../src/lib/internal-errors'); +const configErrors = require('../../../../../src/lib/configuration-errors'); +const errorHelpers = require('../../../../../src/lib/error-helpers'); +const { snapshots, snapshotsResponse } = require('./snapshots.mocks'); + +/** + * + * @param sinon + * @param command + * @param methods + */ +function createCloudSdkAPIStub(sinon, command, methods) { + const cloudSdkApiStub = {}; + Object.keys(methods).forEach((k) => { + cloudSdkApiStub[k] = methods[k]; + }); + sinon + .stub(command, 'withCloudSdk') + .callsFake(async (fn) => fn(cloudSdkApiStub)); + return [command, cloudSdkApiStub]; +} + +/** + * + * @param sinon + * @param command + */ +function setupLogCapturing(sinon, command) { + const logs = []; + sinon.stub(command, 'doLog').callsFake((msg) => logs.push(msg)); + command.log = { getCapturedLogOutput: () => logs.join('\n') }; +} + +describe('ListSnapshots', function () { + describe('#runCommand', function () { + let command, cloudSdkApiStub; + + const stubbedEmptySnapshotsResponse = { + status: 200, + json: async () => [], + }; + + beforeEach(() => { + command = new ListSnapshots([], {}); + [command, cloudSdkApiStub] = createCloudSdkAPIStub(sinon, command, { + getSnapshots: sinon.stub().resolves(snapshots), + }); + sinon.stub(command, 'spinnerStart'); + sinon.stub(command, 'spinnerStop'); + setupLogCapturing(sinon, command); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('calls getSnapshots and logs table output for non-empty items', async function () { + cloudSdkApiStub.getSnapshots.resolves(snapshotsResponse); + await command.runCommand([], {}); + expect(cloudSdkApiStub.getSnapshots.calledOnce).to.be.true; + const output = command.log.getCapturedLogOutput(); + expect(output).to.include('snap1'); + expect(output).to.include('snap2'); + expect(output).to.include('snap3'); + expect(output).to.include('1.00 MB'); + expect(output).to.include('1.00 GB'); + expect(output).to.include('4.89 KB'); + }); + + it('logs "There are no snapshots yet." for empty items', async function () { + cloudSdkApiStub.getSnapshots.resolves(stubbedEmptySnapshotsResponse); + await command.runCommand([], {}); + expect(command.log.getCapturedLogOutput()).to.include( + 'There are no snapshots yet.' + ); + }); + + it('throws DIFFERENT_ENV_TYPE error for 400 status', async function () { + cloudSdkApiStub.getSnapshots.resolves({ status: 400 }); + try { + await command.runCommand([], {}); + expect.fail('Should throw DIFFERENT_ENV_TYPE'); + } catch (e) { + expect(e).to.be.instanceOf(configErrors.codes.DIFFERENT_ENV_TYPE); + } + }); + + it('throws PROGRAM_OR_ENVIRONMENT_NOT_FOUND error for 404 status', async function () { + cloudSdkApiStub.getSnapshots.resolves({ status: 404 }); + try { + await command.runCommand([], {}); + expect.fail('Should throw PROGRAM_OR_ENVIRONMENT_NOT_FOUND'); + } catch (e) { + expect(e).to.be.instanceOf( + configErrors.codes.PROGRAM_OR_ENVIRONMENT_NOT_FOUND + ); + } + }); + + it('throws UNKNOWN error for unexpected status', async function () { + cloudSdkApiStub.getSnapshots.resolves({ status: 500 }); + try { + await command.runCommand([], {}); + expect.fail('Should throw UNKNOWN'); + } catch (e) { + expect(e).to.be.instanceOf(internalErrors.codes.UNKNOWN); + } + }); + + it('throws INTERNAL_SNAPSHOT_ERROR if getSnapshots throws', async function () { + cloudSdkApiStub.getSnapshots.rejects(new Error('fail')); + sinon.stub(errorHelpers, 'throwAioError').throws( + new internalErrors.codes.INTERNAL_SNAPSHOT_ERROR({ + messageValues: 'fail', + }) + ); + try { + await command.runCommand([], {}); + expect.fail('Should throw INTERNAL_SNAPSHOT_ERROR'); + } catch (e) { + expect(e).to.be.instanceOf( + internalErrors.codes.INTERNAL_SNAPSHOT_ERROR + ); + } + }); + + it('returns result object with status and snapshots', async function () { + cloudSdkApiStub.getSnapshots.resolves(snapshotsResponse); + const result = await command.runCommand([], {}); + expect(result.status).to.equal(200); + expect(result.snapshots).to.exist; + expect(result.snapshots).to.be.an('array'); + }); + }); +}); diff --git a/test/commands/aem/rde/snapshot/restore.test.js b/test/commands/aem/rde/snapshot/restore.test.js new file mode 100644 index 0000000..1490178 --- /dev/null +++ b/test/commands/aem/rde/snapshot/restore.test.js @@ -0,0 +1,329 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const RestoreSnapshot = require('../../../../../src/commands/aem/rde/snapshot/restore'); +const Spinnies = require('spinnies'); +const assert = require('assert'); + +/** + * + * @param sinon + * @param command + * @param methods + */ +function createCloudSdkAPIStub(sinon, command, methods) { + const cloudSdkApiStub = {}; + Object.keys(methods).forEach((k) => { + cloudSdkApiStub[k] = methods[k]; + }); + sinon + .stub(command, 'withCloudSdk') + .callsFake(async (fn) => fn(cloudSdkApiStub)); + return [command, cloudSdkApiStub]; +} + +/** + * + * @param sinon + * @param command + */ +function createSpinniesStub(sinon, command) { + const spinnies = new Spinnies(); + const addSpy = sinon.spy(spinnies, 'add'); + const stopAllSpy = sinon.spy(spinnies, 'stopAll'); + const succeedSpy = sinon.spy(spinnies, 'succeed'); + sinon.stub(command, 'getSpinnies').callsFake(() => spinnies); + + return { + addSpy, + stopAllSpy, + succeedSpy, + }; +} + +/** + * + * @param sinon + * @param command + */ +function setupLogCapturing(sinon, command) { + const logs = []; + sinon.stub(command, 'doLog').callsFake((msg) => logs.push(msg)); + command.log = { getCapturedLogOutput: () => logs.join('\n') }; +} + +describe('RestoreSnapshot', function () { + describe('#runCommand', function () { + let command, + progressCounter, + cloudSdkApiStub, + addSpinniesSpy, + stopAllSpinniesSpy, + succeedSpinniesSpy; + + beforeEach(function () { + progressCounter = 0; + }); + + const stubbedCreateResponseSuccess = { + status: 200, + json: async () => ({ + actionid: 1234123, + success: true, + }), + }; + + const stubbedRestoreResponseFailure = (status = 400, details = '') => ({ + status, + json: async () => ({ + actionid: 1234123, + success: false, + error: 'Failed to restore snapshot', + details, + }), + }); + + const getSnapshotProgressResponse = ( + percentages = [20, 100], + status = 200 + ) => { + const result = { + status, + json: async () => ({ + action: 'restore-snapshot', + progressPercentage: percentages[progressCounter], + snapshotName: 'snapshot-001', + }), + }; + progressCounter++; + return result; + }; + + const getArtifactsResponse = { + status: 200, + json: async () => ({ + status: 'Ready', + items: [], + }), + }; + + const stub = (response) => sinon.stub().resolves(response); + const stubReject = (message) => sinon.stub().rejects(message); + + const prepareStubs = (cloudSdkMethods) => { + command = new RestoreSnapshot([], {}, 10); + [command, cloudSdkApiStub] = createCloudSdkAPIStub( + sinon, + command, + cloudSdkMethods + ); + const { addSpy, stopAllSpy, succeedSpy } = createSpinniesStub( + sinon, + command + ); + addSpinniesSpy = addSpy; + stopAllSpinniesSpy = stopAllSpy; + succeedSpinniesSpy = succeedSpy; + setupLogCapturing(sinon, command); + }; + + afterEach(() => { + sinon.restore(); + }); + + it('works', async function () { + prepareStubs({ + restoreSnapshot: stub(stubbedCreateResponseSuccess), + getSnapshotProgress: stub(getSnapshotProgressResponse()), + getArtifacts: stub(getArtifactsResponse), + }); + + const result = await command.runCommand([], {}); + + expect(result.totalseconds).to.be.a('number'); + expect(result.waitingforbackend).to.be.a('date'); + expect(result.startTime).to.be.a('date'); + expect(result.processnigsnapshotstarted).to.be.a('date'); + expect(result.processnigsnapshotended).to.be.a('date'); + + assert.equal(cloudSdkApiStub.restoreSnapshot.calledOnce, true); + assert.equal(cloudSdkApiStub.getSnapshotProgress.called, true); + assert.equal(cloudSdkApiStub.getArtifacts.called, true); + + expect(addSpinniesSpy.callCount).to.equal(4); + + expect(addSpinniesSpy.getCall(0).args[0]).to.equal('spinner-requesting'); + expect(addSpinniesSpy.getCall(1).args[0]).to.equal('spinner-backend'); + expect(addSpinniesSpy.getCall(2).args[0]).to.equal('spinner-restore'); + expect(addSpinniesSpy.getCall(3).args[0]).to.equal('spinner-restart'); + + expect(succeedSpinniesSpy.callCount).to.equal(4); + + const verifySpinnySucceeded = ( + callIndex, + expectedSpinnerName, + compareObjectFn + ) => { + const [spinnerName, obj] = succeedSpinniesSpy.getCall(callIndex).args; + expect(spinnerName).to.equal(expectedSpinnerName); + + assert.equal(compareObjectFn(obj), true); + }; + + verifySpinnySucceeded( + 0, + 'spinner-requesting', + (obj) => + obj.text.startsWith( + 'Requested to restore the snapshot successfully.' + ) && obj.successColor === 'greenBright' + ); + + verifySpinnySucceeded( + 1, + 'spinner-backend', + (obj) => + obj.text.startsWith( + 'Backend picked up the job to restore the snapshot.' + ) && obj.successColor === 'greenBright' + ); + verifySpinnySucceeded( + 2, + 'spinner-restore', + (obj) => + obj.text.startsWith('Restored snapshot to RDE successfully.') && + obj.successColor === 'greenBright' + ); + }); + + it('catches a bad progress result (-2)', async function () { + prepareStubs({ + restoreSnapshot: stub(stubbedCreateResponseSuccess), + getSnapshotProgress: stub(getSnapshotProgressResponse([20, -2])), + getArtifacts: stub(getArtifactsResponse), + }); + + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.contain('The snapshot failed to be restored.'); + } + }); + + it('catches 404 on progress response', async function () { + prepareStubs({ + restoreSnapshot: stub(stubbedCreateResponseSuccess), + getArtifacts: stub(getArtifactsResponse), + getSnapshotProgress: stub(getSnapshotProgressResponse([20, 50], 404)), + }); + + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.contain('The snapshot does not exist'); + } + }); + + it('catches unknown on progress response', async function () { + prepareStubs({ + restoreSnapshot: stub(stubbedCreateResponseSuccess), + getArtifacts: stub(getArtifactsResponse), + getSnapshotProgress: stub(getSnapshotProgressResponse([20, 50], 500)), + }); + + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.contain('An unknown error occurred.'); + } + }); + + it('catches a withCloudSdk exception - createSnapshot', async function () { + prepareStubs({ + restoreSnapshot: stubReject('Internal error'), + }); + + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.contain( + 'There was an unexpected error when running a snapshot command.' + ); + } + }); + + it('catches a withCloudSdk exception - getSnapshotProgress', async function () { + prepareStubs({ + restoreSnapshot: stub(stubbedCreateResponseSuccess), + getSnapshotProgress: stubReject('Internal error'), + }); + + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.contain( + 'There was an unexpected error when running a snapshot command.' + ); + } + }); + + it('calls the appropriate api and shows failures', async function () { + const checkError = async (statusCode, statusDetails, expectedMessage) => { + prepareStubs({ + restoreSnapshot: stub( + stubbedRestoreResponseFailure(statusCode, statusDetails) + ), + getSnapshotProgress: stub(getSnapshotProgressResponse()), + getArtifacts: stub(getArtifactsResponse), + }); + try { + await command.runCommand([], {}); + assert.fail('Expected command to throw an error'); + } catch (err) { + expect(err).to.be.an('error'); + expect(err.message).to.include(expectedMessage); + } + expect(stopAllSpinniesSpy.callCount).to.equal(1); + }; + + await checkError(400, '', 'The given environment is not an RDE'); + await checkError( + 404, + 'The requested environment or program does not exist.', + 'The environment or program does not exist' + ); + await checkError( + 404, + 'The requested snapshot does not exist.', + 'The snapshot does not exist' + ); + await checkError( + 404, + 'The snapshot is in deleted state.', + 'The snapshot is in deleted state, change the state to available before restoring.' + ); + + await checkError( + 406, + '', + 'The RDE is not in a state where a snapshot can be created or restored.' + ); + + await checkError( + 503, + '', + 'AEM instances are receiving a deployment and new packages are not accepted temporarily until the instances are done.' + ); + await checkError(500, '', 'An unknown error occurred.'); + }); + }); +}); diff --git a/test/commands/aem/rde/snapshot/snapshots.mocks.js b/test/commands/aem/rde/snapshot/snapshots.mocks.js new file mode 100644 index 0000000..7f1e686 --- /dev/null +++ b/test/commands/aem/rde/snapshot/snapshots.mocks.js @@ -0,0 +1,39 @@ +const snapshots = [ + { + name: 'snap1', + description: 'desc1', + usage: 1, + size: { total_size: 1048576 }, + state: 'AVAILABLE', + created: '2024-06-01T12:00:00Z', + lastUsed: '2024-06-02T12:00:00Z', + }, + { + name: 'snap2', + description: 'desc2', + usage: 2, + size: { total_size: 1073741824 }, + state: 'DELETED', + created: '2024-06-03T12:00:00Z', + lastUsed: '2024-06-04T12:00:00Z', + }, + { + name: 'snap3', + description: 'desc3', + usage: 2, + size: { total_size: 5012 }, + state: 'AVAILABLE', + created: '2024-06-03T12:00:00Z', + lastUsed: '2024-06-04T12:00:00Z', + }, +]; + +const snapshotsResponse = { + status: 200, + json: async () => snapshots, +}; + +module.exports = { + snapshotsResponse, + snapshots, +}; diff --git a/test/commands/aem/rde/snapshot/undelete.test.js b/test/commands/aem/rde/snapshot/undelete.test.js new file mode 100644 index 0000000..cdeb559 --- /dev/null +++ b/test/commands/aem/rde/snapshot/undelete.test.js @@ -0,0 +1,190 @@ +const assert = require('assert'); +const { expect } = require('chai'); +const sinon = require('sinon'); +const UnDeleteSnapshot = require('../../../../../src/commands/aem/rde/snapshot/undelete'); +const { snapshotsResponse, snapshots } = require('./snapshots.mocks'); + +/** + * + * @param sinon + * @param command + * @param methods + */ +function createCloudSdkAPIStub(sinon, command, methods) { + const cloudSdkApiStub = {}; + Object.keys(methods).forEach((k) => { + cloudSdkApiStub[k] = methods[k]; + }); + sinon + .stub(command, 'withCloudSdk') + .callsFake(async (fn) => fn(cloudSdkApiStub)); + return [command, cloudSdkApiStub]; +} + +/** + * + * @param sinon + * @param command + */ +function setupLogCapturing(sinon, command) { + const logs = []; + sinon.stub(command, 'doLog').callsFake((msg) => logs.push(msg)); + command.log = { getCapturedLogOutput: () => logs.join('\n') }; +} + +describe('UnDeleteSnapshots', function () { + describe('#run commands', function () { + let command, cloudSdkApiStub; + + const stubbedUnDeleteResponse = (status, details) => ({ + status, + json: async () => ({ + details, + }), + }); + + const stubCommandResponse = (status, details) => { + [command, cloudSdkApiStub] = createCloudSdkAPIStub(sinon, command, { + getSnapshots: sinon.stub().resolves(snapshotsResponse), + undeleteSnapshot: sinon + .stub() + .resolves(stubbedUnDeleteResponse(status, details)), + }); + }; + + beforeEach(() => { + command = new UnDeleteSnapshot([], {}); + sinon.stub(command, 'spinnerStart'); + sinon.stub(command, 'spinnerStop'); + setupLogCapturing(sinon, command); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('calls undeleteSnapshots successfully', async () => { + stubCommandResponse(200); + + await command.runCommand( + { + name: 'snap1', + }, + { + force: false, + } + ); + expect(cloudSdkApiStub.undeleteSnapshot.calledOnce).to.be.true; + const output = command.log.getCapturedLogOutput(); + expect(output).to.include('Snapshot snap1 undeleted successfully.'); + + expect(command.spinnerStart.calledOnce).to.be.true; + expect(command.spinnerStop.calledOnce).to.be.true; + expect(cloudSdkApiStub.undeleteSnapshot.calledWith('snap1')).to.be.true; + }); + + const executeWithErrorExpected = async ( + snapshotName, + force, + status, + statusMessage, + expectedMessage + ) => { + const isSingle = snapshotName && snapshotName !== 'all'; + stubCommandResponse(status, statusMessage); + try { + const args = {}; + const flags = { + force, + }; + if (isSingle) { + args.name = snapshotName; + } else { + flags.all = true; + } + await command.runCommand(args, flags); + assert.fail('Expected an error to be thrown'); + } catch (err) { + expect(err).to.be.instanceOf(Error); + expect(err.message).to.include(expectedMessage); + } + + if (isSingle) { + assert.equal(cloudSdkApiStub.undeleteSnapshot.callCount, 1); + } else { + assert.equal( + cloudSdkApiStub.undeleteSnapshot.callCount, + snapshots.length + ); + } + + const output = command.log.getCapturedLogOutput(); + expect(command.spinnerStart.called).to.be.true; + expect(command.spinnerStop.called).to.be.true; + if (isSingle) { + expect(cloudSdkApiStub.undeleteSnapshot.calledWith('snap1')).to.be.true; + } else { + for (const snapshot of snapshots) { + expect(cloudSdkApiStub.undeleteSnapshot.calledWith(snapshot.name)).to + .be.true; + } + } + }; + + describe('single snapshot deletion', function () { + it('reacts to error code 404 appropriately - 1', async () => + executeWithErrorExpected( + 'snap1', + false, + 404, + 'The requested environment or program does not exist.', + 'The environment or program does not exist' + )); + + it('reacts to error code 404 appropriately - 2', async () => + executeWithErrorExpected( + 'snap1', + false, + 404, + 'The requested snapshot does not exist.', + 'The snapshot does not exist' + )); + + it('reacts to error code 400 appropriately', async () => + executeWithErrorExpected( + 'snap1', + false, + 400, + null, + 'The given environment is not an RDE' + )); + + it('reacts to error code 451 appropriately', async () => + executeWithErrorExpected( + 'snap1', + false, + 451, + null, + 'The feature is part of the EAP program and not available for general use.' + )); + + it('reacts to error code 507 appropriately', async () => + executeWithErrorExpected( + 'snap1', + false, + 507, + null, + 'Reached the maximum number or diskspace of snapshots.' + )); + + it('reacts to error code 500 appropriately', async () => + executeWithErrorExpected( + 'snap1', + false, + 500, + null, + 'An unknown error occurred.' + )); + }); + }); +}); diff --git a/test/lib/base-command.test.js b/test/lib/base-command.test.js index c478d4f..e13e06f 100644 --- a/test/lib/base-command.test.js +++ b/test/lib/base-command.test.js @@ -225,6 +225,31 @@ describe('Authentication tests', function () { }), }, }); + + class TestCommand extends BaseCommandAuthMock.BaseCommand { + constructor(commandLine) { + super(commandLine?.split(/\s+/) || []); + } + + runCommand(args, flags) { + /* do nothing */ + } + } + + /** + * Utillity to create and run a test command. Running the + * command exercises the logic parsing the command-line arguments + * and initializing various fields in the command. + * + * @param commandLine The command line arguments as a string. + * @returns A fully initialized TestCommand instance. + */ + async function createCommand(commandLine) { + const command = new TestCommand(commandLine); + await command.run(); + return command; + } + beforeEach(function () { getOrganizationsStub.returns( Promise.resolve([ @@ -247,6 +272,13 @@ describe('Authentication tests', function () { }, local: true, }; + case 'context-via-flag': + return { + data: { + client_id: 'context-via-flag-id', + }, + local: true, + }; case 'cli': return { data: {}, @@ -265,14 +297,32 @@ describe('Authentication tests', function () { }); it('should be able to fetch token and api key', async function () { contextGetCurrentStub.returns('my-context'); - const command = new BaseCommandAuthMock.BaseCommand(); + const command = await createCommand(); const result = await command.getTokenAndKey(); assert.equal(result.accessToken, accessToken); assert.equal(result.apiKey, 'my-context-client_id'); assert.equal(result.local, true); }); + it('should use the --context flag', async function () { + const command = await createCommand('--context context-via-flag'); + const { apiKey, local } = await command.getTokenAndKey(); + assert.equal(apiKey, 'context-via-flag-id'); + assert.equal(local, true); + }); + it('should use the --ctx flag (alias of --context)', async function () { + const command = await createCommand('--ctx context-via-flag'); + const { apiKey, local } = await command.getTokenAndKey(); + assert.equal(apiKey, 'context-via-flag-id'); + assert.equal(local, true); + }); + it('should use the --imsContextName flag (alias of --context)', async function () { + const command = await createCommand('--imsContextName context-via-flag'); + const { apiKey, local } = await command.getTokenAndKey(); + assert.equal(apiKey, 'context-via-flag-id'); + assert.equal(local, true); + }); it('should be able to fetch cli token and api key in case of api error', async function () { - const command = new BaseCommandAuthMock.BaseCommand(); + const command = await createCommand(); const result = await command.getTokenAndKey(); assert.equal(result.accessToken, accessToken); assert.equal(result.apiKey, 'jwt_client_id'); @@ -282,7 +332,7 @@ describe('Authentication tests', function () { getTokenStub.returns(undefined); let err; try { - const command = new BaseCommandAuthMock.BaseCommand(); + const command = await createCommand(); await command.getTokenAndKey(); } catch (e) { err = e; @@ -294,7 +344,7 @@ describe('Authentication tests', function () { getTokenStub.returns(jwt.sign({}, 'pKey', {})); let err; try { - const command = new BaseCommandAuthMock.BaseCommand(); + const command = await createCommand(); sinon.stub(command, 'doLog'); await command.getTokenAndKey(); } catch (e) { @@ -315,20 +365,20 @@ describe('Authentication tests', function () { 'You have to implement the method runCommand(args, flags) in the subclass!' ); }); - it('should return default base url', function () { - const command = new BaseCommandAuthMock.BaseCommand(); + it('should return default base url', async function () { + const command = await createCommand(); getConfigStub.returns(undefined); const baseUrl = command.getBaseUrl(false); assert.equal(baseUrl, 'https://cloudmanager.adobe.io'); }); - it('should return stage base url', function () { - const command = new BaseCommandAuthMock.BaseCommand(); + it('should return stage base url', async function () { + const command = await createCommand(); getConfigStub.returns(undefined); const baseUrl = command.getBaseUrl(true); assert.equal(baseUrl, 'https://cloudmanager-stage.adobe.io'); }); it('cloud sdk should be initialized properly', async function () { - const command = new BaseCommandAuthMock.BaseCommand(); + const command = await createCommand(); const flags = { organizationId: 'orgId', programId: 'progId',