diff --git a/package.json b/package.json index 107e56a..75912b5 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "sonar": "tsx src/cli.ts", "build": "tsc", "typecheck": "tsc --noEmit", - "prepublishOnly": "tsc" + "prepublishOnly": "tsc", + "test": "vitest run" }, "dependencies": { "better-sqlite3": "^11", @@ -45,6 +46,7 @@ "biome": "^0.3.3", "ink-link": "^5.0.0", "tsx": "^4", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.1.4" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cefeb3e..20b19d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: typescript: specifier: ^5 version: 5.9.3 + vitest: + specifier: ^4.1.4 + version: 4.1.4(@types/node@22.19.11)(vite@8.0.8(@types/node@22.19.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) packages: @@ -347,6 +350,15 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@envelop/core@5.5.1': resolution: {integrity: sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==} engines: {node: '>=18.0.0'} @@ -830,6 +842,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@napi-rs/wasm-runtime@1.1.3': + resolution: {integrity: sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -842,18 +860,134 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-project/types@0.124.0': + resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@repeaterjs/repeater@3.0.6': resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} + '@rolldown/binding-android-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.15': + resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@theguild/federation-composition@0.21.3': resolution: {integrity: sha512-+LlHTa4UbRpZBog3ggAxjYIFvdfH3UMvvBUptur19TMWkqU4+n3GmN+mDjejU+dyBXIG27c25RsiQP1HyvM99g==} engines: {node: '>=18'} peerDependencies: graphql: ^16.0.0 + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -869,6 +1003,35 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@vitest/expect@4.1.4': + resolution: {integrity: sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==} + + '@vitest/mocker@4.1.4': + resolution: {integrity: sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.4': + resolution: {integrity: sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==} + + '@vitest/runner@4.1.4': + resolution: {integrity: sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==} + + '@vitest/snapshot@4.1.4': + resolution: {integrity: sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==} + + '@vitest/spy@4.1.4': + resolution: {integrity: sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==} + + '@vitest/utils@4.1.4': + resolution: {integrity: sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==} + '@whatwg-node/disposablestack@0.0.6': resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} engines: {node: '>=18.0.0'} @@ -952,6 +1115,10 @@ packages: resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} engines: {node: '>=0.8'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -1057,6 +1224,10 @@ packages: caseless@0.12.0: resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@1.1.3: resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==} engines: {node: '>=0.10.0'} @@ -1315,6 +1486,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} + es-toolkit@1.44.0: resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} @@ -1335,6 +1509,9 @@ packages: resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} engines: {node: '>=8'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + exit-hook@1.1.1: resolution: {integrity: sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg==} engines: {node: '>=0.10.0'} @@ -1343,6 +1520,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -1372,6 +1553,15 @@ packages: fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -1778,6 +1968,80 @@ packages: klaw@1.3.1: resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -1827,6 +2091,9 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + map-cache@0.2.2: resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} engines: {node: '>=0.10.0'} @@ -1889,6 +2156,11 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} @@ -1949,6 +2221,9 @@ packages: resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} engines: {node: '>= 6'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -2045,6 +2320,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} @@ -2055,10 +2333,18 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + plur@5.1.0: resolution: {integrity: sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + postcss@8.5.9: + resolution: {integrity: sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==} + engines: {node: ^10 || ^12 || >=14} + prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} @@ -2181,6 +2467,11 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rolldown@1.0.0-rc.15: + resolution: {integrity: sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + run-async@0.1.0: resolution: {integrity: sha512-qOX+w+IxFgpUpJfkv2oGN0+ExPs68F4sZHfaRRx4dDexAQkG83atugKVEylyT5ARees3HBbfmuvnjbrd8j9Wjw==} @@ -2231,6 +2522,9 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -2266,6 +2560,10 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -2290,6 +2588,12 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} + string-env-interpolation@1.0.1: resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} @@ -2388,6 +2692,21 @@ packages: resolution: {integrity: sha512-YBGpG4bWsHoPvofT6y/5iqulfXIiIErl5B0LdtHT1mGXDFTAhhRrbUpTvBgYbovr+3cKblya2WAOcpoy90XguA==} engines: {node: '>=16'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + title-case@3.0.3: resolution: {integrity: sha512-e1zGYRvbffpcHIrnuqT0Dh+gEJtDaxDSoG4JAIpq4oDFyooziLBIiYQv0GBT4FUAnUop5uZ1hiIAj7oAF6sOCA==} @@ -2502,6 +2821,90 @@ packages: resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} + vite@8.0.8: + resolution: {integrity: sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.4: + resolution: {integrity: sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.4 + '@vitest/browser-preview': 4.1.4 + '@vitest/browser-webdriverio': 4.1.4 + '@vitest/coverage-istanbul': 4.1.4 + '@vitest/coverage-v8': 4.1.4 + '@vitest/ui': 4.1.4 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -2522,6 +2925,11 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + widest-line@6.0.0: resolution: {integrity: sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA==} engines: {node: '>=20'} @@ -2974,6 +3382,22 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + '@envelop/core@5.5.1': dependencies: '@envelop/instrumentation': 1.0.0 @@ -3622,6 +4046,13 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -3634,8 +4065,63 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 + '@oxc-project/types@0.124.0': {} + '@repeaterjs/repeater@3.0.6': {} + '@rolldown/binding-android-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.15': {} + + '@standard-schema/spec@1.1.0': {} + '@theguild/federation-composition@0.21.3(graphql@16.12.0)': dependencies: constant-case: 3.0.4 @@ -3646,10 +4132,24 @@ snapshots: transitivePeerDependencies: - supports-color + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + '@types/better-sqlite3@7.6.13': dependencies: '@types/node': 22.19.11 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + '@types/js-yaml@4.0.9': {} '@types/node@22.19.11': @@ -3666,6 +4166,47 @@ snapshots: dependencies: '@types/node': 22.19.11 + '@vitest/expect@4.1.4': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@22.19.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.8(@types/node@22.19.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + + '@vitest/pretty-format@4.1.4': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.4': + dependencies: + '@vitest/utils': 4.1.4 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + '@vitest/utils': 4.1.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.4': {} + + '@vitest/utils@4.1.4': + dependencies: + '@vitest/pretty-format': 4.1.4 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + '@whatwg-node/disposablestack@0.0.6': dependencies: '@whatwg-node/promise-helpers': 1.3.2 @@ -3739,6 +4280,8 @@ snapshots: assert-plus@1.0.0: {} + assertion-error@2.0.1: {} + astral-regex@2.0.0: {} asynckit@0.4.0: {} @@ -3876,6 +4419,8 @@ snapshots: caseless@0.12.0: {} + chai@6.2.2: {} + chalk@1.1.3: dependencies: ansi-styles: 2.2.1 @@ -4115,6 +4660,8 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-module-lexer@2.0.0: {} + es-toolkit@1.44.0: {} esbuild@0.27.3: @@ -4152,10 +4699,16 @@ snapshots: escape-string-regexp@2.0.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + exit-hook@1.1.1: {} expand-template@2.0.3: {} + expect-type@1.3.0: {} + extend@3.0.2: {} extsprintf@1.3.0: {} @@ -4194,6 +4747,10 @@ snapshots: transitivePeerDependencies: - encoding + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -4625,6 +5182,55 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lines-and-columns@1.2.4: {} listr2@4.0.5: @@ -4678,6 +5284,10 @@ snapshots: dependencies: yallist: 3.1.1 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + map-cache@0.2.2: {} merge2@1.4.1: {} @@ -4725,6 +5335,8 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nanoid@3.3.11: {} + napi-build-utils@2.0.0: {} no-case@3.0.4: @@ -4772,6 +5384,8 @@ snapshots: object-hash@2.2.0: {} + obug@2.1.1: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -4878,16 +5492,26 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + performance-now@2.1.0: {} picocolors@1.1.1: {} picomatch@2.3.1: {} + picomatch@4.0.4: {} + plur@5.1.0: dependencies: irregular-plurals: 3.5.0 + postcss@8.5.9: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prebuild-install@7.1.3: dependencies: detect-libc: 2.1.2 @@ -5040,6 +5664,27 @@ snapshots: dependencies: glob: 7.2.3 + rolldown@1.0.0-rc.15: + dependencies: + '@oxc-project/types': 0.124.0 + '@rolldown/pluginutils': 1.0.0-rc.15 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.15 + '@rolldown/binding-darwin-x64': 1.0.0-rc.15 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.15 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.15 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.15 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.15 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.15 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.15 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 + run-async@0.1.0: dependencies: once: 1.4.0 @@ -5080,6 +5725,8 @@ snapshots: shell-quote@1.8.3: {} + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signedsource@1.0.0: {} @@ -5121,6 +5768,8 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 + source-map-js@1.2.1: {} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -5155,6 +5804,10 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + + std-env@4.0.0: {} + string-env-interpolation@1.0.1: {} string-width@1.0.2: @@ -5263,6 +5916,17 @@ snapshots: timeout-signal@2.0.0: {} + tinybench@2.9.0: {} + + tinyexec@1.1.1: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + title-case@3.0.3: dependencies: tslib: 2.8.1 @@ -5362,6 +6026,48 @@ snapshots: core-util-is: 1.0.2 extsprintf: 1.3.0 + vite@8.0.8(@types/node@22.19.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.9 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 22.19.11 + esbuild: 0.27.3 + fsevents: 2.3.3 + jiti: 2.6.1 + tsx: 4.21.0 + yaml: 2.8.2 + + vitest@4.1.4(@types/node@22.19.11)(vite@8.0.8(@types/node@22.19.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@22.19.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.8(@types/node@22.19.11)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.11 + transitivePeerDependencies: + - msw + wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -5379,6 +6085,11 @@ snapshots: which-module@2.0.1: {} + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + widest-line@6.0.0: dependencies: string-width: 8.2.0 diff --git a/src/lib/__tests__/client.test.ts b/src/lib/__tests__/client.test.ts new file mode 100644 index 0000000..59e0c26 --- /dev/null +++ b/src/lib/__tests__/client.test.ts @@ -0,0 +1,211 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { + gql, + isTransientError, + computeBackoff, + HttpError, + GraphQLError, + type RetryOptions, +} from '../client.js' + +// Stub config so gql() doesn't read real files +vi.mock('../config.js', () => ({ + getToken: () => 'test-token', + getApiUrl: () => 'https://api.test/graphql', +})) + +// ─── isTransientError ────────────────────────────────────────── + +describe('isTransientError', () => { + it('returns true for TypeError (network failure)', () => { + expect(isTransientError(new TypeError('fetch failed'))).toBe(true) + }) + + it('returns true for 5xx HttpError', () => { + expect(isTransientError(new HttpError(500, 'Internal Server Error'))).toBe(true) + expect(isTransientError(new HttpError(502, 'Bad Gateway'))).toBe(true) + expect(isTransientError(new HttpError(503, 'Service Unavailable'))).toBe(true) + }) + + it('returns true for 429 HttpError', () => { + expect(isTransientError(new HttpError(429, 'Too Many Requests'))).toBe(true) + }) + + it('returns false for 4xx HttpError (non-429)', () => { + expect(isTransientError(new HttpError(400, 'Bad Request'))).toBe(false) + expect(isTransientError(new HttpError(401, 'Unauthorized'))).toBe(false) + expect(isTransientError(new HttpError(403, 'Forbidden'))).toBe(false) + expect(isTransientError(new HttpError(404, 'Not Found'))).toBe(false) + }) + + it('returns false for GraphQLError', () => { + expect(isTransientError(new GraphQLError('Field not found'))).toBe(false) + }) + + it('returns false for generic Error', () => { + expect(isTransientError(new Error('random'))).toBe(false) + }) + + it('returns false for non-error values', () => { + expect(isTransientError(null)).toBe(false) + expect(isTransientError('string')).toBe(false) + }) +}) + +// ─── computeBackoff ──────────────────────────────────────────── + +describe('computeBackoff', () => { + const opts: RetryOptions = { + maxRetries: 3, + baseDelayMs: 100, + maxDelayMs: 5000, + } + + it('increases exponentially with attempt number', () => { + // Seed Math.random to get deterministic results + vi.spyOn(Math, 'random').mockReturnValue(0.5) // jitter factor = 1.0 + + const d0 = computeBackoff(0, opts) // 100 * 1 * 1.0 = 100 + const d1 = computeBackoff(1, opts) // 100 * 2 * 1.0 = 200 + const d2 = computeBackoff(2, opts) // 100 * 4 * 1.0 = 400 + + expect(d0).toBe(100) + expect(d1).toBe(200) + expect(d2).toBe(400) + + vi.restoreAllMocks() + }) + + it('caps delay at maxDelayMs', () => { + vi.spyOn(Math, 'random').mockReturnValue(0.5) + + // attempt 10: 100 * 1024 = 102400, capped to 5000 + const d = computeBackoff(10, opts) + expect(d).toBe(5000) + + vi.restoreAllMocks() + }) + + it('applies jitter between 0.5x and 1.5x', () => { + vi.spyOn(Math, 'random').mockReturnValue(0) // jitter = 0.5 + expect(computeBackoff(0, opts)).toBe(50) + + vi.spyOn(Math, 'random').mockReturnValue(1) // jitter = 1.5 + expect(computeBackoff(0, opts)).toBe(150) + + vi.restoreAllMocks() + }) +}) + +// ─── gql retry behavior ─────────────────────────────────────── + +describe('gql', () => { + const fastRetry: RetryOptions = { + maxRetries: 2, + baseDelayMs: 1, + maxDelayMs: 10, + } + + let fetchSpy: ReturnType + + beforeEach(() => { + fetchSpy = vi.fn() + vi.stubGlobal('fetch', fetchSpy) + vi.useFakeTimers({ shouldAdvanceTime: true }) + }) + + afterEach(() => { + vi.restoreAllMocks() + vi.useRealTimers() + }) + + function jsonResponse(data: unknown, status = 200) { + return new Response(JSON.stringify({ data }), { + status, + headers: { 'Content-Type': 'application/json' }, + }) + } + + function errorResponse(status: number, statusText: string) { + return new Response(JSON.stringify({ error: statusText }), { + status, + statusText, + headers: { 'Content-Type': 'application/json' }, + }) + } + + function graphqlErrorResponse(message: string) { + return new Response( + JSON.stringify({ errors: [{ message }] }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ) + } + + it('returns data on success', async () => { + fetchSpy.mockResolvedValueOnce(jsonResponse({ me: { id: 1 } })) + + const result = await gql('query { me { id } }', {}, {}, fastRetry) + expect(result).toEqual({ me: { id: 1 } }) + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) + + it('retries on 5xx and succeeds', async () => { + fetchSpy + .mockResolvedValueOnce(errorResponse(502, 'Bad Gateway')) + .mockResolvedValueOnce(jsonResponse({ ok: true })) + + const result = await gql('query { ok }', {}, {}, fastRetry) + expect(result).toEqual({ ok: true }) + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) + + it('retries on network error (TypeError) and succeeds', async () => { + fetchSpy + .mockRejectedValueOnce(new TypeError('fetch failed')) + .mockResolvedValueOnce(jsonResponse({ ok: true })) + + const result = await gql('query { ok }', {}, {}, fastRetry) + expect(result).toEqual({ ok: true }) + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) + + it('does not retry on 4xx errors', async () => { + fetchSpy.mockResolvedValueOnce(errorResponse(401, 'Unauthorized')) + + await expect(gql('query { me }', {}, {}, fastRetry)).rejects.toThrow( + 'HTTP 401: Unauthorized', + ) + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) + + it('does not retry on GraphQL errors', async () => { + fetchSpy.mockResolvedValueOnce(graphqlErrorResponse('Field "foo" not found')) + + await expect(gql('query { foo }', {}, {}, fastRetry)).rejects.toThrow( + 'Field "foo" not found', + ) + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) + + it('throws after exhausting max retries', async () => { + fetchSpy + .mockResolvedValueOnce(errorResponse(500, 'Internal Server Error')) + .mockResolvedValueOnce(errorResponse(500, 'Internal Server Error')) + .mockResolvedValueOnce(errorResponse(500, 'Internal Server Error')) + + await expect(gql('query { fail }', {}, {}, fastRetry)).rejects.toThrow( + 'HTTP 500: Internal Server Error', + ) + // 1 initial + 2 retries = 3 total + expect(fetchSpy).toHaveBeenCalledTimes(3) + }) + + it('respects maxRetries = 0 (no retries)', async () => { + fetchSpy.mockResolvedValueOnce(errorResponse(500, 'Internal Server Error')) + + await expect( + gql('query { x }', {}, {}, { maxRetries: 0, baseDelayMs: 1, maxDelayMs: 10 }), + ).rejects.toThrow('HTTP 500') + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/lib/__tests__/config-migration.test.ts b/src/lib/__tests__/config-migration.test.ts new file mode 100644 index 0000000..a32ede1 --- /dev/null +++ b/src/lib/__tests__/config-migration.test.ts @@ -0,0 +1,141 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { migrateConfig, readAccountsConfig } from '../config.js' +import type { AccountsConfig } from '../config.js' + +function makeTmpDir(): string { + const dir = join(tmpdir(), `sonar-test-${Date.now()}-${Math.random().toString(36).slice(2)}`) + mkdirSync(dir, { recursive: true }) + return dir +} + +describe('migrateConfig', () => { + let tmpDir: string + + beforeEach(() => { + tmpDir = makeTmpDir() + }) + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }) + }) + + it('migrates legacy config.json to accounts.json', () => { + const legacy = { + token: 'tok_abc123', + apiUrl: 'https://custom.api/graphql', + vendor: 'anthropic', + } + writeFileSync(join(tmpDir, 'config.json'), JSON.stringify(legacy)) + + const result = migrateConfig({ configDir: tmpDir }) + expect(result).toBe(true) + + const accounts = JSON.parse( + readFileSync(join(tmpDir, 'accounts.json'), 'utf8'), + ) as AccountsConfig + + expect(accounts.activeAccount).toBe('default') + expect(accounts.accounts).toHaveLength(1) + expect(accounts.accounts[0]).toEqual({ + name: 'default', + token: 'tok_abc123', + apiUrl: 'https://custom.api/graphql', + vendor: 'anthropic', + }) + }) + + it('uses default apiUrl when not specified in legacy config', () => { + writeFileSync( + join(tmpDir, 'config.json'), + JSON.stringify({ token: 'tok_xyz' }), + ) + + migrateConfig({ configDir: tmpDir }) + + const accounts = JSON.parse( + readFileSync(join(tmpDir, 'accounts.json'), 'utf8'), + ) as AccountsConfig + + expect(accounts.accounts[0].apiUrl).toBe('https://api.sonar.8640p.info/graphql') + }) + + it('omits vendor field when not set in legacy config', () => { + writeFileSync( + join(tmpDir, 'config.json'), + JSON.stringify({ token: 'tok_123', apiUrl: 'https://api.test/graphql' }), + ) + + migrateConfig({ configDir: tmpDir }) + + const accounts = JSON.parse( + readFileSync(join(tmpDir, 'accounts.json'), 'utf8'), + ) as AccountsConfig + + expect(accounts.accounts[0]).not.toHaveProperty('vendor') + }) + + it('returns false if accounts.json already exists (no double migration)', () => { + writeFileSync(join(tmpDir, 'config.json'), JSON.stringify({ token: 'tok' })) + writeFileSync(join(tmpDir, 'accounts.json'), JSON.stringify({ activeAccount: 'x', accounts: [] })) + + expect(migrateConfig({ configDir: tmpDir })).toBe(false) + }) + + it('returns false if no legacy config.json exists', () => { + expect(migrateConfig({ configDir: tmpDir })).toBe(false) + }) + + it('returns false if legacy config has no token', () => { + writeFileSync( + join(tmpDir, 'config.json'), + JSON.stringify({ apiUrl: 'https://api.test/graphql' }), + ) + + expect(migrateConfig({ configDir: tmpDir })).toBe(false) + expect(existsSync(join(tmpDir, 'accounts.json'))).toBe(false) + }) + + it('returns false if config.json is invalid JSON', () => { + writeFileSync(join(tmpDir, 'config.json'), 'not json{{{') + + expect(migrateConfig({ configDir: tmpDir })).toBe(false) + }) +}) + +describe('readAccountsConfig', () => { + let tmpDir: string + + beforeEach(() => { + tmpDir = makeTmpDir() + }) + + afterEach(() => { + rmSync(tmpDir, { recursive: true, force: true }) + }) + + it('reads a valid accounts.json', () => { + const data: AccountsConfig = { + activeAccount: 'work', + accounts: [ + { name: 'work', token: 'tok_w', apiUrl: 'https://work.api/graphql' }, + { name: 'personal', token: 'tok_p', apiUrl: 'https://personal.api/graphql', vendor: 'openai' }, + ], + } + writeFileSync(join(tmpDir, 'accounts.json'), JSON.stringify(data)) + + const result = readAccountsConfig({ configDir: tmpDir }) + expect(result).toEqual(data) + }) + + it('returns null if accounts.json does not exist', () => { + expect(readAccountsConfig({ configDir: tmpDir })).toBeNull() + }) + + it('returns null if accounts.json is invalid JSON', () => { + writeFileSync(join(tmpDir, 'accounts.json'), '{{invalid') + expect(readAccountsConfig({ configDir: tmpDir })).toBeNull() + }) +}) diff --git a/src/lib/client.ts b/src/lib/client.ts index b9ec36f..2287b0e 100644 --- a/src/lib/client.ts +++ b/src/lib/client.ts @@ -4,47 +4,117 @@ interface Flags { debug?: boolean } +export interface RetryOptions { + maxRetries: number + baseDelayMs: number + maxDelayMs: number +} + +const DEFAULT_RETRY: RetryOptions = { + maxRetries: 3, + baseDelayMs: 500, + maxDelayMs: 10_000, +} + +/** + * Returns true for errors that are transient and worth retrying: + * network failures, 5xx server errors, 429 rate limits. + */ +export function isTransientError(error: unknown): boolean { + if (error instanceof TypeError) return true // fetch network errors + if (error instanceof HttpError) { + return error.status >= 500 || error.status === 429 + } + return false +} + +export class HttpError extends Error { + constructor( + public readonly status: number, + public readonly statusText: string, + ) { + super(`HTTP ${status}: ${statusText}`) + this.name = 'HttpError' + } +} + +export class GraphQLError extends Error { + constructor(message: string) { + super(message) + this.name = 'GraphQLError' + } +} + +/** + * Compute delay with exponential backoff and jitter. + * delay = min(baseDelay * 2^attempt, maxDelay) * random(0.5, 1.5) + */ +export function computeBackoff( + attempt: number, + opts: RetryOptions, +): number { + const exponential = opts.baseDelayMs * 2 ** attempt + const capped = Math.min(exponential, opts.maxDelayMs) + const jitter = 0.5 + Math.random() + return capped * jitter +} + export async function gql( query: string, variables: Record = {}, flags: Flags = {}, + retryOpts: RetryOptions = DEFAULT_RETRY, ): Promise { const token = getToken() const url = getApiUrl() - let res: Response - try { + let lastError: unknown - if (flags.debug) { - console.error(url, query, variables) - } - res = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ query, variables }), - }) - } catch { - throw new Error('Unable to reach server, please try again shortly.') - } + for (let attempt = 0; attempt <= retryOpts.maxRetries; attempt++) { + try { + if (flags.debug) { + console.error(url, query, variables) + } - if (!res.ok) { - if (flags.debug) { - console.error(JSON.stringify(await res.json(), null, 2)) - } - throw new Error(`HTTP ${res.status}: ${res.statusText}`) - } + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ query, variables }), + }) - const json = (await res.json()) as { - data?: T - errors?: Array<{ message: string }> - } + if (!res.ok) { + if (flags.debug) { + console.error(JSON.stringify(await res.json(), null, 2)) + } + throw new HttpError(res.status, res.statusText) + } + + const json = (await res.json()) as { + data?: T + errors?: Array<{ message: string }> + } + + if (json.errors && json.errors.length > 0) { + throw new GraphQLError(json.errors[0].message) + } - if (json.errors && json.errors.length > 0) { - throw new Error(json.errors[0].message) + return json.data as T + } catch (err) { + lastError = err + + // Only retry transient errors, and only if we have attempts left + if (!isTransientError(err) || attempt >= retryOpts.maxRetries) { + throw err + } + + const delay = computeBackoff(attempt, retryOpts) + await new Promise((resolve) => setTimeout(resolve, delay)) + } } - return json.data as T + // Unreachable, but satisfies TypeScript + throw lastError } diff --git a/src/lib/config.ts b/src/lib/config.ts index 8f50890..f2da4bd 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -4,6 +4,7 @@ import { join } from 'node:path' const CONFIG_DIR = join(homedir(), '.sonar') const CONFIG_FILE = join(CONFIG_DIR, 'config.json') +const ACCOUNTS_FILE = join(CONFIG_DIR, 'accounts.json') export type Vendor = 'openai' | 'anthropic' @@ -15,6 +16,80 @@ export interface Config { feedWidth?: number } +export interface AccountEntry { + name: string + token: string + apiUrl: string + vendor?: Vendor +} + +export interface AccountsConfig { + activeAccount: string + accounts: AccountEntry[] +} + +/** + * Migrate legacy single-account config.json to multi-account accounts.json. + * Returns true if migration occurred, false if already migrated or no legacy config. + */ +export function migrateConfig(opts?: { + configDir?: string +}): boolean { + const dir = opts?.configDir ?? CONFIG_DIR + const legacyPath = join(dir, 'config.json') + const accountsPath = join(dir, 'accounts.json') + + // Already migrated or no legacy config + if (existsSync(accountsPath) || !existsSync(legacyPath)) { + return false + } + + let legacy: Config + try { + const raw = readFileSync(legacyPath, 'utf8') + legacy = JSON.parse(raw) as Config + } catch { + return false + } + + if (!legacy.token) return false + + const accounts: AccountsConfig = { + activeAccount: 'default', + accounts: [ + { + name: 'default', + token: legacy.token, + apiUrl: legacy.apiUrl ?? 'https://api.sonar.8640p.info/graphql', + ...(legacy.vendor ? { vendor: legacy.vendor } : {}), + }, + ], + } + + mkdirSync(dir, { recursive: true }) + writeFileSync(accountsPath, JSON.stringify(accounts, null, 2), 'utf8') + return true +} + +/** + * Read multi-account config. Falls back to legacy single-account config. + */ +export function readAccountsConfig(opts?: { + configDir?: string +}): AccountsConfig | null { + const dir = opts?.configDir ?? CONFIG_DIR + const accountsPath = join(dir, 'accounts.json') + + if (!existsSync(accountsPath)) return null + + try { + const raw = readFileSync(accountsPath, 'utf8') + return JSON.parse(raw) as AccountsConfig + } catch { + return null + } +} + export function readConfig(): Config { try { const raw = readFileSync(CONFIG_FILE, 'utf8')