From 8f49b73aa83efa12c644740b6515f3bb594e25ec Mon Sep 17 00:00:00 2001 From: Stan Runge Date: Fri, 27 Feb 2026 14:56:58 +0100 Subject: [PATCH] restart from scratch (weekend mvp) --- backend/.env.example | 19 - backend/.gitignore | 34 - backend/CLAUDE.md | 106 --- backend/README.md | 15 - backend/bun.lock | 75 -- backend/package.json | 23 - backend/src/application/events/EventBus.ts | 56 -- backend/src/application/events/index.ts | 1 - .../src/application/services/MatchService.ts | 334 ------- .../src/application/services/QueueService.ts | 183 ---- backend/src/application/services/index.ts | 2 - backend/src/cli/index.ts | 262 ------ backend/src/domain/events/DomainEvent.ts | 13 - backend/src/domain/events/MatchEvents.ts | 115 --- backend/src/domain/events/QueueEvents.ts | 35 - backend/src/domain/events/index.ts | 3 - backend/src/domain/interfaces/IGameService.ts | 18 - .../src/domain/interfaces/IMatchProtocol.ts | 0 .../src/domain/interfaces/IMatchRepository.ts | 12 - .../domain/interfaces/INotificationService.ts | 11 - .../domain/interfaces/IPlayerRepository.ts | 10 - .../src/domain/interfaces/IQueueRepository.ts | 15 - .../src/domain/interfaces/ITeamRepository.ts | 9 - backend/src/domain/interfaces/index.ts | 7 - backend/src/domain/models/Match.ts | 71 -- backend/src/domain/models/MatchState.ts | 20 - backend/src/domain/models/OsuMap.ts | 45 - backend/src/domain/models/Player.ts | 29 - backend/src/domain/models/QueueEntry.ts | 22 - backend/src/domain/models/Score.ts | 38 - backend/src/domain/models/Team.ts | 35 - backend/src/domain/models/index.ts | 7 - .../domain/protocols/HeadToHeadProtocol.ts | 160 ---- .../src/domain/protocols/IMatchProtocol.ts | 63 -- .../src/domain/protocols/ProtocolRegistry.ts | 32 - .../domain/protocols/StandardOsuProtocol.ts | 192 ---- backend/src/domain/protocols/index.ts | 4 - backend/src/index.ts | 197 ---- .../database/PostgresMatchRepository.ts | 293 ------ .../database/PostgresPlayerRepository.ts | 77 -- .../database/PostgresQueueRepository.ts | 173 ---- .../database/PostgresTeamRepository.ts | 128 --- backend/src/infrastructure/database/index.ts | 4 - .../database/migrations/001_initial.sql | 98 -- .../infrastructure/discord/DiscordService.ts | 174 ---- backend/src/infrastructure/discord/index.ts | 1 - .../mocks/InMemoryMatchRepository.ts | 63 -- .../mocks/InMemoryPlayerRepository.ts | 47 - .../mocks/InMemoryQueueRepository.ts | 61 -- .../mocks/InMemoryTeamRepository.ts | 36 - .../infrastructure/mocks/MockGameService.ts | 136 --- .../mocks/MockNotificationService.ts | 50 - backend/src/infrastructure/mocks/index.ts | 6 - backend/src/infrastructure/osu/OsuService.ts | 187 ---- backend/src/infrastructure/osu/index.ts | 1 - .../websocket/WebSocketService.ts | 220 ----- backend/src/infrastructure/websocket/index.ts | 1 - backend/src/presentation/api/routes.ts | 0 backend/src/testing/TestHarness.ts | 217 ----- backend/src/testing/index.ts | 1 - .../tests/application/QueueService.test.ts | 97 -- .../protocols/StandardOsuProtocol.test.ts | 155 ---- .../tests/integration/full-match-flow.test.ts | 121 --- backend/tsconfig.json | 29 - frontend/.env.example | 2 - frontend/.gitignore | 24 - frontend/.npmrc | 1 - frontend/.prettierignore | 10 - frontend/.prettierrc | 16 - frontend/.vscode/settings.json | 5 - frontend/README.md | 42 - frontend/bun.lock | 854 ------------------ frontend/drizzle.config.ts | 11 - frontend/e2e/demo.test.ts | 6 - frontend/eslint.config.js | 41 - frontend/package.json | 61 -- frontend/playwright.config.ts | 6 - frontend/src/app.d.ts | 18 - frontend/src/app.html | 11 - frontend/src/demo.spec.ts | 7 - frontend/src/hooks.server.ts | 28 - frontend/src/lib/assets/favicon.svg | 1 - frontend/src/lib/index.ts | 1 - frontend/src/lib/server/auth.ts | 81 -- frontend/src/lib/server/db/index.ts | 10 - frontend/src/lib/server/db/schema.ts | 20 - frontend/src/routes/+layout.svelte | 9 - frontend/src/routes/+page.svelte | 2 - frontend/src/routes/demo/+page.svelte | 5 - .../src/routes/demo/lucia/+page.server.ts | 31 - frontend/src/routes/demo/lucia/+page.svelte | 12 - .../routes/demo/lucia/login/+page.server.ts | 107 --- .../src/routes/demo/lucia/login/+page.svelte | 34 - frontend/src/routes/layout.css | 3 - frontend/src/routes/page.svelte.spec.ts | 13 - frontend/static/robots.txt | 3 - frontend/svelte.config.js | 6 - frontend/tsconfig.json | 20 - frontend/vite.config.ts | 43 - 99 files changed, 6193 deletions(-) delete mode 100644 backend/.env.example delete mode 100644 backend/.gitignore delete mode 100644 backend/CLAUDE.md delete mode 100644 backend/README.md delete mode 100644 backend/bun.lock delete mode 100644 backend/package.json delete mode 100644 backend/src/application/events/EventBus.ts delete mode 100644 backend/src/application/events/index.ts delete mode 100644 backend/src/application/services/MatchService.ts delete mode 100644 backend/src/application/services/QueueService.ts delete mode 100644 backend/src/application/services/index.ts delete mode 100644 backend/src/cli/index.ts delete mode 100644 backend/src/domain/events/DomainEvent.ts delete mode 100644 backend/src/domain/events/MatchEvents.ts delete mode 100644 backend/src/domain/events/QueueEvents.ts delete mode 100644 backend/src/domain/events/index.ts delete mode 100644 backend/src/domain/interfaces/IGameService.ts delete mode 100644 backend/src/domain/interfaces/IMatchProtocol.ts delete mode 100644 backend/src/domain/interfaces/IMatchRepository.ts delete mode 100644 backend/src/domain/interfaces/INotificationService.ts delete mode 100644 backend/src/domain/interfaces/IPlayerRepository.ts delete mode 100644 backend/src/domain/interfaces/IQueueRepository.ts delete mode 100644 backend/src/domain/interfaces/ITeamRepository.ts delete mode 100644 backend/src/domain/interfaces/index.ts delete mode 100644 backend/src/domain/models/Match.ts delete mode 100644 backend/src/domain/models/MatchState.ts delete mode 100644 backend/src/domain/models/OsuMap.ts delete mode 100644 backend/src/domain/models/Player.ts delete mode 100644 backend/src/domain/models/QueueEntry.ts delete mode 100644 backend/src/domain/models/Score.ts delete mode 100644 backend/src/domain/models/Team.ts delete mode 100644 backend/src/domain/models/index.ts delete mode 100644 backend/src/domain/protocols/HeadToHeadProtocol.ts delete mode 100644 backend/src/domain/protocols/IMatchProtocol.ts delete mode 100644 backend/src/domain/protocols/ProtocolRegistry.ts delete mode 100644 backend/src/domain/protocols/StandardOsuProtocol.ts delete mode 100644 backend/src/domain/protocols/index.ts delete mode 100644 backend/src/index.ts delete mode 100644 backend/src/infrastructure/database/PostgresMatchRepository.ts delete mode 100644 backend/src/infrastructure/database/PostgresPlayerRepository.ts delete mode 100644 backend/src/infrastructure/database/PostgresQueueRepository.ts delete mode 100644 backend/src/infrastructure/database/PostgresTeamRepository.ts delete mode 100644 backend/src/infrastructure/database/index.ts delete mode 100644 backend/src/infrastructure/database/migrations/001_initial.sql delete mode 100644 backend/src/infrastructure/discord/DiscordService.ts delete mode 100644 backend/src/infrastructure/discord/index.ts delete mode 100644 backend/src/infrastructure/mocks/InMemoryMatchRepository.ts delete mode 100644 backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts delete mode 100644 backend/src/infrastructure/mocks/InMemoryQueueRepository.ts delete mode 100644 backend/src/infrastructure/mocks/InMemoryTeamRepository.ts delete mode 100644 backend/src/infrastructure/mocks/MockGameService.ts delete mode 100644 backend/src/infrastructure/mocks/MockNotificationService.ts delete mode 100644 backend/src/infrastructure/mocks/index.ts delete mode 100644 backend/src/infrastructure/osu/OsuService.ts delete mode 100644 backend/src/infrastructure/osu/index.ts delete mode 100644 backend/src/infrastructure/websocket/WebSocketService.ts delete mode 100644 backend/src/infrastructure/websocket/index.ts delete mode 100644 backend/src/presentation/api/routes.ts delete mode 100644 backend/src/testing/TestHarness.ts delete mode 100644 backend/src/testing/index.ts delete mode 100644 backend/tests/application/QueueService.test.ts delete mode 100644 backend/tests/domain/protocols/StandardOsuProtocol.test.ts delete mode 100644 backend/tests/integration/full-match-flow.test.ts delete mode 100644 backend/tsconfig.json delete mode 100644 frontend/.env.example delete mode 100644 frontend/.gitignore delete mode 100644 frontend/.npmrc delete mode 100644 frontend/.prettierignore delete mode 100644 frontend/.prettierrc delete mode 100644 frontend/.vscode/settings.json delete mode 100644 frontend/README.md delete mode 100644 frontend/bun.lock delete mode 100644 frontend/drizzle.config.ts delete mode 100644 frontend/e2e/demo.test.ts delete mode 100644 frontend/eslint.config.js delete mode 100644 frontend/package.json delete mode 100644 frontend/playwright.config.ts delete mode 100644 frontend/src/app.d.ts delete mode 100644 frontend/src/app.html delete mode 100644 frontend/src/demo.spec.ts delete mode 100644 frontend/src/hooks.server.ts delete mode 100644 frontend/src/lib/assets/favicon.svg delete mode 100644 frontend/src/lib/index.ts delete mode 100644 frontend/src/lib/server/auth.ts delete mode 100644 frontend/src/lib/server/db/index.ts delete mode 100644 frontend/src/lib/server/db/schema.ts delete mode 100644 frontend/src/routes/+layout.svelte delete mode 100644 frontend/src/routes/+page.svelte delete mode 100644 frontend/src/routes/demo/+page.svelte delete mode 100644 frontend/src/routes/demo/lucia/+page.server.ts delete mode 100644 frontend/src/routes/demo/lucia/+page.svelte delete mode 100644 frontend/src/routes/demo/lucia/login/+page.server.ts delete mode 100644 frontend/src/routes/demo/lucia/login/+page.svelte delete mode 100644 frontend/src/routes/layout.css delete mode 100644 frontend/src/routes/page.svelte.spec.ts delete mode 100644 frontend/static/robots.txt delete mode 100644 frontend/svelte.config.js delete mode 100644 frontend/tsconfig.json delete mode 100644 frontend/vite.config.ts diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index 46e2b23..0000000 --- a/backend/.env.example +++ /dev/null @@ -1,19 +0,0 @@ -# Server Configuration -PORT=3000 - -# Database (PostgreSQL) -DATABASE_URL=postgres://user:password@localhost:5432/vash_esports - -# Discord Bot -DISCORD_TOKEN=your_discord_bot_token -DISCORD_GUILD_ID=your_guild_id -DISCORD_ANNOUNCEMENT_CHANNEL_ID=your_channel_id -DISCORD_MATCH_RESULTS_CHANNEL_ID=your_channel_id - -# osu! API -OSU_CLIENT_ID=your_client_id -OSU_CLIENT_SECRET=your_client_secret - -# osu! IRC (for BanchoBot) -OSU_IRC_USERNAME=your_osu_username -OSU_IRC_PASSWORD=your_irc_password diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index a14702c..0000000 --- a/backend/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -# dependencies (bun install) -node_modules - -# output -out -dist -*.tgz - -# code coverage -coverage -*.lcov - -# logs -logs -_.log -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# caches -.eslintcache -.cache -*.tsbuildinfo - -# IntelliJ based IDEs -.idea - -# Finder (MacOS) folder config -.DS_Store diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md deleted file mode 100644 index 1ee6890..0000000 --- a/backend/CLAUDE.md +++ /dev/null @@ -1,106 +0,0 @@ - -Default to using Bun instead of Node.js. - -- Use `bun ` instead of `node ` or `ts-node ` -- Use `bun test` instead of `jest` or `vitest` -- Use `bun build ` instead of `webpack` or `esbuild` -- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` -- Use `bun run - - -``` - -With the following `frontend.tsx`: - -```tsx#frontend.tsx -import React from "react"; - -// import .css files directly and it works -import './index.css'; - -import { createRoot } from "react-dom/client"; - -const root = createRoot(document.body); - -export default function Frontend() { - return

Hello, world!

; -} - -root.render(); -``` - -Then, run index.ts - -```sh -bun --hot ./index.ts -``` - -For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 3dd5e6d..0000000 --- a/backend/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# backend - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.3.3. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/backend/bun.lock b/backend/bun.lock deleted file mode 100644 index dab0933..0000000 --- a/backend/bun.lock +++ /dev/null @@ -1,75 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "backend", - "dependencies": { - "discord.js": "^14.14.1", - }, - "devDependencies": { - "@types/bun": "latest", - }, - "peerDependencies": { - "typescript": "^5", - }, - }, - }, - "packages": { - "@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="], - - "@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="], - - "@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="], - - "@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="], - - "@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="], - - "@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="], - - "@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="], - - "@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="], - - "@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="], - - "@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="], - - "@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="], - - "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - - "@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="], - - "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], - - "discord-api-types": ["discord-api-types@0.38.38", "", {}, "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q=="], - - "discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], - - "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], - - "magic-bytes.js": ["magic-bytes.js@1.13.0", "", {}, "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg=="], - - "ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="], - - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - - "@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], - - "@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="], - } -} diff --git a/backend/package.json b/backend/package.json deleted file mode 100644 index 5ad313b..0000000 --- a/backend/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "vash-esports-backend", - "version": "1.0.0", - "module": "src/index.ts", - "type": "module", - "private": true, - "scripts": { - "dev": "bun --hot src/index.ts", - "start": "bun src/index.ts", - "test": "bun test", - "cli": "bun src/cli/index.ts", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "discord.js": "^14.14.1" - }, - "devDependencies": { - "@types/bun": "latest" - }, - "peerDependencies": { - "typescript": "^5" - } -} diff --git a/backend/src/application/events/EventBus.ts b/backend/src/application/events/EventBus.ts deleted file mode 100644 index 6691ac0..0000000 --- a/backend/src/application/events/EventBus.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { DomainEvent } from "../../domain/events/DomainEvent"; - -type EventHandler = (event: T) => void | Promise; - -export class EventBus { - private handlers: Map> = new Map(); - private allHandlers: Set = new Set(); - - subscribe( - type: T["type"], - handler: EventHandler - ): () => void { - if (!this.handlers.has(type)) { - this.handlers.set(type, new Set()); - } - this.handlers.get(type)!.add(handler as EventHandler); - - return () => { - this.handlers.get(type)?.delete(handler as EventHandler); - }; - } - - subscribeAll(handler: EventHandler): () => void { - this.allHandlers.add(handler); - return () => { - this.allHandlers.delete(handler); - }; - } - - async publish(event: DomainEvent): Promise { - const handlers = this.handlers.get(event.type) ?? new Set(); - - const allPromises: Promise[] = []; - - for (const handler of handlers) { - const result = handler(event); - if (result instanceof Promise) { - allPromises.push(result); - } - } - - for (const handler of this.allHandlers) { - const result = handler(event); - if (result instanceof Promise) { - allPromises.push(result); - } - } - - await Promise.all(allPromises); - } - - clear(): void { - this.handlers.clear(); - this.allHandlers.clear(); - } -} diff --git a/backend/src/application/events/index.ts b/backend/src/application/events/index.ts deleted file mode 100644 index ac9eb5d..0000000 --- a/backend/src/application/events/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./EventBus"; diff --git a/backend/src/application/services/MatchService.ts b/backend/src/application/services/MatchService.ts deleted file mode 100644 index 1743bca..0000000 --- a/backend/src/application/services/MatchService.ts +++ /dev/null @@ -1,334 +0,0 @@ -import type { IMatchRepository } from "../../domain/interfaces/IMatchRepository"; -import type { IPlayerRepository } from "../../domain/interfaces/IPlayerRepository"; -import type { IGameService } from "../../domain/interfaces/IGameService"; -import type { INotificationService } from "../../domain/interfaces/INotificationService"; -import type { ProtocolRegistry } from "../../domain/protocols/ProtocolRegistry"; -import type { ProtocolContext, RollResult } from "../../domain/protocols/IMatchProtocol"; -import type { EventBus } from "../events/EventBus"; -import type { Team } from "../../domain/models/Team"; -import type { Player } from "../../domain/models/Player"; -import type { OsuMap } from "../../domain/models/OsuMap"; -import type { Score } from "../../domain/models/Score"; -import { type Match, createMatch, allPlayersJoined, getTeamWins } from "../../domain/models/Match"; -import { MatchState } from "../../domain/models/MatchState"; -import { createMatchScore } from "../../domain/models/Score"; -import { MatchEvents } from "../../domain/events/MatchEvents"; - -export class MatchService { - constructor( - private matchRepo: IMatchRepository, - private playerRepo: IPlayerRepository, - private protocolRegistry: ProtocolRegistry, - private gameService: IGameService, - private notificationService: INotificationService, - private eventBus: EventBus - ) { - this.setupGameServiceCallbacks(); - } - - private setupGameServiceCallbacks(): void { - this.gameService.onPlayerJoin(async (osuId, lobbyId) => { - const match = await this.matchRepo.findByLobbyId(lobbyId); - if (!match) return; - - const player = await this.playerRepo.findByOsuId(osuId); - if (!player) return; - - await this.handlePlayerJoined(match.id, player); - }); - - this.gameService.onRoll(async (osuId, lobbyId, roll) => { - const match = await this.matchRepo.findByLobbyId(lobbyId); - if (!match) return; - - const player = await this.playerRepo.findByOsuId(osuId); - if (!player) return; - - await this.handleRoll(match.id, player, roll); - }); - - this.gameService.onMatchFinish(async (lobbyId, scores) => { - const match = await this.matchRepo.findByLobbyId(lobbyId); - if (!match) return; - - await this.handleMapComplete(match.id, scores); - }); - } - - async createMatch(teams: Team[], protocolName: string): Promise { - const protocol = this.protocolRegistry.get(protocolName); - - const match = createMatch({ - teams, - protocol: protocolName, - }); - - await this.matchRepo.save(match); - await this.eventBus.publish(MatchEvents.created(match)); - - return match; - } - - async startMatch(matchId: string): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) { - throw new Error(`Match ${matchId} not found`); - } - - const protocol = this.protocolRegistry.get(match.protocol); - const context = this.createProtocolContext(match); - - const lobbyId = await this.gameService.createLobby(match); - match.lobbyId = lobbyId; - match.startedAt = new Date(); - - const newState = await protocol.onMatchStart(match, context); - if (newState) { - await this.transitionState(match, newState); - } - - for (const team of match.teams) { - for (const player of team.players) { - await this.gameService.invitePlayer(player, lobbyId); - } - } - - await this.notificationService.announceMatch(match); - await this.matchRepo.save(match); - } - - async handlePlayerJoined(matchId: string, player: Player): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - if (match.state !== MatchState.WAITING_PLAYERS) return; - - const isMatchPlayer = match.teams.some((t) => - t.players.some((p) => p.id === player.id) - ); - if (!isMatchPlayer) return; - - match.joinedPlayers.add(player.id); - await this.eventBus.publish(MatchEvents.playerJoined(match, player)); - - if (allPlayersJoined(match)) { - const protocol = this.protocolRegistry.get(match.protocol); - const context = this.createProtocolContext(match); - - const newState = await protocol.onAllPlayersJoined(match, context); - if (newState) { - await this.transitionState(match, newState); - } - } - - await this.matchRepo.save(match); - } - - async handleRoll(matchId: string, player: Player, roll: number): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - if (match.state !== MatchState.ROLLING) return; - - const team = match.teams.find((t) => t.players.some((p) => p.id === player.id)); - if (!team) return; - - if (match.rolls.has(team.id)) return; - - match.rolls.set(team.id, roll); - await this.eventBus.publish(MatchEvents.rollCompleted(match, player, roll)); - - const rolls: RollResult[] = Array.from(match.rolls.entries()).map( - ([teamId, rollValue]) => ({ - teamId, - playerId: player.id, - roll: rollValue, - }) - ); - - if (rolls.length === match.teams.length) { - const protocol = this.protocolRegistry.get(match.protocol); - const context = this.createProtocolContext(match); - - const newState = await protocol.onRollComplete(match, rolls, context); - if (newState) { - await this.transitionState(match, newState); - } - } - - await this.matchRepo.save(match); - } - - async handlePick(matchId: string, team: Team, map: OsuMap): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - if (match.state !== MatchState.PICKING) return; - - const protocol = this.protocolRegistry.get(match.protocol); - if (!protocol.isValidPick(match, team, map)) return; - - const context = this.createProtocolContext(match); - await this.eventBus.publish(MatchEvents.mapPicked(match, team, map)); - - const newState = await protocol.onPick(match, team, map, context); - if (newState) { - await this.transitionState(match, newState); - } - - await this.matchRepo.save(match); - } - - async handleBan(matchId: string, team: Team, map: OsuMap): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - if (match.state !== MatchState.BANNING) return; - - const protocol = this.protocolRegistry.get(match.protocol); - if (!protocol.isValidBan(match, team, map)) return; - - const context = this.createProtocolContext(match); - await this.eventBus.publish(MatchEvents.mapBanned(match, team, map)); - - const newState = await protocol.onBan(match, team, map, context); - if (newState) { - await this.transitionState(match, newState); - } - - await this.matchRepo.save(match); - } - - async handleMapComplete(matchId: string, scores: Score[]): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - if (match.state !== MatchState.PLAYING) return; - - const protocol = this.protocolRegistry.get(match.protocol); - const winnerId = protocol.calculateMapWinner(match, scores); - - const teamScores = new Map(); - const playerScores = new Map(); - - for (const team of match.teams) { - const teamPlayerIds = team.players.map((p) => p.id); - const teamTotal = scores - .filter((s) => teamPlayerIds.includes(s.playerId)) - .reduce((sum, s) => sum + s.score, 0); - teamScores.set(team.id, teamTotal); - } - - for (const score of scores) { - playerScores.set(score.playerId, score); - } - - const currentMapId = match.picks[match.picks.length - 1]; - const matchScore = createMatchScore({ - mapId: currentMapId, - teamScores, - playerScores, - winner: winnerId, - }); - - match.scores.push(matchScore); - await this.eventBus.publish(MatchEvents.mapCompleted(match, matchScore)); - await this.notificationService.announceMapResult(match, matchScore); - - const context = this.createProtocolContext(match); - const newState = await protocol.onMapComplete(match, scores, context); - - if (newState === MatchState.COMPLETED) { - const winner = match.teams.find((t) => t.id === protocol.calculateMatchWinner(match)); - if (winner) { - match.winner = winner; - match.endedAt = new Date(); - await this.eventBus.publish(MatchEvents.completed(match, winner)); - await this.notificationService.announceMatchResult(match, winner); - await this.updatePlayerRatings(match); - } - } - - if (newState) { - await this.transitionState(match, newState); - } - - await this.matchRepo.save(match); - } - - async cancelMatch(matchId: string, reason: string): Promise { - const match = await this.matchRepo.findById(matchId); - if (!match) return; - - match.endedAt = new Date(); - await this.transitionState(match, MatchState.CANCELLED); - await this.eventBus.publish(MatchEvents.cancelled(match, reason)); - - if (match.lobbyId) { - await this.gameService.closeLobby(match.lobbyId); - } - - await this.matchRepo.save(match); - } - - private async transitionState(match: Match, newState: MatchState): Promise { - const previousState = match.state; - match.state = newState; - await this.matchRepo.updateState(match.id, newState); - await this.eventBus.publish(MatchEvents.stateChanged(match, previousState)); - } - - private createProtocolContext(match: Match): ProtocolContext { - return { - sendMessage: async (message: string) => { - if (match.lobbyId) { - await this.gameService.sendMessage(match.lobbyId, message); - } - }, - setMap: async (mapId: number) => { - if (match.lobbyId) { - await this.gameService.setMap(match.lobbyId, mapId); - } - }, - startGame: async () => { - if (match.lobbyId) { - await this.gameService.startMatch(match.lobbyId); - } - }, - }; - } - - private async updatePlayerRatings(match: Match): Promise { - if (!match.winner) return; - - const K = 32; - const winningTeam = match.winner; - const losingTeam = match.teams.find((t) => t.id !== winningTeam.id); - if (!losingTeam) return; - - const winnerAvgRating = - winningTeam.players.reduce((sum, p) => sum + p.rating, 0) / - winningTeam.players.length; - const loserAvgRating = - losingTeam.players.reduce((sum, p) => sum + p.rating, 0) / - losingTeam.players.length; - - const expectedWinner = - 1 / (1 + Math.pow(10, (loserAvgRating - winnerAvgRating) / 400)); - const expectedLoser = 1 - expectedWinner; - - const winnerDelta = Math.round(K * (1 - expectedWinner)); - const loserDelta = Math.round(K * (0 - expectedLoser)); - - for (const player of winningTeam.players) { - await this.playerRepo.updateRating(player.id, player.rating + winnerDelta); - } - - for (const player of losingTeam.players) { - await this.playerRepo.updateRating( - player.id, - Math.max(100, player.rating + loserDelta) - ); - } - } -} diff --git a/backend/src/application/services/QueueService.ts b/backend/src/application/services/QueueService.ts deleted file mode 100644 index 9eb7142..0000000 --- a/backend/src/application/services/QueueService.ts +++ /dev/null @@ -1,183 +0,0 @@ -import type { IQueueRepository } from "../../domain/interfaces/IQueueRepository"; -import type { MatchService } from "./MatchService"; -import type { EventBus } from "../events/EventBus"; -import type { Team } from "../../domain/models/Team"; -import type { Match } from "../../domain/models/Match"; -import { type QueueEntry, createQueueEntry } from "../../domain/models/QueueEntry"; -import { getAverageRating } from "../../domain/models/Team"; -import { QueueEvents } from "../../domain/events/QueueEvents"; - -export interface MatchmakingConfig { - maxRatingDifference: number; - ratingExpansionRate: number; - minQueueTime: number; - protocol: string; -} - -const DEFAULT_CONFIG: MatchmakingConfig = { - maxRatingDifference: 200, - ratingExpansionRate: 50, - minQueueTime: 10000, - protocol: "standard-osu", -}; - -export class QueueService { - private config: MatchmakingConfig; - private matchmakingInterval?: ReturnType; - - constructor( - private queueRepo: IQueueRepository, - private matchService: MatchService, - private eventBus: EventBus, - config?: Partial - ) { - this.config = { ...DEFAULT_CONFIG, ...config }; - } - - async joinQueue(team: Team, queueType: string): Promise { - const existing = await this.queueRepo.findByTeamId(team.id); - if (existing) { - throw new Error(`Team ${team.id} is already in queue`); - } - - const entry = createQueueEntry({ - team, - queueType, - }); - - await this.queueRepo.add(entry); - await this.eventBus.publish(QueueEvents.teamJoined(entry)); - - return entry; - } - - async leaveQueue(teamId: string): Promise { - const entry = await this.queueRepo.findByTeamId(teamId); - if (!entry) { - throw new Error(`Team ${teamId} is not in queue`); - } - - await this.queueRepo.remove(teamId); - await this.eventBus.publish(QueueEvents.teamLeft(teamId, entry.queueType)); - } - - async findMatch(queueType: string): Promise { - const entries = await this.queueRepo.findByType(queueType); - if (entries.length < 2) { - return null; - } - - const matchPair = this.findBestMatch(entries); - if (!matchPair) { - return null; - } - - const [entry1, entry2] = matchPair; - - await this.queueRepo.remove(entry1.team.id); - await this.queueRepo.remove(entry2.team.id); - - const teams = [entry1.team, entry2.team]; - await this.eventBus.publish(QueueEvents.matchFound(teams, queueType)); - - const match = await this.matchService.createMatch(teams, this.config.protocol); - await this.matchService.startMatch(match.id); - - return match; - } - - async runMatchmaking(): Promise { - const queueTypes = new Set(); - const allEntries = await this.queueRepo.findAll(); - - for (const entry of allEntries) { - queueTypes.add(entry.queueType); - } - - for (const queueType of queueTypes) { - await this.findMatch(queueType); - } - } - - startAutoMatchmaking(intervalMs: number = 5000): void { - if (this.matchmakingInterval) { - clearInterval(this.matchmakingInterval); - } - this.matchmakingInterval = setInterval(() => { - this.runMatchmaking().catch(console.error); - }, intervalMs); - } - - stopAutoMatchmaking(): void { - if (this.matchmakingInterval) { - clearInterval(this.matchmakingInterval); - this.matchmakingInterval = undefined; - } - } - - private findBestMatch(entries: QueueEntry[]): [QueueEntry, QueueEntry] | null { - const now = Date.now(); - let bestPair: [QueueEntry, QueueEntry] | null = null; - let bestScore = Infinity; - - for (let i = 0; i < entries.length; i++) { - for (let j = i + 1; j < entries.length; j++) { - const entry1 = entries[i]; - const entry2 = entries[j]; - - if (!entry1 || !entry2) continue; - - const rating1 = getAverageRating(entry1.team); - const rating2 = getAverageRating(entry2.team); - const ratingDiff = Math.abs(rating1 - rating2); - - const waitTime1 = now - entry1.joinedAt.getTime(); - const waitTime2 = now - entry2.joinedAt.getTime(); - const minWaitTime = Math.min(waitTime1, waitTime2); - - if (minWaitTime < this.config.minQueueTime) { - continue; - } - - const expansionMultiplier = Math.floor(minWaitTime / this.config.minQueueTime); - const maxAllowedDiff = - this.config.maxRatingDifference + - expansionMultiplier * this.config.ratingExpansionRate; - - if (ratingDiff > maxAllowedDiff) { - continue; - } - - const score = ratingDiff - minWaitTime / 1000; - - if (score < bestScore) { - bestScore = score; - bestPair = [entry1, entry2]; - } - } - } - - return bestPair; - } - - async getQueueStatus(queueType: string): Promise<{ - count: number; - entries: Array<{ - teamName: string; - rating: number; - waitTime: number; - }>; - }> { - const entries = await this.queueRepo.findByType(queueType); - const now = Date.now(); - - return { - count: entries.length, - entries: entries.map((e) => ({ - teamName: e.team.name, - rating: getAverageRating(e.team), - waitTime: now - e.joinedAt.getTime(), - })), - }; - } -} diff --git a/backend/src/application/services/index.ts b/backend/src/application/services/index.ts deleted file mode 100644 index e35ce21..0000000 --- a/backend/src/application/services/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./MatchService"; -export * from "./QueueService"; diff --git a/backend/src/cli/index.ts b/backend/src/cli/index.ts deleted file mode 100644 index 19bfe29..0000000 --- a/backend/src/cli/index.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { parseArgs } from "util"; -import { EventBus } from "../application/events/EventBus"; -import { MatchService } from "../application/services/MatchService"; -import { QueueService } from "../application/services/QueueService"; -import { ProtocolRegistry } from "../domain/protocols/ProtocolRegistry"; -import { StandardOsuProtocol } from "../domain/protocols/StandardOsuProtocol"; -import { HeadToHeadProtocol } from "../domain/protocols/HeadToHeadProtocol"; -import { InMemoryPlayerRepository } from "../infrastructure/mocks/InMemoryPlayerRepository"; -import { InMemoryTeamRepository } from "../infrastructure/mocks/InMemoryTeamRepository"; -import { InMemoryMatchRepository } from "../infrastructure/mocks/InMemoryMatchRepository"; -import { InMemoryQueueRepository } from "../infrastructure/mocks/InMemoryQueueRepository"; -import { MockGameService } from "../infrastructure/mocks/MockGameService"; -import { MockNotificationService } from "../infrastructure/mocks/MockNotificationService"; -import { createPlayer } from "../domain/models/Player"; -import { createTeam, getAverageRating } from "../domain/models/Team"; -import { MatchState } from "../domain/models/MatchState"; - -const eventBus = new EventBus(); -const mockGameService = new MockGameService(); -const mockNotificationService = new MockNotificationService(); - -const playerRepo = new InMemoryPlayerRepository(); -const teamRepo = new InMemoryTeamRepository(); -const matchRepo = new InMemoryMatchRepository(); -const queueRepo = new InMemoryQueueRepository(); - -const protocolRegistry = new ProtocolRegistry(); -protocolRegistry.register(new StandardOsuProtocol()); -protocolRegistry.register(new HeadToHeadProtocol()); - -const matchService = new MatchService( - matchRepo, - playerRepo, - protocolRegistry, - mockGameService, - mockNotificationService, - eventBus -); - -const queueService = new QueueService(queueRepo, matchService, eventBus, { - minQueueTime: 0, -}); - -eventBus.subscribeAll((event) => { - console.log(`[Event] ${event.type}`); -}); - -type CommandHandler = (...args: string[]) => Promise; - -const commands: Record = { - "player:create": async (osuName?: string, rating?: string) => { - const player = createPlayer({ - discordId: `discord-${Date.now()}`, - osuId: Math.floor(Math.random() * 1000000), - osuName: osuName ?? `Player${Date.now()}`, - rating: rating ? parseInt(rating) : 1000, - }); - await playerRepo.save(player); - console.log(`Created player: ${player.id} (${player.osuName}, rating: ${player.rating})`); - }, - - "player:list": async () => { - const players = await playerRepo.findAll(); - if (players.length === 0) { - console.log("No players found"); - return; - } - console.log("Players:"); - for (const p of players) { - console.log(` ${p.id}: ${p.osuName} (rating: ${p.rating})`); - } - }, - - "team:create": async (name: string, ...playerIds: string[]) => { - if (!name || playerIds.length === 0) { - console.log("Usage: team:create [playerId2] ..."); - return; - } - const players = await Promise.all( - playerIds.map((id) => playerRepo.findById(id)) - ); - const validPlayers = players.filter((p) => p !== null); - if (validPlayers.length === 0) { - console.log("No valid players found"); - return; - } - const team = createTeam({ name, players: validPlayers }); - await teamRepo.save(team); - console.log(`Created team: ${team.id} (${team.name}, avg rating: ${getAverageRating(team)})`); - }, - - "team:list": async () => { - const teams = await teamRepo.findAll(); - if (teams.length === 0) { - console.log("No teams found"); - return; - } - console.log("Teams:"); - for (const t of teams) { - console.log(` ${t.id}: ${t.name} (${t.players.length} players, avg: ${getAverageRating(t)})`); - } - }, - - "queue:join": async (teamId: string, queueType: string = "ranked") => { - if (!teamId) { - console.log("Usage: queue:join [queueType]"); - return; - } - const team = await teamRepo.findById(teamId); - if (!team) { - console.log(`Team ${teamId} not found`); - return; - } - try { - const entry = await queueService.joinQueue(team, queueType); - console.log(`Team ${team.name} joined ${queueType} queue at ${entry.joinedAt.toISOString()}`); - } catch (e) { - console.log(`Error: ${(e as Error).message}`); - } - }, - - "queue:leave": async (teamId: string) => { - if (!teamId) { - console.log("Usage: queue:leave "); - return; - } - try { - await queueService.leaveQueue(teamId); - console.log(`Team ${teamId} left the queue`); - } catch (e) { - console.log(`Error: ${(e as Error).message}`); - } - }, - - "queue:list": async (queueType: string = "ranked") => { - const status = await queueService.getQueueStatus(queueType); - console.log(`Queue (${queueType}): ${status.count} teams`); - for (const e of status.entries) { - console.log(` ${e.teamName} (rating: ${e.rating}, wait: ${Math.round(e.waitTime / 1000)}s)`); - } - }, - - "matchmaking:run": async () => { - const match = await queueService.findMatch("ranked"); - if (match) { - console.log(`Created match ${match.id}: ${match.teams.map((t) => t.name).join(" vs ")}`); - } else { - console.log("No match found (need 2+ teams in queue)"); - } - }, - - "match:status": async (matchId: string) => { - if (!matchId) { - console.log("Usage: match:status "); - return; - } - const match = await matchRepo.findById(matchId); - if (!match) { - console.log(`Match ${matchId} not found`); - return; - } - console.log(`Match: ${match.id}`); - console.log(` State: ${match.state}`); - console.log(` Protocol: ${match.protocol}`); - console.log(` Teams: ${match.teams.map((t) => t.name).join(" vs ")}`); - console.log(` Scores: ${match.teams.map((t) => `${t.name}: ${match.scores.filter((s) => s.winner === t.id).length}`).join(", ")}`); - if (match.winner) { - console.log(` Winner: ${match.winner.name}`); - } - }, - - "match:list": async () => { - const matches = await matchRepo.findAll(); - if (matches.length === 0) { - console.log("No matches found"); - return; - } - console.log("Matches:"); - for (const m of matches) { - console.log(` ${m.id}: ${m.teams.map((t) => t.name).join(" vs ")} [${m.state}]`); - } - }, - - "match:simulate-join": async (matchId: string, osuIdStr: string) => { - if (!matchId || !osuIdStr) { - console.log("Usage: match:simulate-join "); - return; - } - const match = await matchRepo.findById(matchId); - if (!match || !match.lobbyId) { - console.log(`Match ${matchId} not found or has no lobby`); - return; - } - const osuId = parseInt(osuIdStr); - mockGameService.simulatePlayerJoin(match.lobbyId, osuId); - console.log(`Simulated player ${osuId} joining lobby ${match.lobbyId}`); - }, - - "match:simulate-roll": async (matchId: string, osuIdStr: string, rollStr: string) => { - if (!matchId || !osuIdStr || !rollStr) { - console.log("Usage: match:simulate-roll "); - return; - } - const match = await matchRepo.findById(matchId); - if (!match || !match.lobbyId) { - console.log(`Match ${matchId} not found or has no lobby`); - return; - } - const osuId = parseInt(osuIdStr); - const roll = parseInt(rollStr); - mockGameService.simulateRoll(match.lobbyId, osuId, roll); - console.log(`Simulated roll ${roll} from player ${osuId}`); - }, - - "protocol:list": async () => { - const protocols = protocolRegistry.list(); - console.log("Available protocols:"); - for (const p of protocols) { - console.log(` ${p.name}: ${p.description} (${p.teamSize}v${p.teamSize}, best of ${p.bestOf})`); - } - }, - - help: async () => { - console.log("Available commands:"); - console.log(" player:create [osuName] [rating]"); - console.log(" player:list"); - console.log(" team:create [playerId2] ..."); - console.log(" team:list"); - console.log(" queue:join [queueType]"); - console.log(" queue:leave "); - console.log(" queue:list [queueType]"); - console.log(" matchmaking:run"); - console.log(" match:status "); - console.log(" match:list"); - console.log(" match:simulate-join "); - console.log(" match:simulate-roll "); - console.log(" protocol:list"); - console.log(" help"); - }, -}; - -async function main() { - const args = process.argv.slice(2); - const command = args[0]; - const commandArgs = args.slice(1); - - if (!command) { - await commands.help(); - process.exit(0); - } - - const handler = commands[command]; - if (!handler) { - console.log(`Unknown command: ${command}`); - await commands.help(); - process.exit(1); - } - - await handler(...commandArgs); -} - -main().catch(console.error); diff --git a/backend/src/domain/events/DomainEvent.ts b/backend/src/domain/events/DomainEvent.ts deleted file mode 100644 index 7a32728..0000000 --- a/backend/src/domain/events/DomainEvent.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface DomainEvent { - type: string; - timestamp: Date; - payload: T; -} - -export function createEvent(type: string, payload: T): DomainEvent { - return { - type, - timestamp: new Date(), - payload, - }; -} diff --git a/backend/src/domain/events/MatchEvents.ts b/backend/src/domain/events/MatchEvents.ts deleted file mode 100644 index 8ebb232..0000000 --- a/backend/src/domain/events/MatchEvents.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { Match } from "../models/Match"; -import type { MatchState } from "../models/MatchState"; -import type { Player } from "../models/Player"; -import type { Team } from "../models/Team"; -import type { MatchScore } from "../models/Score"; -import type { OsuMap } from "../models/OsuMap"; -import type { DomainEvent } from "./DomainEvent"; -import { createEvent } from "./DomainEvent"; - -export interface MatchCreatedPayload { - match: Match; -} - -export interface MatchStateChangedPayload { - match: Match; - previousState: MatchState; -} - -export interface PlayerJoinedLobbyPayload { - match: Match; - player: Player; -} - -export interface RollCompletedPayload { - match: Match; - player: Player; - roll: number; -} - -export interface MapPickedPayload { - match: Match; - team: Team; - map: OsuMap; -} - -export interface MapBannedPayload { - match: Match; - team: Team; - map: OsuMap; -} - -export interface MapStartedPayload { - match: Match; - map: OsuMap; -} - -export interface MapCompletedPayload { - match: Match; - scores: MatchScore; -} - -export interface MatchCompletedPayload { - match: Match; - winner: Team; -} - -export interface MatchCancelledPayload { - match: Match; - reason: string; -} - -export type MatchCreatedEvent = DomainEvent & { type: "match.created" }; -export type MatchStateChangedEvent = DomainEvent & { type: "match.state_changed" }; -export type PlayerJoinedLobbyEvent = DomainEvent & { type: "match.player_joined" }; -export type RollCompletedEvent = DomainEvent & { type: "match.roll_completed" }; -export type MapPickedEvent = DomainEvent & { type: "match.map_picked" }; -export type MapBannedEvent = DomainEvent & { type: "match.map_banned" }; -export type MapStartedEvent = DomainEvent & { type: "match.map_started" }; -export type MapCompletedEvent = DomainEvent & { type: "match.map_completed" }; -export type MatchCompletedEvent = DomainEvent & { type: "match.completed" }; -export type MatchCancelledEvent = DomainEvent & { type: "match.cancelled" }; - -export type MatchEvent = - | MatchCreatedEvent - | MatchStateChangedEvent - | PlayerJoinedLobbyEvent - | RollCompletedEvent - | MapPickedEvent - | MapBannedEvent - | MapStartedEvent - | MapCompletedEvent - | MatchCompletedEvent - | MatchCancelledEvent; - -export const MatchEvents = { - created: (match: Match): MatchCreatedEvent => - createEvent("match.created", { match }) as MatchCreatedEvent, - - stateChanged: (match: Match, previousState: MatchState): MatchStateChangedEvent => - createEvent("match.state_changed", { match, previousState }) as MatchStateChangedEvent, - - playerJoined: (match: Match, player: Player): PlayerJoinedLobbyEvent => - createEvent("match.player_joined", { match, player }) as PlayerJoinedLobbyEvent, - - rollCompleted: (match: Match, player: Player, roll: number): RollCompletedEvent => - createEvent("match.roll_completed", { match, player, roll }) as RollCompletedEvent, - - mapPicked: (match: Match, team: Team, map: OsuMap): MapPickedEvent => - createEvent("match.map_picked", { match, team, map }) as MapPickedEvent, - - mapBanned: (match: Match, team: Team, map: OsuMap): MapBannedEvent => - createEvent("match.map_banned", { match, team, map }) as MapBannedEvent, - - mapStarted: (match: Match, map: OsuMap): MapStartedEvent => - createEvent("match.map_started", { match, map }) as MapStartedEvent, - - mapCompleted: (match: Match, scores: MatchScore): MapCompletedEvent => - createEvent("match.map_completed", { match, scores }) as MapCompletedEvent, - - completed: (match: Match, winner: Team): MatchCompletedEvent => - createEvent("match.completed", { match, winner }) as MatchCompletedEvent, - - cancelled: (match: Match, reason: string): MatchCancelledEvent => - createEvent("match.cancelled", { match, reason }) as MatchCancelledEvent, -}; diff --git a/backend/src/domain/events/QueueEvents.ts b/backend/src/domain/events/QueueEvents.ts deleted file mode 100644 index 77e0058..0000000 --- a/backend/src/domain/events/QueueEvents.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Team } from "../models/Team"; -import type { QueueEntry } from "../models/QueueEntry"; -import type { DomainEvent } from "./DomainEvent"; -import { createEvent } from "./DomainEvent"; - -export interface TeamQueuedPayload { - entry: QueueEntry; -} - -export interface TeamDequeuedPayload { - teamId: string; - queueType: string; -} - -export interface MatchFoundPayload { - teams: Team[]; - queueType: string; -} - -export type TeamQueuedEvent = DomainEvent & { type: "queue.team_joined" }; -export type TeamDequeuedEvent = DomainEvent & { type: "queue.team_left" }; -export type MatchFoundEvent = DomainEvent & { type: "queue.match_found" }; - -export type QueueEvent = TeamQueuedEvent | TeamDequeuedEvent | MatchFoundEvent; - -export const QueueEvents = { - teamJoined: (entry: QueueEntry): TeamQueuedEvent => - createEvent("queue.team_joined", { entry }) as TeamQueuedEvent, - - teamLeft: (teamId: string, queueType: string): TeamDequeuedEvent => - createEvent("queue.team_left", { teamId, queueType }) as TeamDequeuedEvent, - - matchFound: (teams: Team[], queueType: string): MatchFoundEvent => - createEvent("queue.match_found", { teams, queueType }) as MatchFoundEvent, -}; diff --git a/backend/src/domain/events/index.ts b/backend/src/domain/events/index.ts deleted file mode 100644 index 9d7d966..0000000 --- a/backend/src/domain/events/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./DomainEvent"; -export * from "./MatchEvents"; -export * from "./QueueEvents"; diff --git a/backend/src/domain/interfaces/IGameService.ts b/backend/src/domain/interfaces/IGameService.ts deleted file mode 100644 index 4063bc9..0000000 --- a/backend/src/domain/interfaces/IGameService.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Match } from "../models/Match"; -import type { Player } from "../models/Player"; -import type { Score } from "../models/Score"; - -export interface IGameService { - createLobby(match: Match): Promise; - closeLobby(lobbyId: string): Promise; - invitePlayer(player: Player, lobbyId: string): Promise; - sendMessage(lobbyId: string, message: string): Promise; - setMap(lobbyId: string, mapId: number): Promise; - startMatch(lobbyId: string): Promise; - - onPlayerJoin(callback: (osuId: number, lobbyId: string) => void): void; - onPlayerLeave(callback: (osuId: number, lobbyId: string) => void): void; - onMatchStart(callback: (lobbyId: string) => void): void; - onMatchFinish(callback: (lobbyId: string, scores: Score[]) => void): void; - onRoll(callback: (osuId: number, lobbyId: string, roll: number) => void): void; -} diff --git a/backend/src/domain/interfaces/IMatchProtocol.ts b/backend/src/domain/interfaces/IMatchProtocol.ts deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/domain/interfaces/IMatchRepository.ts b/backend/src/domain/interfaces/IMatchRepository.ts deleted file mode 100644 index f386c95..0000000 --- a/backend/src/domain/interfaces/IMatchRepository.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Match } from "../models/Match"; -import type { MatchState } from "../models/MatchState"; - -export interface IMatchRepository { - findById(id: string): Promise; - findActive(): Promise; - findByPlayer(playerId: string, limit?: number): Promise; - findByLobbyId(lobbyId: string): Promise; - save(match: Match): Promise; - updateState(matchId: string, state: MatchState): Promise; - findAll(): Promise; -} diff --git a/backend/src/domain/interfaces/INotificationService.ts b/backend/src/domain/interfaces/INotificationService.ts deleted file mode 100644 index a2165f8..0000000 --- a/backend/src/domain/interfaces/INotificationService.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Match } from "../models/Match"; -import type { Team } from "../models/Team"; -import type { MatchScore } from "../models/Score"; - -export interface INotificationService { - announceMatch(match: Match): Promise; - announceMatchResult(match: Match, winner: Team): Promise; - notifyPlayer(playerId: string, message: string): Promise; - notifyTeam(teamId: string, message: string): Promise; - announceMapResult(match: Match, scores: MatchScore): Promise; -} diff --git a/backend/src/domain/interfaces/IPlayerRepository.ts b/backend/src/domain/interfaces/IPlayerRepository.ts deleted file mode 100644 index 1df6117..0000000 --- a/backend/src/domain/interfaces/IPlayerRepository.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Player } from "../models/Player"; - -export interface IPlayerRepository { - findById(id: string): Promise; - findByOsuId(osuId: number): Promise; - findByDiscordId(discordId: string): Promise; - save(player: Player): Promise; - updateRating(playerId: string, newRating: number): Promise; - findAll(): Promise; -} diff --git a/backend/src/domain/interfaces/IQueueRepository.ts b/backend/src/domain/interfaces/IQueueRepository.ts deleted file mode 100644 index 8bdb222..0000000 --- a/backend/src/domain/interfaces/IQueueRepository.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { QueueEntry } from "../models/QueueEntry"; - -export interface IQueueRepository { - add(entry: QueueEntry): Promise; - remove(teamId: string): Promise; - findByTeamId(teamId: string): Promise; - findByType(queueType: string): Promise; - findClosestByRating( - rating: number, - queueType: string, - exclude?: string[] - ): Promise; - findAll(): Promise; - clear(): Promise; -} diff --git a/backend/src/domain/interfaces/ITeamRepository.ts b/backend/src/domain/interfaces/ITeamRepository.ts deleted file mode 100644 index 516a5d7..0000000 --- a/backend/src/domain/interfaces/ITeamRepository.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Team } from "../models/Team"; - -export interface ITeamRepository { - findById(id: string): Promise; - findByPlayer(playerId: string): Promise; - save(team: Team): Promise; - delete(teamId: string): Promise; - findAll(): Promise; -} diff --git a/backend/src/domain/interfaces/index.ts b/backend/src/domain/interfaces/index.ts deleted file mode 100644 index 4bc264b..0000000 --- a/backend/src/domain/interfaces/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./IPlayerRepository"; -export * from "./ITeamRepository"; -export * from "./IMatchRepository"; -export * from "./IQueueRepository"; -export * from "./IGameService"; -export * from "./INotificationService"; -export * from "./IMatchProtocol"; diff --git a/backend/src/domain/models/Match.ts b/backend/src/domain/models/Match.ts deleted file mode 100644 index dfd3a33..0000000 --- a/backend/src/domain/models/Match.ts +++ /dev/null @@ -1,71 +0,0 @@ -import type { Team } from "./Team"; -import type { MatchScore } from "./Score"; -import { MatchState } from "./MatchState"; - -export interface Match { - id: string; - teams: Team[]; - protocol: string; - state: MatchState; - scores: MatchScore[]; - winner?: Team; - lobbyId?: string; - createdAt: Date; - startedAt?: Date; - endedAt?: Date; - - rolls: Map; - bans: Map; - picks: number[]; - currentPicker?: string; - currentBanner?: string; - joinedPlayers: Set; -} - -export function createMatch(params: { - id?: string; - teams: Team[]; - protocol: string; - state?: MatchState; - lobbyId?: string; - createdAt?: Date; -}): Match { - if (params.teams.length < 2) { - throw new Error("Match must have at least two teams"); - } - - return { - id: params.id ?? crypto.randomUUID(), - teams: params.teams, - protocol: params.protocol, - state: params.state ?? MatchState.CREATED, - scores: [], - lobbyId: params.lobbyId, - createdAt: params.createdAt ?? new Date(), - rolls: new Map(), - bans: new Map(), - picks: [], - joinedPlayers: new Set(), - }; -} - -export function getTeamWins(match: Match, teamId: string): number { - return match.scores.filter((s) => s.winner === teamId).length; -} - -export function getMatchWinner(match: Match, bestOf: number): Team | null { - const winsNeeded = Math.ceil(bestOf / 2); - - for (const team of match.teams) { - if (getTeamWins(match, team.id) >= winsNeeded) { - return team; - } - } - - return null; -} - -export function allPlayersJoined(match: Match): boolean { - const allPlayerIds = match.teams.flatMap((t) => t.players.map((p) => p.id)); - return allPlayerIds.every((id) => match.joinedPlayers.has(id)); -} diff --git a/backend/src/domain/models/MatchState.ts b/backend/src/domain/models/MatchState.ts deleted file mode 100644 index 7419610..0000000 --- a/backend/src/domain/models/MatchState.ts +++ /dev/null @@ -1,20 +0,0 @@ -export enum MatchState { - CREATED = "created", - LOBBY_CREATED = "lobby_created", - WAITING_PLAYERS = "waiting_players", - ROLLING = "rolling", - PICKING = "picking", - BANNING = "banning", - PLAYING = "playing", - MAP_COMPLETE = "map_complete", - COMPLETED = "completed", - CANCELLED = "cancelled", -} - -export function isTerminalState(state: MatchState): boolean { - return state === MatchState.COMPLETED || state === MatchState.CANCELLED; -} - -export function isActiveState(state: MatchState): boolean { - return !isTerminalState(state); -} diff --git a/backend/src/domain/models/OsuMap.ts b/backend/src/domain/models/OsuMap.ts deleted file mode 100644 index b7c6354..0000000 --- a/backend/src/domain/models/OsuMap.ts +++ /dev/null @@ -1,45 +0,0 @@ -export interface OsuMap { - id: number; - beatmapSetId: number; - title: string; - artist: string; - difficulty: string; - starRating: number; - bpm: number; - length: number; - mod?: string; -} - -export interface Mappool { - id: string; - name: string; - maps: OsuMap[]; -} - -export function createOsuMap(params: { - id: number; - beatmapSetId?: number; - title: string; - artist: string; - difficulty: string; - starRating?: number; - bpm?: number; - length?: number; - mod?: string; -}): OsuMap { - return { - id: params.id, - beatmapSetId: params.beatmapSetId ?? 0, - title: params.title, - artist: params.artist, - difficulty: params.difficulty, - starRating: params.starRating ?? 0, - bpm: params.bpm ?? 0, - length: params.length ?? 0, - mod: params.mod, - }; -} - -export function getMapsByMod(mappool: Mappool, mod: string): OsuMap[] { - return mappool.maps.filter((m) => m.mod === mod); -} diff --git a/backend/src/domain/models/Player.ts b/backend/src/domain/models/Player.ts deleted file mode 100644 index 078224d..0000000 --- a/backend/src/domain/models/Player.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface Player { - id: string; - discordId: string; - osuId: number; - osuName: string; - displayName: string; - rating: number; - createdAt: Date; -} - -export function createPlayer(params: { - id?: string; - discordId: string; - osuId: number; - osuName: string; - displayName?: string; - rating?: number; - createdAt?: Date; -}): Player { - return { - id: params.id ?? crypto.randomUUID(), - discordId: params.discordId, - osuId: params.osuId, - osuName: params.osuName, - displayName: params.displayName ?? params.osuName, - rating: params.rating ?? 1000, - createdAt: params.createdAt ?? new Date(), - }; -} diff --git a/backend/src/domain/models/QueueEntry.ts b/backend/src/domain/models/QueueEntry.ts deleted file mode 100644 index 0bd159f..0000000 --- a/backend/src/domain/models/QueueEntry.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Team } from "./Team"; - -export interface QueueEntry { - id: string; - team: Team; - queueType: string; - joinedAt: Date; -} - -export function createQueueEntry(params: { - id?: string; - team: Team; - queueType: string; - joinedAt?: Date; -}): QueueEntry { - return { - id: params.id ?? crypto.randomUUID(), - team: params.team, - queueType: params.queueType, - joinedAt: params.joinedAt ?? new Date(), - }; -} diff --git a/backend/src/domain/models/Score.ts b/backend/src/domain/models/Score.ts deleted file mode 100644 index 16e4c15..0000000 --- a/backend/src/domain/models/Score.ts +++ /dev/null @@ -1,38 +0,0 @@ -export interface Score { - playerId: string; - score: number; - accuracy: number; - maxCombo: number; - misses: number; - mods?: string[]; -} - -export interface MatchScore { - id: string; - mapId: number; - teamScores: Map; - playerScores: Map; - winner: string; -} - -export function createMatchScore(params: { - id?: string; - mapId: number; - teamScores: Map; - playerScores: Map; - winner: string; -}): MatchScore { - return { - id: params.id ?? crypto.randomUUID(), - mapId: params.mapId, - teamScores: params.teamScores, - playerScores: params.playerScores, - winner: params.winner, - }; -} - -export function calculateTeamScore(scores: Score[], teamPlayerIds: string[]): number { - return scores - .filter((s) => teamPlayerIds.includes(s.playerId)) - .reduce((sum, s) => sum + s.score, 0); -} diff --git a/backend/src/domain/models/Team.ts b/backend/src/domain/models/Team.ts deleted file mode 100644 index 6e8bc39..0000000 --- a/backend/src/domain/models/Team.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Player } from "./Player"; - -export interface Team { - id: string; - name: string; - players: Player[]; - captain: Player; - createdAt: Date; -} - -export function createTeam(params: { - id?: string; - name: string; - players: Player[]; - captain?: Player; - createdAt?: Date; -}): Team { - if (params.players.length === 0) { - throw new Error("Team must have at least one player"); - } - - return { - id: params.id ?? crypto.randomUUID(), - name: params.name, - players: params.players, - captain: params.captain ?? params.players[0], - createdAt: params.createdAt ?? new Date(), - }; -} - -export function getAverageRating(team: Team): number { - if (team.players.length === 0) return 0; - const total = team.players.reduce((sum, p) => sum + p.rating, 0); - return Math.round(total / team.players.length); -} diff --git a/backend/src/domain/models/index.ts b/backend/src/domain/models/index.ts deleted file mode 100644 index 4263f9e..0000000 --- a/backend/src/domain/models/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./Player"; -export * from "./Team"; -export * from "./Match"; -export * from "./MatchState"; -export * from "./Score"; -export * from "./QueueEntry"; -export * from "./OsuMap"; diff --git a/backend/src/domain/protocols/HeadToHeadProtocol.ts b/backend/src/domain/protocols/HeadToHeadProtocol.ts deleted file mode 100644 index 7a67378..0000000 --- a/backend/src/domain/protocols/HeadToHeadProtocol.ts +++ /dev/null @@ -1,160 +0,0 @@ -import type { - IMatchProtocol, - ProtocolContext, - RollResult, -} from "./IMatchProtocol"; -import type { Match } from "../models/Match"; -import { MatchState } from "../models/MatchState"; -import type { Team } from "../models/Team"; -import type { OsuMap } from "../models/OsuMap"; -import type { Score } from "../models/Score"; -import { getTeamWins } from "../models/Match"; - -export class HeadToHeadProtocol implements IMatchProtocol { - readonly name = "head-to-head"; - readonly description = "1v1 head-to-head format"; - readonly teamSize = 1; - readonly bestOf = 5; - readonly hasPickBan = false; - readonly hasFreemod = false; - - async onMatchStart( - match: Match, - context: ProtocolContext - ): Promise { - const team1 = match.teams[0]; - const team2 = match.teams[1]; - await context.sendMessage(`1v1 Match: ${team1.name} vs ${team2.name}`); - await context.sendMessage("Waiting for players to join..."); - return MatchState.WAITING_PLAYERS; - } - - async onAllPlayersJoined( - _match: Match, - context: ProtocolContext - ): Promise { - await context.sendMessage("Both players have joined! Please roll (!roll)"); - return MatchState.ROLLING; - } - - async onRollComplete( - match: Match, - rolls: RollResult[], - context: ProtocolContext - ): Promise { - const team1Roll = rolls.find((r) => r.teamId === match.teams[0].id); - const team2Roll = rolls.find((r) => r.teamId === match.teams[1].id); - - if (!team1Roll || !team2Roll) { - return null; - } - - const winningTeam = - team1Roll.roll > team2Roll.roll ? match.teams[0] : match.teams[1]; - - match.currentPicker = winningTeam.id; - - await context.sendMessage( - `${winningTeam.name} wins the roll and picks first!` - ); - - return MatchState.PICKING; - } - - async onBan( - _match: Match, - _team: Team, - _map: OsuMap, - _context: ProtocolContext - ): Promise { - return null; - } - - async onPick( - match: Match, - team: Team, - map: OsuMap, - context: ProtocolContext - ): Promise { - match.picks.push(map.id); - await context.sendMessage(`${team.name} picked ${map.title} [${map.difficulty}]`); - await context.setMap(map.id); - - return MatchState.PLAYING; - } - - async onMapComplete( - match: Match, - _scores: Score[], - context: ProtocolContext - ): Promise { - const winner = this.calculateMatchWinner(match); - if (winner) { - return MatchState.COMPLETED; - } - - const currentPicker = match.currentPicker; - match.currentPicker = - currentPicker === match.teams[0].id - ? match.teams[1].id - : match.teams[0].id; - - const team1 = match.teams[0]; - const team2 = match.teams[1]; - await context.sendMessage( - `Score: ${team1.name} ${getTeamWins(match, team1.id)} - ${getTeamWins(match, team2.id)} ${team2.name}` - ); - - return MatchState.PICKING; - } - - isValidPick(match: Match, team: Team, map: OsuMap): boolean { - if (match.currentPicker !== team.id) return false; - if (match.picks.includes(map.id)) return false; - return true; - } - - isValidBan(_match: Match, _team: Team, _map: OsuMap): boolean { - return false; - } - - calculateMapWinner(match: Match, scores: Score[]): string { - const team1 = match.teams[0]; - const team2 = match.teams[1]; - - const team1Score = scores.find((s) => - team1.players.some((p) => p.id === s.playerId) - )?.score ?? 0; - - const team2Score = scores.find((s) => - team2.players.some((p) => p.id === s.playerId) - )?.score ?? 0; - - return team1Score > team2Score ? team1.id : team2.id; - } - - calculateMatchWinner(match: Match): string | null { - const winsNeeded = this.getWinsNeeded(); - - for (const team of match.teams) { - if (getTeamWins(match, team.id) >= winsNeeded) { - return team.id; - } - } - - return null; - } - - getPickingTeam(match: Match): Team { - const team = match.teams.find((t) => t.id === match.currentPicker); - return team ?? match.teams[0]; - } - - getBanningTeam(match: Match): Team { - return match.teams[0]; - } - - getWinsNeeded(): number { - return Math.ceil(this.bestOf / 2); - } -} diff --git a/backend/src/domain/protocols/IMatchProtocol.ts b/backend/src/domain/protocols/IMatchProtocol.ts deleted file mode 100644 index 8a8551e..0000000 --- a/backend/src/domain/protocols/IMatchProtocol.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Match } from "../models/Match"; -import type { MatchState } from "../models/MatchState"; -import type { Team } from "../models/Team"; -import type { OsuMap } from "../models/OsuMap"; -import type { Score } from "../models/Score"; - -export interface RollResult { - teamId: string; - playerId: string; - roll: number; -} - -export interface ProtocolContext { - sendMessage: (message: string) => Promise; - setMap: (mapId: number) => Promise; - startGame: () => Promise; -} - -export interface IMatchProtocol { - readonly name: string; - readonly description: string; - - readonly teamSize: number; - readonly bestOf: number; - readonly hasPickBan: boolean; - readonly hasFreemod: boolean; - - onMatchStart(match: Match, context: ProtocolContext): Promise; - onAllPlayersJoined(match: Match, context: ProtocolContext): Promise; - onRollComplete( - match: Match, - rolls: RollResult[], - context: ProtocolContext - ): Promise; - onPick( - match: Match, - team: Team, - map: OsuMap, - context: ProtocolContext - ): Promise; - onBan( - match: Match, - team: Team, - map: OsuMap, - context: ProtocolContext - ): Promise; - onMapComplete( - match: Match, - scores: Score[], - context: ProtocolContext - ): Promise; - - isValidPick(match: Match, team: Team, map: OsuMap): boolean; - isValidBan(match: Match, team: Team, map: OsuMap): boolean; - - calculateMapWinner(match: Match, scores: Score[]): string; - calculateMatchWinner(match: Match): string | null; - - getPickingTeam(match: Match): Team; - getBanningTeam(match: Match): Team; - - getWinsNeeded(): number; -} diff --git a/backend/src/domain/protocols/ProtocolRegistry.ts b/backend/src/domain/protocols/ProtocolRegistry.ts deleted file mode 100644 index e54aeef..0000000 --- a/backend/src/domain/protocols/ProtocolRegistry.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { IMatchProtocol } from "./IMatchProtocol"; - -export class ProtocolRegistry { - private protocols: Map = new Map(); - - register(protocol: IMatchProtocol): void { - if (this.protocols.has(protocol.name)) { - throw new Error(`Protocol "${protocol.name}" is already registered`); - } - this.protocols.set(protocol.name, protocol); - } - - get(name: string): IMatchProtocol { - const protocol = this.protocols.get(name); - if (!protocol) { - throw new Error(`Protocol "${name}" not found`); - } - return protocol; - } - - has(name: string): boolean { - return this.protocols.has(name); - } - - list(): IMatchProtocol[] { - return Array.from(this.protocols.values()); - } - - names(): string[] { - return Array.from(this.protocols.keys()); - } -} diff --git a/backend/src/domain/protocols/StandardOsuProtocol.ts b/backend/src/domain/protocols/StandardOsuProtocol.ts deleted file mode 100644 index 36fa643..0000000 --- a/backend/src/domain/protocols/StandardOsuProtocol.ts +++ /dev/null @@ -1,192 +0,0 @@ -import type { - IMatchProtocol, - ProtocolContext, - RollResult, -} from "./IMatchProtocol"; -import type { Match } from "../models/Match"; -import { MatchState } from "../models/MatchState"; -import type { Team } from "../models/Team"; -import type { OsuMap } from "../models/OsuMap"; -import type { Score } from "../models/Score"; -import { getTeamWins } from "../models/Match"; - -export class StandardOsuProtocol implements IMatchProtocol { - readonly name = "standard-osu"; - readonly description = "Standard 4v4 tournament format with pick/ban"; - readonly teamSize = 4; - readonly bestOf = 9; - readonly hasPickBan = true; - readonly hasFreemod = true; - - async onMatchStart( - _match: Match, - context: ProtocolContext - ): Promise { - await context.sendMessage("Welcome to the match!"); - await context.sendMessage("Waiting for all players to join..."); - return MatchState.WAITING_PLAYERS; - } - - async onAllPlayersJoined( - _match: Match, - context: ProtocolContext - ): Promise { - await context.sendMessage("All players have joined! Please roll (!roll)"); - return MatchState.ROLLING; - } - - async onRollComplete( - match: Match, - rolls: RollResult[], - context: ProtocolContext - ): Promise { - const team1Roll = rolls.find((r) => r.teamId === match.teams[0].id); - const team2Roll = rolls.find((r) => r.teamId === match.teams[1].id); - - if (!team1Roll || !team2Roll) { - return null; - } - - const winningTeam = - team1Roll.roll > team2Roll.roll ? match.teams[0] : match.teams[1]; - const losingTeam = - team1Roll.roll > team2Roll.roll ? match.teams[1] : match.teams[0]; - - match.currentBanner = winningTeam.id; - match.currentPicker = losingTeam.id; - - await context.sendMessage( - `${winningTeam.name} wins the roll! They will ban first, ${losingTeam.name} picks first.` - ); - - return MatchState.BANNING; - } - - async onBan( - match: Match, - team: Team, - map: OsuMap, - context: ProtocolContext - ): Promise { - const existingBans = match.bans.get(team.id); - const bans = existingBans ? [...existingBans] : []; - bans.push(map.id); - match.bans.set(team.id, bans); - - await context.sendMessage(`${team.name} banned ${map.title} [${map.difficulty}]`); - - const totalBans = Array.from(match.bans.values()).flat().length; - if (totalBans >= 2) { - return MatchState.PICKING; - } - - const currentBanner = match.currentBanner; - match.currentBanner = - currentBanner === match.teams[0].id - ? match.teams[1].id - : match.teams[0].id; - - return null; - } - - async onPick( - match: Match, - team: Team, - map: OsuMap, - context: ProtocolContext - ): Promise { - match.picks.push(map.id); - await context.sendMessage(`${team.name} picked ${map.title} [${map.difficulty}]`); - await context.setMap(map.id); - - return MatchState.PLAYING; - } - - async onMapComplete( - match: Match, - _scores: Score[], - context: ProtocolContext - ): Promise { - const winner = this.calculateMatchWinner(match); - if (winner) { - return MatchState.COMPLETED; - } - - const currentPicker = match.currentPicker; - match.currentPicker = - currentPicker === match.teams[0].id - ? match.teams[1].id - : match.teams[0].id; - - const team1 = match.teams[0]; - const team2 = match.teams[1]; - await context.sendMessage( - `Score: ${team1.name} ${getTeamWins(match, team1.id)} - ${getTeamWins(match, team2.id)} ${team2.name}` - ); - - return MatchState.PICKING; - } - - isValidPick(match: Match, team: Team, map: OsuMap): boolean { - if (match.currentPicker !== team.id) return false; - - const allBans = Array.from(match.bans.values()).flat(); - if (allBans.includes(map.id)) return false; - - if (match.picks.includes(map.id)) return false; - - return true; - } - - isValidBan(match: Match, team: Team, map: OsuMap): boolean { - if (match.currentBanner !== team.id) return false; - - const allBans = Array.from(match.bans.values()).flat(); - if (allBans.includes(map.id)) return false; - - return true; - } - - calculateMapWinner(match: Match, scores: Score[]): string { - const team1 = match.teams[0]; - const team2 = match.teams[1]; - const team1PlayerIds = team1.players.map((p) => p.id); - const team2PlayerIds = team2.players.map((p) => p.id); - - const team1Score = scores - .filter((s) => team1PlayerIds.includes(s.playerId)) - .reduce((sum, s) => sum + s.score, 0); - - const team2Score = scores - .filter((s) => team2PlayerIds.includes(s.playerId)) - .reduce((sum, s) => sum + s.score, 0); - - return team1Score > team2Score ? team1.id : team2.id; - } - - calculateMatchWinner(match: Match): string | null { - const winsNeeded = this.getWinsNeeded(); - - for (const team of match.teams) { - if (getTeamWins(match, team.id) >= winsNeeded) { - return team.id; - } - } - - return null; - } - - getPickingTeam(match: Match): Team { - const team = match.teams.find((t) => t.id === match.currentPicker); - return team ?? match.teams[0]; - } - - getBanningTeam(match: Match): Team { - const team = match.teams.find((t) => t.id === match.currentBanner); - return team ?? match.teams[0]; - } - - getWinsNeeded(): number { - return Math.ceil(this.bestOf / 2); - } -} diff --git a/backend/src/domain/protocols/index.ts b/backend/src/domain/protocols/index.ts deleted file mode 100644 index 5b304d3..0000000 --- a/backend/src/domain/protocols/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./IMatchProtocol"; -export * from "./StandardOsuProtocol"; -export * from "./HeadToHeadProtocol"; -export * from "./ProtocolRegistry"; diff --git a/backend/src/index.ts b/backend/src/index.ts deleted file mode 100644 index 04a9174..0000000 --- a/backend/src/index.ts +++ /dev/null @@ -1,197 +0,0 @@ -import type { ServerWebSocket } from "bun"; -import { EventBus } from "./application/events/EventBus"; -import { MatchService } from "./application/services/MatchService"; -import { QueueService } from "./application/services/QueueService"; -import { ProtocolRegistry } from "./domain/protocols/ProtocolRegistry"; -import { StandardOsuProtocol } from "./domain/protocols/StandardOsuProtocol"; -import { HeadToHeadProtocol } from "./domain/protocols/HeadToHeadProtocol"; -import { InMemoryPlayerRepository } from "./infrastructure/mocks/InMemoryPlayerRepository"; -import { InMemoryTeamRepository } from "./infrastructure/mocks/InMemoryTeamRepository"; -import { InMemoryMatchRepository } from "./infrastructure/mocks/InMemoryMatchRepository"; -import { InMemoryQueueRepository } from "./infrastructure/mocks/InMemoryQueueRepository"; -import { MockGameService } from "./infrastructure/mocks/MockGameService"; -import { MockNotificationService } from "./infrastructure/mocks/MockNotificationService"; -import { WebSocketService } from "./infrastructure/websocket/WebSocketService"; -import { getTeamWins } from "./domain/models/Match"; -import { getAverageRating } from "./domain/models/Team"; - -interface WebSocketData { - id: string; - subscribedMatches: Set; -} - -const eventBus = new EventBus(); -const mockGameService = new MockGameService(); -const mockNotificationService = new MockNotificationService(); - -const playerRepo = new InMemoryPlayerRepository(); -const teamRepo = new InMemoryTeamRepository(); -const matchRepo = new InMemoryMatchRepository(); -const queueRepo = new InMemoryQueueRepository(); - -const protocolRegistry = new ProtocolRegistry(); -protocolRegistry.register(new StandardOsuProtocol()); -protocolRegistry.register(new HeadToHeadProtocol()); - -const matchService = new MatchService( - matchRepo, - playerRepo, - protocolRegistry, - mockGameService, - mockNotificationService, - eventBus -); - -const queueService = new QueueService(queueRepo, matchService, eventBus, { - minQueueTime: 0, - protocol: "head-to-head", -}); - -const wsService = new WebSocketService(eventBus); - -const server = Bun.serve({ - port: process.env.PORT ? parseInt(process.env.PORT) : 3000, - - async fetch(req, server) { - const url = new URL(req.url); - - if (url.pathname === "/ws") { - const upgraded = server.upgrade(req, { - data: { - id: crypto.randomUUID(), - subscribedMatches: new Set(), - }, - }); - if (upgraded) return undefined; - return new Response("WebSocket upgrade failed", { status: 400 }); - } - - if (url.pathname === "/api/health") { - return Response.json({ status: "ok", timestamp: new Date().toISOString() }); - } - - if (url.pathname === "/api/queue/status" && req.method === "GET") { - const queueType = url.searchParams.get("type") ?? "ranked"; - const status = await queueService.getQueueStatus(queueType); - return Response.json(status); - } - - if (url.pathname === "/api/queue/join" && req.method === "POST") { - try { - const body = await req.json(); - const team = await teamRepo.findById(body.teamId); - if (!team) { - return Response.json({ error: "Team not found" }, { status: 404 }); - } - const entry = await queueService.joinQueue(team, body.queueType ?? "ranked"); - return Response.json({ success: true, entry: { id: entry.id, joinedAt: entry.joinedAt } }); - } catch (error) { - return Response.json({ error: (error as Error).message }, { status: 400 }); - } - } - - if (url.pathname === "/api/queue/leave" && req.method === "POST") { - try { - const body = await req.json(); - await queueService.leaveQueue(body.teamId); - return Response.json({ success: true }); - } catch (error) { - return Response.json({ error: (error as Error).message }, { status: 400 }); - } - } - - if (url.pathname.startsWith("/api/matches/") && req.method === "GET") { - const matchId = url.pathname.split("/").pop(); - if (!matchId) { - return Response.json({ error: "Match ID required" }, { status: 400 }); - } - const match = await matchRepo.findById(matchId); - if (!match) { - return Response.json({ error: "Match not found" }, { status: 404 }); - } - return Response.json({ - id: match.id, - state: match.state, - protocol: match.protocol, - teams: match.teams.map((t) => ({ - id: t.id, - name: t.name, - players: t.players.map((p) => ({ - id: p.id, - displayName: p.displayName, - rating: p.rating, - })), - averageRating: getAverageRating(t), - wins: getTeamWins(match, t.id), - })), - winner: match.winner ? { id: match.winner.id, name: match.winner.name } : null, - createdAt: match.createdAt, - startedAt: match.startedAt, - endedAt: match.endedAt, - }); - } - - if (url.pathname === "/api/matches" && req.method === "GET") { - const matches = await matchRepo.findAll(); - return Response.json( - matches.map((m) => ({ - id: m.id, - state: m.state, - teams: m.teams.map((t) => t.name), - winner: m.winner?.name ?? null, - createdAt: m.createdAt, - })) - ); - } - - if (url.pathname === "/api/players" && req.method === "GET") { - const players = await playerRepo.findAll(); - return Response.json(players); - } - - if (url.pathname === "/api/teams" && req.method === "GET") { - const teams = await teamRepo.findAll(); - return Response.json( - teams.map((t) => ({ - id: t.id, - name: t.name, - playerCount: t.players.length, - averageRating: getAverageRating(t), - })) - ); - } - - if (url.pathname === "/api/protocols" && req.method === "GET") { - const protocols = protocolRegistry.list(); - return Response.json( - protocols.map((p) => ({ - name: p.name, - description: p.description, - teamSize: p.teamSize, - bestOf: p.bestOf, - hasPickBan: p.hasPickBan, - })) - ); - } - - return new Response("Not Found", { status: 404 }); - }, - - websocket: { - open(ws: ServerWebSocket) { - wsService.handleOpen(ws); - }, - message(ws: ServerWebSocket, message: string | Buffer) { - wsService.handleMessage(ws, message); - }, - close(ws: ServerWebSocket) { - wsService.handleClose(ws); - }, - }, -}); - -console.log(`Server running at http://localhost:${server.port}`); -console.log(`WebSocket available at ws://localhost:${server.port}/ws`); - -queueService.startAutoMatchmaking(5000); -console.log("Auto-matchmaking started (5s interval)"); diff --git a/backend/src/infrastructure/database/PostgresMatchRepository.ts b/backend/src/infrastructure/database/PostgresMatchRepository.ts deleted file mode 100644 index 27a0efd..0000000 --- a/backend/src/infrastructure/database/PostgresMatchRepository.ts +++ /dev/null @@ -1,293 +0,0 @@ -import type { SQL } from "bun"; -import type { IMatchRepository } from "../../domain/interfaces/IMatchRepository"; -import type { Match } from "../../domain/models/Match"; -import { MatchState, isActiveState } from "../../domain/models/MatchState"; -import type { Team } from "../../domain/models/Team"; -import type { Player } from "../../domain/models/Player"; -import type { MatchScore, Score } from "../../domain/models/Score"; - -interface MatchRow { - id: string; - protocol: string; - state: string; - lobby_id: string | null; - winner_team_id: string | null; - created_at: Date; - started_at: Date | null; - ended_at: Date | null; -} - -interface PlayerRow { - id: string; - discord_id: string; - osu_id: number; - osu_name: string; - display_name: string; - rating: number; - created_at: Date; -} - -interface TeamRow { - id: string; - name: string; - captain_id: string; - created_at: Date; -} - -export class PostgresMatchRepository implements IMatchRepository { - constructor(private db: SQL) {} - - private rowToPlayer(row: PlayerRow): Player { - return { - id: row.id, - discordId: row.discord_id, - osuId: row.osu_id, - osuName: row.osu_name, - displayName: row.display_name, - rating: row.rating, - createdAt: row.created_at, - }; - } - - private async loadTeamPlayers(teamId: string): Promise { - const rows = await this.db` - SELECT p.* FROM players p - JOIN team_players tp ON tp.player_id = p.id - WHERE tp.team_id = ${teamId} - `; - return rows.map((row) => this.rowToPlayer(row as PlayerRow)); - } - - private async loadMatchTeams(matchId: string): Promise { - const teamRows = await this.db` - SELECT t.*, mt.team_order FROM teams t - JOIN match_teams mt ON mt.team_id = t.id - WHERE mt.match_id = ${matchId} - ORDER BY mt.team_order - `; - - const teams: Team[] = []; - for (const row of teamRows as (TeamRow & { team_order: number })[]) { - const players = await this.loadTeamPlayers(row.id); - const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; - teams.push({ - id: row.id, - name: row.name, - players, - captain, - createdAt: row.created_at, - }); - } - - return teams; - } - - private async loadMatchScores(matchId: string): Promise { - const scoreRows = await this.db` - SELECT * FROM match_scores WHERE match_id = ${matchId} ORDER BY map_order - `; - - const scores: MatchScore[] = []; - for (const row of scoreRows as any[]) { - const playerScoreRows = await this.db` - SELECT * FROM player_scores WHERE match_score_id = ${row.id} - `; - - const teamScores = new Map(); - const playerScores = new Map(); - - for (const ps of playerScoreRows as any[]) { - playerScores.set(ps.player_id, { - playerId: ps.player_id, - score: ps.score, - accuracy: ps.accuracy, - maxCombo: ps.max_combo, - misses: ps.misses, - mods: ps.mods, - }); - } - - scores.push({ - id: row.id, - mapId: row.map_id, - teamScores, - playerScores, - winner: row.winner_team_id, - }); - } - - return scores; - } - - private async loadMatchRolls(matchId: string): Promise> { - const rows = await this.db`SELECT * FROM match_rolls WHERE match_id = ${matchId}`; - const rolls = new Map(); - for (const row of rows as any[]) { - rolls.set(row.team_id, row.roll_value); - } - return rolls; - } - - private async loadMatchBans(matchId: string): Promise> { - const rows = await this.db` - SELECT * FROM match_bans WHERE match_id = ${matchId} ORDER BY ban_order - `; - const bans = new Map(); - for (const row of rows as any[]) { - const teamBans = bans.get(row.team_id) ?? []; - teamBans.push(row.map_id); - bans.set(row.team_id, teamBans); - } - return bans; - } - - private async loadMatchPicks(matchId: string): Promise { - const rows = await this.db` - SELECT * FROM match_picks WHERE match_id = ${matchId} ORDER BY pick_order - `; - return (rows as any[]).map((r) => r.map_id); - } - - async findById(id: string): Promise { - const rows = await this.db`SELECT * FROM matches WHERE id = ${id}`; - if (rows.length === 0) return null; - - const row = rows[0] as MatchRow; - const teams = await this.loadMatchTeams(id); - const scores = await this.loadMatchScores(id); - const rolls = await this.loadMatchRolls(id); - const bans = await this.loadMatchBans(id); - const picks = await this.loadMatchPicks(id); - - const winner = row.winner_team_id - ? teams.find((t) => t.id === row.winner_team_id) - : undefined; - - return { - id: row.id, - teams, - protocol: row.protocol, - state: row.state as MatchState, - scores, - winner, - lobbyId: row.lobby_id ?? undefined, - createdAt: row.created_at, - startedAt: row.started_at ?? undefined, - endedAt: row.ended_at ?? undefined, - rolls, - bans, - picks, - joinedPlayers: new Set(), - currentPicker: undefined, - currentBanner: undefined, - }; - } - - async findActive(): Promise { - const rows = await this.db` - SELECT id FROM matches WHERE state NOT IN ('completed', 'cancelled') - `; - const matches: Match[] = []; - for (const row of rows as { id: string }[]) { - const match = await this.findById(row.id); - if (match) matches.push(match); - } - return matches; - } - - async findByPlayer(playerId: string, limit?: number): Promise { - const query = limit - ? this.db` - SELECT DISTINCT m.id FROM matches m - JOIN match_teams mt ON mt.match_id = m.id - JOIN team_players tp ON tp.team_id = mt.team_id - WHERE tp.player_id = ${playerId} - ORDER BY m.created_at DESC - LIMIT ${limit} - ` - : this.db` - SELECT DISTINCT m.id FROM matches m - JOIN match_teams mt ON mt.match_id = m.id - JOIN team_players tp ON tp.team_id = mt.team_id - WHERE tp.player_id = ${playerId} - ORDER BY m.created_at DESC - `; - - const rows = await query; - const matches: Match[] = []; - for (const row of rows as { id: string }[]) { - const match = await this.findById(row.id); - if (match) matches.push(match); - } - return matches; - } - - async findByLobbyId(lobbyId: string): Promise { - const rows = await this.db`SELECT id FROM matches WHERE lobby_id = ${lobbyId}`; - if (rows.length === 0) return null; - return this.findById((rows[0] as { id: string }).id); - } - - async save(match: Match): Promise { - await this.db` - INSERT INTO matches (id, protocol, state, lobby_id, winner_team_id, created_at, started_at, ended_at) - VALUES (${match.id}, ${match.protocol}, ${match.state}, ${match.lobbyId ?? null}, ${match.winner?.id ?? null}, ${match.createdAt}, ${match.startedAt ?? null}, ${match.endedAt ?? null}) - ON CONFLICT (id) DO UPDATE SET - protocol = EXCLUDED.protocol, - state = EXCLUDED.state, - lobby_id = EXCLUDED.lobby_id, - winner_team_id = EXCLUDED.winner_team_id, - started_at = EXCLUDED.started_at, - ended_at = EXCLUDED.ended_at - `; - - await this.db`DELETE FROM match_teams WHERE match_id = ${match.id}`; - for (let i = 0; i < match.teams.length; i++) { - await this.db` - INSERT INTO match_teams (match_id, team_id, team_order) - VALUES (${match.id}, ${match.teams[i].id}, ${i}) - `; - } - - await this.db`DELETE FROM match_rolls WHERE match_id = ${match.id}`; - for (const [teamId, roll] of match.rolls) { - await this.db` - INSERT INTO match_rolls (match_id, team_id, roll_value) - VALUES (${match.id}, ${teamId}, ${roll}) - `; - } - - await this.db`DELETE FROM match_bans WHERE match_id = ${match.id}`; - let banOrder = 0; - for (const [teamId, mapIds] of match.bans) { - for (const mapId of mapIds) { - await this.db` - INSERT INTO match_bans (match_id, team_id, map_id, ban_order) - VALUES (${match.id}, ${teamId}, ${mapId}, ${banOrder++}) - `; - } - } - - await this.db`DELETE FROM match_picks WHERE match_id = ${match.id}`; - for (let i = 0; i < match.picks.length; i++) { - await this.db` - INSERT INTO match_picks (match_id, map_id, pick_order) - VALUES (${match.id}, ${match.picks[i]}, ${i}) - `; - } - } - - async updateState(matchId: string, state: MatchState): Promise { - await this.db`UPDATE matches SET state = ${state} WHERE id = ${matchId}`; - } - - async findAll(): Promise { - const rows = await this.db`SELECT id FROM matches ORDER BY created_at DESC`; - const matches: Match[] = []; - for (const row of rows as { id: string }[]) { - const match = await this.findById(row.id); - if (match) matches.push(match); - } - return matches; - } -} diff --git a/backend/src/infrastructure/database/PostgresPlayerRepository.ts b/backend/src/infrastructure/database/PostgresPlayerRepository.ts deleted file mode 100644 index e9122c4..0000000 --- a/backend/src/infrastructure/database/PostgresPlayerRepository.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { SQL } from "bun"; -import type { IPlayerRepository } from "../../domain/interfaces/IPlayerRepository"; -import type { Player } from "../../domain/models/Player"; - -interface PlayerRow { - id: string; - discord_id: string; - osu_id: number; - osu_name: string; - display_name: string; - rating: number; - created_at: Date; -} - -export class PostgresPlayerRepository implements IPlayerRepository { - constructor(private db: SQL) {} - - private rowToPlayer(row: PlayerRow): Player { - return { - id: row.id, - discordId: row.discord_id, - osuId: row.osu_id, - osuName: row.osu_name, - displayName: row.display_name, - rating: row.rating, - createdAt: row.created_at, - }; - } - - async findById(id: string): Promise { - const rows = await this.db` - SELECT * FROM players WHERE id = ${id} - `; - if (rows.length === 0) return null; - return this.rowToPlayer(rows[0] as PlayerRow); - } - - async findByOsuId(osuId: number): Promise { - const rows = await this.db` - SELECT * FROM players WHERE osu_id = ${osuId} - `; - if (rows.length === 0) return null; - return this.rowToPlayer(rows[0] as PlayerRow); - } - - async findByDiscordId(discordId: string): Promise { - const rows = await this.db` - SELECT * FROM players WHERE discord_id = ${discordId} - `; - if (rows.length === 0) return null; - return this.rowToPlayer(rows[0] as PlayerRow); - } - - async save(player: Player): Promise { - await this.db` - INSERT INTO players (id, discord_id, osu_id, osu_name, display_name, rating, created_at) - VALUES (${player.id}, ${player.discordId}, ${player.osuId}, ${player.osuName}, ${player.displayName}, ${player.rating}, ${player.createdAt}) - ON CONFLICT (id) DO UPDATE SET - discord_id = EXCLUDED.discord_id, - osu_id = EXCLUDED.osu_id, - osu_name = EXCLUDED.osu_name, - display_name = EXCLUDED.display_name, - rating = EXCLUDED.rating - `; - } - - async updateRating(playerId: string, newRating: number): Promise { - await this.db` - UPDATE players SET rating = ${newRating} WHERE id = ${playerId} - `; - } - - async findAll(): Promise { - const rows = await this.db`SELECT * FROM players ORDER BY created_at DESC`; - return rows.map((row) => this.rowToPlayer(row as PlayerRow)); - } -} diff --git a/backend/src/infrastructure/database/PostgresQueueRepository.ts b/backend/src/infrastructure/database/PostgresQueueRepository.ts deleted file mode 100644 index b08b81e..0000000 --- a/backend/src/infrastructure/database/PostgresQueueRepository.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { SQL } from "bun"; -import type { IQueueRepository } from "../../domain/interfaces/IQueueRepository"; -import type { QueueEntry } from "../../domain/models/QueueEntry"; -import type { Team } from "../../domain/models/Team"; -import type { Player } from "../../domain/models/Player"; -import { getAverageRating } from "../../domain/models/Team"; - -interface PlayerRow { - id: string; - discord_id: string; - osu_id: number; - osu_name: string; - display_name: string; - rating: number; - created_at: Date; -} - -interface TeamRow { - id: string; - name: string; - captain_id: string; - created_at: Date; -} - -interface QueueEntryRow { - id: string; - team_id: string; - queue_type: string; - joined_at: Date; -} - -export class PostgresQueueRepository implements IQueueRepository { - constructor(private db: SQL) {} - - private rowToPlayer(row: PlayerRow): Player { - return { - id: row.id, - discordId: row.discord_id, - osuId: row.osu_id, - osuName: row.osu_name, - displayName: row.display_name, - rating: row.rating, - createdAt: row.created_at, - }; - } - - private async loadTeamPlayers(teamId: string): Promise { - const rows = await this.db` - SELECT p.* FROM players p - JOIN team_players tp ON tp.player_id = p.id - WHERE tp.team_id = ${teamId} - `; - return rows.map((row) => this.rowToPlayer(row as PlayerRow)); - } - - private async loadTeam(teamId: string): Promise { - const rows = await this.db`SELECT * FROM teams WHERE id = ${teamId}`; - if (rows.length === 0) return null; - - const row = rows[0] as TeamRow; - const players = await this.loadTeamPlayers(teamId); - const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; - - return { - id: row.id, - name: row.name, - players, - captain, - createdAt: row.created_at, - }; - } - - async add(entry: QueueEntry): Promise { - await this.db` - INSERT INTO queue_entries (id, team_id, queue_type, joined_at) - VALUES (${entry.id}, ${entry.team.id}, ${entry.queueType}, ${entry.joinedAt}) - ON CONFLICT (team_id) DO UPDATE SET - queue_type = EXCLUDED.queue_type, - joined_at = EXCLUDED.joined_at - `; - } - - async remove(teamId: string): Promise { - await this.db`DELETE FROM queue_entries WHERE team_id = ${teamId}`; - } - - async findByTeamId(teamId: string): Promise { - const rows = await this.db`SELECT * FROM queue_entries WHERE team_id = ${teamId}`; - if (rows.length === 0) return null; - - const row = rows[0] as QueueEntryRow; - const team = await this.loadTeam(row.team_id); - if (!team) return null; - - return { - id: row.id, - team, - queueType: row.queue_type, - joinedAt: row.joined_at, - }; - } - - async findByType(queueType: string): Promise { - const rows = await this.db` - SELECT * FROM queue_entries - WHERE queue_type = ${queueType} - ORDER BY joined_at ASC - `; - - const entries: QueueEntry[] = []; - for (const row of rows as QueueEntryRow[]) { - const team = await this.loadTeam(row.team_id); - if (team) { - entries.push({ - id: row.id, - team, - queueType: row.queue_type, - joinedAt: row.joined_at, - }); - } - } - - return entries; - } - - async findClosestByRating( - rating: number, - queueType: string, - exclude?: string[] - ): Promise { - const entries = await this.findByType(queueType); - - let closest: QueueEntry | null = null; - let closestDiff = Infinity; - - for (const entry of entries) { - if (exclude?.includes(entry.team.id)) continue; - - const teamRating = getAverageRating(entry.team); - const diff = Math.abs(teamRating - rating); - - if (diff < closestDiff) { - closestDiff = diff; - closest = entry; - } - } - - return closest; - } - - async findAll(): Promise { - const rows = await this.db`SELECT * FROM queue_entries ORDER BY joined_at ASC`; - - const entries: QueueEntry[] = []; - for (const row of rows as QueueEntryRow[]) { - const team = await this.loadTeam(row.team_id); - if (team) { - entries.push({ - id: row.id, - team, - queueType: row.queue_type, - joinedAt: row.joined_at, - }); - } - } - - return entries; - } - - async clear(): Promise { - await this.db`DELETE FROM queue_entries`; - } -} diff --git a/backend/src/infrastructure/database/PostgresTeamRepository.ts b/backend/src/infrastructure/database/PostgresTeamRepository.ts deleted file mode 100644 index abd10ef..0000000 --- a/backend/src/infrastructure/database/PostgresTeamRepository.ts +++ /dev/null @@ -1,128 +0,0 @@ -import type { SQL } from "bun"; -import type { ITeamRepository } from "../../domain/interfaces/ITeamRepository"; -import type { Team } from "../../domain/models/Team"; -import type { Player } from "../../domain/models/Player"; - -interface TeamRow { - id: string; - name: string; - captain_id: string; - created_at: Date; -} - -interface PlayerRow { - id: string; - discord_id: string; - osu_id: number; - osu_name: string; - display_name: string; - rating: number; - created_at: Date; -} - -export class PostgresTeamRepository implements ITeamRepository { - constructor(private db: SQL) {} - - private rowToPlayer(row: PlayerRow): Player { - return { - id: row.id, - discordId: row.discord_id, - osuId: row.osu_id, - osuName: row.osu_name, - displayName: row.display_name, - rating: row.rating, - createdAt: row.created_at, - }; - } - - private async loadTeamPlayers(teamId: string): Promise { - const rows = await this.db` - SELECT p.* FROM players p - JOIN team_players tp ON tp.player_id = p.id - WHERE tp.team_id = ${teamId} - `; - return rows.map((row) => this.rowToPlayer(row as PlayerRow)); - } - - async findById(id: string): Promise { - const rows = await this.db`SELECT * FROM teams WHERE id = ${id}`; - if (rows.length === 0) return null; - - const row = rows[0] as TeamRow; - const players = await this.loadTeamPlayers(id); - const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; - - return { - id: row.id, - name: row.name, - players, - captain, - createdAt: row.created_at, - }; - } - - async findByPlayer(playerId: string): Promise { - const teamRows = await this.db` - SELECT t.* FROM teams t - JOIN team_players tp ON tp.team_id = t.id - WHERE tp.player_id = ${playerId} - `; - - const teams: Team[] = []; - for (const row of teamRows as TeamRow[]) { - const players = await this.loadTeamPlayers(row.id); - const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; - teams.push({ - id: row.id, - name: row.name, - players, - captain, - createdAt: row.created_at, - }); - } - - return teams; - } - - async save(team: Team): Promise { - await this.db` - INSERT INTO teams (id, name, captain_id, created_at) - VALUES (${team.id}, ${team.name}, ${team.captain.id}, ${team.createdAt}) - ON CONFLICT (id) DO UPDATE SET - name = EXCLUDED.name, - captain_id = EXCLUDED.captain_id - `; - - await this.db`DELETE FROM team_players WHERE team_id = ${team.id}`; - - for (const player of team.players) { - await this.db` - INSERT INTO team_players (team_id, player_id) - VALUES (${team.id}, ${player.id}) - `; - } - } - - async delete(teamId: string): Promise { - await this.db`DELETE FROM teams WHERE id = ${teamId}`; - } - - async findAll(): Promise { - const teamRows = await this.db`SELECT * FROM teams ORDER BY created_at DESC`; - const teams: Team[] = []; - - for (const row of teamRows as TeamRow[]) { - const players = await this.loadTeamPlayers(row.id); - const captain = players.find((p) => p.id === row.captain_id) ?? players[0]; - teams.push({ - id: row.id, - name: row.name, - players, - captain, - createdAt: row.created_at, - }); - } - - return teams; - } -} diff --git a/backend/src/infrastructure/database/index.ts b/backend/src/infrastructure/database/index.ts deleted file mode 100644 index b90cb54..0000000 --- a/backend/src/infrastructure/database/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./PostgresPlayerRepository"; -export * from "./PostgresTeamRepository"; -export * from "./PostgresMatchRepository"; -export * from "./PostgresQueueRepository"; diff --git a/backend/src/infrastructure/database/migrations/001_initial.sql b/backend/src/infrastructure/database/migrations/001_initial.sql deleted file mode 100644 index 109a6bf..0000000 --- a/backend/src/infrastructure/database/migrations/001_initial.sql +++ /dev/null @@ -1,98 +0,0 @@ --- Initial database schema for Vash Esports Backend - -CREATE TABLE IF NOT EXISTS players ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - discord_id VARCHAR(255) UNIQUE NOT NULL, - osu_id INTEGER UNIQUE NOT NULL, - osu_name VARCHAR(255) NOT NULL, - display_name VARCHAR(255) NOT NULL, - rating INTEGER DEFAULT 1000, - created_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE IF NOT EXISTS teams ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - name VARCHAR(255) NOT NULL, - captain_id UUID REFERENCES players(id), - created_at TIMESTAMP DEFAULT NOW() -); - -CREATE TABLE IF NOT EXISTS team_players ( - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - player_id UUID REFERENCES players(id) ON DELETE CASCADE, - PRIMARY KEY (team_id, player_id) -); - -CREATE TABLE IF NOT EXISTS matches ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - protocol VARCHAR(100) NOT NULL, - state VARCHAR(50) NOT NULL, - lobby_id VARCHAR(255), - winner_team_id UUID REFERENCES teams(id), - created_at TIMESTAMP DEFAULT NOW(), - started_at TIMESTAMP, - ended_at TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS match_teams ( - match_id UUID REFERENCES matches(id) ON DELETE CASCADE, - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - team_order INTEGER NOT NULL, - PRIMARY KEY (match_id, team_id) -); - -CREATE TABLE IF NOT EXISTS match_rolls ( - match_id UUID REFERENCES matches(id) ON DELETE CASCADE, - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - roll_value INTEGER NOT NULL, - PRIMARY KEY (match_id, team_id) -); - -CREATE TABLE IF NOT EXISTS match_bans ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - match_id UUID REFERENCES matches(id) ON DELETE CASCADE, - team_id UUID REFERENCES teams(id) ON DELETE CASCADE, - map_id INTEGER NOT NULL, - ban_order INTEGER NOT NULL -); - -CREATE TABLE IF NOT EXISTS match_picks ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - match_id UUID REFERENCES matches(id) ON DELETE CASCADE, - map_id INTEGER NOT NULL, - pick_order INTEGER NOT NULL -); - -CREATE TABLE IF NOT EXISTS match_scores ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - match_id UUID REFERENCES matches(id) ON DELETE CASCADE, - map_id INTEGER NOT NULL, - winner_team_id UUID REFERENCES teams(id), - map_order INTEGER NOT NULL -); - -CREATE TABLE IF NOT EXISTS player_scores ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - match_score_id UUID REFERENCES match_scores(id) ON DELETE CASCADE, - player_id UUID REFERENCES players(id), - score INTEGER NOT NULL, - accuracy DECIMAL(5,2), - max_combo INTEGER, - misses INTEGER, - mods TEXT[] -); - -CREATE TABLE IF NOT EXISTS queue_entries ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - team_id UUID REFERENCES teams(id) ON DELETE CASCADE UNIQUE, - queue_type VARCHAR(50) NOT NULL, - joined_at TIMESTAMP DEFAULT NOW() -); - --- Indexes for common queries -CREATE INDEX IF NOT EXISTS idx_players_osu_id ON players(osu_id); -CREATE INDEX IF NOT EXISTS idx_players_discord_id ON players(discord_id); -CREATE INDEX IF NOT EXISTS idx_matches_state ON matches(state) WHERE state NOT IN ('completed', 'cancelled'); -CREATE INDEX IF NOT EXISTS idx_matches_lobby_id ON matches(lobby_id) WHERE lobby_id IS NOT NULL; -CREATE INDEX IF NOT EXISTS idx_queue_entries_type ON queue_entries(queue_type, joined_at); -CREATE INDEX IF NOT EXISTS idx_team_players_player ON team_players(player_id); diff --git a/backend/src/infrastructure/discord/DiscordService.ts b/backend/src/infrastructure/discord/DiscordService.ts deleted file mode 100644 index 3f1644a..0000000 --- a/backend/src/infrastructure/discord/DiscordService.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { Client, GatewayIntentBits, EmbedBuilder, type TextChannel } from "discord.js"; -import type { INotificationService } from "../../domain/interfaces/INotificationService"; -import type { Match } from "../../domain/models/Match"; -import type { Team } from "../../domain/models/Team"; -import type { MatchScore } from "../../domain/models/Score"; -import type { EventBus } from "../../application/events/EventBus"; -import { getTeamWins } from "../../domain/models/Match"; - -export interface DiscordConfig { - token: string; - guildId: string; - announcementChannelId: string; - matchResultsChannelId: string; -} - -export class DiscordService implements INotificationService { - private client: Client; - private ready = false; - - constructor( - private config: DiscordConfig, - private eventBus?: EventBus - ) { - this.client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - ], - }); - - this.setupClient(); - } - - private async setupClient(): Promise { - this.client.on("ready", () => { - console.log(`[DiscordService] Logged in as ${this.client.user?.tag}`); - this.ready = true; - }); - - this.client.on("error", (error) => { - console.error("[DiscordService] Client error:", error); - }); - - if (this.config.token) { - try { - await this.client.login(this.config.token); - } catch (error) { - console.error("[DiscordService] Failed to login:", error); - } - } - } - - async announceMatch(match: Match): Promise { - if (!this.ready) { - console.log("[DiscordService] Not ready, skipping announcement"); - return; - } - - const channel = await this.client.channels.fetch( - this.config.announcementChannelId - ); - if (!channel || !channel.isTextBased()) return; - - const embed = new EmbedBuilder() - .setTitle("New Match Starting!") - .setDescription( - `**${match.teams[0].name}** vs **${match.teams[1].name}**` - ) - .addFields( - { - name: match.teams[0].name, - value: match.teams[0].players.map((p) => p.displayName).join("\n"), - inline: true, - }, - { - name: match.teams[1].name, - value: match.teams[1].players.map((p) => p.displayName).join("\n"), - inline: true, - } - ) - .setColor(0x00ae86) - .setTimestamp(); - - await (channel as TextChannel).send({ embeds: [embed] }); - } - - async announceMatchResult(match: Match, winner: Team): Promise { - if (!this.ready) { - console.log("[DiscordService] Not ready, skipping result announcement"); - return; - } - - const channel = await this.client.channels.fetch( - this.config.matchResultsChannelId - ); - if (!channel || !channel.isTextBased()) return; - - const team1Wins = getTeamWins(match, match.teams[0].id); - const team2Wins = getTeamWins(match, match.teams[1].id); - - const embed = new EmbedBuilder() - .setTitle("Match Completed!") - .setDescription(`**${winner.name}** wins!`) - .addFields({ - name: "Final Score", - value: `${match.teams[0].name} **${team1Wins}** - **${team2Wins}** ${match.teams[1].name}`, - }) - .setColor(winner.id === match.teams[0].id ? 0x3498db : 0xe74c3c) - .setTimestamp(); - - await (channel as TextChannel).send({ embeds: [embed] }); - } - - async notifyPlayer(playerId: string, message: string): Promise { - if (!this.ready) { - console.log("[DiscordService] Not ready, skipping player notification"); - return; - } - - try { - const user = await this.client.users.fetch(playerId); - await user.send(message); - } catch (error) { - console.error(`[DiscordService] Failed to DM player ${playerId}:`, error); - } - } - - async notifyTeam(teamId: string, message: string): Promise { - // In a real implementation, you'd look up the team's players - // and notify each one. For now, just log. - console.log(`[DiscordService] Team ${teamId}: ${message}`); - } - - async announceMapResult(match: Match, scores: MatchScore): Promise { - if (!this.ready) { - console.log("[DiscordService] Not ready, skipping map result"); - return; - } - - const channel = await this.client.channels.fetch( - this.config.announcementChannelId - ); - if (!channel || !channel.isTextBased()) return; - - const winner = match.teams.find((t) => t.id === scores.winner); - const team1Score = scores.teamScores.get(match.teams[0].id) ?? 0; - const team2Score = scores.teamScores.get(match.teams[1].id) ?? 0; - const team1Wins = getTeamWins(match, match.teams[0].id); - const team2Wins = getTeamWins(match, match.teams[1].id); - - const embed = new EmbedBuilder() - .setTitle("Map Complete") - .setDescription(`**${winner?.name}** wins the map!`) - .addFields( - { - name: "Map Score", - value: `${match.teams[0].name}: ${team1Score.toLocaleString()}\n${match.teams[1].name}: ${team2Score.toLocaleString()}`, - }, - { - name: "Match Score", - value: `${match.teams[0].name} **${team1Wins}** - **${team2Wins}** ${match.teams[1].name}`, - } - ) - .setColor(0x9b59b6) - .setTimestamp(); - - await (channel as TextChannel).send({ embeds: [embed] }); - } - - async destroy(): Promise { - await this.client.destroy(); - } -} diff --git a/backend/src/infrastructure/discord/index.ts b/backend/src/infrastructure/discord/index.ts deleted file mode 100644 index dbedea0..0000000 --- a/backend/src/infrastructure/discord/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./DiscordService"; diff --git a/backend/src/infrastructure/mocks/InMemoryMatchRepository.ts b/backend/src/infrastructure/mocks/InMemoryMatchRepository.ts deleted file mode 100644 index f95a5f6..0000000 --- a/backend/src/infrastructure/mocks/InMemoryMatchRepository.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { IMatchRepository } from "../../domain/interfaces/IMatchRepository"; -import type { Match } from "../../domain/models/Match"; -import { MatchState, isActiveState } from "../../domain/models/MatchState"; - -export class InMemoryMatchRepository implements IMatchRepository { - private matches: Map = new Map(); - - async findById(id: string): Promise { - return this.matches.get(id) ?? null; - } - - async findActive(): Promise { - const result: Match[] = []; - for (const match of this.matches.values()) { - if (isActiveState(match.state)) { - result.push(match); - } - } - return result; - } - - async findByPlayer(playerId: string, limit?: number): Promise { - const result: Match[] = []; - for (const match of this.matches.values()) { - const hasPlayer = match.teams.some((t) => - t.players.some((p) => p.id === playerId) - ); - if (hasPlayer) { - result.push(match); - if (limit && result.length >= limit) break; - } - } - return result; - } - - async findByLobbyId(lobbyId: string): Promise { - for (const match of this.matches.values()) { - if (match.lobbyId === lobbyId) { - return match; - } - } - return null; - } - - async save(match: Match): Promise { - this.matches.set(match.id, match); - } - - async updateState(matchId: string, state: MatchState): Promise { - const match = this.matches.get(matchId); - if (match) { - match.state = state; - } - } - - async findAll(): Promise { - return Array.from(this.matches.values()); - } - - clear(): void { - this.matches.clear(); - } -} diff --git a/backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts b/backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts deleted file mode 100644 index 138df03..0000000 --- a/backend/src/infrastructure/mocks/InMemoryPlayerRepository.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { IPlayerRepository } from "../../domain/interfaces/IPlayerRepository"; -import type { Player } from "../../domain/models/Player"; - -export class InMemoryPlayerRepository implements IPlayerRepository { - private players: Map = new Map(); - - async findById(id: string): Promise { - return this.players.get(id) ?? null; - } - - async findByOsuId(osuId: number): Promise { - for (const player of this.players.values()) { - if (player.osuId === osuId) { - return player; - } - } - return null; - } - - async findByDiscordId(discordId: string): Promise { - for (const player of this.players.values()) { - if (player.discordId === discordId) { - return player; - } - } - return null; - } - - async save(player: Player): Promise { - this.players.set(player.id, player); - } - - async updateRating(playerId: string, newRating: number): Promise { - const player = this.players.get(playerId); - if (player) { - player.rating = newRating; - } - } - - async findAll(): Promise { - return Array.from(this.players.values()); - } - - clear(): void { - this.players.clear(); - } -} diff --git a/backend/src/infrastructure/mocks/InMemoryQueueRepository.ts b/backend/src/infrastructure/mocks/InMemoryQueueRepository.ts deleted file mode 100644 index af402b9..0000000 --- a/backend/src/infrastructure/mocks/InMemoryQueueRepository.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { IQueueRepository } from "../../domain/interfaces/IQueueRepository"; -import type { QueueEntry } from "../../domain/models/QueueEntry"; -import { getAverageRating } from "../../domain/models/Team"; - -export class InMemoryQueueRepository implements IQueueRepository { - private entries: Map = new Map(); - - async add(entry: QueueEntry): Promise { - this.entries.set(entry.team.id, entry); - } - - async remove(teamId: string): Promise { - this.entries.delete(teamId); - } - - async findByTeamId(teamId: string): Promise { - return this.entries.get(teamId) ?? null; - } - - async findByType(queueType: string): Promise { - const result: QueueEntry[] = []; - for (const entry of this.entries.values()) { - if (entry.queueType === queueType) { - result.push(entry); - } - } - return result.sort((a, b) => a.joinedAt.getTime() - b.joinedAt.getTime()); - } - - async findClosestByRating( - rating: number, - queueType: string, - exclude?: string[] - ): Promise { - let closest: QueueEntry | null = null; - let closestDiff = Infinity; - - for (const entry of this.entries.values()) { - if (entry.queueType !== queueType) continue; - if (exclude?.includes(entry.team.id)) continue; - - const teamRating = getAverageRating(entry.team); - const diff = Math.abs(teamRating - rating); - - if (diff < closestDiff) { - closestDiff = diff; - closest = entry; - } - } - - return closest; - } - - async findAll(): Promise { - return Array.from(this.entries.values()); - } - - async clear(): Promise { - this.entries.clear(); - } -} diff --git a/backend/src/infrastructure/mocks/InMemoryTeamRepository.ts b/backend/src/infrastructure/mocks/InMemoryTeamRepository.ts deleted file mode 100644 index 9025482..0000000 --- a/backend/src/infrastructure/mocks/InMemoryTeamRepository.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { ITeamRepository } from "../../domain/interfaces/ITeamRepository"; -import type { Team } from "../../domain/models/Team"; - -export class InMemoryTeamRepository implements ITeamRepository { - private teams: Map = new Map(); - - async findById(id: string): Promise { - return this.teams.get(id) ?? null; - } - - async findByPlayer(playerId: string): Promise { - const result: Team[] = []; - for (const team of this.teams.values()) { - if (team.players.some((p) => p.id === playerId)) { - result.push(team); - } - } - return result; - } - - async save(team: Team): Promise { - this.teams.set(team.id, team); - } - - async delete(teamId: string): Promise { - this.teams.delete(teamId); - } - - async findAll(): Promise { - return Array.from(this.teams.values()); - } - - clear(): void { - this.teams.clear(); - } -} diff --git a/backend/src/infrastructure/mocks/MockGameService.ts b/backend/src/infrastructure/mocks/MockGameService.ts deleted file mode 100644 index 37788c0..0000000 --- a/backend/src/infrastructure/mocks/MockGameService.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { IGameService } from "../../domain/interfaces/IGameService"; -import type { Match } from "../../domain/models/Match"; -import type { Player } from "../../domain/models/Player"; -import type { Score } from "../../domain/models/Score"; - -interface MockLobby { - id: string; - matchId: string; - players: Set; - messages: string[]; - currentMap?: number; -} - -export class MockGameService implements IGameService { - public lobbies: Map = new Map(); - public messageLog: Array<{ lobbyId: string; message: string }> = []; - - private playerJoinCallbacks: Array<(osuId: number, lobbyId: string) => void> = []; - private playerLeaveCallbacks: Array<(osuId: number, lobbyId: string) => void> = []; - private matchStartCallbacks: Array<(lobbyId: string) => void> = []; - private matchFinishCallbacks: Array<(lobbyId: string, scores: Score[]) => void> = []; - private rollCallbacks: Array<(osuId: number, lobbyId: string, roll: number) => void> = []; - - async createLobby(match: Match): Promise { - const lobbyId = `mock-lobby-${match.id}`; - this.lobbies.set(lobbyId, { - id: lobbyId, - matchId: match.id, - players: new Set(), - messages: [], - }); - return lobbyId; - } - - async closeLobby(lobbyId: string): Promise { - this.lobbies.delete(lobbyId); - } - - async invitePlayer(player: Player, lobbyId: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.messages.push(`Invited ${player.osuName}`); - } - } - - async sendMessage(lobbyId: string, message: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.messages.push(message); - } - this.messageLog.push({ lobbyId, message }); - } - - async setMap(lobbyId: string, mapId: number): Promise { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.currentMap = mapId; - lobby.messages.push(`Changed map to ${mapId}`); - } - } - - async startMatch(lobbyId: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.messages.push("Match started"); - } - } - - onPlayerJoin(callback: (osuId: number, lobbyId: string) => void): void { - this.playerJoinCallbacks.push(callback); - } - - onPlayerLeave(callback: (osuId: number, lobbyId: string) => void): void { - this.playerLeaveCallbacks.push(callback); - } - - onMatchStart(callback: (lobbyId: string) => void): void { - this.matchStartCallbacks.push(callback); - } - - onMatchFinish(callback: (lobbyId: string, scores: Score[]) => void): void { - this.matchFinishCallbacks.push(callback); - } - - onRoll(callback: (osuId: number, lobbyId: string, roll: number) => void): void { - this.rollCallbacks.push(callback); - } - - simulatePlayerJoin(lobbyId: string, osuId: number): void { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.players.add(osuId); - } - for (const cb of this.playerJoinCallbacks) { - cb(osuId, lobbyId); - } - } - - simulatePlayerLeave(lobbyId: string, osuId: number): void { - const lobby = this.lobbies.get(lobbyId); - if (lobby) { - lobby.players.delete(osuId); - } - for (const cb of this.playerLeaveCallbacks) { - cb(osuId, lobbyId); - } - } - - simulateRoll(lobbyId: string, osuId: number, roll: number): void { - for (const cb of this.rollCallbacks) { - cb(osuId, lobbyId, roll); - } - } - - simulateMatchStart(lobbyId: string): void { - for (const cb of this.matchStartCallbacks) { - cb(lobbyId); - } - } - - simulateMatchFinish(lobbyId: string, scores: Score[]): void { - for (const cb of this.matchFinishCallbacks) { - cb(lobbyId, scores); - } - } - - clear(): void { - this.lobbies.clear(); - this.messageLog = []; - this.playerJoinCallbacks = []; - this.playerLeaveCallbacks = []; - this.matchStartCallbacks = []; - this.matchFinishCallbacks = []; - this.rollCallbacks = []; - } -} diff --git a/backend/src/infrastructure/mocks/MockNotificationService.ts b/backend/src/infrastructure/mocks/MockNotificationService.ts deleted file mode 100644 index 98d1ce4..0000000 --- a/backend/src/infrastructure/mocks/MockNotificationService.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { INotificationService } from "../../domain/interfaces/INotificationService"; -import type { Match } from "../../domain/models/Match"; -import type { Team } from "../../domain/models/Team"; -import type { MatchScore } from "../../domain/models/Score"; - -interface Notification { - type: string; - targetId?: string; - message?: string; - match?: Match; - winner?: Team; - scores?: MatchScore; -} - -export class MockNotificationService implements INotificationService { - public notifications: Notification[] = []; - public announcements: Match[] = []; - public playerNotifications: Array<{ playerId: string; message: string }> = []; - public teamNotifications: Array<{ teamId: string; message: string }> = []; - - async announceMatch(match: Match): Promise { - this.announcements.push(match); - this.notifications.push({ type: "match_announced", match }); - } - - async announceMatchResult(match: Match, winner: Team): Promise { - this.notifications.push({ type: "match_result", match, winner }); - } - - async notifyPlayer(playerId: string, message: string): Promise { - this.playerNotifications.push({ playerId, message }); - this.notifications.push({ type: "player_notification", targetId: playerId, message }); - } - - async notifyTeam(teamId: string, message: string): Promise { - this.teamNotifications.push({ teamId, message }); - this.notifications.push({ type: "team_notification", targetId: teamId, message }); - } - - async announceMapResult(match: Match, scores: MatchScore): Promise { - this.notifications.push({ type: "map_result", match, scores }); - } - - clear(): void { - this.notifications = []; - this.announcements = []; - this.playerNotifications = []; - this.teamNotifications = []; - } -} diff --git a/backend/src/infrastructure/mocks/index.ts b/backend/src/infrastructure/mocks/index.ts deleted file mode 100644 index b036d12..0000000 --- a/backend/src/infrastructure/mocks/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./InMemoryPlayerRepository"; -export * from "./InMemoryTeamRepository"; -export * from "./InMemoryMatchRepository"; -export * from "./InMemoryQueueRepository"; -export * from "./MockGameService"; -export * from "./MockNotificationService"; diff --git a/backend/src/infrastructure/osu/OsuService.ts b/backend/src/infrastructure/osu/OsuService.ts deleted file mode 100644 index c4d4ec5..0000000 --- a/backend/src/infrastructure/osu/OsuService.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { IGameService } from "../../domain/interfaces/IGameService"; -import type { Match } from "../../domain/models/Match"; -import type { Player } from "../../domain/models/Player"; -import type { Score } from "../../domain/models/Score"; - -type PlayerJoinCallback = (osuId: number, lobbyId: string) => void; -type PlayerLeaveCallback = (osuId: number, lobbyId: string) => void; -type MatchStartCallback = (lobbyId: string) => void; -type MatchFinishCallback = (lobbyId: string, scores: Score[]) => void; -type RollCallback = (osuId: number, lobbyId: string, roll: number) => void; - -export interface OsuApiConfig { - clientId: string; - clientSecret: string; -} - -export interface OsuIrcConfig { - username: string; - password: string; - server?: string; - port?: number; -} - -export class OsuService implements IGameService { - private playerJoinCallbacks: PlayerJoinCallback[] = []; - private playerLeaveCallbacks: PlayerLeaveCallback[] = []; - private matchStartCallbacks: MatchStartCallback[] = []; - private matchFinishCallbacks: MatchFinishCallback[] = []; - private rollCallbacks: RollCallback[] = []; - - private lobbies: Map = new Map(); - - constructor( - private apiConfig: OsuApiConfig, - private ircConfig: OsuIrcConfig - ) { - this.connectIrc(); - } - - private async connectIrc(): Promise { - // TODO: Implement IRC connection using Bun's WebSocket - // Connect to irc.ppy.sh - // Authenticate with BanchoBot - console.log("[OsuService] IRC connection not yet implemented"); - } - - async createLobby(match: Match): Promise { - const lobbyName = `VASH: ${match.teams.map((t) => t.name).join(" vs ")}`; - // TODO: Send IRC command to create lobby - // !mp make - // Parse response to get lobby ID - - const lobbyId = `osu_${match.id.slice(0, 8)}`; - this.lobbies.set(lobbyId, { - matchId: match.id, - channel: `#mp_${lobbyId}`, - }); - - console.log(`[OsuService] Created lobby: ${lobbyId}`); - return lobbyId; - } - - async closeLobby(lobbyId: string): Promise { - // TODO: Send IRC command to close lobby - // !mp close - this.lobbies.delete(lobbyId); - console.log(`[OsuService] Closed lobby: ${lobbyId}`); - } - - async invitePlayer(player: Player, lobbyId: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (!lobby) { - throw new Error(`Lobby ${lobbyId} not found`); - } - // TODO: Send IRC command - // !mp invite - console.log(`[OsuService] Invited ${player.osuName} to ${lobbyId}`); - } - - async sendMessage(lobbyId: string, message: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (!lobby) { - throw new Error(`Lobby ${lobbyId} not found`); - } - // TODO: Send IRC message to lobby channel - console.log(`[OsuService] [${lobbyId}] ${message}`); - } - - async setMap(lobbyId: string, mapId: number): Promise { - const lobby = this.lobbies.get(lobbyId); - if (!lobby) { - throw new Error(`Lobby ${lobbyId} not found`); - } - // TODO: Send IRC command - // !mp map - console.log(`[OsuService] Set map ${mapId} in ${lobbyId}`); - } - - async startMatch(lobbyId: string): Promise { - const lobby = this.lobbies.get(lobbyId); - if (!lobby) { - throw new Error(`Lobby ${lobbyId} not found`); - } - // TODO: Send IRC command - // !mp start - console.log(`[OsuService] Started match in ${lobbyId}`); - } - - onPlayerJoin(callback: PlayerJoinCallback): void { - this.playerJoinCallbacks.push(callback); - } - - onPlayerLeave(callback: PlayerLeaveCallback): void { - this.playerLeaveCallbacks.push(callback); - } - - onMatchStart(callback: MatchStartCallback): void { - this.matchStartCallbacks.push(callback); - } - - onMatchFinish(callback: MatchFinishCallback): void { - this.matchFinishCallbacks.push(callback); - } - - onRoll(callback: RollCallback): void { - this.rollCallbacks.push(callback); - } - - // IRC message parsing - called when messages are received - private handleIrcMessage(channel: string, user: string, message: string): void { - // Find lobby by channel - let lobbyId: string | null = null; - for (const [id, lobby] of this.lobbies) { - if (lobby.channel === channel) { - lobbyId = id; - break; - } - } - if (!lobbyId) return; - - // Parse BanchoBot messages - if (user === "BanchoBot") { - // Player joined: " joined in slot X" - const joinMatch = message.match(/^(\S+) joined in slot \d+/); - if (joinMatch) { - // TODO: Look up osuId from username - // For now, we'd need an API call or cache - console.log(`[OsuService] Player joined: ${joinMatch[1]}`); - } - - // Match finished - parse scores - // Format varies, typically shows all player scores - if (message.includes("The match has finished")) { - // TODO: Parse scores from recent messages or API - console.log(`[OsuService] Match finished in ${lobbyId}`); - } - } - - // Parse roll command: " rolls X point(s)" - const rollMatch = message.match(/^(\S+) rolls (\d+) point/); - if (rollMatch) { - const username = rollMatch[1]; - const roll = parseInt(rollMatch[2]); - // TODO: Look up osuId from username - console.log(`[OsuService] ${username} rolled ${roll}`); - } - } - - // Expose for testing - _triggerPlayerJoin(osuId: number, lobbyId: string): void { - for (const cb of this.playerJoinCallbacks) { - cb(osuId, lobbyId); - } - } - - _triggerRoll(osuId: number, lobbyId: string, roll: number): void { - for (const cb of this.rollCallbacks) { - cb(osuId, lobbyId, roll); - } - } - - _triggerMatchFinish(lobbyId: string, scores: Score[]): void { - for (const cb of this.matchFinishCallbacks) { - cb(lobbyId, scores); - } - } -} diff --git a/backend/src/infrastructure/osu/index.ts b/backend/src/infrastructure/osu/index.ts deleted file mode 100644 index c2eeb6c..0000000 --- a/backend/src/infrastructure/osu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./OsuService"; diff --git a/backend/src/infrastructure/websocket/WebSocketService.ts b/backend/src/infrastructure/websocket/WebSocketService.ts deleted file mode 100644 index 16206d0..0000000 --- a/backend/src/infrastructure/websocket/WebSocketService.ts +++ /dev/null @@ -1,220 +0,0 @@ -import type { ServerWebSocket } from "bun"; -import type { EventBus } from "../../application/events/EventBus"; -import type { DomainEvent } from "../../domain/events/DomainEvent"; - -interface WebSocketData { - id: string; - subscribedMatches: Set; -} - -type WebSocketClient = ServerWebSocket; - -export interface ClientMessage { - type: string; - matchId?: string; - payload?: unknown; -} - -export class WebSocketService { - private clients: Map = new Map(); - private matchSubscriptions: Map> = new Map(); - - constructor(private eventBus: EventBus) { - this.setupEventSubscriptions(); - } - - private setupEventSubscriptions(): void { - const eventsToForward = [ - "match.created", - "match.state_changed", - "match.player_joined", - "match.roll_completed", - "match.map_picked", - "match.map_banned", - "match.map_started", - "match.map_completed", - "match.completed", - "match.cancelled", - "queue.team_joined", - "queue.team_left", - "queue.match_found", - ]; - - for (const eventType of eventsToForward) { - this.eventBus.subscribe(eventType as any, (event: DomainEvent) => { - this.broadcastEvent(event); - }); - } - } - - handleOpen(ws: WebSocketClient): void { - const clientId = crypto.randomUUID(); - ws.data = { - id: clientId, - subscribedMatches: new Set(), - }; - this.clients.set(clientId, ws); - console.log(`[WebSocket] Client connected: ${clientId}`); - - ws.send(JSON.stringify({ type: "connected", clientId })); - } - - handleMessage(ws: WebSocketClient, message: string | Buffer): void { - try { - const msg: ClientMessage = - typeof message === "string" ? JSON.parse(message) : JSON.parse(message.toString()); - - switch (msg.type) { - case "subscribe": - if (msg.matchId) { - this.subscribeToMatch(ws, msg.matchId); - } - break; - - case "unsubscribe": - if (msg.matchId) { - this.unsubscribeFromMatch(ws, msg.matchId); - } - break; - - case "ping": - ws.send(JSON.stringify({ type: "pong" })); - break; - - default: - console.log(`[WebSocket] Unknown message type: ${msg.type}`); - } - } catch (error) { - console.error("[WebSocket] Failed to parse message:", error); - } - } - - handleClose(ws: WebSocketClient): void { - const clientId = ws.data?.id; - if (clientId) { - // Clean up subscriptions - for (const matchId of ws.data.subscribedMatches) { - const subs = this.matchSubscriptions.get(matchId); - if (subs) { - subs.delete(clientId); - if (subs.size === 0) { - this.matchSubscriptions.delete(matchId); - } - } - } - - this.clients.delete(clientId); - console.log(`[WebSocket] Client disconnected: ${clientId}`); - } - } - - subscribeToMatch(ws: WebSocketClient, matchId: string): void { - const clientId = ws.data?.id; - if (!clientId) return; - - ws.data.subscribedMatches.add(matchId); - - if (!this.matchSubscriptions.has(matchId)) { - this.matchSubscriptions.set(matchId, new Set()); - } - this.matchSubscriptions.get(matchId)!.add(clientId); - - ws.send(JSON.stringify({ type: "subscribed", matchId })); - console.log(`[WebSocket] Client ${clientId} subscribed to match ${matchId}`); - } - - unsubscribeFromMatch(ws: WebSocketClient, matchId: string): void { - const clientId = ws.data?.id; - if (!clientId) return; - - ws.data.subscribedMatches.delete(matchId); - - const subs = this.matchSubscriptions.get(matchId); - if (subs) { - subs.delete(clientId); - if (subs.size === 0) { - this.matchSubscriptions.delete(matchId); - } - } - - ws.send(JSON.stringify({ type: "unsubscribed", matchId })); - } - - private broadcastEvent(event: DomainEvent): void { - const payload = event.payload as any; - const matchId = payload?.match?.id; - - const message = JSON.stringify({ - type: "event", - eventType: event.type, - timestamp: event.timestamp.toISOString(), - payload: this.serializePayload(payload), - }); - - if (matchId) { - // Send to match subscribers - const subscribers = this.matchSubscriptions.get(matchId); - if (subscribers) { - for (const clientId of subscribers) { - const client = this.clients.get(clientId); - if (client) { - client.send(message); - } - } - } - } else { - // Broadcast to all clients - for (const client of this.clients.values()) { - client.send(message); - } - } - } - - private serializePayload(payload: any): any { - if (!payload) return payload; - - // Handle Maps - if (payload instanceof Map) { - return Object.fromEntries(payload); - } - - // Handle Sets - if (payload instanceof Set) { - return Array.from(payload); - } - - // Handle objects with Maps/Sets - if (typeof payload === "object") { - const result: any = {}; - for (const [key, value] of Object.entries(payload)) { - if (value instanceof Map) { - result[key] = Object.fromEntries(value); - } else if (value instanceof Set) { - result[key] = Array.from(value); - } else if (typeof value === "object" && value !== null) { - result[key] = this.serializePayload(value); - } else { - result[key] = value; - } - } - return result; - } - - return payload; - } - - broadcast(message: object): void { - const msg = JSON.stringify(message); - for (const client of this.clients.values()) { - client.send(msg); - } - } - - getConnectedCount(): number { - return this.clients.size; - } - - getMatchSubscriberCount(matchId: string): number { - return this.matchSubscriptions.get(matchId)?.size ?? 0; - } -} diff --git a/backend/src/infrastructure/websocket/index.ts b/backend/src/infrastructure/websocket/index.ts deleted file mode 100644 index 6be4791..0000000 --- a/backend/src/infrastructure/websocket/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./WebSocketService"; diff --git a/backend/src/presentation/api/routes.ts b/backend/src/presentation/api/routes.ts deleted file mode 100644 index e69de29..0000000 diff --git a/backend/src/testing/TestHarness.ts b/backend/src/testing/TestHarness.ts deleted file mode 100644 index 316bf6d..0000000 --- a/backend/src/testing/TestHarness.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { EventBus } from "../application/events/EventBus"; -import { MatchService } from "../application/services/MatchService"; -import { QueueService } from "../application/services/QueueService"; -import { ProtocolRegistry } from "../domain/protocols/ProtocolRegistry"; -import { StandardOsuProtocol } from "../domain/protocols/StandardOsuProtocol"; -import { HeadToHeadProtocol } from "../domain/protocols/HeadToHeadProtocol"; -import { InMemoryPlayerRepository } from "../infrastructure/mocks/InMemoryPlayerRepository"; -import { InMemoryTeamRepository } from "../infrastructure/mocks/InMemoryTeamRepository"; -import { InMemoryMatchRepository } from "../infrastructure/mocks/InMemoryMatchRepository"; -import { InMemoryQueueRepository } from "../infrastructure/mocks/InMemoryQueueRepository"; -import { MockGameService } from "../infrastructure/mocks/MockGameService"; -import { MockNotificationService } from "../infrastructure/mocks/MockNotificationService"; -import { type Player, createPlayer } from "../domain/models/Player"; -import { type Team, createTeam } from "../domain/models/Team"; -import type { Match } from "../domain/models/Match"; -import type { Score } from "../domain/models/Score"; -import { MatchState } from "../domain/models/MatchState"; -import type { DomainEvent } from "../domain/events/DomainEvent"; - -export class TestHarness { - public eventBus: EventBus; - public mockGameService: MockGameService; - public mockNotificationService: MockNotificationService; - public matchService: MatchService; - public queueService: QueueService; - public protocolRegistry: ProtocolRegistry; - - public playerRepo: InMemoryPlayerRepository; - public teamRepo: InMemoryTeamRepository; - public matchRepo: InMemoryMatchRepository; - public queueRepo: InMemoryQueueRepository; - - public events: DomainEvent[] = []; - - private playerCounter = 1; - private teamCounter = 1; - - constructor() { - this.eventBus = new EventBus(); - this.mockGameService = new MockGameService(); - this.mockNotificationService = new MockNotificationService(); - - this.playerRepo = new InMemoryPlayerRepository(); - this.teamRepo = new InMemoryTeamRepository(); - this.matchRepo = new InMemoryMatchRepository(); - this.queueRepo = new InMemoryQueueRepository(); - - this.protocolRegistry = new ProtocolRegistry(); - this.protocolRegistry.register(new StandardOsuProtocol()); - this.protocolRegistry.register(new HeadToHeadProtocol()); - - this.matchService = new MatchService( - this.matchRepo, - this.playerRepo, - this.protocolRegistry, - this.mockGameService, - this.mockNotificationService, - this.eventBus - ); - - this.queueService = new QueueService( - this.queueRepo, - this.matchService, - this.eventBus, - { minQueueTime: 0, protocol: "head-to-head" } - ); - - this.eventBus.subscribeAll((event) => { - this.events.push(event); - }); - } - - createPlayer(overrides?: Partial): Player { - const id = this.playerCounter++; - const player = createPlayer({ - discordId: `discord-${id}`, - osuId: 100000 + id, - osuName: `Player${id}`, - displayName: `Player ${id}`, - rating: 1000, - ...overrides, - }); - - this.playerRepo.save(player); - return player; - } - - createPlayers(count: number, baseRating: number = 1000): Player[] { - const players: Player[] = []; - for (let i = 0; i < count; i++) { - players.push(this.createPlayer({ rating: baseRating })); - } - return players; - } - - createTeam(players: Player[], overrides?: Partial): Team { - const id = this.teamCounter++; - const team = createTeam({ - name: overrides?.name ?? `Team ${id}`, - players, - ...overrides, - }); - - this.teamRepo.save(team); - return team; - } - - async simulateAllPlayersJoin(match: Match): Promise { - for (const team of match.teams) { - for (const player of team.players) { - if (match.lobbyId) { - this.mockGameService.simulatePlayerJoin(match.lobbyId, player.osuId); - } - await this.matchService.handlePlayerJoined(match.id, player); - } - } - } - - async simulateRolls( - match: Match, - rolls: Map - ): Promise { - for (const team of match.teams) { - const roll = rolls.get(team.id) ?? Math.floor(Math.random() * 100) + 1; - const captain = team.captain; - if (match.lobbyId) { - this.mockGameService.simulateRoll(match.lobbyId, captain.osuId, roll); - } - await this.matchService.handleRoll(match.id, captain, roll); - } - } - - async simulateMapComplete(match: Match, winningTeamId: string): Promise { - const scores: Score[] = []; - const winningTeam = match.teams.find((t) => t.id === winningTeamId)!; - const losingTeam = match.teams.find((t) => t.id !== winningTeamId)!; - - for (const player of winningTeam.players) { - scores.push({ - playerId: player.id, - score: 500000 + Math.floor(Math.random() * 100000), - accuracy: 95 + Math.random() * 5, - maxCombo: 800 + Math.floor(Math.random() * 200), - misses: Math.floor(Math.random() * 5), - }); - } - - for (const player of losingTeam.players) { - scores.push({ - playerId: player.id, - score: 400000 + Math.floor(Math.random() * 80000), - accuracy: 90 + Math.random() * 8, - maxCombo: 600 + Math.floor(Math.random() * 200), - misses: Math.floor(Math.random() * 10), - }); - } - - if (match.lobbyId) { - this.mockGameService.simulateMatchFinish(match.lobbyId, scores); - } - await this.matchService.handleMapComplete(match.id, scores); - } - - async runFullMatch( - team1: Team, - team2: Team, - protocolName: string = "head-to-head" - ): Promise { - await this.queueService.joinQueue(team1, "ranked"); - await this.queueService.joinQueue(team2, "ranked"); - - const match = await this.queueService.findMatch("ranked"); - if (!match) { - throw new Error("Failed to create match"); - } - - await this.simulateAllPlayersJoin(match); - - const rolls = new Map(); - rolls.set(team1.id, 75); - rolls.set(team2.id, 50); - await this.simulateRolls(match, rolls); - - const protocol = this.protocolRegistry.get(protocolName); - const winsNeeded = protocol.getWinsNeeded(); - - for (let i = 0; i < winsNeeded; i++) { - const updatedMatch = await this.matchRepo.findById(match.id); - if (!updatedMatch || updatedMatch.state === MatchState.COMPLETED) break; - - await this.simulateMapComplete(match, team1.id); - } - - return (await this.matchRepo.findById(match.id))!; - } - - getEventsByType(type: string): DomainEvent[] { - return this.events.filter((e) => e.type === type); - } - - clearEvents(): void { - this.events = []; - } - - reset(): void { - this.playerRepo.clear(); - this.teamRepo.clear(); - this.matchRepo.clear(); - this.queueRepo.clear(); - this.mockGameService.clear(); - this.mockNotificationService.clear(); - this.eventBus.clear(); - this.events = []; - this.playerCounter = 1; - this.teamCounter = 1; - } -} diff --git a/backend/src/testing/index.ts b/backend/src/testing/index.ts deleted file mode 100644 index 202c7a8..0000000 --- a/backend/src/testing/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./TestHarness"; diff --git a/backend/tests/application/QueueService.test.ts b/backend/tests/application/QueueService.test.ts deleted file mode 100644 index 4a1f27f..0000000 --- a/backend/tests/application/QueueService.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { test, expect, beforeEach } from "bun:test"; -import { TestHarness } from "../../src/testing/TestHarness"; - -let harness: TestHarness; - -beforeEach(() => { - harness = new TestHarness(); -}); - -test("team can join queue", async () => { - const players = harness.createPlayers(1); - const team = harness.createTeam(players, { name: "Test Team" }); - - const entry = await harness.queueService.joinQueue(team, "ranked"); - - expect(entry.team.id).toBe(team.id); - expect(entry.queueType).toBe("ranked"); -}); - -test("team cannot join queue twice", async () => { - const players = harness.createPlayers(1); - const team = harness.createTeam(players, { name: "Test Team" }); - - await harness.queueService.joinQueue(team, "ranked"); - - expect(async () => { - await harness.queueService.joinQueue(team, "ranked"); - }).toThrow(); -}); - -test("team can leave queue", async () => { - const players = harness.createPlayers(1); - const team = harness.createTeam(players, { name: "Test Team" }); - - await harness.queueService.joinQueue(team, "ranked"); - await harness.queueService.leaveQueue(team.id); - - const status = await harness.queueService.getQueueStatus("ranked"); - expect(status.count).toBe(0); -}); - -test("findMatch returns null when less than 2 teams in queue", async () => { - const players = harness.createPlayers(1); - const team = harness.createTeam(players, { name: "Test Team" }); - - await harness.queueService.joinQueue(team, "ranked"); - - const match = await harness.queueService.findMatch("ranked"); - expect(match).toBeNull(); -}); - -test("findMatch creates match when 2 teams in queue", async () => { - const players1 = harness.createPlayers(1, 1000); - const players2 = harness.createPlayers(1, 1010); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - const match = await harness.queueService.findMatch("ranked"); - - expect(match).not.toBeNull(); - expect(match!.teams.length).toBe(2); -}); - -test("getQueueStatus returns correct count", async () => { - const players1 = harness.createPlayers(1); - const players2 = harness.createPlayers(1); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - const status = await harness.queueService.getQueueStatus("ranked"); - - expect(status.count).toBe(2); - expect(status.entries.length).toBe(2); -}); - -test("queue events are published", async () => { - const players = harness.createPlayers(1); - const team = harness.createTeam(players, { name: "Test Team" }); - - await harness.queueService.joinQueue(team, "ranked"); - - const joinEvents = harness.getEventsByType("queue.team_joined"); - expect(joinEvents.length).toBe(1); - - await harness.queueService.leaveQueue(team.id); - - const leaveEvents = harness.getEventsByType("queue.team_left"); - expect(leaveEvents.length).toBe(1); -}); diff --git a/backend/tests/domain/protocols/StandardOsuProtocol.test.ts b/backend/tests/domain/protocols/StandardOsuProtocol.test.ts deleted file mode 100644 index 64a883b..0000000 --- a/backend/tests/domain/protocols/StandardOsuProtocol.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { test, expect, beforeEach } from "bun:test"; -import { StandardOsuProtocol } from "../../../src/domain/protocols/StandardOsuProtocol"; -import { createMatch } from "../../../src/domain/models/Match"; -import { createTeam } from "../../../src/domain/models/Team"; -import { createPlayer } from "../../../src/domain/models/Player"; -import type { Score } from "../../../src/domain/models/Score"; - -let protocol: StandardOsuProtocol; - -beforeEach(() => { - protocol = new StandardOsuProtocol(); -}); - -test("has correct configuration", () => { - expect(protocol.name).toBe("standard-osu"); - expect(protocol.teamSize).toBe(4); - expect(protocol.bestOf).toBe(9); - expect(protocol.hasPickBan).toBe(true); - expect(protocol.hasFreemod).toBe(true); -}); - -test("getWinsNeeded returns correct value for best of 9", () => { - expect(protocol.getWinsNeeded()).toBe(5); -}); - -test("calculateMapWinner returns winning team based on scores", () => { - const players1 = [ - createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "Player1" }), - createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "Player2" }), - ]; - const players2 = [ - createPlayer({ id: "p3", discordId: "d3", osuId: 3, osuName: "Player3" }), - createPlayer({ id: "p4", discordId: "d4", osuId: 4, osuName: "Player4" }), - ]; - - const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); - const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); - - const match = createMatch({ - teams: [team1, team2], - protocol: "standard-osu", - }); - - const scores: Score[] = [ - { playerId: "p1", score: 500000, accuracy: 95, maxCombo: 800, misses: 2 }, - { playerId: "p2", score: 450000, accuracy: 94, maxCombo: 750, misses: 3 }, - { playerId: "p3", score: 400000, accuracy: 92, maxCombo: 700, misses: 5 }, - { playerId: "p4", score: 380000, accuracy: 91, maxCombo: 680, misses: 6 }, - ]; - - const winner = protocol.calculateMapWinner(match, scores); - expect(winner).toBe("team1"); -}); - -test("calculateMatchWinner returns null when no team has enough wins", () => { - const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; - const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; - - const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); - const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); - - const match = createMatch({ - teams: [team1, team2], - protocol: "standard-osu", - }); - - expect(protocol.calculateMatchWinner(match)).toBeNull(); -}); - -test("isValidPick returns false when not the picking team", () => { - const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; - const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; - - const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); - const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); - - const match = createMatch({ - teams: [team1, team2], - protocol: "standard-osu", - }); - - match.currentPicker = "team1"; - - const map = { - id: 123, - beatmapSetId: 456, - title: "Test Map", - artist: "Artist", - difficulty: "Hard", - starRating: 5.5, - bpm: 180, - length: 120, - }; - - expect(protocol.isValidPick(match, team1, map)).toBe(true); - expect(protocol.isValidPick(match, team2, map)).toBe(false); -}); - -test("isValidPick returns false for banned maps", () => { - const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; - const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; - - const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); - const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); - - const match = createMatch({ - teams: [team1, team2], - protocol: "standard-osu", - }); - - match.currentPicker = "team1"; - match.bans.set("team2", [123]); - - const map = { - id: 123, - beatmapSetId: 456, - title: "Test Map", - artist: "Artist", - difficulty: "Hard", - starRating: 5.5, - bpm: 180, - length: 120, - }; - - expect(protocol.isValidPick(match, team1, map)).toBe(false); -}); - -test("isValidPick returns false for already picked maps", () => { - const players1 = [createPlayer({ id: "p1", discordId: "d1", osuId: 1, osuName: "P1" })]; - const players2 = [createPlayer({ id: "p2", discordId: "d2", osuId: 2, osuName: "P2" })]; - - const team1 = createTeam({ id: "team1", name: "Team 1", players: players1 }); - const team2 = createTeam({ id: "team2", name: "Team 2", players: players2 }); - - const match = createMatch({ - teams: [team1, team2], - protocol: "standard-osu", - }); - - match.currentPicker = "team1"; - match.picks.push(123); - - const map = { - id: 123, - beatmapSetId: 456, - title: "Test Map", - artist: "Artist", - difficulty: "Hard", - starRating: 5.5, - bpm: 180, - length: 120, - }; - - expect(protocol.isValidPick(match, team1, map)).toBe(false); -}); diff --git a/backend/tests/integration/full-match-flow.test.ts b/backend/tests/integration/full-match-flow.test.ts deleted file mode 100644 index 2c145f9..0000000 --- a/backend/tests/integration/full-match-flow.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { test, expect, beforeEach } from "bun:test"; -import { TestHarness } from "../../src/testing/TestHarness"; -import { MatchState } from "../../src/domain/models/MatchState"; - -let harness: TestHarness; - -beforeEach(() => { - harness = new TestHarness(); -}); - -test("complete match flow from queue to completion", async () => { - const players1 = harness.createPlayers(1, 1000); - const players2 = harness.createPlayers(1, 1020); - - const team1 = harness.createTeam(players1, { name: "Team Alpha" }); - const team2 = harness.createTeam(players2, { name: "Team Beta" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - const match = await harness.queueService.findMatch("ranked"); - - expect(match).not.toBeNull(); - expect(match!.teams.length).toBe(2); - expect(match!.lobbyId).toBeDefined(); - - await harness.simulateAllPlayersJoin(match!); - - let updatedMatch = await harness.matchRepo.findById(match!.id); - expect(updatedMatch!.state).toBe(MatchState.ROLLING); - - const rolls = new Map(); - rolls.set(team1.id, 75); - rolls.set(team2.id, 50); - await harness.simulateRolls(match!, rolls); - - updatedMatch = await harness.matchRepo.findById(match!.id); - expect(updatedMatch!.state).toBe(MatchState.PICKING); - - const events = harness.getEventsByType("match.created"); - expect(events.length).toBe(1); - - const stateChanges = harness.getEventsByType("match.state_changed"); - expect(stateChanges.length).toBeGreaterThan(0); -}); - -test("match creates lobby on start", async () => { - const players1 = harness.createPlayers(1); - const players2 = harness.createPlayers(1); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - const match = await harness.queueService.findMatch("ranked"); - - expect(match!.lobbyId).toBeDefined(); - expect(harness.mockGameService.lobbies.has(match!.lobbyId!)).toBe(true); -}); - -test("notifications are sent when match is created", async () => { - const players1 = harness.createPlayers(1); - const players2 = harness.createPlayers(1); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - await harness.queueService.findMatch("ranked"); - - expect(harness.mockNotificationService.announcements.length).toBe(1); -}); - -test("match state transitions correctly through rolling phase", async () => { - const players1 = harness.createPlayers(1); - const players2 = harness.createPlayers(1); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - const match = await harness.queueService.findMatch("ranked"); - expect(match!.state).toBe(MatchState.WAITING_PLAYERS); - - await harness.simulateAllPlayersJoin(match!); - - let updatedMatch = await harness.matchRepo.findById(match!.id); - expect(updatedMatch!.state).toBe(MatchState.ROLLING); - - const rolls = new Map(); - rolls.set(team1.id, 75); - rolls.set(team2.id, 50); - await harness.simulateRolls(match!, rolls); - - updatedMatch = await harness.matchRepo.findById(match!.id); - expect(updatedMatch!.state).toBe(MatchState.PICKING); - // In head-to-head protocol, roll winner picks first - expect(updatedMatch!.currentPicker).toBe(team1.id); -}); - -test("queue status is empty after match found", async () => { - const players1 = harness.createPlayers(1); - const players2 = harness.createPlayers(1); - - const team1 = harness.createTeam(players1, { name: "Team 1" }); - const team2 = harness.createTeam(players2, { name: "Team 2" }); - - await harness.queueService.joinQueue(team1, "ranked"); - await harness.queueService.joinQueue(team2, "ranked"); - - await harness.queueService.findMatch("ranked"); - - const status = await harness.queueService.getQueueStatus("ranked"); - expect(status.count).toBe(0); -}); diff --git a/backend/tsconfig.json b/backend/tsconfig.json deleted file mode 100644 index bfa0fea..0000000 --- a/backend/tsconfig.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, - - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } -} diff --git a/frontend/.env.example b/frontend/.env.example deleted file mode 100644 index ec707ff..0000000 --- a/frontend/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -# Replace with your DB credentials! -DATABASE_URL="postgres://user:password@host:port/db-name" diff --git a/frontend/.gitignore b/frontend/.gitignore deleted file mode 100644 index bff793d..0000000 --- a/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -test-results -node_modules - -# Output -.output -.vercel -.netlify -.wrangler -/.svelte-kit -/build - -# OS -.DS_Store -Thumbs.db - -# Env -.env -.env.* -!.env.example -!.env.test - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/frontend/.npmrc b/frontend/.npmrc deleted file mode 100644 index b6f27f1..0000000 --- a/frontend/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index 0243576..0000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,10 +0,0 @@ -# Package Managers -package-lock.json -pnpm-lock.yaml -yarn.lock -bun.lock -bun.lockb - -# Miscellaneous -/static/ -/drizzle/ diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index 819fa57..0000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,16 +0,0 @@ -{ - "useTabs": true, - "singleQuote": true, - "trailingComma": "none", - "printWidth": 100, - "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], - "overrides": [ - { - "files": "*.svelte", - "options": { - "parser": "svelte" - } - } - ], - "tailwindStylesheet": "./src/routes/layout.css" -} diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json deleted file mode 100644 index bc31e15..0000000 --- a/frontend/.vscode/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "files.associations": { - "*.css": "tailwindcss" - } -} diff --git a/frontend/README.md b/frontend/README.md deleted file mode 100644 index 10af74d..0000000 --- a/frontend/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# sv - -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```sh -# create a new project -npx sv create my-app -``` - -To recreate this project with the same configuration: - -```sh -# recreate this project -bun x sv create --template minimal --types ts --add prettier eslint vitest="usages:unit,component" playwright tailwindcss="plugins:typography,forms" sveltekit-adapter="adapter:node" devtools-json drizzle="database:postgresql+postgresql:postgres.js+docker:no" lucia="demo:yes" --install bun . -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```sh -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```sh -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/frontend/bun.lock b/frontend/bun.lock deleted file mode 100644 index 11cd1e6..0000000 --- a/frontend/bun.lock +++ /dev/null @@ -1,854 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "frontend", - "dependencies": { - "@node-rs/argon2": "^2.0.2", - "postgres": "^3.4.7", - }, - "devDependencies": { - "@eslint/compat": "^1.4.0", - "@eslint/js": "^9.39.1", - "@oslojs/crypto": "^1.0.1", - "@oslojs/encoding": "^1.1.0", - "@playwright/test": "^1.57.0", - "@sveltejs/adapter-node": "^5.4.0", - "@sveltejs/kit": "^2.49.1", - "@sveltejs/vite-plugin-svelte": "^6.2.1", - "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.17", - "@types/node": "^22", - "@vitest/browser-playwright": "^4.0.15", - "drizzle-kit": "^0.31.8", - "drizzle-orm": "^0.45.0", - "eslint": "^9.39.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-svelte": "^3.13.1", - "globals": "^16.5.0", - "playwright": "^1.57.0", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.0", - "prettier-plugin-tailwindcss": "^0.7.2", - "svelte": "^5.45.6", - "svelte-check": "^4.3.4", - "tailwindcss": "^4.1.17", - "typescript": "^5.9.3", - "typescript-eslint": "^8.48.1", - "vite": "^7.2.6", - "vite-plugin-devtools-json": "^1.0.0", - "vitest": "^4.0.15", - "vitest-browser-svelte": "^2.0.1", - }, - }, - }, - "packages": { - "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], - - "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - - "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - - "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], - - "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - - "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - - "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - - "@eslint/compat": ["@eslint/compat@1.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0" }, "peerDependencies": { "eslint": "^8.40 || 9" }, "optionalPeers": ["eslint"] }, "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w=="], - - "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], - - "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], - - "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], - - "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], - - "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], - - "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], - - "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - - "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], - - "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], - - "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], - - "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], - - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - - "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], - - "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], - - "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], - - "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], - - "@node-rs/argon2": ["@node-rs/argon2@2.0.2", "", { "optionalDependencies": { "@node-rs/argon2-android-arm-eabi": "2.0.2", "@node-rs/argon2-android-arm64": "2.0.2", "@node-rs/argon2-darwin-arm64": "2.0.2", "@node-rs/argon2-darwin-x64": "2.0.2", "@node-rs/argon2-freebsd-x64": "2.0.2", "@node-rs/argon2-linux-arm-gnueabihf": "2.0.2", "@node-rs/argon2-linux-arm64-gnu": "2.0.2", "@node-rs/argon2-linux-arm64-musl": "2.0.2", "@node-rs/argon2-linux-x64-gnu": "2.0.2", "@node-rs/argon2-linux-x64-musl": "2.0.2", "@node-rs/argon2-wasm32-wasi": "2.0.2", "@node-rs/argon2-win32-arm64-msvc": "2.0.2", "@node-rs/argon2-win32-ia32-msvc": "2.0.2", "@node-rs/argon2-win32-x64-msvc": "2.0.2" } }, "sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg=="], - - "@node-rs/argon2-android-arm-eabi": ["@node-rs/argon2-android-arm-eabi@2.0.2", "", { "os": "android", "cpu": "arm" }, "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw=="], - - "@node-rs/argon2-android-arm64": ["@node-rs/argon2-android-arm64@2.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg=="], - - "@node-rs/argon2-darwin-arm64": ["@node-rs/argon2-darwin-arm64@2.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA=="], - - "@node-rs/argon2-darwin-x64": ["@node-rs/argon2-darwin-x64@2.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw=="], - - "@node-rs/argon2-freebsd-x64": ["@node-rs/argon2-freebsd-x64@2.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg=="], - - "@node-rs/argon2-linux-arm-gnueabihf": ["@node-rs/argon2-linux-arm-gnueabihf@2.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww=="], - - "@node-rs/argon2-linux-arm64-gnu": ["@node-rs/argon2-linux-arm64-gnu@2.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew=="], - - "@node-rs/argon2-linux-arm64-musl": ["@node-rs/argon2-linux-arm64-musl@2.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA=="], - - "@node-rs/argon2-linux-x64-gnu": ["@node-rs/argon2-linux-x64-gnu@2.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ZM3jrHuJ0dKOhvA80gKJqBpBRmTJTFSo2+xVZR+phQcbAKRlDMSZMFDiKbSTnctkfwNFtjgDdh5g1vaEV04AvA=="], - - "@node-rs/argon2-linux-x64-musl": ["@node-rs/argon2-linux-x64-musl@2.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw=="], - - "@node-rs/argon2-wasm32-wasi": ["@node-rs/argon2-wasm32-wasi@2.0.2", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.5" }, "cpu": "none" }, "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ=="], - - "@node-rs/argon2-win32-arm64-msvc": ["@node-rs/argon2-win32-arm64-msvc@2.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ=="], - - "@node-rs/argon2-win32-ia32-msvc": ["@node-rs/argon2-win32-ia32-msvc@2.0.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ=="], - - "@node-rs/argon2-win32-x64-msvc": ["@node-rs/argon2-win32-x64-msvc@2.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw=="], - - "@oslojs/asn1": ["@oslojs/asn1@1.0.0", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="], - - "@oslojs/binary": ["@oslojs/binary@1.0.0", "", {}, "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ=="], - - "@oslojs/crypto": ["@oslojs/crypto@1.0.1", "", { "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" } }, "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ=="], - - "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], - - "@playwright/test": ["@playwright/test@1.58.0", "", { "dependencies": { "playwright": "1.58.0" }, "bin": { "playwright": "cli.js" } }, "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg=="], - - "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - - "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.9", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA=="], - - "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="], - - "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="], - - "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.56.0", "", { "os": "android", "cpu": "arm" }, "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.56.0", "", { "os": "android", "cpu": "arm64" }, "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.56.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.56.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.56.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.56.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.56.0", "", { "os": "linux", "cpu": "arm" }, "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.56.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg=="], - - "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw=="], - - "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.56.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.56.0", "", { "os": "linux", "cpu": "none" }, "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.56.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.56.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA=="], - - "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.56.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.56.0", "", { "os": "none", "cpu": "arm64" }, "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.56.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.56.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg=="], - - "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ=="], - - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.56.0", "", { "os": "win32", "cpu": "x64" }, "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="], - - "@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.2", "", { "dependencies": { "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.9.5" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-L15Djwpr7HrSAPj/Z8PYfc0pa9A1tllrr18phKI0WJHJeoWw45yinPf0IGgVTmakqx1B3JQ+C/OFl9ZwmxHU1Q=="], - - "@sveltejs/kit": ["@sveltejs/kit@2.50.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.6.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-XRHD2i3zC4ukhz2iCQzO4mbsts081PAZnnMAQ7LNpWeYgeBmwMsalf0FGSwhFXBbtr2XViPKnFJBDCckWqrsLw=="], - - "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], - - "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], - - "@tailwindcss/forms": ["@tailwindcss/forms@0.5.11", "", { "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA=="], - - "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], - - "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], - - "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], - - "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], - - "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], - - "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], - - "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], - - "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], - - "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], - - "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], - - "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], - - "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], - - "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], - - "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], - - "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], - - "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], - - "@testing-library/svelte-core": ["@testing-library/svelte-core@1.0.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" } }, "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ=="], - - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - - "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], - - "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], - - "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - - "@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], - - "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], - - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/type-utils": "8.53.1", "@typescript-eslint/utils": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag=="], - - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg=="], - - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.1", "@typescript-eslint/types": "^8.53.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog=="], - - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1" } }, "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ=="], - - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA=="], - - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w=="], - - "@typescript-eslint/types": ["@typescript-eslint/types@8.53.1", "", {}, "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A=="], - - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.1", "@typescript-eslint/tsconfig-utils": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/visitor-keys": "8.53.1", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg=="], - - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.1", "@typescript-eslint/types": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg=="], - - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.1", "", { "dependencies": { "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg=="], - - "@vitest/browser": ["@vitest/browser@4.0.18", "", { "dependencies": { "@vitest/mocker": "4.0.18", "@vitest/utils": "4.0.18", "magic-string": "^0.30.21", "pixelmatch": "7.1.0", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.0.3", "ws": "^8.18.3" }, "peerDependencies": { "vitest": "4.0.18" } }, "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng=="], - - "@vitest/browser-playwright": ["@vitest/browser-playwright@4.0.18", "", { "dependencies": { "@vitest/browser": "4.0.18", "@vitest/mocker": "4.0.18", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "playwright": "*", "vitest": "4.0.18" } }, "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g=="], - - "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], - - "@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="], - - "@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="], - - "@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="], - - "@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="], - - "@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="], - - "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - - "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - - "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], - - "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], - - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], - - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "devalue": ["devalue@5.6.2", "", {}, "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg=="], - - "drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], - - "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], - - "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], - - "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], - - "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - - "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], - - "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], - - "eslint-config-prettier": ["eslint-config-prettier@10.1.8", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w=="], - - "eslint-plugin-svelte": ["eslint-plugin-svelte@3.14.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.6.1", "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", "svelte-eslint-parser": "^1.4.0" }, "peerDependencies": { "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-Isw0GvaMm0yHxAj71edAdGFh28ufYs+6rk2KlbbZphnqZAzrH3Se3t12IFh2H9+1F/jlDhBBL4oiOJmLqmYX0g=="], - - "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], - - "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - - "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], - - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], - - "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], - - "esrap": ["esrap@2.2.2", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ=="], - - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - - "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], - - "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], - - "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], - - "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - - "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], - - "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], - - "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - - "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - - "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], - - "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - - "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], - - "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], - - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - - "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], - - "known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="], - - "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], - - "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], - - "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], - - "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], - - "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], - - "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], - - "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], - - "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], - - "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], - - "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], - - "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], - - "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], - - "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], - - "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], - - "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], - - "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - - "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], - - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - - "mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="], - - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], - - "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], - - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - - "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], - - "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - - "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], - - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], - - "pixelmatch": ["pixelmatch@7.1.0", "", { "dependencies": { "pngjs": "^7.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng=="], - - "playwright": ["playwright@1.58.0", "", { "dependencies": { "playwright-core": "1.58.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ=="], - - "playwright-core": ["playwright-core@1.58.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw=="], - - "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], - - "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - - "postcss-load-config": ["postcss-load-config@3.1.4", "", { "dependencies": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" }, "peerDependencies": { "postcss": ">=8.0.9", "ts-node": ">=9.0.0" }, "optionalPeers": ["postcss", "ts-node"] }, "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg=="], - - "postcss-safe-parser": ["postcss-safe-parser@7.0.1", "", { "peerDependencies": { "postcss": "^8.4.31" } }, "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A=="], - - "postcss-scss": ["postcss-scss@4.0.9", "", { "peerDependencies": { "postcss": "^8.4.29" } }, "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A=="], - - "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], - - "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="], - - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], - - "prettier-plugin-svelte": ["prettier-plugin-svelte@3.4.1", "", { "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "sha512-xL49LCloMoZRvSwa6IEdN2GV6cq2IqpYGstYtMT+5wmml1/dClEoI0MZR78MiVPpu6BdQFfN0/y73yO6+br5Pg=="], - - "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.7.2", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-svelte"] }, "sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA=="], - - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], - - "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], - - "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], - - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - - "rollup": ["rollup@4.56.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.56.0", "@rollup/rollup-android-arm64": "4.56.0", "@rollup/rollup-darwin-arm64": "4.56.0", "@rollup/rollup-darwin-x64": "4.56.0", "@rollup/rollup-freebsd-arm64": "4.56.0", "@rollup/rollup-freebsd-x64": "4.56.0", "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", "@rollup/rollup-linux-arm-musleabihf": "4.56.0", "@rollup/rollup-linux-arm64-gnu": "4.56.0", "@rollup/rollup-linux-arm64-musl": "4.56.0", "@rollup/rollup-linux-loong64-gnu": "4.56.0", "@rollup/rollup-linux-loong64-musl": "4.56.0", "@rollup/rollup-linux-ppc64-gnu": "4.56.0", "@rollup/rollup-linux-ppc64-musl": "4.56.0", "@rollup/rollup-linux-riscv64-gnu": "4.56.0", "@rollup/rollup-linux-riscv64-musl": "4.56.0", "@rollup/rollup-linux-s390x-gnu": "4.56.0", "@rollup/rollup-linux-x64-gnu": "4.56.0", "@rollup/rollup-linux-x64-musl": "4.56.0", "@rollup/rollup-openbsd-x64": "4.56.0", "@rollup/rollup-openharmony-arm64": "4.56.0", "@rollup/rollup-win32-arm64-msvc": "4.56.0", "@rollup/rollup-win32-ia32-msvc": "4.56.0", "@rollup/rollup-win32-x64-gnu": "4.56.0", "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg=="], - - "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], - - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - - "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], - - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], - - "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "svelte": ["svelte@5.48.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.2", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-+NUe82VoFP1RQViZI/esojx70eazGF4u0O/9ucqZ4rPcOZD+n5EVp17uYsqwdzjUjZyTpGKunHbDziW6AIAVkQ=="], - - "svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="], - - "svelte-eslint-parser": ["svelte-eslint-parser@1.4.1", "", { "dependencies": { "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.0.0", "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", "postcss-selector-parser": "^7.0.0" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA=="], - - "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], - - "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], - - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - - "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], - - "tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="], - - "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], - - "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - - "typescript-eslint": ["typescript-eslint@8.53.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.1", "@typescript-eslint/parser": "8.53.1", "@typescript-eslint/typescript-estree": "8.53.1", "@typescript-eslint/utils": "8.53.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg=="], - - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], - - "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], - - "vite-plugin-devtools-json": ["vite-plugin-devtools-json@1.0.0", "", { "dependencies": { "uuid": "^11.1.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw=="], - - "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], - - "vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="], - - "vitest-browser-svelte": ["vitest-browser-svelte@2.0.2", "", { "dependencies": { "@testing-library/svelte-core": "^1.0.0" }, "peerDependencies": { "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", "vitest": "^4.0.0" } }, "sha512-OLJVYoIYflwToFIy3s41pZ9mVp6dwXfYd8IIsWoc57g8DyN3SxsNJ5GB1xWFPxLFlKM+1MPExjPxLaqdELrfRQ=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - - "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], - - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - - "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], - - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], - - "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], - - "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - - "@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], - - "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - - "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], - - "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - - "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - - "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "svelte-eslint-parser/postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], - - "vite/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], - - "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], - - "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], - - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], - - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], - - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], - - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], - - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], - - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], - - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], - - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], - - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], - - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], - - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], - - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], - - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], - - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], - - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], - - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], - - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], - - "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], - - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], - - "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], - - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], - - "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], - - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], - - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], - - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], - - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], - } -} diff --git a/frontend/drizzle.config.ts b/frontend/drizzle.config.ts deleted file mode 100644 index d57e081..0000000 --- a/frontend/drizzle.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'drizzle-kit'; - -if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); - -export default defineConfig({ - schema: './src/lib/server/db/schema.ts', - dialect: 'postgresql', - dbCredentials: { url: process.env.DATABASE_URL }, - verbose: true, - strict: true -}); diff --git a/frontend/e2e/demo.test.ts b/frontend/e2e/demo.test.ts deleted file mode 100644 index 9985ce1..0000000 --- a/frontend/e2e/demo.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { expect, test } from '@playwright/test'; - -test('home page has expected h1', async ({ page }) => { - await page.goto('/'); - await expect(page.locator('h1')).toBeVisible(); -}); diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js deleted file mode 100644 index 7df665d..0000000 --- a/frontend/eslint.config.js +++ /dev/null @@ -1,41 +0,0 @@ -import prettier from 'eslint-config-prettier'; -import { fileURLToPath } from 'node:url'; -import { includeIgnoreFile } from '@eslint/compat'; -import js from '@eslint/js'; -import svelte from 'eslint-plugin-svelte'; -import { defineConfig } from 'eslint/config'; -import globals from 'globals'; -import ts from 'typescript-eslint'; -import svelteConfig from './svelte.config.js'; - -const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); - -export default defineConfig( - includeIgnoreFile(gitignorePath), - js.configs.recommended, - ...ts.configs.recommended, - ...svelte.configs.recommended, - prettier, - ...svelte.configs.prettier, - { - languageOptions: { globals: { ...globals.browser, ...globals.node } }, - - rules: { - // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. - // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors - 'no-undef': 'off' - } - }, - { - files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], - - languageOptions: { - parserOptions: { - projectService: true, - extraFileExtensions: ['.svelte'], - parser: ts.parser, - svelteConfig - } - } - } -); diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 79cdc92..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "frontend", - "private": true, - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "prepare": "svelte-kit sync || echo ''", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "prettier --check . && eslint .", - "format": "prettier --write .", - "test:unit": "vitest", - "test": "npm run test:unit -- --run && npm run test:e2e", - "test:e2e": "playwright test", - "db:push": "drizzle-kit push", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio" - }, - "devDependencies": { - "@eslint/compat": "^1.4.0", - "@eslint/js": "^9.39.1", - "@oslojs/crypto": "^1.0.1", - "@oslojs/encoding": "^1.1.0", - "@playwright/test": "^1.57.0", - "@sveltejs/adapter-node": "^5.4.0", - "@sveltejs/kit": "^2.49.1", - "@sveltejs/vite-plugin-svelte": "^6.2.1", - "@tailwindcss/forms": "^0.5.10", - "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.17", - "@types/node": "^22", - "@vitest/browser-playwright": "^4.0.15", - "drizzle-kit": "^0.31.8", - "drizzle-orm": "^0.45.0", - "eslint": "^9.39.1", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-svelte": "^3.13.1", - "globals": "^16.5.0", - "playwright": "^1.57.0", - "prettier": "^3.7.4", - "prettier-plugin-svelte": "^3.4.0", - "prettier-plugin-tailwindcss": "^0.7.2", - "svelte": "^5.45.6", - "svelte-check": "^4.3.4", - "tailwindcss": "^4.1.17", - "typescript": "^5.9.3", - "typescript-eslint": "^8.48.1", - "vite": "^7.2.6", - "vite-plugin-devtools-json": "^1.0.0", - "vitest": "^4.0.15", - "vitest-browser-svelte": "^2.0.1" - }, - "dependencies": { - "@node-rs/argon2": "^2.0.2", - "postgres": "^3.4.7" - } -} diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts deleted file mode 100644 index 8f5062c..0000000 --- a/frontend/playwright.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from '@playwright/test'; - -export default defineConfig({ - webServer: { command: 'npm run build && npm run preview', port: 4173 }, - testDir: 'e2e' -}); diff --git a/frontend/src/app.d.ts b/frontend/src/app.d.ts deleted file mode 100644 index fc79747..0000000 --- a/frontend/src/app.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces -declare global { - namespace App { - interface Locals { - user: import('$lib/server/auth').SessionValidationResult['user']; - session: import('$lib/server/auth').SessionValidationResult['session']; - } - - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/frontend/src/app.html b/frontend/src/app.html deleted file mode 100644 index f273cc5..0000000 --- a/frontend/src/app.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/frontend/src/demo.spec.ts b/frontend/src/demo.spec.ts deleted file mode 100644 index e07cbbd..0000000 --- a/frontend/src/demo.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -describe('sum test', () => { - it('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); - }); -}); diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts deleted file mode 100644 index 3c37e24..0000000 --- a/frontend/src/hooks.server.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Handle } from '@sveltejs/kit'; -import * as auth from '$lib/server/auth'; - -const handleAuth: Handle = async ({ event, resolve }) => { - const sessionToken = event.cookies.get(auth.sessionCookieName); - - if (!sessionToken) { - event.locals.user = null; - event.locals.session = null; - - return resolve(event); - } - - const { session, user } = await auth.validateSessionToken(sessionToken); - - if (session) { - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - } else { - auth.deleteSessionTokenCookie(event); - } - - event.locals.user = user; - event.locals.session = session; - - return resolve(event); -}; - -export const handle: Handle = handleAuth; diff --git a/frontend/src/lib/assets/favicon.svg b/frontend/src/lib/assets/favicon.svg deleted file mode 100644 index cc5dc66..0000000 --- a/frontend/src/lib/assets/favicon.svg +++ /dev/null @@ -1 +0,0 @@ -svelte-logo \ No newline at end of file diff --git a/frontend/src/lib/index.ts b/frontend/src/lib/index.ts deleted file mode 100644 index 856f2b6..0000000 --- a/frontend/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/frontend/src/lib/server/auth.ts b/frontend/src/lib/server/auth.ts deleted file mode 100644 index 38c9930..0000000 --- a/frontend/src/lib/server/auth.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { RequestEvent } from '@sveltejs/kit'; -import { eq } from 'drizzle-orm'; -import { sha256 } from '@oslojs/crypto/sha2'; -import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding'; -import { db } from '$lib/server/db'; -import * as table from '$lib/server/db/schema'; - -const DAY_IN_MS = 1000 * 60 * 60 * 24; - -export const sessionCookieName = 'auth-session'; - -export function generateSessionToken() { - const bytes = crypto.getRandomValues(new Uint8Array(18)); - const token = encodeBase64url(bytes); - return token; -} - -export async function createSession(token: string, userId: string) { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const session: table.Session = { - id: sessionId, - userId, - expiresAt: new Date(Date.now() + DAY_IN_MS * 30) - }; - await db.insert(table.session).values(session); - return session; -} - -export async function validateSessionToken(token: string) { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const [result] = await db - .select({ - // Adjust user table here to tweak returned data - user: { id: table.user.id, username: table.user.username }, - session: table.session - }) - .from(table.session) - .innerJoin(table.user, eq(table.session.userId, table.user.id)) - .where(eq(table.session.id, sessionId)); - - if (!result) { - return { session: null, user: null }; - } - const { session, user } = result; - - const sessionExpired = Date.now() >= session.expiresAt.getTime(); - if (sessionExpired) { - await db.delete(table.session).where(eq(table.session.id, session.id)); - return { session: null, user: null }; - } - - const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15; - if (renewSession) { - session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30); - await db - .update(table.session) - .set({ expiresAt: session.expiresAt }) - .where(eq(table.session.id, session.id)); - } - - return { session, user }; -} - -export type SessionValidationResult = Awaited>; - -export async function invalidateSession(sessionId: string) { - await db.delete(table.session).where(eq(table.session.id, sessionId)); -} - -export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) { - event.cookies.set(sessionCookieName, token, { - expires: expiresAt, - path: '/' - }); -} - -export function deleteSessionTokenCookie(event: RequestEvent) { - event.cookies.delete(sessionCookieName, { - path: '/' - }); -} diff --git a/frontend/src/lib/server/db/index.ts b/frontend/src/lib/server/db/index.ts deleted file mode 100644 index fb8b355..0000000 --- a/frontend/src/lib/server/db/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { drizzle } from 'drizzle-orm/postgres-js'; -import postgres from 'postgres'; -import * as schema from './schema'; -import { env } from '$env/dynamic/private'; - -if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set'); - -const client = postgres(env.DATABASE_URL); - -export const db = drizzle(client, { schema }); diff --git a/frontend/src/lib/server/db/schema.ts b/frontend/src/lib/server/db/schema.ts deleted file mode 100644 index cbb6d1a..0000000 --- a/frontend/src/lib/server/db/schema.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { pgTable, serial, integer, text, timestamp } from 'drizzle-orm/pg-core'; - -export const user = pgTable('user', { - id: text('id').primaryKey(), - age: integer('age'), - username: text('username').notNull().unique(), - passwordHash: text('password_hash').notNull() -}); - -export const session = pgTable('session', { - id: text('id').primaryKey(), - userId: text('user_id') - .notNull() - .references(() => user.id), - expiresAt: timestamp('expires_at', { withTimezone: true, mode: 'date' }).notNull() -}); - -export type Session = typeof session.$inferSelect; - -export type User = typeof user.$inferSelect; diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte deleted file mode 100644 index 0d8eb03..0000000 --- a/frontend/src/routes/+layout.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - -{@render children()} diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte deleted file mode 100644 index cc88df0..0000000 --- a/frontend/src/routes/+page.svelte +++ /dev/null @@ -1,2 +0,0 @@ -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

diff --git a/frontend/src/routes/demo/+page.svelte b/frontend/src/routes/demo/+page.svelte deleted file mode 100644 index 501e687..0000000 --- a/frontend/src/routes/demo/+page.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - -lucia diff --git a/frontend/src/routes/demo/lucia/+page.server.ts b/frontend/src/routes/demo/lucia/+page.server.ts deleted file mode 100644 index 9fe83e5..0000000 --- a/frontend/src/routes/demo/lucia/+page.server.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as auth from '$lib/server/auth'; -import { fail, redirect } from '@sveltejs/kit'; -import { getRequestEvent } from '$app/server'; -import type { Actions, PageServerLoad } from './$types'; - -export const load: PageServerLoad = async () => { - const user = requireLogin(); - return { user }; -}; - -export const actions: Actions = { - logout: async (event) => { - if (!event.locals.session) { - return fail(401); - } - await auth.invalidateSession(event.locals.session.id); - auth.deleteSessionTokenCookie(event); - - return redirect(302, '/demo/lucia/login'); - } -}; - -function requireLogin() { - const { locals } = getRequestEvent(); - - if (!locals.user) { - return redirect(302, '/demo/lucia/login'); - } - - return locals.user; -} diff --git a/frontend/src/routes/demo/lucia/+page.svelte b/frontend/src/routes/demo/lucia/+page.svelte deleted file mode 100644 index cefb2d1..0000000 --- a/frontend/src/routes/demo/lucia/+page.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -

Hi, {data.user.username}!

-

Your user ID is {data.user.id}.

-
- -
diff --git a/frontend/src/routes/demo/lucia/login/+page.server.ts b/frontend/src/routes/demo/lucia/login/+page.server.ts deleted file mode 100644 index 97d1235..0000000 --- a/frontend/src/routes/demo/lucia/login/+page.server.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { hash, verify } from '@node-rs/argon2'; -import { encodeBase32LowerCase } from '@oslojs/encoding'; -import { fail, redirect } from '@sveltejs/kit'; -import { eq } from 'drizzle-orm'; -import * as auth from '$lib/server/auth'; -import { db } from '$lib/server/db'; -import * as table from '$lib/server/db/schema'; -import type { Actions, PageServerLoad } from './$types'; - -export const load: PageServerLoad = async (event) => { - if (event.locals.user) { - return redirect(302, '/demo/lucia'); - } - return {}; -}; - -export const actions: Actions = { - login: async (event) => { - const formData = await event.request.formData(); - const username = formData.get('username'); - const password = formData.get('password'); - - if (!validateUsername(username)) { - return fail(400, { - message: 'Invalid username (min 3, max 31 characters, alphanumeric only)' - }); - } - if (!validatePassword(password)) { - return fail(400, { message: 'Invalid password (min 6, max 255 characters)' }); - } - - const results = await db.select().from(table.user).where(eq(table.user.username, username)); - - const existingUser = results.at(0); - if (!existingUser) { - return fail(400, { message: 'Incorrect username or password' }); - } - - const validPassword = await verify(existingUser.passwordHash, password, { - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1 - }); - if (!validPassword) { - return fail(400, { message: 'Incorrect username or password' }); - } - - const sessionToken = auth.generateSessionToken(); - const session = await auth.createSession(sessionToken, existingUser.id); - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - - return redirect(302, '/demo/lucia'); - }, - register: async (event) => { - const formData = await event.request.formData(); - const username = formData.get('username'); - const password = formData.get('password'); - - if (!validateUsername(username)) { - return fail(400, { message: 'Invalid username' }); - } - if (!validatePassword(password)) { - return fail(400, { message: 'Invalid password' }); - } - - const userId = generateUserId(); - const passwordHash = await hash(password, { - // recommended minimum parameters - memoryCost: 19456, - timeCost: 2, - outputLen: 32, - parallelism: 1 - }); - - try { - await db.insert(table.user).values({ id: userId, username, passwordHash }); - - const sessionToken = auth.generateSessionToken(); - const session = await auth.createSession(sessionToken, userId); - auth.setSessionTokenCookie(event, sessionToken, session.expiresAt); - } catch { - return fail(500, { message: 'An error has occurred' }); - } - return redirect(302, '/demo/lucia'); - } -}; - -function generateUserId() { - // ID with 120 bits of entropy, or about the same as UUID v4. - const bytes = crypto.getRandomValues(new Uint8Array(15)); - const id = encodeBase32LowerCase(bytes); - return id; -} - -function validateUsername(username: unknown): username is string { - return ( - typeof username === 'string' && - username.length >= 3 && - username.length <= 31 && - /^[a-z0-9_-]+$/.test(username) - ); -} - -function validatePassword(password: unknown): password is string { - return typeof password === 'string' && password.length >= 6 && password.length <= 255; -} diff --git a/frontend/src/routes/demo/lucia/login/+page.svelte b/frontend/src/routes/demo/lucia/login/+page.svelte deleted file mode 100644 index 39c8037..0000000 --- a/frontend/src/routes/demo/lucia/login/+page.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -

Login/Register

-
- - - - -
-

{form?.message ?? ''}

diff --git a/frontend/src/routes/layout.css b/frontend/src/routes/layout.css deleted file mode 100644 index cd67023..0000000 --- a/frontend/src/routes/layout.css +++ /dev/null @@ -1,3 +0,0 @@ -@import 'tailwindcss'; -@plugin '@tailwindcss/forms'; -@plugin '@tailwindcss/typography'; diff --git a/frontend/src/routes/page.svelte.spec.ts b/frontend/src/routes/page.svelte.spec.ts deleted file mode 100644 index d54f00a..0000000 --- a/frontend/src/routes/page.svelte.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { page } from 'vitest/browser'; -import { describe, expect, it } from 'vitest'; -import { render } from 'vitest-browser-svelte'; -import Page from './+page.svelte'; - -describe('/+page.svelte', () => { - it('should render h1', async () => { - render(Page); - - const heading = page.getByRole('heading', { level: 1 }); - await expect.element(heading).toBeInTheDocument(); - }); -}); diff --git a/frontend/static/robots.txt b/frontend/static/robots.txt deleted file mode 100644 index b6dd667..0000000 --- a/frontend/static/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# allow crawling everything by default -User-agent: * -Disallow: diff --git a/frontend/svelte.config.js b/frontend/svelte.config.js deleted file mode 100644 index e744595..0000000 --- a/frontend/svelte.config.js +++ /dev/null @@ -1,6 +0,0 @@ -import adapter from '@sveltejs/adapter-node'; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { kit: { adapter: adapter() } }; - -export default config; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json deleted file mode 100644 index 2c2ed3c..0000000 --- a/frontend/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "rewriteRelativeImportExtensions": true, - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // To make changes to top-level options such as include and exclude, we recommend extending - // the generated config; see https://svelte.dev/docs/kit/configuration#typescript -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts deleted file mode 100644 index c4ffa6e..0000000 --- a/frontend/vite.config.ts +++ /dev/null @@ -1,43 +0,0 @@ -import devtoolsJson from 'vite-plugin-devtools-json'; -import tailwindcss from '@tailwindcss/vite'; -import { defineConfig } from 'vitest/config'; -import { playwright } from '@vitest/browser-playwright'; -import { sveltekit } from '@sveltejs/kit/vite'; - -export default defineConfig({ - plugins: [tailwindcss(), sveltekit(), devtoolsJson()], - - test: { - expect: { requireAssertions: true }, - - projects: [ - { - extends: './vite.config.ts', - - test: { - name: 'client', - - browser: { - enabled: true, - provider: playwright(), - instances: [{ browser: 'chromium', headless: true }] - }, - - include: ['src/**/*.svelte.{test,spec}.{js,ts}'], - exclude: ['src/lib/server/**'] - } - }, - - { - extends: './vite.config.ts', - - test: { - name: 'server', - environment: 'node', - include: ['src/**/*.{test,spec}.{js,ts}'], - exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'] - } - } - ] - } -});