diff --git a/.github/.dependabot.yml b/.github/.dependabot.yml index 88cb6099..6f39ced4 100644 --- a/.github/.dependabot.yml +++ b/.github/.dependabot.yml @@ -3,11 +3,11 @@ version: 2 updates: - - package-ecosystem: "npm" - directory: "/" + - package-ecosystem: npm + directory: / schedule: - interval: "monthly" + interval: monthly allow: - - dependency-name: "@storyblok/region-helper" + - dependency-name: '@storyblok/region-helper' reviewers: - - "storyblok/plugins-team" \ No newline at end of file + - storyblok/plugins-team diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 95e87e1b..b357b006 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,7 +7,6 @@ assignees: '' --- - **Current behavior:** diff --git a/.vscode/launch.json b/.vscode/launch.json index 00d4fb04..cd95b2d7 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,31 +4,47 @@ { "type": "node", "request": "launch", - "name": "Debug Jest Tests", - "runtimeArgs": [ - "--experimental-vm-modules" - ], - "args": [ - "--silent", - "--runInBand" - ], + "name": "Debug Vitest Tests", + "program": "${workspaceFolder}/node_modules/vitest/vitest.mjs", + "args": ["run"], + "autoAttachChildProcesses": true, + "smartStep": true, "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "windows": { - "program": "${workspaceFolder}\\node_modules\\jest\\bin\\jest.js" - } + "skipFiles": ["/**"] }, { "type": "node", "request": "launch", - "name": "Debug pull-components", - "program": "${workspaceFolder}/dist/cli.mjs", - "args": ["push-components", "components.295017.json", "--space", "295018"], + "name": "Debug login", + "program": "${workspaceFolder}/dist/index.mjs", + "args": ["login"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/dist/**/*.js"] + }, + { + "type": "node", + "request": "launch", + "name": "Debug logout", + "program": "${workspaceFolder}/dist/index.mjs", + "args": ["logout"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "sourceMaps": true, + "outFiles": ["${workspaceFolder}/dist/**/*.js"] + }, + { + "type": "node", + "request": "launch", + "name": "Debug test", + "program": "${workspaceFolder}/dist/index.mjs", + "args": ["test", "--verbose"], "cwd": "${workspaceFolder}", "console": "integratedTerminal", "sourceMaps": true, "outFiles": ["${workspaceFolder}/dist/**/*.js"] } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index e2839582..953f331b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,8 +1,45 @@ { + // Enable the ESlint flat config support + "eslint.experimental.useFlatConfig": true, + "eslint.format.enable": true, + + // Disable the default formatter, use eslint instead + "prettier.enable": false, "editor.formatOnSave": false, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, + + // Auto fix + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" }, - "editor.defaultFormatter": "esbenp.prettier-vscode" + + // Silent the stylistic rules in you IDE, but still auto fix them + "eslint.rules.customizations": [ + { "rule": "style/*", "severity": "off" }, + { "rule": "format/*", "severity": "off" }, + { "rule": "*-indent", "severity": "off" }, + { "rule": "*-spacing", "severity": "off" }, + { "rule": "*-spaces", "severity": "off" }, + { "rule": "*-order", "severity": "off" }, + { "rule": "*-dangle", "severity": "off" }, + { "rule": "*-newline", "severity": "off" }, + { "rule": "*quotes", "severity": "off" }, + { "rule": "*semi", "severity": "off" } + ], + + // Enable eslint for all supported languages + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + "html", + "markdown", + "json", + "jsonc", + "yaml", + "toml", + "yml" + ] } diff --git a/__mocks__/fs.cjs b/__mocks__/fs.cjs new file mode 100644 index 00000000..1d156260 --- /dev/null +++ b/__mocks__/fs.cjs @@ -0,0 +1,6 @@ +// we can also use `import`, but then +// every export should be explicitly defined + +const { fs } = require('memfs') + +module.exports = fs diff --git a/__mocks__/fs/promises.cjs b/__mocks__/fs/promises.cjs new file mode 100644 index 00000000..9fa31bcf --- /dev/null +++ b/__mocks__/fs/promises.cjs @@ -0,0 +1,6 @@ +// we can also use `import`, but then +// every export should be explicitly defined + +const { fs } = require('memfs') + +module.exports = fs.promises diff --git a/__mocks__/test.netrc b/__mocks__/test.netrc new file mode 100644 index 00000000..1967a0c9 --- /dev/null +++ b/__mocks__/test.netrc @@ -0,0 +1,4 @@ +machine api.storyblok.com + login julio.iglesias@storyblok.com + password my_access_token + region eu \ No newline at end of file diff --git a/build.config.ts b/build.config.ts index 9e330b08..4d5f0091 100644 --- a/build.config.ts +++ b/build.config.ts @@ -1,7 +1,8 @@ -import { defineBuildConfig } from 'unbuild'; +import { defineBuildConfig } from 'unbuild' export default defineBuildConfig({ declaration: true, entries: ['./src/index'], externals: ['consola', 'pathe'], -}); \ No newline at end of file + failOnWarn: false, +}) diff --git a/eslint.config.js b/eslint.config.mjs similarity index 66% rename from eslint.config.js rename to eslint.config.mjs index cfc4570b..7475dab7 100644 --- a/eslint.config.js +++ b/eslint.config.mjs @@ -1,9 +1,7 @@ import { storyblokLintConfig } from '@storyblok/eslint-config' export default storyblokLintConfig({ - rules: [ - { - 'no-console': 'off' - } - ] + rules: { + 'no-console': 'off', + }, }) diff --git a/package.json b/package.json index 30366907..b16a207d 100644 --- a/package.json +++ b/package.json @@ -26,23 +26,29 @@ "scripts": { "build": "unbuild", "build:stub": "unbuild --stub", - "dev": "node dist/index.mjs", + "dev": "pnpm run build:stub && STUB=true node dist/index.mjs", "lint": "eslint .", "lint:fix": "eslint . --fix", - "test": "vitest" - + "test": "vitest", + "coverage": "vitest run --coverage" }, "dependencies": { + "@inquirer/prompts": "^6.0.1", "chalk": "^5.3.0", "commander": "^12.1.0", "consola": "^3.2.3", - "inquirer": "^10.2.2" + "dotenv": "^16.4.5", + "ofetch": "^1.4.0", + "storyblok-js-client": "^6.9.2" }, "devDependencies": { "@storyblok/eslint-config": "^0.2.0", "@types/inquirer": "^9.0.7", "@types/node": "^22.5.4", + "@vitest/coverage-v8": "^2.1.1", "eslint": "^9.10.0", + "memfs": "^4.11.2", + "msw": "^2.4.11", "pathe": "^1.1.2", "typescript": "^5.6.2", "unbuild": "^2.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46dda474..a71d9f2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@inquirer/prompts': + specifier: ^6.0.1 + version: 6.0.1 chalk: specifier: ^5.3.0 version: 5.3.0 @@ -17,22 +20,37 @@ importers: consola: specifier: ^3.2.3 version: 3.2.3 - inquirer: - specifier: ^10.2.2 - version: 10.2.2 + dotenv: + specifier: ^16.4.5 + version: 16.4.5 + ofetch: + specifier: ^1.4.0 + version: 1.4.0 + storyblok-js-client: + specifier: ^6.9.2 + version: 6.9.2 devDependencies: '@storyblok/eslint-config': specifier: ^0.2.0 - version: 0.2.0(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.5)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4)) + version: 0.2.0(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.5)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2))) '@types/inquirer': specifier: ^9.0.7 version: 9.0.7 '@types/node': specifier: ^22.5.4 version: 22.5.4 + '@vitest/coverage-v8': + specifier: ^2.1.1 + version: 2.1.1(vitest@2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2))) eslint: specifier: ^9.10.0 version: 9.10.0(jiti@1.21.6) + memfs: + specifier: ^4.11.2 + version: 4.11.2 + msw: + specifier: ^2.4.11 + version: 2.4.11(typescript@5.6.2) pathe: specifier: ^1.1.2 version: 1.1.2 @@ -47,7 +65,7 @@ importers: version: 5.4.5(@types/node@22.5.4) vitest: specifier: ^2.1.1 - version: 2.1.1(@types/node@22.5.4) + version: 2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2)) packages: @@ -182,6 +200,18 @@ packages: resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@bundled-es-modules/cookie@2.0.0': + resolution: {integrity: sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==} + + '@bundled-es-modules/statuses@1.0.1': + resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} + + '@bundled-es-modules/tough-cookie@0.1.6': + resolution: {integrity: sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==} + '@clack/core@0.3.4': resolution: {integrity: sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==} @@ -677,62 +707,78 @@ packages: resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} engines: {node: '>=18.18'} - '@inquirer/checkbox@2.5.0': - resolution: {integrity: sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==} + '@inquirer/checkbox@3.0.1': + resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==} engines: {node: '>=18'} '@inquirer/confirm@3.2.0': resolution: {integrity: sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==} engines: {node: '>=18'} - '@inquirer/core@9.1.0': - resolution: {integrity: sha512-RZVfH//2ytTjmaBIzeKT1zefcQZzuruwkpTwwbe/i2jTl4o9M+iML5ChULzz6iw1Ok8iUBBsRCjY2IEbD8Ft4w==} + '@inquirer/confirm@4.0.1': + resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==} + engines: {node: '>=18'} + + '@inquirer/core@9.2.1': + resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} engines: {node: '>=18'} - '@inquirer/editor@2.2.0': - resolution: {integrity: sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==} + '@inquirer/editor@3.0.1': + resolution: {integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==} engines: {node: '>=18'} - '@inquirer/expand@2.3.0': - resolution: {integrity: sha512-qnJsUcOGCSG1e5DTOErmv2BPQqrtT6uzqn1vI/aYGiPKq+FgslGZmtdnXbhuI7IlT7OByDoEEqdnhUnVR2hhLw==} + '@inquirer/expand@3.0.1': + resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==} engines: {node: '>=18'} - '@inquirer/figures@1.0.5': - resolution: {integrity: sha512-79hP/VWdZ2UVc9bFGJnoQ/lQMpL74mGgzSYX1xUqCVk7/v73vJCMw1VuyWN1jGkZ9B3z7THAbySqGbCNefcjfA==} + '@inquirer/figures@1.0.6': + resolution: {integrity: sha512-yfZzps3Cso2UbM7WlxKwZQh2Hs6plrbjs1QnzQDZhK2DgyCo6D8AaHps9olkNcUFlcYERMqU3uJSp1gmy3s/qQ==} engines: {node: '>=18'} - '@inquirer/input@2.3.0': - resolution: {integrity: sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==} + '@inquirer/input@3.0.1': + resolution: {integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==} engines: {node: '>=18'} - '@inquirer/number@1.1.0': - resolution: {integrity: sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==} + '@inquirer/number@2.0.1': + resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==} engines: {node: '>=18'} - '@inquirer/password@2.2.0': - resolution: {integrity: sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==} + '@inquirer/password@3.0.1': + resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==} engines: {node: '>=18'} - '@inquirer/prompts@5.5.0': - resolution: {integrity: sha512-BHDeL0catgHdcHbSFFUddNzvx/imzJMft+tWDPwTm3hfu8/tApk1HrooNngB2Mb4qY+KaRWF+iZqoVUPeslEog==} + '@inquirer/prompts@6.0.1': + resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==} engines: {node: '>=18'} - '@inquirer/rawlist@2.3.0': - resolution: {integrity: sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==} + '@inquirer/rawlist@3.0.1': + resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==} engines: {node: '>=18'} - '@inquirer/search@1.1.0': - resolution: {integrity: sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==} + '@inquirer/search@2.0.1': + resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==} engines: {node: '>=18'} - '@inquirer/select@2.5.0': - resolution: {integrity: sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==} + '@inquirer/select@3.0.1': + resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==} engines: {node: '>=18'} - '@inquirer/type@1.5.3': - resolution: {integrity: sha512-xUQ14WQGR/HK5ei+2CvgcwoH9fQ4PgPGmVFSN0pc1+fVyDL3MREhyAY7nxEErSu6CkllBM3D7e3e+kOvtu+eIg==} + '@inquirer/type@1.5.5': + resolution: {integrity: sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==} engines: {node: '>=18'} + '@inquirer/type@2.0.0': + resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} + engines: {node: '>=18'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -751,6 +797,28 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.1.0': + resolution: {integrity: sha512-zlQONA+msXPPwHWZMKFVS78ewFczIll5lXiVPwFPCZUsrOKdxc2AvxU1HoNBmMRhqDZUR9HkC3UOm+6pME6Xsg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.3.0': + resolution: {integrity: sha512-Cebt4Vk7k1xHy87kHY7KSPLT77A7Ev7IfOblyLZhtYEhrdQ6fX4EoLq3xOQ3O/DRMEh2ok5nyC180E+ABS8Wmw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@mswjs/interceptors@0.35.9': + resolution: {integrity: sha512-SSnyl/4ni/2ViHKkiZb8eajA/eN1DNFaHjhGiLUdZvDz6PKF4COSf/17xqSz64nOo2Ia29SA6B2KNCsyCbVmaQ==} + engines: {node: '>=18'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -763,6 +831,19 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@open-draft/deferred-promise@2.2.0': + resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} + + '@open-draft/logger@0.3.0': + resolution: {integrity: sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==} + + '@open-draft/until@2.1.0': + resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pkgr/core@0.1.1': resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -916,6 +997,9 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -937,15 +1021,24 @@ packages: '@types/node@22.5.4': resolution: {integrity: sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==} + '@types/node@22.5.5': + resolution: {integrity: sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/statuses@2.0.5': + resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} + '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1009,6 +1102,15 @@ packages: resolution: {integrity: sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/coverage-v8@2.1.1': + resolution: {integrity: sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==} + peerDependencies: + '@vitest/browser': 2.1.1 + vitest: 2.1.1 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/eslint-plugin@1.1.4': resolution: {integrity: sha512-kudjgefmJJ7xQ2WfbUU6pZbm7Ou4gLYRaao/8Ynide3G0QhVKHd978sDyWX4KOH0CCMH9cyrGAkFd55eGzJ48Q==} peerDependencies: @@ -1090,6 +1192,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -1098,6 +1204,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + are-docs-informative@0.0.2: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} @@ -1195,10 +1305,6 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - cli-width@4.1.0: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} @@ -1251,6 +1357,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + core-js-compat@3.38.1: resolution: {integrity: sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==} @@ -1344,6 +1454,9 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -1368,12 +1481,22 @@ packages: domutils@3.1.0: resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + electron-to-chromium@1.5.22: resolution: {integrity: sha512-tKYm5YHPU1djz0O+CGJ+oJIvimtsCcwR2Z9w7Skh08lUdyzXY5djods3q+z2JkWdb7tCcmM//eVavSRAiaPRNg==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enhanced-resolve@5.17.1: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} @@ -1668,6 +1791,10 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -1704,6 +1831,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} @@ -1735,6 +1866,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql@16.9.0: + resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} @@ -1747,12 +1882,22 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + headers-polyfill@4.0.3: + resolution: {integrity: sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -1780,10 +1925,6 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - inquirer@10.2.2: - resolution: {integrity: sha512-tyao/4Vo36XnUItZ7DnUXX4f1jVao2mSrleV/5IPtW/XAEA26hRVsbc68nuTEKWcr5vMP/1mVoT2O7u8H4v1Vg==} - engines: {node: '>=18'} - is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -1810,6 +1951,9 @@ packages: is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-node-process@1.2.0: + resolution: {integrity: sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==} + is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -1824,6 +1968,25 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@1.21.6: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true @@ -1918,12 +2081,22 @@ packages: loupe@3.1.1: resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + markdown-table@3.0.3: resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} @@ -1966,6 +2139,10 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + memfs@4.11.2: + resolution: {integrity: sha512-VcR7lEtgQgv7AxGkrNNeUAimFLT+Ov8uGu1LuOfbe/iF/dKoh/QgpoaMZlhfejvLtMxtXYyeoT7Ar1jEbWdbPA==} + engines: {node: '>= 4.0.0'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2073,6 +2250,10 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + mkdist@1.5.9: resolution: {integrity: sha512-PdJimzhcgDxaHpk1SUabw56gT3BU15vBHUTHkeeus8Kl7jUkpgG7+z0PiS/y23XXgO8TiU/dKP3L1oG55qrP1g==} hasBin: true @@ -2098,6 +2279,16 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msw@2.4.11: + resolution: {integrity: sha512-TVEw9NOPTc6ufOQLJ53234S9NBRxQbu7xFMxs+OCP43JQcNEIOKiZHxEm2nDzYIrwccoIhUxUf8wr99SukD76A==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + typescript: '>= 4.8.x' + peerDependenciesMeta: + typescript: + optional: true + mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -2113,6 +2304,9 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} @@ -2126,6 +2320,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + ofetch@1.4.0: + resolution: {integrity: sha512-MuHgsEhU6zGeX+EMh+8mSMrYTnsqJQQrpM00Q6QHMKNqQ0bKy0B43tk8tL1wg+CnsSTy1kg4Ir2T5Ig6rD+dfQ==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2137,6 +2334,9 @@ packages: resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} engines: {node: '>=0.10.0'} + outvariant@1.4.3: + resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -2157,6 +2357,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.0: resolution: {integrity: sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog==} @@ -2187,6 +2390,13 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -2412,10 +2622,16 @@ packages: resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} engines: {node: ^14.13.1 || >=16.0.0} + psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2447,6 +2663,9 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2479,10 +2698,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - run-async@3.0.0: - resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==} - engines: {node: '>=0.12.0'} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -2562,17 +2777,35 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} + storyblok-js-client@6.9.2: + resolution: {integrity: sha512-31GM5X/SIP4eJsSMCpAnaPDRmmUotSSWD3Umnuzf3CGqjyakot2Gv5QmuV23fRM7TCDUQlg5wurROmAzkKMKKg==} + + strict-event-emitter@0.5.1: + resolution: {integrity: sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -2616,9 +2849,19 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + thingies@1.21.0: + resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2653,6 +2896,16 @@ packages: resolution: {integrity: sha512-khrZo4buq4qVmsGzS5yQjKe/WsFvV8fGfOjDQN0q4iy9FjRfPWRgTFrU8u1R2iu/SfWLhY9WnCi4Jhdrcbtg+g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + + tree-dump@1.0.2: + resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -2682,6 +2935,10 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + type-fest@4.26.1: + resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==} + engines: {node: '>=16'} + typescript@5.6.2: resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} engines: {node: '>=14.17'} @@ -2714,6 +2971,10 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + untyped@1.4.2: resolution: {integrity: sha512-nC5q0DnPEPVURPhfPQLahhSTnemVtPzdx7ofiRxXpOB2SYnb3MfdU3DVGyJdS8Lx+tBWeAePO8BfU/3EgksM7Q==} hasBin: true @@ -2727,6 +2988,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -2822,6 +3086,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -2871,7 +3139,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@3.6.0(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.5)(eslint-plugin-format@0.1.2(eslint@9.10.0(jiti@1.21.6)))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4))': + '@antfu/eslint-config@3.6.0(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.5)(eslint-plugin-format@0.1.2(eslint@9.10.0(jiti@1.21.6)))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2)))': dependencies: '@antfu/install-pkg': 0.4.1 '@clack/prompts': 0.7.0 @@ -2880,7 +3148,7 @@ snapshots: '@stylistic/eslint-plugin': 2.8.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/eslint-plugin': 8.5.0(@typescript-eslint/parser@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) '@typescript-eslint/parser': 8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) - '@vitest/eslint-plugin': 1.1.4(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4)) + '@vitest/eslint-plugin': 1.1.4(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2))) eslint: 9.10.0(jiti@1.21.6) eslint-config-flat-gitignore: 0.3.0(eslint@9.10.0(jiti@1.21.6)) eslint-flat-config-utils: 0.4.0 @@ -3040,6 +3308,21 @@ snapshots: '@babel/helper-validator-identifier': 7.24.7 to-fast-properties: 2.0.0 + '@bcoe/v8-coverage@0.2.3': {} + + '@bundled-es-modules/cookie@2.0.0': + dependencies: + cookie: 0.5.0 + + '@bundled-es-modules/statuses@1.0.1': + dependencies: + statuses: 2.0.1 + + '@bundled-es-modules/tough-cookie@0.1.6': + dependencies: + '@types/tough-cookie': 4.0.5 + tough-cookie: 4.1.4 + '@clack/core@0.3.4': dependencies: picocolors: 1.1.0 @@ -3331,28 +3614,32 @@ snapshots: '@humanwhocodes/retry@0.3.0': {} - '@inquirer/checkbox@2.5.0': + '@inquirer/checkbox@3.0.1': dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/figures': 1.0.5 - '@inquirer/type': 1.5.3 + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.6 + '@inquirer/type': 2.0.0 ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 '@inquirer/confirm@3.2.0': dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/type': 1.5.3 + '@inquirer/core': 9.2.1 + '@inquirer/type': 1.5.5 + + '@inquirer/confirm@4.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 - '@inquirer/core@9.1.0': + '@inquirer/core@9.2.1': dependencies: - '@inquirer/figures': 1.0.5 - '@inquirer/type': 1.5.3 + '@inquirer/figures': 1.0.6 + '@inquirer/type': 2.0.0 '@types/mute-stream': 0.0.4 - '@types/node': 22.5.4 + '@types/node': 22.5.5 '@types/wrap-ansi': 3.0.0 ansi-escapes: 4.3.2 - cli-spinners: 2.9.2 cli-width: 4.1.0 mute-stream: 1.0.0 signal-exit: 4.1.0 @@ -3360,74 +3647,89 @@ snapshots: wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.2 - '@inquirer/editor@2.2.0': + '@inquirer/editor@3.0.1': dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/type': 1.5.3 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 external-editor: 3.1.0 - '@inquirer/expand@2.3.0': + '@inquirer/expand@3.0.1': dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/type': 1.5.3 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 yoctocolors-cjs: 2.1.2 - '@inquirer/figures@1.0.5': {} + '@inquirer/figures@1.0.6': {} - '@inquirer/input@2.3.0': + '@inquirer/input@3.0.1': dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/type': 1.5.3 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 - '@inquirer/number@1.1.0': + '@inquirer/number@2.0.1': dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/type': 1.5.3 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 - '@inquirer/password@2.2.0': + '@inquirer/password@3.0.1': dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/type': 1.5.3 + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 ansi-escapes: 4.3.2 - '@inquirer/prompts@5.5.0': + '@inquirer/prompts@6.0.1': dependencies: - '@inquirer/checkbox': 2.5.0 - '@inquirer/confirm': 3.2.0 - '@inquirer/editor': 2.2.0 - '@inquirer/expand': 2.3.0 - '@inquirer/input': 2.3.0 - '@inquirer/number': 1.1.0 - '@inquirer/password': 2.2.0 - '@inquirer/rawlist': 2.3.0 - '@inquirer/search': 1.1.0 - '@inquirer/select': 2.5.0 - - '@inquirer/rawlist@2.3.0': - dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/type': 1.5.3 + '@inquirer/checkbox': 3.0.1 + '@inquirer/confirm': 4.0.1 + '@inquirer/editor': 3.0.1 + '@inquirer/expand': 3.0.1 + '@inquirer/input': 3.0.1 + '@inquirer/number': 2.0.1 + '@inquirer/password': 3.0.1 + '@inquirer/rawlist': 3.0.1 + '@inquirer/search': 2.0.1 + '@inquirer/select': 3.0.1 + + '@inquirer/rawlist@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 yoctocolors-cjs: 2.1.2 - '@inquirer/search@1.1.0': + '@inquirer/search@2.0.1': dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/figures': 1.0.5 - '@inquirer/type': 1.5.3 + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.6 + '@inquirer/type': 2.0.0 yoctocolors-cjs: 2.1.2 - '@inquirer/select@2.5.0': + '@inquirer/select@3.0.1': dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/figures': 1.0.5 - '@inquirer/type': 1.5.3 + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.6 + '@inquirer/type': 2.0.0 ansi-escapes: 4.3.2 yoctocolors-cjs: 2.1.2 - '@inquirer/type@1.5.3': + '@inquirer/type@1.5.5': + dependencies: + mute-stream: 1.0.0 + + '@inquirer/type@2.0.0': dependencies: mute-stream: 1.0.0 + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -3445,6 +3747,31 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jsonjoy.com/base64@1.1.2(tslib@2.7.0)': + dependencies: + tslib: 2.7.0 + + '@jsonjoy.com/json-pack@1.1.0(tslib@2.7.0)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.7.0) + '@jsonjoy.com/util': 1.3.0(tslib@2.7.0) + hyperdyperid: 1.2.0 + thingies: 1.21.0(tslib@2.7.0) + tslib: 2.7.0 + + '@jsonjoy.com/util@1.3.0(tslib@2.7.0)': + dependencies: + tslib: 2.7.0 + + '@mswjs/interceptors@0.35.9': + dependencies: + '@open-draft/deferred-promise': 2.2.0 + '@open-draft/logger': 0.3.0 + '@open-draft/until': 2.1.0 + is-node-process: 1.2.0 + outvariant: 1.4.3 + strict-event-emitter: 0.5.1 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3457,6 +3784,18 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@open-draft/deferred-promise@2.2.0': {} + + '@open-draft/logger@0.3.0': + dependencies: + is-node-process: 1.2.0 + outvariant: 1.4.3 + + '@open-draft/until@2.1.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + '@pkgr/core@0.1.1': {} '@rollup/plugin-alias@5.1.0(rollup@3.29.4)': @@ -3556,9 +3895,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.21.3': optional: true - '@storyblok/eslint-config@0.2.0(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.5)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4))': + '@storyblok/eslint-config@0.2.0(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.5)(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2)))': dependencies: - '@antfu/eslint-config': 3.6.0(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.5)(eslint-plugin-format@0.1.2(eslint@9.10.0(jiti@1.21.6)))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4)) + '@antfu/eslint-config': 3.6.0(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(@vue/compiler-sfc@3.5.5)(eslint-plugin-format@0.1.2(eslint@9.10.0(jiti@1.21.6)))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2))) eslint: 9.10.0(jiti@1.21.6) eslint-plugin-format: 0.1.2(eslint@9.10.0(jiti@1.21.6)) transitivePeerDependencies: @@ -3595,6 +3934,8 @@ snapshots: '@trysound/sax@0.2.0': {} + '@types/cookie@0.6.0': {} + '@types/debug@4.1.12': dependencies: '@types/ms': 0.7.34 @@ -3620,14 +3961,22 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/node@22.5.5': + dependencies: + undici-types: 6.19.8 + '@types/normalize-package-data@2.4.4': {} '@types/resolve@1.20.2': {} + '@types/statuses@2.0.5': {} + '@types/through@0.0.33': dependencies: '@types/node': 22.5.4 + '@types/tough-cookie@4.0.5': {} + '@types/unist@3.0.3': {} '@types/wrap-ansi@3.0.0': {} @@ -3713,13 +4062,31 @@ snapshots: '@typescript-eslint/types': 8.5.0 eslint-visitor-keys: 3.4.3 - '@vitest/eslint-plugin@1.1.4(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4))': + '@vitest/coverage-v8@2.1.1(vitest@2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2)))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.7 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.11 + magicast: 0.3.5 + std-env: 3.7.0 + test-exclude: 7.0.1 + tinyrainbow: 1.2.0 + vitest: 2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2)) + transitivePeerDependencies: + - supports-color + + '@vitest/eslint-plugin@1.1.4(@typescript-eslint/utils@8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2))(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2)(vitest@2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2)))': dependencies: eslint: 9.10.0(jiti@1.21.6) optionalDependencies: '@typescript-eslint/utils': 8.5.0(eslint@9.10.0(jiti@1.21.6))(typescript@5.6.2) typescript: 5.6.2 - vitest: 2.1.1(@types/node@22.5.4) + vitest: 2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2)) '@vitest/expect@2.1.1': dependencies: @@ -3728,12 +4095,13 @@ snapshots: chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.4.5(@types/node@22.5.4))': + '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(msw@2.4.11(typescript@5.6.2))(vite@5.4.5(@types/node@22.5.4))': dependencies: '@vitest/spy': 2.1.1 estree-walker: 3.0.3 magic-string: 0.30.11 optionalDependencies: + msw: 2.4.11(typescript@5.6.2) vite: 5.4.5(@types/node@22.5.4) '@vitest/pretty-format@2.1.1': @@ -3812,6 +4180,8 @@ snapshots: ansi-regex@5.0.1: {} + ansi-regex@6.1.0: {} + ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 @@ -3820,6 +4190,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.1: {} + are-docs-informative@0.0.2: {} argparse@2.0.1: {} @@ -3914,8 +4286,6 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - cli-spinners@2.9.2: {} - cli-width@4.1.0: {} cliui@8.0.1: @@ -3954,6 +4324,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@0.5.0: {} + core-js-compat@3.38.1: dependencies: browserslist: 4.23.3 @@ -4060,6 +4432,8 @@ snapshots: dequal@2.0.3: {} + destr@2.0.3: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -4090,10 +4464,16 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dotenv@16.4.5: {} + + eastasianwidth@0.2.0: {} + electron-to-chromium@1.5.22: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + enhanced-resolve@5.17.1: dependencies: graceful-fs: 4.2.11 @@ -4543,6 +4923,11 @@ snapshots: flatted@3.3.1: {} + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + fraction.js@4.3.7: {} fs.realpath@1.0.0: {} @@ -4570,6 +4955,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.0 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@8.1.0: dependencies: fs.realpath: 1.0.0 @@ -4600,6 +4994,8 @@ snapshots: graphemer@1.4.0: {} + graphql@16.9.0: {} + has-flag@3.0.0: {} has-flag@4.0.0: {} @@ -4608,10 +5004,16 @@ snapshots: dependencies: function-bind: 1.1.2 + headers-polyfill@4.0.3: {} + hookable@5.5.3: {} hosted-git-info@2.8.9: {} + html-escaper@2.0.2: {} + + hyperdyperid@1.2.0: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -4634,17 +5036,6 @@ snapshots: inherits@2.0.4: {} - inquirer@10.2.2: - dependencies: - '@inquirer/core': 9.1.0 - '@inquirer/prompts': 5.5.0 - '@inquirer/type': 1.5.3 - '@types/mute-stream': 0.0.4 - ansi-escapes: 4.3.2 - mute-stream: 1.0.0 - run-async: 3.0.0 - rxjs: 7.8.1 - is-arrayish@0.2.1: {} is-builtin-module@3.2.1: @@ -4665,6 +5056,8 @@ snapshots: is-module@1.0.0: {} + is-node-process@1.2.0: {} + is-number@7.0.0: {} is-path-inside@3.0.3: {} @@ -4675,6 +5068,33 @@ snapshots: isexe@2.0.0: {} + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.3.7 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.1.7: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jiti@1.21.6: {} js-tokens@4.0.0: {} @@ -4748,6 +5168,8 @@ snapshots: dependencies: get-func-name: 2.0.2 + lru-cache@10.4.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -4756,6 +5178,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.6.3 + markdown-table@3.0.3: {} mdast-util-find-and-replace@3.0.1: @@ -4863,6 +5295,13 @@ snapshots: mdn-data@2.0.30: {} + memfs@4.11.2: + dependencies: + '@jsonjoy.com/json-pack': 1.1.0(tslib@2.7.0) + '@jsonjoy.com/util': 1.3.0(tslib@2.7.0) + tree-dump: 1.0.2(tslib@2.7.0) + tslib: 2.7.0 + merge2@1.4.1: {} micromark-core-commonmark@2.0.1: @@ -5075,6 +5514,8 @@ snapshots: dependencies: brace-expansion: 2.0.1 + minipass@7.1.2: {} + mkdist@1.5.9(typescript@5.6.2): dependencies: autoprefixer: 10.4.20(postcss@8.4.45) @@ -5104,6 +5545,28 @@ snapshots: ms@2.1.3: {} + msw@2.4.11(typescript@5.6.2): + dependencies: + '@bundled-es-modules/cookie': 2.0.0 + '@bundled-es-modules/statuses': 1.0.1 + '@bundled-es-modules/tough-cookie': 0.1.6 + '@inquirer/confirm': 3.2.0 + '@mswjs/interceptors': 0.35.9 + '@open-draft/until': 2.1.0 + '@types/cookie': 0.6.0 + '@types/statuses': 2.0.5 + chalk: 4.1.2 + graphql: 16.9.0 + headers-polyfill: 4.0.3 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + strict-event-emitter: 0.5.1 + type-fest: 4.26.1 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.6.2 + mute-stream@1.0.0: {} nanoid@3.3.7: {} @@ -5112,6 +5575,8 @@ snapshots: natural-compare@1.4.0: {} + node-fetch-native@1.6.4: {} + node-releases@2.0.18: {} normalize-package-data@2.5.0: @@ -5127,6 +5592,12 @@ snapshots: dependencies: boolbase: 1.0.0 + ofetch@1.4.0: + dependencies: + destr: 2.0.3 + node-fetch-native: 1.6.4 + ufo: 1.5.4 + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -5142,6 +5613,8 @@ snapshots: os-tmpdir@1.0.2: {} + outvariant@1.4.3: {} + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -5160,6 +5633,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.0: {} parent-module@1.0.1: @@ -5186,6 +5661,13 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} pathe@1.1.2: {} @@ -5383,8 +5865,12 @@ snapshots: pretty-bytes@6.1.1: {} + psl@1.9.0: {} + punycode@2.3.1: {} + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} read-pkg-up@7.0.1: @@ -5417,6 +5903,8 @@ snapshots: require-directory@2.1.1: {} + requires-port@1.0.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -5463,8 +5951,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.21.3 fsevents: 2.3.3 - run-async@3.0.0: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -5530,18 +6016,34 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + std-env@3.7.0: {} + storyblok-js-client@6.9.2: {} + + strict-event-emitter@0.5.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 @@ -5585,8 +6087,18 @@ snapshots: tapable@2.2.1: {} + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 + text-table@0.2.0: {} + thingies@1.21.0(tslib@2.7.0): + dependencies: + tslib: 2.7.0 + tinybench@2.9.0: {} tinyexec@0.3.0: {} @@ -5611,6 +6123,17 @@ snapshots: dependencies: eslint-visitor-keys: 3.4.3 + tough-cookie@4.1.4: + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + + tree-dump@1.0.2(tslib@2.7.0): + dependencies: + tslib: 2.7.0 + ts-api-utils@1.3.0(typescript@5.6.2): dependencies: typescript: 5.6.2 @@ -5629,6 +6152,8 @@ snapshots: type-fest@0.8.1: {} + type-fest@4.26.1: {} + typescript@5.6.2: {} ufo@1.5.4: {} @@ -5687,6 +6212,8 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universalify@0.2.0: {} + untyped@1.4.2: dependencies: '@babel/core': 7.25.2 @@ -5709,6 +6236,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + util-deprecate@1.0.2: {} validate-npm-package-license@3.0.4: @@ -5742,10 +6274,10 @@ snapshots: '@types/node': 22.5.4 fsevents: 2.3.3 - vitest@2.1.1(@types/node@22.5.4): + vitest@2.1.1(@types/node@22.5.4)(msw@2.4.11(typescript@5.6.2)): dependencies: '@vitest/expect': 2.1.1 - '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.4.5(@types/node@22.5.4)) + '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(msw@2.4.11(typescript@5.6.2))(vite@5.4.5(@types/node@22.5.4)) '@vitest/pretty-format': 2.1.1 '@vitest/runner': 2.1.1 '@vitest/snapshot': 2.1.1 @@ -5812,6 +6344,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} xml-name-validator@4.0.0: {} diff --git a/src/api.test.ts b/src/api.test.ts new file mode 100644 index 00000000..045173f2 --- /dev/null +++ b/src/api.test.ts @@ -0,0 +1,69 @@ +import { apiClient } from './api' + +// Mock the StoryblokClient to prevent actual HTTP requests +vi.mock('storyblok-js-client', () => { + const StoryblokClientMock = vi.fn().mockImplementation((config) => { + return { + config, + } + }) + + return { + default: StoryblokClientMock, + __esModule: true, // Important for ESM modules + } +}) + +// Mocking the session module +vi.mock('./session', () => { + let _cache: Record | null = null + const session = () => { + if (!_cache) { + _cache = { + state: { + isLoggedIn: true, + password: 'test-token', + region: 'eu', + }, + updateSession: vi.fn(), + persistCredentials: vi.fn(), + initializeSession: vi.fn(), + } + } + return _cache + } + + return { + session, + } +}) + +describe('storyblok API Client', () => { + beforeEach(async () => { + // Reset the module state before each test to ensure test isolation + vi.resetModules() + vi.clearAllMocks() + }) + + it('should have a default region of "eu"', () => { + const { region } = apiClient() + expect(region).toBe('eu') + }) + + it('should return the same client instance when called multiple times without changes', () => { + const api1 = apiClient() + const client1 = api1.client + + const api2 = apiClient() + const client2 = api2.client + + expect(client1).toBe(client2) + }) + + it('should set the region on the client', () => { + const { setRegion } = apiClient() + setRegion('us') + const { region } = apiClient() + expect(region).toBe('us') + }) +}) diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 00000000..9d065a85 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,51 @@ +import StoryblokClient from 'storyblok-js-client' +import { session } from './session' +import type { RegionCode } from './constants' + +export interface ApiClientState { + region: RegionCode + accessToken: string + client: StoryblokClient | null +} + +const state: ApiClientState = { + region: 'eu', + accessToken: '', + client: null, +} + +export function apiClient() { + if (!state.client) { + createClient() + } + + function createClient() { + const userSession = session() + if (!userSession.state.isLoggedIn) { + throw new Error('User is not logged in') + } + state.client = new StoryblokClient({ + accessToken: userSession.state.password!, + region: userSession.state.region!, + }) + } + + function setAccessToken(accessToken: string) { + state.accessToken = accessToken + state.client = null + createClient() + } + + function setRegion(region: RegionCode) { + state.region = region + state.client = null + createClient() + } + + return { + region: state.region, + client: state.client, + setAccessToken, + setRegion, + } +} diff --git a/src/commands/login/actions.test.ts b/src/commands/login/actions.test.ts new file mode 100644 index 00000000..5f4ceae4 --- /dev/null +++ b/src/commands/login/actions.test.ts @@ -0,0 +1,109 @@ +import { afterAll, afterEach, beforeAll, expect } from 'vitest' + +import { setupServer } from 'msw/node' +import { http, HttpResponse } from 'msw' +import { loginWithEmailAndPassword, loginWithOtp, loginWithToken } from './actions' +import chalk from 'chalk' + +const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/ + +const handlers = [ + http.get('https://api.storyblok.com/v1/users/me', async ({ request }) => { + const token = request.headers.get('Authorization') + if (token === 'valid-token') { + return HttpResponse.json({ data: 'user data' }) + } + return new HttpResponse('Unauthorized', { status: 401 }) + }), + http.post('https://api.storyblok.com/v1/users/login', async ({ request }) => { + const body = await request.json() as { email: string, password: string } + + if (!emailRegex.test(body.email)) { + return new HttpResponse('Unprocessable Entity', { status: 422 }) + } + + if (body?.email === 'julio.iglesias@storyblok.com' && body?.password === 'password') { + return HttpResponse.json({ otp_required: true }) + } + else { + return new HttpResponse('Unauthorized', { status: 401 }) + } + }), +] + +const server = setupServer(...handlers) + +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) + +afterEach(() => server.resetHandlers()) +afterAll(() => server.close()) + +describe('login actions', () => { + describe('loginWithToken', () => { + it('should login successfully with a valid token', async () => { + const mockResponse = { data: 'user data' } + const result = await loginWithToken('valid-token', 'eu') + expect(result).toEqual(mockResponse) + }) + + it('should throw an masked error for invalid token', async () => { + await expect(loginWithToken('invalid-token', 'eu')).rejects.toThrow( + new Error(`The token provided ${chalk.bold('inva*********')} is invalid. + Please make sure you are using the correct token and try again.`), + ) + }) + + it('should throw a network error if response is empty (network)', async () => { + server.use( + http.get('https://api.storyblok.com/v1/users/me', () => { + return new HttpResponse(null, { status: 500 }) + }), + ) + await expect(loginWithToken('any-token', 'eu')).rejects.toThrow( + 'No response from server, please check if you are correctly connected to internet', + ) + }) + }) + + describe('loginWithEmailAndPassword', () => { + it('should get if the user requires otp', async () => { + const expected = { otp_required: true } + const result = await loginWithEmailAndPassword('julio.iglesias@storyblok.com', 'password', 'eu') + expect(result).toEqual(expected) + }) + + it('should throw an error for invalid email', async () => { + await expect(loginWithEmailAndPassword('invalid-email', 'password', 'eu')).rejects.toThrow( + 'The provided credentials are invalid', + ) + }) + + it('should throw an error for invalid credentials', async () => { + await expect(loginWithEmailAndPassword('david.bisbal@storyblok.com', 'password', 'eu')).rejects.toThrow( + 'The user is not authorized to access the API', + ) + }) + }) + + describe('loginWithOtp', () => { + it('should login successfully with valid email, password, and otp', async () => { + server.use( + http.post('https://api.storyblok.com/v1/users/login', async ({ request }) => { + const body = await request.json() as { email: string, password: string, otp_attempt: string } + if (body?.email === 'julio.iglesias@storyblok.com' && body?.password === 'password' && body?.otp_attempt === '123456') { + return HttpResponse.json({ access_token: 'Awiwi' }) + } + + else { + return new HttpResponse('Unauthorized', { status: 401 }) + } + }), + ) + const expected = { access_token: 'Awiwi' } + + const result = await loginWithOtp('julio.iglesias@storyblok.com', 'password', '123456', 'eu') + + expect(result).toEqual(expected) + }) + }) +}) diff --git a/src/commands/login/actions.ts b/src/commands/login/actions.ts index 53950f36..eb609b68 100644 --- a/src/commands/login/actions.ts +++ b/src/commands/login/actions.ts @@ -1,4 +1,55 @@ -export const login = () => { - // eslint-disable-next-line no-console - console.log('Login') +import chalk from 'chalk' +import type { RegionCode } from '../../constants' +import { regionsDomain } from '../../constants' +import { FetchError, ofetch } from 'ofetch' +import { APIError, handleAPIError, maskToken } from '../../utils' + +export const loginWithToken = async (token: string, region: RegionCode) => { + try { + return await ofetch(`https://${regionsDomain[region]}/v1/users/me`, { + headers: { + Authorization: token, + }, + }) + } + catch (error) { + if (error instanceof FetchError) { + const status = error.response?.status + + switch (status) { + case 401: + throw new APIError('unauthorized', 'login_with_token', error, `The token provided ${chalk.bold(maskToken(token))} is invalid. + Please make sure you are using the correct token and try again.`) + default: + throw new APIError('network_error', 'login_with_token', error) + } + } + else { + throw new APIError('generic', 'login_with_token', error as Error) + } + } +} + +export const loginWithEmailAndPassword = async (email: string, password: string, region: RegionCode) => { + try { + return await ofetch(`https://${regionsDomain[region]}/v1/users/login`, { + method: 'POST', + body: JSON.stringify({ email, password }), + }) + } + catch (error) { + handleAPIError('login_email_password', error as Error) + } +} + +export const loginWithOtp = async (email: string, password: string, otp: string, region: RegionCode) => { + try { + return await ofetch(`https://${regionsDomain[region]}/v1/users/login`, { + method: 'POST', + body: JSON.stringify({ email, password, otp_attempt: otp }), + }) + } + catch (error) { + handleAPIError('login_with_otp', error as Error) + } } diff --git a/src/commands/login/index.test.ts b/src/commands/login/index.test.ts new file mode 100644 index 00000000..be7fb574 --- /dev/null +++ b/src/commands/login/index.test.ts @@ -0,0 +1,232 @@ +import { describe, expect, it, vi } from 'vitest' +import { loginWithEmailAndPassword, loginWithOtp, loginWithToken } from './actions' +import { loginCommand } from './' +import { konsola } from '../../utils' +import { input, password, select } from '@inquirer/prompts' +import { regions } from '../../constants' +import chalk from 'chalk' +import { session } from '../../session' // Import as module to mock properly + +vi.mock('./actions', () => ({ + loginWithEmailAndPassword: vi.fn(), + loginWithOtp: vi.fn(), + loginWithToken: vi.fn(), +})) + +vi.mock('../../creds', () => ({ + addNetrcEntry: vi.fn(), + isAuthorized: vi.fn(), + getNetrcCredentials: vi.fn(), + getCredentialsForMachine: vi.fn(), +})) + +// Mocking the session module +vi.mock('../../session', () => { + let _cache: Record | null = null + const session = () => { + if (!_cache) { + _cache = { + state: { + isLoggedIn: false, + }, + updateSession: vi.fn(), + persistCredentials: vi.fn(), + initializeSession: vi.fn(), + } + } + return _cache + } + + return { + session, + } +}) + +vi.mock('../../utils', async () => { + const actualUtils = await vi.importActual('../../utils') + return { + ...actualUtils, + konsola: { + ok: vi.fn(), + title: vi.fn(), + error: vi.fn(), + }, + handleError: (error: Error, header = false) => { + konsola.error(error, header) + // Optionally, prevent process.exit during tests + }, + } +}) + +vi.mock('@inquirer/prompts', () => ({ + input: vi.fn(), + password: vi.fn(), + select: vi.fn(), +})) + +describe('loginCommand', () => { + beforeEach(() => { + vi.resetAllMocks() + vi.clearAllMocks() + }) + + describe('default interactive login', () => { + it('should prompt the user for login strategy when no token is provided', async () => { + await loginCommand.parseAsync(['node', 'test']) + + expect(select).toHaveBeenCalledWith(expect.objectContaining({ + message: 'How would you like to login?', + })) + }) + + describe('login-with-email strategy', () => { + beforeEach(() => { + vi.resetAllMocks() + }) + it('should prompt the user for email and password when login-with-email is selected', async () => { + vi.mocked(select) + .mockResolvedValueOnce('login-with-email') // For login strategy + .mockResolvedValueOnce('eu') // For region + + vi.mocked(input) + .mockResolvedValueOnce('user@example.com') // For email + .mockResolvedValueOnce('123456') // For OTP code + + vi.mocked(password).mockResolvedValueOnce('test-password') + + await loginCommand.parseAsync(['node', 'test']) + + expect(input).toHaveBeenCalledWith(expect.objectContaining({ + message: 'Please enter your email address:', + })) + + expect(password).toHaveBeenCalledWith(expect.objectContaining({ + message: 'Please enter your password:', + })) + + expect(select).toHaveBeenCalledWith(expect.objectContaining({ + message: 'Please select the region you would like to work in:', + })) + }) + + it('should login with email and password if provided using login-with-email strategy', async () => { + vi.mocked(select) + .mockResolvedValueOnce('login-with-email') // For login strategy + .mockResolvedValueOnce('eu') // For region + + vi.mocked(input) + .mockResolvedValueOnce('user@example.com') // For email + .mockResolvedValueOnce('123456') // For OTP code + + vi.mocked(password).mockResolvedValueOnce('test-password') + + vi.mocked(loginWithEmailAndPassword).mockResolvedValueOnce({ otp_required: true }) + vi.mocked(loginWithOtp).mockResolvedValueOnce({ access_token: 'test-token' }) + + await loginCommand.parseAsync(['node', 'test']) + + expect(loginWithEmailAndPassword).toHaveBeenCalledWith('user@example.com', 'test-password', 'eu') + + expect(loginWithOtp).toHaveBeenCalledWith('user@example.com', 'test-password', '123456', 'eu') + }) + + it('should throw an error for invalid email and password', async () => { + vi.mocked(select).mockResolvedValueOnce('login-with-email') + vi.mocked(input).mockResolvedValueOnce('eu') + + const mockError = new Error('Error logging in with email and password') + loginWithEmailAndPassword.mockRejectedValueOnce(mockError) + + await loginCommand.parseAsync(['node', 'test']) + + expect(konsola.error).toHaveBeenCalledWith(mockError, false) + }) + }) + + describe('login-with-token strategy', () => { + it('should prompt the user for token when login-with-token is selected', async () => { + select.mockResolvedValueOnce('login-with-token') + password.mockResolvedValueOnce('test-token') + + await loginCommand.parseAsync(['node', 'test']) + + expect(password).toHaveBeenCalledWith(expect.objectContaining({ + message: 'Please enter your token:', + })) + }) + + it('should login with token if token is provided using login-with-token strategy', async () => { + vi.mocked(select).mockResolvedValueOnce('login-with-token') + vi.mocked(password).mockResolvedValueOnce('test-token') + const mockUser = { email: 'user@example.com' } + vi.mocked(loginWithToken).mockResolvedValue({ user: mockUser }) + + await loginCommand.parseAsync(['node', 'test']) + + expect(password).toHaveBeenCalledWith(expect.objectContaining({ + message: 'Please enter your token:', + })) + // Verify that loginWithToken was called with the correct arguments + expect(loginWithToken).toHaveBeenCalledWith('test-token', 'eu') + + // Verify that updateSession was called with the correct arguments + expect(session().updateSession).toHaveBeenCalledWith(mockUser.email, 'test-token', 'eu') + }) + }) + }) + + describe('--token', () => { + it('should login with a valid token', async () => { + const mockToken = 'test-token' + const mockUser = { email: 'test@example.com' } + vi.mocked(loginWithToken).mockResolvedValue({ user: mockUser }) + + await loginCommand.parseAsync(['node', 'test', '--token', mockToken]) + + expect(loginWithToken).toHaveBeenCalledWith(mockToken, 'eu') + + expect(konsola.ok).toHaveBeenCalledWith('Successfully logged in with token') + }) + + it('should login with a valid token in another region --region', async () => { + const mockToken = 'test-token' + const mockUser = { email: 'test@example.com' } + vi.mocked(loginWithToken).mockResolvedValue({ user: mockUser }) + + await loginCommand.parseAsync(['node', 'test', '--token', mockToken, '--region', 'us']) + + expect(loginWithToken).toHaveBeenCalledWith(mockToken, 'us') + + expect(konsola.ok).toHaveBeenCalledWith('Successfully logged in with token') + }) + + it('should throw an error for an invalid token', async () => { + const mockError = new Error(`The token provided ${chalk.bold('inva*********')} is invalid: ${chalk.bold('401 Unauthorized')} + + Please make sure you are using the correct token and try again.`) + + vi.mocked(loginWithToken).mockRejectedValue(mockError) + + await loginCommand.parseAsync(['node', 'test', '--token', 'invalid-token']) + + // expect(handleError).toHaveBeenCalledWith(mockError, true) + expect(konsola.error).toHaveBeenCalledWith(mockError, false) + }) + }) + + describe('--region', () => { + it('should handle invalid region error with correct message', async () => { + await loginCommand.parseAsync(['node', 'test', '--region', 'invalid-region']) + + expect(konsola.error).toHaveBeenCalledWith(expect.any(Error), false) + + // Access the error argument + const errorArg = vi.mocked(konsola.error).mock.calls[0][0] + + // Build the expected error message + const expectedMessage = `The provided region: invalid-region is not valid. Please use one of the following values: ${Object.values(regions).join(' | ')}` + + expect(errorArg.message).toBe(expectedMessage) + }) + }) +}) diff --git a/src/commands/login/index.ts b/src/commands/login/index.ts index 81e5f95a..044d53ae 100644 --- a/src/commands/login/index.ts +++ b/src/commands/login/index.ts @@ -1,19 +1,132 @@ import chalk from 'chalk' -import { commands } from '../../constants' +import { input, password, select } from '@inquirer/prompts' +import type { RegionCode } from '../../constants' +import { commands, regionNames, regions, regionsDomain } from '../../constants' import { getProgram } from '../../program' -import { formatHeader, handleError } from '../../utils' +import { CommandError, handleError, isRegion, konsola } from '../../utils' +import { loginWithEmailAndPassword, loginWithOtp, loginWithToken } from './actions' + +import { session } from '../../session' const program = getProgram() // Get the shared singleton instance +const allRegionsText = Object.values(regions).join(',') +const loginStrategy = { + message: 'How would you like to login?', + choices: [ + { + name: 'With email', + value: 'login-with-email', + short: 'Email', + }, + { + name: 'With Token (SSO)', + value: 'login-with-token', + short: 'Token', + }, + ], +} + export const loginCommand = program .command(commands.LOGIN) .description('Login to the Storyblok CLI') - .action(async () => { - try { - console.log(formatHeader(chalk.bgHex('#8556D3').bold.white(` ${commands.LOGIN} `))) - /* login() */ + .option('-t, --token ', 'Token to login directly without questions, like for CI environments') + .option( + '-r, --region ', + `The region you would like to work in. Please keep in mind that the region must match the region of your space. This region flag will be used for the other cli's commands. You can use the values: ${allRegionsText}.`, + regions.EU, + ) + .action(async (options: { + token: string + region: RegionCode + }) => { + konsola.title(` ${commands.LOGIN} `, '#8556D3') + const verbose = program.opts().verbose + const { token, region } = options + if (!isRegion(region)) { + handleError(new CommandError(`The provided region: ${region} is not valid. Please use one of the following values: ${Object.values(regions).join(' | ')}`)) + } + + const { state, updateSession, persistCredentials, initializeSession } = session() + + await initializeSession() + + if (state.isLoggedIn && !state.envLogin) { + konsola.ok(`You are already logged in. If you want to login with a different account, please logout first.`) + return } - catch (error) { - handleError(error as Error) + + if (token) { + try { + const { user } = await loginWithToken(token, region) + updateSession(user.email, token, region) + await persistCredentials(regionsDomain[region]) + + konsola.ok(`Successfully logged in with token`) + } + catch (error) { + handleError(error as Error, verbose) + } + } + else { + try { + const strategy = await select(loginStrategy) + if (strategy === 'login-with-token') { + const userToken = await password({ + message: 'Please enter your token:', + validate: (value: string) => { + return value.length > 0 + }, + }) + + const { user } = await loginWithToken(userToken, region) + + updateSession(user.email, userToken, region) + await persistCredentials(regionsDomain[region]) + + konsola.ok(`Successfully logged in with token`) + } + + else { + const userEmail = await input({ + message: 'Please enter your email address:', + required: true, + validate: (value: string) => { + const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/ + return emailRegex.test(value) + }, + }) + const userPassword = await password({ + message: 'Please enter your password:', + }) + const userRegion = await select({ + message: 'Please select the region you would like to work in:', + choices: Object.values(regions).map((region: RegionCode) => ({ + name: regionNames[region], + value: region, + })), + default: regions.EU, + }) + const response = await loginWithEmailAndPassword(userEmail, userPassword, userRegion) + + if (response.otp_required) { + const otp = await input({ + message: 'Add the code from your Authenticator app, or the one we sent to your e-mail / phone:', + required: true, + }) + + const { access_token } = await loginWithOtp(userEmail, userPassword, otp, userRegion) + updateSession(userEmail, access_token, userRegion) + } + else { + updateSession(userEmail, response.access_token, userRegion) + } + await persistCredentials(regionsDomain[userRegion]) + konsola.ok(`Successfully logged in with email ${chalk.hex('#45bfb9')(userEmail)}`) + } + } + catch (error) { + handleError(error as Error, verbose) + } } }) diff --git a/src/commands/logout/index.test.ts b/src/commands/logout/index.test.ts new file mode 100644 index 00000000..54c01a91 --- /dev/null +++ b/src/commands/logout/index.test.ts @@ -0,0 +1,28 @@ +import { isAuthorized, removeAllNetrcEntries } from '../../creds' +import { logoutCommand } from './' + +vi.mock('../../creds', () => ({ + isAuthorized: vi.fn(), + removeNetrcEntry: vi.fn(), + removeAllNetrcEntries: vi.fn(), +})) + +describe('logoutCommand', () => { + beforeEach(() => { + vi.resetAllMocks() + vi.clearAllMocks() + }) + + it('should log out the user if has previously login', async () => { + vi.mocked(isAuthorized).mockResolvedValue(true) + + await logoutCommand.parseAsync(['node', 'test']) + expect(removeAllNetrcEntries).toHaveBeenCalled() + }) + + it('should not log out the user if has not previously login', async () => { + vi.mocked(isAuthorized).mockResolvedValue(false) + await logoutCommand.parseAsync(['node', 'test']) + expect(removeAllNetrcEntries).not.toHaveBeenCalled() + }) +}) diff --git a/src/commands/logout/index.ts b/src/commands/logout/index.ts new file mode 100644 index 00000000..5a63a72c --- /dev/null +++ b/src/commands/logout/index.ts @@ -0,0 +1,26 @@ +import { isAuthorized, removeAllNetrcEntries } from '../../creds' +import { commands } from '../../constants' +import { getProgram } from '../../program' +import { handleError, konsola } from '../../utils' + +const program = getProgram() // Get the shared singleton instance + +export const logoutCommand = program + .command(commands.LOGOUT) + .description('Logout from the Storyblok CLI') + .action(async () => { + const verbose = program.opts().verbose + try { + const isAuth = await isAuthorized() + if (!isAuth) { + konsola.ok(`You are already logged out. If you want to login, please use the login command.`) + return + } + await removeAllNetrcEntries() + + konsola.ok(`Successfully logged out`) + } + catch (error) { + handleError(error as Error, verbose) + } + }) diff --git a/src/constants.ts b/src/constants.ts index e69317c8..2012db69 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,47 @@ export const commands = { LOGIN: 'login', + LOGOUT: 'logout', +} as const + +export interface ReadonlyArray { + includes: (searchElement: any, fromIndex?: number) => searchElement is T } +export const regionCodes = ['eu', 'us', 'cn', 'ca', 'ap'] as const +export type RegionCode = typeof regionCodes[number] + +export const regions: Record, RegionCode> = { + EU: 'eu', + US: 'us', + CN: 'cn', + CA: 'ca', + AP: 'ap', +} as const + +export const regionsDomain: Record = { + eu: 'api.storyblok.com', + us: 'api-us.storyblok.com', + cn: 'app.storyblokchina.cn', + ca: 'api-ca.storyblok.com', + ap: 'api-ap.storyblok.com', +} as const + +export const managementApiRegions: Record = { + eu: 'mapi.storyblok.com', + us: 'mapi-us.storyblok.com', + cn: 'mapi.storyblokchina.cn', + ca: 'mapi-ca.storyblok.com', + ap: 'mapi-ap.storyblok.com', +} as const + +export const regionNames: Record = { + eu: 'Europe', + us: 'United States', + cn: 'China', + ca: 'Canada', + ap: 'Australia', +} as const + +export const DEFAULT_AGENT = { + SB_Agent: 'SB-CLI', + SB_Agent_Version: process.env.npm_package_version || '4.x', +} as const diff --git a/src/creds.test.ts b/src/creds.test.ts new file mode 100644 index 00000000..5d8585a2 --- /dev/null +++ b/src/creds.test.ts @@ -0,0 +1,231 @@ +import { addNetrcEntry, getNetrcCredentials, getNetrcFilePath, isAuthorized, removeAllNetrcEntries, removeNetrcEntry } from './creds' +import { vol } from 'memfs' +import { join } from 'pathe' +// tell vitest to use fs mock from __mocks__ folder +// this can be done in a setup file if fs should always be mocked +vi.mock('node:fs') +vi.mock('node:fs/promises') + +beforeEach(() => { + // reset the state of in-memory fs + vol.reset() +}) + +describe('creds', async () => { + describe('getNetrcFilePath', async () => { + const originalPlatform = process.platform + const originalEnv = { ...process.env } + const originalCwd = process.cwd + + beforeEach(() => { + process.env = { ...originalEnv } + }) + + afterEach(() => { + // Restore the original platform after each test + Object.defineProperty(process, 'platform', { + value: originalPlatform, + }) + // Restore process.cwd() + process.cwd = originalCwd + }) + + it('should return the correct path on Unix-like systems when HOME is set', () => { + // Mock the platform to be Unix-like + Object.defineProperty(process, 'platform', { + value: 'linux', + }) + + // Set the HOME environment variable + process.env.HOME = '/home/testuser' + + const expectedPath = join('/home/testuser', '.netrc') + const result = getNetrcFilePath() + + expect(result).toBe(expectedPath) + }) + + it('should return the correct path on Windows systems when USERPROFILE is set', () => { + // Mock the platform to be Windows + Object.defineProperty(process, 'platform', { + value: 'win32', + }) + + // Set the USERPROFILE environment variable + process.env.USERPROFILE = 'C:/Users/TestUser' + + const expectedPath = join('C:/Users/TestUser', '.netrc') + const result = getNetrcFilePath() + + expect(result).toBe(expectedPath) + }) + + it('should use process.cwd() when home directory is not set', () => { + // Mock the platform to be Unix-like + Object.defineProperty(process, 'platform', { + value: 'linux', + }) + + // Remove HOME and USERPROFILE + delete process.env.HOME + delete process.env.USERPROFILE + + // Mock process.cwd() + process.cwd = vi.fn().mockReturnValue('/current/working/directory') + + const expectedPath = join('/current/working/directory', '.netrc') + const result = getNetrcFilePath() + + expect(result).toBe(expectedPath) + }) + + it('should use process.cwd() when HOME is empty', () => { + // Mock the platform to be Unix-like + Object.defineProperty(process, 'platform', { + value: 'linux', + }) + + // Set HOME to an empty string + process.env.HOME = '' + + // Mock process.cwd() + process.cwd = vi.fn().mockReturnValue('/current/working/directory') + + const expectedPath = join('/current/working/directory', '.netrc') + const result = getNetrcFilePath() + + expect(result).toBe(expectedPath) + }) + + it('should handle Windows platform when USERPROFILE is not set', () => { + // Mock the platform to be Windows + Object.defineProperty(process, 'platform', { + value: 'win32', + }) + + // Remove USERPROFILE + delete process.env.USERPROFILE + + // Mock process.cwd() + process.cwd = vi.fn().mockReturnValue('C:/Current/Directory') + + const expectedPath = join('C:/Current/Directory', '.netrc') + const result = getNetrcFilePath() + + expect(result).toBe(expectedPath) + }) + }) + + describe('getNetrcCredentials', () => { + it('should return empty object if .netrc file does not exist', async () => { + const creds = await getNetrcCredentials() + expect(creds).toEqual({}) + }) + it('should return the parsed content of .netrc file', async () => { + vol.fromJSON({ + 'test/.netrc': `machine api.storyblok.com + login julio.iglesias@storyblok.com + password my_access_token + region eu`, + }, '/temp') + + const credentials = await getNetrcCredentials('/temp/test/.netrc') + + expect(credentials['api.storyblok.com']).toEqual({ + login: 'julio.iglesias@storyblok.com', + password: 'my_access_token', + region: 'eu', + }) + }) + }) + + describe('addNetrcEntry', () => { + it('should add a new entry to an empty .netrc file', async () => { + vol.fromJSON({ + 'test/.netrc': '', + }, '/temp') + + await addNetrcEntry({ + filePath: '/temp/test/.netrc', + machineName: 'api.storyblok.com', + login: 'julio.iglesias@storyblok.com', + password: 'my_access_token', + region: 'eu', + }) + + const content = vol.readFileSync('/temp/test/.netrc', 'utf8') + + expect(content).toBe(`machine api.storyblok.com + login julio.iglesias@storyblok.com + password my_access_token + region eu +`) + }) + }) + + describe('removeNetrcEntry', () => { + it('should remove an entry from .netrc file', async () => { + vol.fromJSON({ + 'test/.netrc': `machine api.storyblok.com + login julio.iglesias@storyblok.com + password my_access_token + region eu`, + }, '/temp') + + await removeNetrcEntry('api.storyblok.com', '/temp/test/.netrc') + + const content = vol.readFileSync('/temp/test/.netrc', 'utf8') + + expect(content).toBe('') + }) + }) + + describe('removeAllNetrcEntries', () => { + it('should remove all entries from .netrc file', async () => { + vol.fromJSON({ + 'test/.netrc': `machine api.storyblok.com + login julio.iglesias@storyblok.com + password my_access_token + region eu`, + }, '/temp') + + await removeAllNetrcEntries('/temp/test/.netrc') + + const content = vol.readFileSync('/temp/test/.netrc', 'utf8') + + expect(content).toBe('') + }) + }) + describe('isAuthorized', () => { + beforeEach(() => { + vol.reset() + process.env.HOME = '/temp' // Ensure getNetrcFilePath points to /temp/.netrc + + vol.fromJSON({ + '/temp/.netrc': `machine api.storyblok.com + login julio.iglesias@storyblok.com + password my_access_token + region eu`, + }) + }) + it('should return true if .netrc file contains an entry', async () => { + vi.doMock('./creds', () => { + return { + getNetrcCredentials: async () => { + return { + 'api.storyblok.com': { + login: 'julio.iglesias@storyblok.com', + password: 'my_access', + region: 'eu', + }, + } + }, + } + }) + + const result = await isAuthorized() + + expect(result).toBe(true) + }) + }) +}) diff --git a/src/creds.ts b/src/creds.ts new file mode 100644 index 00000000..a4f019ab --- /dev/null +++ b/src/creds.ts @@ -0,0 +1,260 @@ +import { access, readFile, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { FileSystemError, handleFileSystemError, konsola } from './utils' +import chalk from 'chalk' +import { regionCodes } from './constants' + +export interface NetrcMachine { + login: string + password: string + region: string +} + +export const getNetrcFilePath = () => { + const homeDirectory = process.env[ + process.platform.startsWith('win') ? 'USERPROFILE' : 'HOME' + ] || process.cwd() + + return join(homeDirectory, '.netrc') +} + +const readNetrcFileAsync = async (filePath: string) => { + return await readFile(filePath, 'utf8') +} + +const preprocessNetrcContent = (content: string) => { + return content + .split('\n') + .map(line => line.split('#')[0].trim()) + .filter(line => line.length > 0) + .join(' ') +} + +const tokenizeNetrcContent = (content: string) => { + return content + .split(/\s+/) + .filter(token => token.length > 0) +} + +function includes(coll: ReadonlyArray, el: U): el is T { + return coll.includes(el as T) +} + +const parseNetrcTokens = (tokens: string[]) => { + const machines: Record = {} + let i = 0 + + while (i < tokens.length) { + const token = tokens[i] + + if (token === 'machine' || token === 'default') { + const machineName = token === 'default' ? 'default' : tokens[++i] + const machineData: Partial = {} + i++ + + while ( + i < tokens.length + && tokens[i] !== 'machine' + && tokens[i] !== 'default' + ) { + const key = tokens[i] + const value = tokens[++i] + if (key === 'region' && includes(regionCodes, value)) { + machineData[key] = value + } + else if (key === 'login' || key === 'password') { + machineData[key] = value + } + i++ + } + + machines[machineName] = machineData as NetrcMachine + } + else { + i++ + } + } + + return machines +} + +const parseNetrcContent = (content: string) => { + const preprocessedContent = preprocessNetrcContent(content) + const tokens = tokenizeNetrcContent(preprocessedContent) + return parseNetrcTokens(tokens) +} + +export const getNetrcCredentials = async (filePath: string = getNetrcFilePath()) => { + try { + await access(filePath) + } + catch { + return {} + } + try { + const content = await readNetrcFileAsync(filePath) + + const machines = parseNetrcContent(content) + return machines + } + catch (error) { + handleFileSystemError('read', error as NodeJS.ErrnoException) + return {} + } +} + +export const getCredentialsForMachine = ( + machines: Record = {}, + machineName?: string, +) => { + if (machineName) { + // Machine name provided + if (machines[machineName]) { + return machines[machineName] + } + else if (machines.default) { + return machines.default + } + else { + return null + } + } + else { + // No machine name provided + if (machines.default) { + return machines.default + } + else { + const machineNames = Object.keys(machines) + if (machineNames.length > 0) { + return machines[machineNames[0]] + } + else { + return null + } + } + } +} + +// Function to serialize machines object back into .netrc format +const serializeNetrcMachines = (machines: Record = {}) => { + let content = '' + for (const [machineName, properties] of Object.entries(machines)) { + content += `machine ${machineName}\n` + for (const [key, value] of Object.entries(properties)) { + content += ` ${key} ${value}\n` + } + } + return content +} + +// Function to add or update an entry in the .netrc file asynchronously +export const addNetrcEntry = async ({ + filePath = getNetrcFilePath(), + machineName, + login, + password, + region, +}: Record) => { + try { + let machines: Record = {} + + // Check if the file exists + try { + await access(filePath) + // File exists, read and parse it + const content = await readFile(filePath, 'utf8') + machines = parseNetrcContent(content) + } + catch { + // File does not exist + konsola.ok(`.netrc file not found at path: ${filePath}. A new file will be created.`) + } + + // Add or update the machine entry + machines[machineName] = { + login, + password, + region, + } as NetrcMachine + + // Serialize machines back into .netrc format + const newContent = serializeNetrcMachines(machines) + + // Write the updated content back to the .netrc file + await writeFile(filePath, newContent, { + mode: 0o600, // Set file permissions + }) + + konsola.ok(`Successfully added/updated entry for machine ${machineName} in ${chalk.hex('#45bfb9')(filePath)}`, true) + } + catch (error) { + throw new FileSystemError('invalid_argument', 'write', error as NodeJS.ErrnoException, `Error adding/updating entry for machine ${machineName} in .netrc file`) + } +} + +// Function to remove an entry from the .netrc file asynchronously +export const removeNetrcEntry = async ( + machineName: string, + filePath = getNetrcFilePath(), +) => { + try { + let machines: Record = {} + + // Check if the file exists + try { + await access(filePath) + // File exists, read and parse it + const content = await readFile(filePath, 'utf8') + machines = parseNetrcContent(content) + } + catch { + return + } + + if (machines[machineName]) { + // Remove the machine entry + delete machines[machineName] + // Serialize machines back into .netrc format + const newContent = serializeNetrcMachines(machines) + + // Write the updated content back to the .netrc file + await writeFile(filePath, newContent, { + mode: 0o600, // Set file permissions + }) + + konsola.ok(`Successfully removed entry from ${chalk.hex('#45bfb9')(filePath)}`, true) + } + } + catch (error: unknown) { + throw new Error(`Error removing entry for machine ${machineName} from .netrc file: ${(error as Error).message}`) + } +} + +export function removeAllNetrcEntries(filePath = getNetrcFilePath()) { + try { + writeFile(filePath, '', { + mode: 0o600, // Set file permissions + }) + } + catch (error) { + handleFileSystemError('write', error as NodeJS.ErrnoException) + } +} + +export async function isAuthorized() { + try { + const machines = await getNetrcCredentials() + // Check if there is any machine with a valid email and token + for (const machine of Object.values(machines)) { + if (machine.login && machine.password) { + return true + } + } + return false + } + catch (error: unknown) { + handleFileSystemError('authorization_check', error as NodeJS.ErrnoException) + return false + } +} diff --git a/src/index.ts b/src/index.ts index f8235c55..7a6a7df9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,26 +1,54 @@ #!/usr/bin/env node import chalk from 'chalk' -import { __dirname, formatHeader, handleError } from './utils' +import dotenv from 'dotenv' + +import { formatHeader, handleError, konsola } from './utils' import { getProgram } from './program' import './commands/login' +import './commands/logout' +import { loginWithToken } from './commands/login/actions' +dotenv.config() // This will load variables from .env into process.env const program = getProgram() console.clear() const introText = chalk.bgHex('#45bfb9').bold.black(` Storyblok CLI `) -const messageText = ` Starting Blok machine... ` +const messageText = ` ` console.log(formatHeader(` ${introText} ${messageText}`)) +program.option('-s, --space [value]', 'space ID') +program.option('-v, --verbose', 'Enable verbose output') program.on('command:*', () => { console.error(`Invalid command: ${program.args.join(' ')}`) program.help() + konsola.br() // Add a line break +}) + +program.command('test').action(async () => { + konsola.title(`Test`, '#8556D3', 'Attempting a test...') + const verbose = program.opts().verbose + try { + // await loginWithEmailAndPassword('aw', 'passwrod', 'eu') + await loginWithToken('WYSYDHYASDHSYD', 'eu') + } + catch (error) { + handleError(error as Error, verbose) + } }) +/* console.log(` +${chalk.hex('#45bfb9')(' ─────╮')} +${chalk.hex('#45bfb9')('│ │')} +${chalk.hex('#45bfb9')('│')} ◠ ◡ ◠ +${chalk.hex('#45bfb9')('|_ __|')} +${chalk.hex('#45bfb9')(' |/ ')} +`) */ + try { program.parse(process.argv) + konsola.br() // Add a line break } catch (error) { handleError(error as Error) } - diff --git a/src/program.test.ts b/src/program.test.ts index 7b3afeb2..803c388d 100644 --- a/src/program.test.ts +++ b/src/program.test.ts @@ -1,10 +1,11 @@ // program.test.ts -import { beforeAll, describe, expect, it, vi } from 'vitest' +import { beforeAll, describe, expect, it } from 'vitest' // Import the function after setting up mocks import { getProgram } from './program' // Import resolve to mock +import type { Command } from 'commander' -let program +let program: Command describe('program', () => { beforeAll(() => { program = getProgram() diff --git a/src/program.ts b/src/program.ts index 5a493043..22bdbb11 100644 --- a/src/program.ts +++ b/src/program.ts @@ -5,7 +5,7 @@ import { resolve } from 'pathe' import { __dirname, handleError } from './utils' // Read package.json for metadata -const packageJsonPath = resolve(__dirname, '../../package.json') +const packageJsonPath = resolve(__dirname, process.env.VITEST || process.env.STUB ? '../../package.json' : '../package.json') const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) // Declare a variable to hold the singleton instance diff --git a/src/session.test.ts b/src/session.test.ts new file mode 100644 index 00000000..5dbb0839 --- /dev/null +++ b/src/session.test.ts @@ -0,0 +1,121 @@ +// session.test.ts +import { session } from './session' + +import { getCredentialsForMachine } from './creds' +import type { Mock } from 'vitest' + +vi.mock('./creds', () => ({ + getNetrcCredentials: vi.fn(), + getCredentialsForMachine: vi.fn(), +})) + +const mockedGetCredentialsForMachine = getCredentialsForMachine as Mock + +describe('session', () => { + beforeEach(() => { + vi.resetAllMocks() + vi.clearAllMocks() + }) + describe('session initialization with netrc', () => { + it('should initialize session with netrc credentials', async () => { + mockedGetCredentialsForMachine.mockReturnValue({ + login: 'test_login', + password: 'test_token', + region: 'test_region', + }) + const userSession = session() + await userSession.initializeSession() + expect(userSession.state.isLoggedIn).toBe(true) + expect(userSession.state.login).toBe('test_login') + expect(userSession.state.password).toBe('test_token') + expect(userSession.state.region).toBe('test_region') + }) + it('should initialize session with netrc credentials for a specific machine', async () => { + mockedGetCredentialsForMachine.mockReturnValue({ + login: 'test_login', + password: 'test_token', + region: 'test_region', + }) + const userSession = session() + await userSession.initializeSession('test-machine') + expect(userSession.state.isLoggedIn).toBe(true) + expect(userSession.state.login).toBe('test_login') + expect(userSession.state.password).toBe('test_token') + expect(userSession.state.region).toBe('test_region') + }) + + it('should initialize session with netrc credentials for a specific machine when no matching machine is present', async () => { + mockedGetCredentialsForMachine.mockReturnValue(undefined) + const userSession = session() + await userSession.initializeSession('nonexistent-machine') + expect(userSession.state.isLoggedIn).toBe(false) + expect(userSession.state.login).toBe(undefined) + expect(userSession.state.password).toBe(undefined) + expect(userSession.state.region).toBe(undefined) + }) + /* + it('should initialize session with netrc credentials for a specific machine', async () => { + const userSession = session() + await userSession.initializeSession('test-machine') + expect(userSession.state.isLoggedIn).toBe(true) + expect(userSession.state.login).toBe('test_login') + expect(userSession.state.password).toBe('test_token') + expect(userSession.state.region).toBe('test_region') + }) + + it('should initialize session with netrc credentials for a specific machine when multiple machines are present', async () => { + const userSession = session() + await userSession.initializeSession('test-machine-2') + expect(userSession.state.isLoggedIn).toBe(true) + expect(userSession.state.login).toBe('test_login_2') + expect(userSession.state.password).toBe('test_token_2') + expect(userSession.state.region).toBe('test_region_2') + }) + + it('should initialize session with netrc credentials for a specific machine when no matching machine is present', async () => { + const userSession = session() + await userSession.initializeSession('nonexistent-machine') + expect(userSession.state.isLoggedIn).toBe(false) + expect(userSession.state.login).toBe(undefined) + expect(userSession.state.password).toBe(undefined) + expect(userSession.state.region).toBe(undefined) + }) */ + }) + describe('session initialization with environment variables', () => { + beforeEach(() => { + // Clear environment variables before each test + delete process.env.STORYBLOK_LOGIN + delete process.env.STORYBLOK_TOKEN + delete process.env.STORYBLOK_REGION + delete process.env.TRAVIS_STORYBLOK_LOGIN + delete process.env.TRAVIS_STORYBLOK_TOKEN + delete process.env.TRAVIS_STORYBLOK_REGION + }) + + it('should initialize session from STORYBLOK_ environment variables', async () => { + process.env.STORYBLOK_LOGIN = 'test_login' + process.env.STORYBLOK_TOKEN = 'test_token' + process.env.STORYBLOK_REGION = 'test_region' + + const userSession = session() + await userSession.initializeSession() + expect(userSession.state.isLoggedIn).toBe(true) + expect(userSession.state.login).toBe('test_login') + expect(userSession.state.password).toBe('test_token') + expect(userSession.state.region).toBe('test_region') + }) + + it('should initialize session from TRAVIS_STORYBLOK_ environment variables', async () => { + process.env.TRAVIS_STORYBLOK_LOGIN = 'test_login' + process.env.TRAVIS_STORYBLOK_TOKEN = 'test_token' + process.env.TRAVIS_STORYBLOK_REGION = 'test_region' + + const userSession = session() + await userSession.initializeSession() + expect(userSession.state.isLoggedIn).toBe(true) + expect(userSession.state.login).toBe('test_login') + expect(userSession.state.password).toBe('test_token') + expect(userSession.state.region).toBe('test_region') + }) + }) +}) diff --git a/src/session.ts b/src/session.ts new file mode 100644 index 00000000..d370e137 --- /dev/null +++ b/src/session.ts @@ -0,0 +1,108 @@ +// session.ts +import type { RegionCode } from './constants' +import { addNetrcEntry, getCredentialsForMachine, getNetrcCredentials } from './creds' + +interface SessionState { + isLoggedIn: boolean + login?: string + password?: string + region?: string + envLogin?: boolean +} + +let sessionInstance: ReturnType | null = null + +function createSession() { + const state: SessionState = { + isLoggedIn: false, + } + + async function initializeSession(machineName?: string) { + // First, check for environment variables + const envCredentials = getEnvCredentials() + if (envCredentials) { + state.isLoggedIn = true + state.login = envCredentials.login + state.password = envCredentials.password + state.region = envCredentials.region + state.envLogin = true + return + } + + // If no environment variables, fall back to netrc + const machines = await getNetrcCredentials() + const creds = getCredentialsForMachine(machines, machineName) + if (creds) { + state.isLoggedIn = true + state.login = creds.login + state.password = creds.password + state.region = creds.region + } + else { + // No credentials found; set state to logged out + state.isLoggedIn = false + state.login = undefined + state.password = undefined + state.region = undefined + } + state.envLogin = false + } + + function getEnvCredentials() { + const envLogin = process.env.STORYBLOK_LOGIN || process.env.TRAVIS_STORYBLOK_LOGIN + const envPassword = process.env.STORYBLOK_TOKEN || process.env.TRAVIS_STORYBLOK_TOKEN + const envRegion = process.env.STORYBLOK_REGION || process.env.TRAVIS_STORYBLOK_REGION + + if (envLogin && envPassword && envRegion) { + return { + login: envLogin, + password: envPassword, + region: envRegion, + } + } + return null + } + + async function persistCredentials(machineName: string) { + if (state.isLoggedIn && state.login && state.password && state.region) { + await addNetrcEntry({ + machineName, + login: state.login, + password: state.password, + region: state.region, + }) + } + else { + throw new Error('No credentials to save.') + } + } + + function updateSession(login: string, password: string, region: RegionCode) { + state.isLoggedIn = true + state.login = login + state.password = password + state.region = region + } + + function logout() { + state.isLoggedIn = false + state.login = undefined + state.password = undefined + state.region = undefined + } + + return { + state, + initializeSession, + updateSession, + persistCredentials, + logout, + } +} + +export function session() { + if (!sessionInstance) { + sessionInstance = createSession() + } + return sessionInstance +} diff --git a/src/utils/error.ts b/src/utils/error.ts deleted file mode 100644 index a0afffba..00000000 --- a/src/utils/error.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { konsola } from '../utils'; - -export function handleError(error: Error): void { - konsola.error(error) - // TODO: add conditional to detect if this runs on tests - /* process.exit(1); */ -} \ No newline at end of file diff --git a/src/utils/error/api-error.ts b/src/utils/error/api-error.ts new file mode 100644 index 00000000..dda384f0 --- /dev/null +++ b/src/utils/error/api-error.ts @@ -0,0 +1,64 @@ +import { FetchError } from 'ofetch' + +export const API_ACTIONS = { + login: 'login', + login_with_token: 'Failed to log in with token', + login_with_otp: 'Failed to log in with email, password and otp', + login_email_password: 'Failed to log in with email and password', +} as const + +export const API_ERRORS = { + unauthorized: 'The user is not authorized to access the API', + network_error: 'No response from server, please check if you are correctly connected to internet', + invalid_credentials: 'The provided credentials are invalid', + timeout: 'The API request timed out', + generic: 'Error logging in', +} as const + +export function handleAPIError(action: keyof typeof API_ACTIONS, error: Error): void { + if (error instanceof FetchError) { + const status = error.response?.status + + switch (status) { + case 401: + throw new APIError('unauthorized', action, error) + case 422: + throw new APIError('invalid_credentials', action, error) + default: + throw new APIError('network_error', action, error) + } + } + else if (error.message.includes('NetworkError') || error.message.includes('Failed to fetch')) { + throw new APIError('network_error', action, error) + } + throw new APIError('generic', action, error) +} + +export class APIError extends Error { + errorId: string + cause: string + code: number + messageStack: string[] + error: FetchError | undefined + + constructor(errorId: keyof typeof API_ERRORS, action: keyof typeof API_ACTIONS, error?: FetchError, customMessage?: string) { + super(customMessage || API_ERRORS[errorId]) + this.name = 'API Error' + this.errorId = errorId + this.cause = API_ERRORS[errorId] + this.code = error?.response?.status || 0 + this.messageStack = [API_ACTIONS[action], customMessage || API_ERRORS[errorId]] + this.error = error + } + + getInfo() { + return { + name: this.name, + message: this.message, + httpCode: this.code, + cause: this.cause, + errorId: this.errorId, + stack: this.stack, + } + } +} diff --git a/src/utils/error/command-error.ts b/src/utils/error/command-error.ts new file mode 100644 index 00000000..1b44c3de --- /dev/null +++ b/src/utils/error/command-error.ts @@ -0,0 +1,14 @@ +export class CommandError extends Error { + constructor(message: string) { + super(message) + this.name = 'Command Error' + } + + getInfo() { + return { + name: this.name, + message: this.message, + stack: this.stack, + } + } +} diff --git a/src/utils/error.test.ts b/src/utils/error/error.test.ts similarity index 68% rename from src/utils/error.test.ts rename to src/utils/error/error.test.ts index 83df6c40..f1d6fcf9 100644 --- a/src/utils/error.test.ts +++ b/src/utils/error/error.test.ts @@ -1,5 +1,5 @@ -import { handleError } from "./error" -import { beforeAll, describe, expect, it, vi } from 'vitest' +import { handleError } from './error' +import { describe, expect, it, vi } from 'vitest' describe('error handling', () => { it('should prompt an error message', () => { @@ -7,4 +7,4 @@ describe('error handling', () => { handleError(new Error('This is an error')) expect(consoleSpy).toBeCalled() }) -}) \ No newline at end of file +}) diff --git a/src/utils/error/error.ts b/src/utils/error/error.ts new file mode 100644 index 00000000..93f22594 --- /dev/null +++ b/src/utils/error/error.ts @@ -0,0 +1,46 @@ +import { konsola } from '..' +import { APIError } from './api-error' +import { CommandError } from './command-error' +import { FileSystemError } from './filesystem-error' + +export function handleError(error: Error, verbose = false): void { + // Print the message stack if it exists + if ((error as any).messageStack) { + const messageStack = (error as any).messageStack + messageStack.forEach((message: string, index: number) => { + konsola.error(message, null, { + header: index === 0, + margin: false, + }) + }) + } + else { + konsola.error(error.message, null, { + header: true, + }) + } + if (verbose && typeof (error as any).getInfo === 'function') { + const errorDetails = (error as any).getInfo() + if (error instanceof CommandError) { + konsola.error(`Command Error: ${error.getInfo().message}`, errorDetails) + } + else if (error instanceof APIError) { + konsola.error(`API Error: ${error.getInfo().cause}`, errorDetails) + } + else if (error instanceof FileSystemError) { + konsola.error(`File System Error: ${error.getInfo().cause}`, errorDetails) + } + else { + konsola.error(`Unexpected Error: ${error.message}`, errorDetails) + } + } + else { + konsola.br() + konsola.info('For more information about the error, run the command with the `--verbose` flag') + } + + if (!process.env.VITEST) { + console.log('') // Add a line break for readability + process.exit(1) // Exit process if not in a test environment + } +} diff --git a/src/utils/error/filesystem-error.ts b/src/utils/error/filesystem-error.ts new file mode 100644 index 00000000..ab42bad1 --- /dev/null +++ b/src/utils/error/filesystem-error.ts @@ -0,0 +1,82 @@ +const FS_ERRORS = { + file_not_found: 'The file requested was not found', + permission_denied: 'Permission denied while accessing the file', + operation_on_directory: 'The operation is not allowed on a directory', + not_a_directory: 'The path provided is not a directory', + file_already_exists: 'The file already exists', + directory_not_empty: 'The directory is not empty', + too_many_open_files: 'Too many open files', + no_space_left: 'No space left on the device', + invalid_argument: 'An invalid argument was provided', + unknown_error: 'An unknown error occurred', +} + +const FS_ACTIONS = { + read: 'Failed to read/parse the .netrc file:', + write: 'Writing file', + delete: 'Deleting file', + mkdir: 'Creating directory', + rmdir: 'Removing directory', + authorization_check: 'Failed to check authorization in .netrc file:', +} + +export function handleFileSystemError(action: keyof typeof FS_ACTIONS, error: NodeJS.ErrnoException): void { + if (error.code) { + switch (error.code) { + case 'ENOENT': + throw new FileSystemError('file_not_found', action, error) + case 'EACCES': + case 'EPERM': + throw new FileSystemError('permission_denied', action, error) + case 'EISDIR': + throw new FileSystemError('operation_on_directory', action, error) + case 'ENOTDIR': + throw new FileSystemError('not_a_directory', action, error) + case 'EEXIST': + throw new FileSystemError('file_already_exists', action, error) + case 'ENOTEMPTY': + throw new FileSystemError('directory_not_empty', action, error) + case 'EMFILE': + throw new FileSystemError('too_many_open_files', action, error) + case 'ENOSPC': + throw new FileSystemError('no_space_left', action, error) + case 'EINVAL': + throw new FileSystemError('invalid_argument', action, error) + default: + throw new FileSystemError('unknown_error', action, error) + } + } + else { + // In case the error does not have a known `fs` error code, throw a general error + throw new FileSystemError('unknown_error', action, error) + } +} + +export class FileSystemError extends Error { + errorId: string + cause: string + code: string | undefined + messageStack: string[] + error: NodeJS.ErrnoException | undefined + + constructor(errorId: keyof typeof FS_ERRORS, action: keyof typeof FS_ACTIONS, error: NodeJS.ErrnoException, customMessage?: string) { + super(customMessage || FS_ERRORS[errorId]) + this.name = 'File System Error' + this.errorId = errorId + this.cause = FS_ERRORS[errorId] + this.code = error.code + this.messageStack = [FS_ACTIONS[action], customMessage || FS_ERRORS[errorId]] + this.error = error + } + + getInfo() { + return { + name: this.name, + message: this.message, + code: this.code, + cause: this.cause, + errorId: this.errorId, + stack: this.stack, + } + } +} diff --git a/src/utils/error/index.ts b/src/utils/error/index.ts new file mode 100644 index 00000000..61bc96df --- /dev/null +++ b/src/utils/error/index.ts @@ -0,0 +1,4 @@ +export * from './api-error' +export * from './command-error' +export * from './error' +export * from './filesystem-error' diff --git a/src/utils/index.ts b/src/utils/index.ts index 0c466e3b..f601a669 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,7 +1,24 @@ -import { fileURLToPath } from 'node:url'; -import { dirname } from 'pathe'; +import { fileURLToPath } from 'node:url' +import { dirname } from 'pathe' +import type { RegionCode } from '../constants' +import { regions } from '../constants' -export * from './error'; +export * from './error/' export * from './konsola' -export const __filename = fileURLToPath(import.meta.url); -export const __dirname = dirname(__filename); \ No newline at end of file +export const __filename = fileURLToPath(import.meta.url) +export const __dirname = dirname(__filename) + +export function isRegion(value: RegionCode): value is RegionCode { + return Object.values(regions).includes(value) +} + +export function maskToken(token: string): string { + // Show only the first 4 characters and replace the rest with asterisks + if (token.length <= 4) { + // If the token is too short, just return it as is + return token + } + const visiblePart = token.slice(0, 4) + const maskedPart = '*'.repeat(token.length - 4) + return `${visiblePart}${maskedPart}` +} diff --git a/src/utils/konsola.test.ts b/src/utils/konsola.test.ts index 02e4b3c0..771eb268 100644 --- a/src/utils/konsola.test.ts +++ b/src/utils/konsola.test.ts @@ -1,21 +1,46 @@ -import chalk from "chalk" -import { konsola, formatHeader } from "./konsola" -import { beforeAll, describe, expect, it, vi } from 'vitest' - +import chalk from 'chalk' +import { formatHeader, konsola } from './konsola' +import { describe, expect, it, vi } from 'vitest' describe('konsola', () => { + describe('title', () => { + it('should prompt a title message', () => { + const consoleSpy = vi.spyOn(console, 'log') + konsola.title('This is a test title', '#45bfb9') + + expect(consoleSpy).toHaveBeenCalledWith(formatHeader(chalk.bgHex('#45bfb9').bold.white(` This is a test title `))) + }) + }) + describe('warn', () => { + it('should prompt a warning message', () => { + const consoleSpy = vi.spyOn(console, 'warn') + konsola.warn('This is a test warning message') + + expect(consoleSpy).toHaveBeenCalledWith(`${chalk.yellow('⚠️')} This is a test warning message`) + }) + + it('should prompt a warning message with header', () => { + const consoleSpy = vi.spyOn(console, 'warn') + konsola.warn('This is a test warning message', true) + const warnText = chalk.bgYellow.bold.black(` Warning `) + + expect(consoleSpy).toHaveBeenCalledWith(formatHeader(warnText, + )) + }) + }) + describe('success', () => { it('should prompt an success message', () => { const consoleSpy = vi.spyOn(console, 'log') - konsola.ok('Component A created succesfully') + konsola.ok('Component A created succesfully') expect(consoleSpy).toHaveBeenCalledWith(`${chalk.green('✔')} Component A created succesfully`) }) - + it('should prompt an success message with header', () => { const consoleSpy = vi.spyOn(console, 'log') konsola.ok('Component A created succesfully', true) - const successText = chalk.bgGreen.bold.white(` Success `) + const successText = chalk.bgGreen.bold.white(` Success `) expect(consoleSpy).toHaveBeenCalledWith(formatHeader(successText)) }) @@ -24,16 +49,23 @@ describe('konsola', () => { it('should prompt an error message', () => { const consoleSpy = vi.spyOn(console, 'error') - konsola.error(new Error('Oh gosh, this is embarrasing')) - expect(consoleSpy).toHaveBeenCalledWith(chalk.red(`Oh gosh, this is embarrasing`)) + konsola.error('Oh gosh, this is embarrasing') + const errorText = `${chalk.red.bold('▲ error')} Oh gosh, this is embarrasing` + expect(consoleSpy).toHaveBeenCalledWith(errorText, '') }) - + it('should prompt an error message with header', () => { const consoleSpy = vi.spyOn(console, 'error') - konsola.error(new Error('Oh gosh, this is embarrasing'), true) - const errorText = chalk.bgRed.bold.white(` Error `) - + konsola.error('Oh gosh, this is embarrasing', null, { header: true }) + const errorText = chalk.bgRed.bold.white(` Error `) + expect(consoleSpy).toHaveBeenCalledWith(formatHeader(errorText)) }) + + it('should add a line break if margin set to true ', () => { + const consoleSpy = vi.spyOn(console, 'error') + konsola.error('Oh gosh, this is embarrasing', null, { margin: true }) + expect(consoleSpy).toHaveBeenCalledWith('') + }) }) -}) \ No newline at end of file +}) diff --git a/src/utils/konsola.ts b/src/utils/konsola.ts index 165f5506..5ddb8102 100644 --- a/src/utils/konsola.ts +++ b/src/utils/konsola.ts @@ -1,26 +1,72 @@ import chalk from 'chalk' -export function formatHeader(title:string) { - return `${title} - - ` +export interface KonsolaFormatOptions { + header?: boolean + margin?: boolean +} + +export function formatHeader(title: string) { + return `${title}` } export const konsola = { - + title: (message: string, color: string, subtitle?: string) => { + console.log('') // Add a line break + console.log('') // Add a line break + if (subtitle) { + console.log(`${formatHeader(chalk.bgHex(color).bold.white(` ${message} `))} ${subtitle}`) + } + else { + console.log(formatHeader(chalk.bgHex(color).bold.white(` ${message} `))) + } + console.log('') // Add a line break + console.log('') // Add a line break + }, + br: () => { + console.log('') // Add a line break + }, ok: (message?: string, header: boolean = false) => { - if(header) { - const successHeader = chalk.bgGreen.bold.white(` Success `) + if (header) { + console.log('') // Add a line break + const successHeader = chalk.bgGreen.bold.white(` Success `) console.log(formatHeader(successHeader)) } console.log(message ? `${chalk.green('✔')} ${message}` : '') }, - error: (err: Error, header: boolean = false) => { - if(header) { - const errorHeader = chalk.bgRed.bold.white(` Error `) + info: (message: string, options: KonsolaFormatOptions = { + header: false, + margin: true, + }) => { + if (options.header) { + console.log('') // Add a line break + const infoHeader = chalk.bgBlue.bold.white(` Info `) + console.log(formatHeader(infoHeader)) + } + + console.log(message ? `${chalk.blue('ℹ')} ${message}` : '') + if (options.margin) { + console.error('') // Add a line break + } + }, + warn: (message?: string, header: boolean = false) => { + if (header) { + console.log('') // Add a line break + const warnHeader = chalk.bgYellow.bold.black(` Warning `) + console.warn(formatHeader(warnHeader)) + } + + console.warn(message ? `${chalk.yellow('⚠️')} ${message}` : '') + }, + error: (message: string, info?: unknown, options?: KonsolaFormatOptions) => { + if (options?.header) { + const errorHeader = chalk.bgRed.bold.white(` Error `) console.error(formatHeader(errorHeader)) + console.log('') // Add a line break } - console.error(chalk.red(err.message || err)); - } -} \ No newline at end of file + console.error(`${chalk.red.bold('▲ error')} ${message}`, info || '') + if (options?.margin) { + console.error('') // Add a line break + } + }, +} diff --git a/tsconfig.json b/tsconfig.json index 40b6b55a..594e7bc2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,27 +2,23 @@ "compilerOptions": { "target": "esnext", "lib": ["esnext", "DOM", "DOM.Iterable"], - "useDefineForClassFields": true, "baseUrl": ".", "module": "esnext", - - /* Bundler mode */ "moduleResolution": "bundler", "resolveJsonModule": true, - "types": [ "node", "vitest/globals"], + "types": ["node", "vitest/globals"], "allowImportingTsExtensions": true, - /* Linting */ "strict": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, "noEmit": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, "isolatedModules": true, "skipLibCheck": true - }, - "references": [{ "path": "./tsconfig.node.json" }], "include": ["src"], - "exclude": ["node_modules", "src/**/*.cy.ts", "src/**/*.test.ts"] + "exclude": ["node_modules"] } diff --git a/tsconfig.node.json b/tsconfig.node.json deleted file mode 100644 index a1f2e8b7..00000000 --- a/tsconfig.node.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "baseUrl": ".", - "module": "ESNext", - "moduleResolution": "Node", - "types": ["vitest/globals"], - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts", "src/**/*.test.ts"] -} diff --git a/vitest.config.ts b/vitest.config.ts index 6aedff33..55107ecb 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,6 +3,10 @@ import { defineConfig } from 'vite' export default defineConfig({ test: { + globals: true, // ... Specify options here. + coverage: { + reporter: ['text', 'json', 'html'], + }, }, })