From 9972537b63fe94a7c51686a2ec3a94ecd2680ab2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 3 Oct 2026 12:16:59 +0200 Subject: [PATCH] Add a small, tested base for all three clients, and Gitea CI/release workflows internal/client (new): a typed Go HTTP client for the API - login, register, logout, me, and full CRUD for games/platforms - shared by the TUI and the Fyne GUI so request-building, auth and error handling (an APIError with the HTTP status) live in one tested place instead of being duplicated. 8 tests against an httptest server covering success, 401, 403 and that the token is stored/cleared at the right times. cmd/tui (rewrite): drops the old unauthenticated read-only viewer. Now a login screen (bubbles/textinput) -> a scrollable games table (bubbles/table) with 'r' to refresh, built as an explicit state machine (screenLogin/screenLoading/screenGames) so Update's transitions are testable without a real terminal. 9 tests driving Update() directly. cmd/gui (rewrite): drops raylib for Fyne. Same shape as the TUI - a login form, then a games table, with networking on a goroutine and fyne.Do for the UI updates. The table-cell and login-validation logic are plain functions with no Fyne/display dependency, so they're tested directly (7 tests) rather than needing Fyne's headless test driver. internal/api: tagged every operation (Auth/Platforms/Games/Misc) so the generated OpenAPI spec groups into named services instead of one bucket; api/openapi.json regenerated. Added CORS middleware (wildcard origin, no credentials needed since clients use a bearer token, not the cookie) - needed for the new web client's browser requests to the API's a different origin. web/ (new): an Angular 22 + Material app, API client generated from api/openapi.json via ng-openapi-gen (services: true for per-resource classes - AuthService/GamesService/PlatformsService - instead of one generic Api facade). A Session service holds the token/user as signals, persists the token to localStorage, and is the only thing that calls the generated AuthService; an HttpInterceptorFn attaches the bearer token to every request; a CanActivateFn guard protects /games. Login and Games are standalone, lazy-loaded route components. 21 tests (Vitest, Angular's new default - no browser/Karma needed) covering the service, interceptor, guard and both components, run via `npm test` exactly as CI will call it. Justfile: fixed run-tui/run-gui, which used `go run .../main.go` - broken now that both packages span multiple files, since that only compiles the one named file. Added web-install/web-dev/web-build/web-test. .gitea/workflows/ci.yml (moved from .github, which Gitea doesn't read): on push to main/develop - gofmt, go vet, go build, go test, go mod tidy diff (with the Fyne build's system deps installed), sqlc diff, and the web app's build+test. .gitea/workflows/release.yml (new): on a vX.Y.Z tag, cross-compiles api+tui for linux/darwin/windows (amd64+arm64, pure Go, no cgo), builds the Fyne GUI for linux/amd64 only (cgo + its OpenGL/X11 link means it can't cross-compile from one Linux runner the way api/tui do, and this repo has no registered macOS/Windows runner to build it on natively - documented in the workflow's comments), builds the web app, and creates a Gitea release with all of it attached via the Gitea API. Written to Gitea's documented GitHub-Actions-compatible syntax but not executable from here - there's no Gitea server in this environment to run it against, unlike everything else in this commit, which was. Verified: every Go package builds/vets/tests clean; sqlc diff and go mod tidy -diff are clean; the Angular app builds for production and its test suite passes via `npm test`/`npm run build` (the exact commands CI runs), including after a clean `npm ci` from the committed lockfile; the new CORS config was checked with real preflight and GET requests against a running server, not just read. Co-Authored-By: Claude Sonnet 5 --- .gitea/workflows/ci.yml | 81 + .gitea/workflows/release.yml | 168 + .github/workflows/ci.yml | 39 - Justfile | 26 +- README.md | 51 +- api/openapi.json | 2 +- cmd/api/main.go | 9 + cmd/gui/app.go | 117 + cmd/gui/main.go | 147 +- cmd/gui/table.go | 44 + cmd/gui/table_test.go | 59 + cmd/gui/validate.go | 16 + cmd/gui/validate_test.go | 23 + cmd/tui/main.go | 142 +- cmd/tui/model.go | 298 + cmd/tui/model_test.go | 179 + go.mod | 33 +- go.sum | 72 +- internal/api/auth_routes.go | 4 + internal/api/routes.go | 11 + internal/client/client.go | 218 + internal/client/client_test.go | 186 + web/.editorconfig | 17 + web/.gitignore | 44 + web/.prettierrc | 12 + web/.vscode/extensions.json | 4 + web/.vscode/launch.json | 20 + web/.vscode/tasks.json | 42 + web/README.md | 59 + web/angular.json | 71 + web/ng-openapi-gen.json | 7 + web/package-lock.json | 7131 +++++++++++++++++ web/package.json | 36 + web/public/favicon.ico | Bin 0 -> 15086 bytes web/src/app/api/api-configuration.ts | 27 + web/src/app/api/api.ts | 64 + web/src/app/api/base-service.ts | 35 + web/src/app/api/fn/auth/get-me.ts | 30 + web/src/app/api/fn/auth/login.ts | 33 + web/src/app/api/fn/auth/logout.ts | 29 + web/src/app/api/fn/auth/register.ts | 33 + web/src/app/api/fn/games/create-game.ts | 33 + web/src/app/api/fn/games/delete-game.ts | 31 + web/src/app/api/fn/games/get-game.ts | 32 + web/src/app/api/fn/games/list-games.ts | 30 + web/src/app/api/fn/games/update-game.ts | 35 + web/src/app/api/fn/misc/get-hello.ts | 30 + .../app/api/fn/platforms/create-platform.ts | 33 + .../app/api/fn/platforms/delete-platform.ts | 31 + web/src/app/api/fn/platforms/get-platform.ts | 32 + .../app/api/fn/platforms/list-platforms.ts | 30 + .../app/api/fn/platforms/update-platform.ts | 35 + web/src/app/api/functions.ts | 33 + web/src/app/api/models.ts | 14 + web/src/app/api/models/auth-response.ts | 13 + web/src/app/api/models/error-detail.ts | 20 + web/src/app/api/models/error-model.ts | 41 + web/src/app/api/models/game-input.ts | 23 + web/src/app/api/models/game.ts | 25 + web/src/app/api/models/get-hello-response.ts | 11 + web/src/app/api/models/login-input.ts | 12 + web/src/app/api/models/platform-input.ts | 15 + web/src/app/api/models/platform.ts | 12 + web/src/app/api/models/register-input.ts | 13 + web/src/app/api/models/user.ts | 15 + web/src/app/api/request-builder.ts | 369 + web/src/app/api/services.ts | 7 + web/src/app/api/services/auth.service.ts | 161 + web/src/app/api/services/games.service.ts | 195 + web/src/app/api/services/misc.service.ts | 55 + web/src/app/api/services/platforms.service.ts | 195 + web/src/app/api/strict-http-response.ts | 11 + web/src/app/app.config.ts | 24 + web/src/app/app.routes.ts | 16 + web/src/app/app.spec.ts | 18 + web/src/app/app.ts | 9 + web/src/app/core/auth-guard.spec.ts | 43 + web/src/app/core/auth-guard.ts | 12 + web/src/app/core/auth-interceptor.spec.ts | 45 + web/src/app/core/auth-interceptor.ts | 13 + web/src/app/core/session.spec.ts | 90 + web/src/app/core/session.ts | 92 + web/src/app/features/games/games.css | 26 + web/src/app/features/games/games.html | 51 + web/src/app/features/games/games.spec.ts | 109 + web/src/app/features/games/games.ts | 58 + web/src/app/features/login/login.css | 18 + web/src/app/features/login/login.html | 32 + web/src/app/features/login/login.spec.ts | 84 + web/src/app/features/login/login.ts | 68 + web/src/index.html | 23 + web/src/main.ts | 6 + web/src/material-theme.scss | 38 + web/src/styles.css | 1 + web/tsconfig.app.json | 14 + web/tsconfig.json | 31 + web/tsconfig.spec.json | 14 + 97 files changed, 11810 insertions(+), 336 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitea/workflows/release.yml delete mode 100644 .github/workflows/ci.yml create mode 100644 cmd/gui/app.go create mode 100644 cmd/gui/table.go create mode 100644 cmd/gui/table_test.go create mode 100644 cmd/gui/validate.go create mode 100644 cmd/gui/validate_test.go create mode 100644 cmd/tui/model.go create mode 100644 cmd/tui/model_test.go create mode 100644 internal/client/client.go create mode 100644 internal/client/client_test.go create mode 100644 web/.editorconfig create mode 100644 web/.gitignore create mode 100644 web/.prettierrc create mode 100644 web/.vscode/extensions.json create mode 100644 web/.vscode/launch.json create mode 100644 web/.vscode/tasks.json create mode 100644 web/README.md create mode 100644 web/angular.json create mode 100644 web/ng-openapi-gen.json create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/public/favicon.ico create mode 100644 web/src/app/api/api-configuration.ts create mode 100644 web/src/app/api/api.ts create mode 100644 web/src/app/api/base-service.ts create mode 100644 web/src/app/api/fn/auth/get-me.ts create mode 100644 web/src/app/api/fn/auth/login.ts create mode 100644 web/src/app/api/fn/auth/logout.ts create mode 100644 web/src/app/api/fn/auth/register.ts create mode 100644 web/src/app/api/fn/games/create-game.ts create mode 100644 web/src/app/api/fn/games/delete-game.ts create mode 100644 web/src/app/api/fn/games/get-game.ts create mode 100644 web/src/app/api/fn/games/list-games.ts create mode 100644 web/src/app/api/fn/games/update-game.ts create mode 100644 web/src/app/api/fn/misc/get-hello.ts create mode 100644 web/src/app/api/fn/platforms/create-platform.ts create mode 100644 web/src/app/api/fn/platforms/delete-platform.ts create mode 100644 web/src/app/api/fn/platforms/get-platform.ts create mode 100644 web/src/app/api/fn/platforms/list-platforms.ts create mode 100644 web/src/app/api/fn/platforms/update-platform.ts create mode 100644 web/src/app/api/functions.ts create mode 100644 web/src/app/api/models.ts create mode 100644 web/src/app/api/models/auth-response.ts create mode 100644 web/src/app/api/models/error-detail.ts create mode 100644 web/src/app/api/models/error-model.ts create mode 100644 web/src/app/api/models/game-input.ts create mode 100644 web/src/app/api/models/game.ts create mode 100644 web/src/app/api/models/get-hello-response.ts create mode 100644 web/src/app/api/models/login-input.ts create mode 100644 web/src/app/api/models/platform-input.ts create mode 100644 web/src/app/api/models/platform.ts create mode 100644 web/src/app/api/models/register-input.ts create mode 100644 web/src/app/api/models/user.ts create mode 100644 web/src/app/api/request-builder.ts create mode 100644 web/src/app/api/services.ts create mode 100644 web/src/app/api/services/auth.service.ts create mode 100644 web/src/app/api/services/games.service.ts create mode 100644 web/src/app/api/services/misc.service.ts create mode 100644 web/src/app/api/services/platforms.service.ts create mode 100644 web/src/app/api/strict-http-response.ts create mode 100644 web/src/app/app.config.ts create mode 100644 web/src/app/app.routes.ts create mode 100644 web/src/app/app.spec.ts create mode 100644 web/src/app/app.ts create mode 100644 web/src/app/core/auth-guard.spec.ts create mode 100644 web/src/app/core/auth-guard.ts create mode 100644 web/src/app/core/auth-interceptor.spec.ts create mode 100644 web/src/app/core/auth-interceptor.ts create mode 100644 web/src/app/core/session.spec.ts create mode 100644 web/src/app/core/session.ts create mode 100644 web/src/app/features/games/games.css create mode 100644 web/src/app/features/games/games.html create mode 100644 web/src/app/features/games/games.spec.ts create mode 100644 web/src/app/features/games/games.ts create mode 100644 web/src/app/features/login/login.css create mode 100644 web/src/app/features/login/login.html create mode 100644 web/src/app/features/login/login.spec.ts create mode 100644 web/src/app/features/login/login.ts create mode 100644 web/src/index.html create mode 100644 web/src/main.ts create mode 100644 web/src/material-theme.scss create mode 100644 web/src/styles.css create mode 100644 web/tsconfig.app.json create mode 100644 web/tsconfig.json create mode 100644 web/tsconfig.spec.json diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..d67ddf4 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,81 @@ +name: CI + +on: + push: + branches: [main, develop] + +jobs: + go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + # cmd/gui (Fyne) uses cgo and links against the system's OpenGL/X11 + # libraries, so building or testing ./... needs these present even + # though the GUI itself never opens a window in CI. + - name: Install system dependencies for the Fyne GUI build + run: | + sudo apt-get update + sudo apt-get install -y libgl1-mesa-dev xorg-dev + + - name: gofmt + run: | + fmt_out="$(gofmt -l .)" + if [ -n "$fmt_out" ]; then + echo "The following files are not gofmt'd:" + echo "$fmt_out" + exit 1 + fi + + - name: go vet + run: go vet ./... + + - name: go build + run: go build ./... + + - name: go test + run: go test ./... + + - name: go mod tidy (no diff expected) + run: | + go mod tidy + git diff --exit-code -- go.mod go.sum + + sqlc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: sqlc-dev/setup-sqlc@v4 + with: + sqlc-version: "1.31.1" + + - name: sqlc diff (generated code must match the schema/queries) + run: sqlc diff + + web: + runs-on: ubuntu-latest + defaults: + run: + working-directory: web + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Test + run: npm test diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..13136d8 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,168 @@ +name: Release + +# A pushed tag like v1.2.3 builds every client and attaches the result to a +# Gitea release of the same name. Tag a commit that has already passed CI +# on main; this workflow doesn't re-run those checks. +on: + push: + tags: ["v*.*.*"] + +jobs: + # cmd/api and cmd/tui are pure Go (no cgo), so they cross-compile cleanly + # from one Linux runner for every OS/arch below. + build-go: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + - goos: windows + goarch: amd64 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Build api and tui + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: "0" + run: | + mkdir -p dist + ext="" + if [ "${{ matrix.goos }}" = "windows" ]; then ext=".exe"; fi + go build -o "dist/completed-api${ext}" ./cmd/api + go build -o "dist/completed-tui${ext}" ./cmd/tui + + - name: Archive + run: | + cd dist + base="completed_${{ matrix.goos }}_${{ matrix.goarch }}" + if [ "${{ matrix.goos }}" = "windows" ]; then + zip "../${base}.zip" completed-api.exe completed-tui.exe + else + tar czf "../${base}.tar.gz" completed-api completed-tui + fi + + - uses: actions/upload-artifact@v4 + with: + name: completed_${{ matrix.goos }}_${{ matrix.goarch }} + path: completed_${{ matrix.goos }}_${{ matrix.goarch }}.* + retention-days: 7 + + # cmd/gui (Fyne) uses cgo and links the system's native GL/windowing + # libraries, so - unlike build-go above - it can't be cross-compiled for + # other operating systems from this one Linux runner. This job only + # produces a linux/amd64 build. A macOS or Windows build of the GUI would + # need this workflow extended with a job running on a matching + # self-hosted runner (there isn't one registered for this repo today). + build-gui-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgl1-mesa-dev xorg-dev + + - name: Build gui + run: | + mkdir -p dist + go build -o dist/completed-gui ./cmd/gui + + - name: Archive + run: tar czf completed-gui_linux_amd64.tar.gz -C dist completed-gui + + - uses: actions/upload-artifact@v4 + with: + name: completed-gui_linux_amd64 + path: completed-gui_linux_amd64.tar.gz + retention-days: 7 + + build-web: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: web/package-lock.json + + - name: Build + working-directory: web + run: | + npm ci + npm run build + + - name: Archive + run: tar czf completed-web_dist.tar.gz -C web/dist/web . + + - uses: actions/upload-artifact@v4 + with: + name: completed-web_dist + path: completed-web_dist.tar.gz + retention-days: 7 + + release: + needs: [build-go, build-gui-linux, build-web] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: artifacts + merge-multiple: true + + - name: Create the Gitea release and upload each artifact + env: + # Gitea Actions auto-provides this for the compatibility GITHUB_* + # context; it needs the repo's Settings > Actions permissions set + # to allow writing contents. If your instance doesn't populate it, + # replace with a secret holding a personal/repo access token that + # has repo write access instead. + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SERVER_URL: ${{ github.server_url }} + REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + sudo apt-get update && sudo apt-get install -y jq + + release_json=$(curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"draft\":false,\"prerelease\":false}" \ + "${SERVER_URL}/api/v1/repos/${REPO}/releases") + release_id=$(echo "$release_json" | jq -r '.id') + + if [ -z "$release_id" ] || [ "$release_id" = "null" ]; then + echo "Failed to create release: $release_json" >&2 + exit 1 + fi + + for f in artifacts/*; do + name=$(basename "$f") + echo "Uploading $name" + curl -sf -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -F "attachment=@${f}" \ + "${SERVER_URL}/api/v1/repos/${REPO}/releases/${release_id}/assets?name=${name}" + done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 15c8c3d..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: CI - -on: - push: - branches: [main] - pull_request: - -jobs: - go: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: go vet - run: go vet ./... - - - name: go test - run: go test ./... - - - name: go mod tidy (no diff expected) - run: | - go mod tidy - git diff --exit-code -- go.mod go.sum - - sqlc: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: sqlc-dev/setup-sqlc@v4 - with: - sqlc-version: "1.31.1" - - - name: sqlc diff (generated code must match the schema/queries) - run: sqlc diff diff --git a/Justfile b/Justfile index 60b56ef..a9677a6 100644 --- a/Justfile +++ b/Justfile @@ -12,7 +12,7 @@ build-api: # Run the API server locally run-api: - go run ./cmd/api/main.go + go run ./cmd/api # Start the API and Database using Docker Compose docker-up: @@ -42,17 +42,35 @@ build-tui: # Run the Terminal UI run-tui: - go run ./cmd/tui/main.go + go run ./cmd/tui # --- GUI --- -# Build the GUI (Raylib) +# Build the GUI (Fyne) build-gui: go build -o bin/gui ./cmd/gui # Run the GUI run-gui: - go run ./cmd/gui/main.go + go run ./cmd/gui + +# --- Web --- + +# Install web dependencies +web-install: + cd web && npm ci + +# Run the web app's dev server +web-dev: + cd web && npm start + +# Build the web app for production (output: web/dist/web) +web-build: + cd web && npm run build + +# Run the web app's tests +web-test: + cd web && npm test # --- Database --- diff --git a/README.md b/README.md index c61868b..9dbef52 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,12 @@ your score for it, and whether (and when) you finished it. ## Status -Each account has its own private collection. The API is usable; there's no -GUI yet beyond two read-only clients (see below). +Each account has its own private game list, but all accounts share one +platform list (only an admin can add/rename/delete a platform - the first +account ever registered becomes an admin automatically). The API is usable. +There are three small-base clients: a terminal UI, a desktop GUI (Fyne), +and a web app (Angular) — each can log in and show your games, with more +to come. ## Running it @@ -28,7 +32,8 @@ Browse `http://localhost:8080/docs` for the generated API docs. ## Using the API ```sh -# Register (also logs you in) +# Register (also logs you in; the first account ever registered becomes +# an admin, which is required to create platforms - see Status above) curl -X POST localhost:8080/api/auth/register -H 'Content-Type: application/json' \ -d '{"email":"you@example.com","password":"a long password","name":"You"}' # -> {"token": "...", "user": {...}} @@ -44,19 +49,47 @@ curl localhost:8080/api/games -H "Authorization: Bearer $TOKEN" A browser-based client can skip the `Authorization` header: register/login also set an httpOnly `session` cookie. -## Other clients +## Clients -- `just run-tui` — a read-only terminal viewer of `/games` (no login yet) -- `just run-gui` — a read-only raylib desktop viewer of `/games` (no login - yet; planned to move to Fyne) +- `just run-tui` — a terminal UI (Bubble Tea). Log in, see your games in a + scrollable table, `r` to refresh. +- `just run-gui` — a desktop GUI (Fyne). Log in, see your games in a table, + refresh. +- `just web-dev` — a web app (Angular + Material) at `http://localhost:4200`, + proxying nothing itself; it talks directly to the API at `localhost:8080` + (CORS is permissive enough for local dev as-is). Log in, see your games. + +All three point at `http://localhost:8080` by default. The TUI and GUI read +`COMPLETED_API_URL` to use a different API; the web app's URL is a build-time +constant in `web/src/app/app.config.ts`. + +Each client is a small base - listing is there, but adding/editing a game +or managing platforms isn't yet. ## Development ```sh -just test # go test ./... -just sqlc # regenerate internal/db after changing db/query.sql or a migration +just test # go test ./... +just sqlc # regenerate internal/db after changing db/query.sql or a migration +just openapi # regenerate api/openapi.json after changing a route +just web-test # web app unit tests (Vitest) +just web-build # production build of the web app ``` Schema changes go in `db/migrations` as a new `_.up.sql` / `.down.sql` pair; `sqlc.yaml` reads the schema from that directory directly, so there's no separate schema file to keep in sync. + +The TUI and GUI share `internal/client`, a typed Go HTTP client for the API. +The web app's API client (`web/src/app/api/`) is generated from +`api/openapi.json` by `ng-openapi-gen` (`cd web && npx ng-openapi-gen`) and +shouldn't be hand-edited. + +## CI/CD + +`.gitea/workflows/ci.yml` builds and tests everything (Go + web) on every +push to `main` or `develop`. `.gitea/workflows/release.yml` builds all three +clients and attaches them to a Gitea release when a `vX.Y.Z` tag is pushed; +see that file's comments for its one real limitation (the desktop GUI is +only built for linux/amd64, since it needs cgo and this repo has no +registered macOS/Windows runner to cross-build it on). diff --git a/api/openapi.json b/api/openapi.json index 4a277ef..6d044c9 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -1 +1 @@ -{"components":{"schemas":{"AuthResponse":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/AuthResponse.json"],"format":"uri","readOnly":true,"type":"string"},"token":{"type":"string"},"user":{"$ref":"#/components/schemas/User"}},"required":["token","user"],"type":"object"},"ErrorDetail":{"additionalProperties":false,"properties":{"location":{"description":"Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'","type":"string"},"message":{"description":"Error message text","type":"string"},"value":{"description":"The value at the given location"}},"type":"object"},"ErrorModel":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ErrorModel.json"],"format":"uri","readOnly":true,"type":"string"},"detail":{"description":"A human-readable explanation specific to this occurrence of the problem.","examples":["Property foo is required but is missing."],"type":"string"},"errors":{"description":"Optional list of individual error details","items":{"$ref":"#/components/schemas/ErrorDetail"},"type":["array","null"]},"instance":{"description":"A URI reference that identifies the specific occurrence of the problem.","examples":["https://example.com/error-log/abc123"],"format":"uri","type":"string"},"status":{"description":"HTTP status code","examples":[400],"format":"int64","type":"integer"},"title":{"description":"A short, human-readable summary of the problem type. This value should not change between occurrences of the error.","examples":["Bad Request"],"type":"string"},"type":{"default":"about:blank","description":"A URI reference to human-readable documentation for the error.","examples":["https://example.com/errors/example"],"format":"uri","type":"string"}},"type":"object"},"Game":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Game.json"],"format":"uri","readOnly":true,"type":"string"},"finished":{"description":"YYYY-MM-DD (optional)","format":"date","type":"string"},"id":{"type":"string"},"name":{"type":"string"},"platform_id":{"type":"string"},"platform_name":{"type":"string"},"release_date":{"description":"YYYY-MM-DD","format":"date","type":"string"},"score":{"format":"int32","type":"integer"}},"required":["id","name","platform_id","platform_name","score","release_date"],"type":"object"},"GameInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/GameInput.json"],"format":"uri","readOnly":true,"type":"string"},"finished":{"description":"YYYY-MM-DD (optional)","format":"date","type":"string"},"name":{"minLength":1,"type":"string"},"platform_id":{"type":"string"},"release_date":{"description":"YYYY-MM-DD","format":"date","type":"string"},"score":{"format":"int32","maximum":100,"minimum":0,"type":"integer"}},"required":["name","platform_id","score","release_date"],"type":"object"},"Get-helloResponse":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Get-helloResponse.json"],"format":"uri","readOnly":true,"type":"string"},"message":{"type":"string"}},"required":["message"],"type":"object"},"LoginInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/LoginInput.json"],"format":"uri","readOnly":true,"type":"string"},"email":{"format":"email","type":"string"},"password":{"type":"string"}},"required":["email","password"],"type":"object"},"Platform":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Platform.json"],"format":"uri","readOnly":true,"type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"type":"object"},"PlatformInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/PlatformInput.json"],"format":"uri","readOnly":true,"type":"string"},"name":{"description":"Name of the platform","minLength":1,"type":"string"}},"required":["name"],"type":"object"},"RegisterInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RegisterInput.json"],"format":"uri","readOnly":true,"type":"string"},"email":{"format":"email","type":"string"},"name":{"minLength":1,"type":"string"},"password":{"minLength":8,"type":"string"}},"required":["email","password","name"],"type":"object"},"User":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/User.json"],"format":"uri","readOnly":true,"type":"string"},"bio":{"type":"string"},"email":{"type":"string"},"id":{"type":"string"},"is_admin":{"type":"boolean"},"name":{"type":"string"}},"required":["id","email","name","is_admin"],"type":"object"}}},"info":{"title":"Completed API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/auth/login":{"post":{"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}},"description":"OK","headers":{"Set-Cookie":{"schema":{"type":"string"}}}},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Log in to an existing account"}},"/api/auth/logout":{"post":{"operationId":"logout","responses":{"204":{"description":"No Content","headers":{"Set-Cookie":{"schema":{"type":"string"}}}},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Log out of the current session"}},"/api/auth/me":{"get":{"operationId":"get-me","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Get the current user"}},"/api/auth/register":{"post":{"operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}},"description":"OK","headers":{"Set-Cookie":{"schema":{"type":"string"}}}},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Register a new account"}},"/api/games":{"get":{"operationId":"list-games","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Game"},"type":["array","null"]}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List Games"},"post":{"operationId":"create-game","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Game"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Create Game"}},"/api/games/{id}":{"delete":{"operationId":"delete-game","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Delete Game"},"get":{"operationId":"get-game","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Game"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Get Game"},"put":{"operationId":"update-game","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Game"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Update Game"}},"/api/platforms":{"get":{"operationId":"list-platforms","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Platform"},"type":["array","null"]}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List Platforms"},"post":{"operationId":"create-platform","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Platform"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Create Platform"}},"/api/platforms/{id}":{"delete":{"operationId":"delete-platform","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Delete Platform"},"get":{"operationId":"get-platform","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Platform"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Get Platform"},"put":{"operationId":"update-platform","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Platform"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Update Platform"}},"/hello":{"get":{"description":"Returns a friendly hello message.","operationId":"get-hello","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Get-helloResponse"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Say Hello"}}}} +{"components":{"schemas":{"AuthResponse":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/AuthResponse.json"],"format":"uri","readOnly":true,"type":"string"},"token":{"type":"string"},"user":{"$ref":"#/components/schemas/User"}},"required":["token","user"],"type":"object"},"ErrorDetail":{"additionalProperties":false,"properties":{"location":{"description":"Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'","type":"string"},"message":{"description":"Error message text","type":"string"},"value":{"description":"The value at the given location"}},"type":"object"},"ErrorModel":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ErrorModel.json"],"format":"uri","readOnly":true,"type":"string"},"detail":{"description":"A human-readable explanation specific to this occurrence of the problem.","examples":["Property foo is required but is missing."],"type":"string"},"errors":{"description":"Optional list of individual error details","items":{"$ref":"#/components/schemas/ErrorDetail"},"type":["array","null"]},"instance":{"description":"A URI reference that identifies the specific occurrence of the problem.","examples":["https://example.com/error-log/abc123"],"format":"uri","type":"string"},"status":{"description":"HTTP status code","examples":[400],"format":"int64","type":"integer"},"title":{"description":"A short, human-readable summary of the problem type. This value should not change between occurrences of the error.","examples":["Bad Request"],"type":"string"},"type":{"default":"about:blank","description":"A URI reference to human-readable documentation for the error.","examples":["https://example.com/errors/example"],"format":"uri","type":"string"}},"type":"object"},"Game":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Game.json"],"format":"uri","readOnly":true,"type":"string"},"finished":{"description":"YYYY-MM-DD (optional)","format":"date","type":"string"},"id":{"type":"string"},"name":{"type":"string"},"platform_id":{"type":"string"},"platform_name":{"type":"string"},"release_date":{"description":"YYYY-MM-DD","format":"date","type":"string"},"score":{"format":"int32","type":"integer"}},"required":["id","name","platform_id","platform_name","score","release_date"],"type":"object"},"GameInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/GameInput.json"],"format":"uri","readOnly":true,"type":"string"},"finished":{"description":"YYYY-MM-DD (optional)","format":"date","type":"string"},"name":{"minLength":1,"type":"string"},"platform_id":{"type":"string"},"release_date":{"description":"YYYY-MM-DD","format":"date","type":"string"},"score":{"format":"int32","maximum":100,"minimum":0,"type":"integer"}},"required":["name","platform_id","score","release_date"],"type":"object"},"Get-helloResponse":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Get-helloResponse.json"],"format":"uri","readOnly":true,"type":"string"},"message":{"type":"string"}},"required":["message"],"type":"object"},"LoginInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/LoginInput.json"],"format":"uri","readOnly":true,"type":"string"},"email":{"format":"email","type":"string"},"password":{"type":"string"}},"required":["email","password"],"type":"object"},"Platform":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/Platform.json"],"format":"uri","readOnly":true,"type":"string"},"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"type":"object"},"PlatformInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/PlatformInput.json"],"format":"uri","readOnly":true,"type":"string"},"name":{"description":"Name of the platform","minLength":1,"type":"string"}},"required":["name"],"type":"object"},"RegisterInput":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RegisterInput.json"],"format":"uri","readOnly":true,"type":"string"},"email":{"format":"email","type":"string"},"name":{"minLength":1,"type":"string"},"password":{"minLength":8,"type":"string"}},"required":["email","password","name"],"type":"object"},"User":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/User.json"],"format":"uri","readOnly":true,"type":"string"},"bio":{"type":"string"},"email":{"type":"string"},"id":{"type":"string"},"is_admin":{"type":"boolean"},"name":{"type":"string"}},"required":["id","email","name","is_admin"],"type":"object"}}},"info":{"title":"Completed API","version":"1.0.0"},"openapi":"3.1.0","paths":{"/api/auth/login":{"post":{"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}},"description":"OK","headers":{"Set-Cookie":{"schema":{"type":"string"}}}},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Log in to an existing account","tags":["Auth"]}},"/api/auth/logout":{"post":{"operationId":"logout","responses":{"204":{"description":"No Content","headers":{"Set-Cookie":{"schema":{"type":"string"}}}},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Log out of the current session","tags":["Auth"]}},"/api/auth/me":{"get":{"operationId":"get-me","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Get the current user","tags":["Auth"]}},"/api/auth/register":{"post":{"operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthResponse"}}},"description":"OK","headers":{"Set-Cookie":{"schema":{"type":"string"}}}},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Register a new account","tags":["Auth"]}},"/api/games":{"get":{"operationId":"list-games","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Game"},"type":["array","null"]}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List Games","tags":["Games"]},"post":{"operationId":"create-game","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Game"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Create Game","tags":["Games"]}},"/api/games/{id}":{"delete":{"operationId":"delete-game","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Delete Game","tags":["Games"]},"get":{"operationId":"get-game","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Game"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Get Game","tags":["Games"]},"put":{"operationId":"update-game","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Game"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Update Game","tags":["Games"]}},"/api/platforms":{"get":{"operationId":"list-platforms","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/Platform"},"type":["array","null"]}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List Platforms","tags":["Platforms"]},"post":{"operationId":"create-platform","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Platform"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Create Platform","tags":["Platforms"]}},"/api/platforms/{id}":{"delete":{"operationId":"delete-platform","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"No Content"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Delete Platform","tags":["Platforms"]},"get":{"operationId":"get-platform","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Platform"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Get Platform","tags":["Platforms"]},"put":{"operationId":"update-platform","parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlatformInput"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Platform"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Update Platform","tags":["Platforms"]}},"/hello":{"get":{"description":"Returns a friendly hello message.","operationId":"get-hello","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Get-helloResponse"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Say Hello","tags":["Misc"]}}}} diff --git a/cmd/api/main.go b/cmd/api/main.go index 7209db3..9dce95a 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -131,6 +131,15 @@ func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) + // The web client (e.g. the Angular dev server on localhost:4200) calls + // this API from a different origin. It authenticates with a bearer + // token, not cookies, so this doesn't need AllowCredentials - which + // means a wildcard origin is safe here. Tighten AllowOrigins to the + // real deployed origin(s) if this ever serves more than local dev. + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"*"}, + AllowHeaders: []string{echo.HeaderContentType, echo.HeaderAuthorization}, + })) humaApi := humaecho.New(e, huma.DefaultConfig("Completed API", "1.0.0")) diff --git a/cmd/gui/app.go b/cmd/gui/app.go new file mode 100644 index 0000000..9b879c0 --- /dev/null +++ b/cmd/gui/app.go @@ -0,0 +1,117 @@ +package main + +import ( + "context" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/widget" + + "completed/internal/client" + "completed/internal/model" +) + +// guiApp holds the window and the single client all screens share. It's a +// small base: log in, see your games in a table, refresh. Adding/editing +// games is for a later pass. +type guiApp struct { + client *client.Client + window fyne.Window +} + +func newGUIApp(c *client.Client, w fyne.Window) *guiApp { + return &guiApp{client: c, window: w} +} + +// showLogin builds and displays the login form. It's the window's initial +// content. +func (a *guiApp) showLogin() { + email := widget.NewEntry() + email.SetPlaceHolder("email") + password := widget.NewPasswordEntry() + password.SetPlaceHolder("password") + status := widget.NewLabel("") + + form := widget.NewForm( + widget.NewFormItem("Email", email), + widget.NewFormItem("Password", password), + ) + form.SubmitText = "Log in" + form.OnSubmit = func() { + if err := validateLogin(email.Text, password.Text); err != nil { + status.SetText(err.Error()) + return + } + status.SetText("Logging in...") + a.login(email.Text, password.Text, status) + } + + a.window.SetContent(container.NewVBox( + widget.NewLabelWithStyle("Completed", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), + form, + status, + )) +} + +// login runs the login request off the UI goroutine, then either shows the +// games screen or reports the error back on the login form - both via +// fyne.Do, since Fyne widgets may only be touched from the UI goroutine. +func (a *guiApp) login(email, password string, status *widget.Label) { + go func() { + _, err := a.client.Login(context.Background(), email, password) + if err != nil { + fyne.Do(func() { status.SetText(err.Error()) }) + return + } + fyne.Do(a.showGames) + }() +} + +// showGames displays the games table and fetches the current list. +func (a *guiApp) showGames() { + var games []model.Game + + table := widget.NewTable( + func() (int, int) { return gameTableSize(games) }, + func() fyne.CanvasObject { return widget.NewLabel("") }, + func(id widget.TableCellID, obj fyne.CanvasObject) { + obj.(*widget.Label).SetText(gameCellText(games, id.Row, id.Col)) + }, + ) + for i := range gameColumns { + table.SetColumnWidth(i, 120) + } + + status := widget.NewLabel("Loading...") + refresh := widget.NewButton("Refresh", func() { + a.fetchGames(table, &games, status) + }) + + a.window.SetContent(container.NewBorder( + container.NewVBox( + widget.NewLabelWithStyle("Completed", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), + container.NewHBox(refresh, status), + ), + nil, nil, nil, + table, + )) + + a.fetchGames(table, &games, status) +} + +func (a *guiApp) fetchGames(table *widget.Table, games *[]model.Game, status *widget.Label) { + go func() { + result, err := a.client.ListGames(context.Background()) + fyne.Do(func() { + if err != nil { + status.SetText("Error: " + err.Error()) + dialog.ShowError(err, a.window) + return + } + *games = result + status.SetText("") + table.Refresh() + }) + }() +} diff --git a/cmd/gui/main.go b/cmd/gui/main.go index 79d4853..7f100a2 100644 --- a/cmd/gui/main.go +++ b/cmd/gui/main.go @@ -1,145 +1,22 @@ package main import ( - "encoding/json" - "fmt" - "net/http" - "time" + "os" - rl "github.com/gen2brain/raylib-go/raylib" + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + + "completed/internal/client" ) -type Game struct { - ID int64 `json:"id"` - Name string `json:"name"` - PlatformID int64 `json:"platform_id"` - Score int32 `json:"score"` - ReleaseYear string `json:"release_year"` - Finished string `json:"finished,omitempty"` -} - -type GamesResponse struct { - Body []Game `json:"body"` -} - func main() { - rl.InitWindow(800, 600, "My Game Collection - Raylib") - defer rl.CloseWindow() + fyneApp := app.New() + window := fyneApp.NewWindow("Completed") + window.Resize(fyne.NewSize(700, 500)) - rl.SetTargetFPS(60) + c := client.New(os.Getenv("COMPLETED_API_URL")) + a := newGUIApp(c, window) + a.showLogin() - loading := false - var err error - var games []Game - - // Channel to receive data from background goroutine - dataChan := make(chan []Game) - errChan := make(chan error) - - func() { - loading = true - go fetchGames(dataChan, errChan) - }() - - for !rl.WindowShouldClose() { - rl.BeginDrawing() - rl.ClearBackground(rl.RayWhite) - - // Header - rl.DrawRectangle(0, 0, 800, 60, rl.NewColor(100, 100, 255, 255)) - rl.DrawText("My Game Collection", 20, 15, 30, rl.White) - - startY := 80 - - if loading { - rl.DrawText("Loading games...", 20, int32(startY), 20, rl.Gray) - // Simple spinner animation - if int(rl.GetTime()*4)%2 == 0 { - rl.DrawText(".", 180, int32(startY), 20, rl.Gray) - } - } else if err != nil { - rl.DrawText(fmt.Sprintf("Error: %v", err), 20, int32(startY), 20, rl.Red) - rl.DrawText("Press SPACE to retry", 20, int32(startY)+30, 20, rl.DarkGray) - } else { - if len(games) == 0 { - rl.DrawText("No games found in database.", 20, int32(startY), 20, rl.DarkGray) - } else { - for i, g := range games { - yPos := int32(startY + (i * 40)) - - // Row background (alternating) - if i%2 == 0 { - rl.DrawRectangle(20, yPos-5, 760, 35, rl.NewColor(240, 240, 240, 255)) - } - - // Game Name - rl.DrawText(g.Name, 30, yPos, 20, rl.Black) - - // Year - rl.DrawText(g.ReleaseYear, 350, yPos, 20, rl.DarkGray) - - // Score - scoreColor := rl.Orange - if g.Score >= 90 { - scoreColor = rl.Green - } else if g.Score < 70 { - scoreColor = rl.Red - } - rl.DrawText(fmt.Sprintf("%d", g.Score), 500, yPos, 20, scoreColor) - - // Finished Status - if g.Finished != "" { - rl.DrawText("Finished", 600, yPos, 20, rl.NewColor(0, 200, 0, 255)) - } - } - } - } - - // Footer / Help - rl.DrawText("Press SPACE to refresh", 20, 570, 10, rl.Gray) - - rl.EndDrawing() - - // Inputs - if rl.IsKeyPressed(rl.KeySpace) && !loading { - loading = true - err = nil - games = nil - go fetchGames(dataChan, errChan) - } - - // Check channels (non-blocking) - select { - case data := <-dataChan: - loading = false - games = data - case e := <-errChan: - loading = false - err = e - default: - } - } -} - -func fetchGames(dataChan chan<- []Game, errChan chan<- error) { - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Get("http://localhost:8080/games") - if err != nil { - errChan <- err - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - errChan <- fmt.Errorf("status code: %d", resp.StatusCode) - return - } - - var data GamesResponse - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - errChan <- err - return - } - - dataChan <- data.Body + window.ShowAndRun() } diff --git a/cmd/gui/table.go b/cmd/gui/table.go new file mode 100644 index 0000000..ddeec6e --- /dev/null +++ b/cmd/gui/table.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + + "completed/internal/model" +) + +// gameColumns are the games table's column headers, in order. Kept next to +// gameCellText so the two can't drift apart. +var gameColumns = []string{"Name", "Platform", "Score", "Released", "Finished"} + +// gameTableSize returns the (rows, cols) Fyne's widget.Table needs from its +// Length function: one row per game, plus a header row. +func gameTableSize(games []model.Game) (rows, cols int) { + return len(games) + 1, len(gameColumns) +} + +// gameCellText returns the text for row/col in the games table. row 0 is +// the header; row i (i>0) is games[i-1]. It has no Fyne dependency, so it's +// tested directly without needing any widget or display. +func gameCellText(games []model.Game, row, col int) string { + if row == 0 { + return gameColumns[col] + } + g := games[row-1] + switch col { + case 0: + return g.Name + case 1: + return g.PlatformName + case 2: + return fmt.Sprintf("%d", g.Score) + case 3: + return g.ReleaseDate + case 4: + if g.Finished != "" { + return "✓" + } + return "" + default: + return "" + } +} diff --git a/cmd/gui/table_test.go b/cmd/gui/table_test.go new file mode 100644 index 0000000..61f6f77 --- /dev/null +++ b/cmd/gui/table_test.go @@ -0,0 +1,59 @@ +package main + +import ( + "testing" + + "completed/internal/model" +) + +func TestGameTableSize(t *testing.T) { + games := []model.Game{{Name: "Doom"}, {Name: "Half-Life"}} + + rows, cols := gameTableSize(games) + if rows != 3 { + t.Errorf("rows = %d, want 3 (header + 2 games)", rows) + } + if cols != len(gameColumns) { + t.Errorf("cols = %d, want %d", cols, len(gameColumns)) + } +} + +func TestGameTableSizeEmpty(t *testing.T) { + rows, _ := gameTableSize(nil) + if rows != 1 { + t.Errorf("rows = %d, want 1 (header only)", rows) + } +} + +func TestGameCellTextHeader(t *testing.T) { + for col, want := range gameColumns { + if got := gameCellText(nil, 0, col); got != want { + t.Errorf("gameCellText(nil, 0, %d) = %q, want %q", col, got, want) + } + } +} + +func TestGameCellTextDataRows(t *testing.T) { + games := []model.Game{ + {Name: "Doom", PlatformName: "PC", Score: 95, ReleaseDate: "1993-12-10"}, + {Name: "Half-Life", PlatformName: "PC", Score: 96, ReleaseDate: "1998-11-19", Finished: "1999-01-01"}, + } + + tests := []struct { + row, col int + want string + }{ + {1, 0, "Doom"}, + {1, 1, "PC"}, + {1, 2, "95"}, + {1, 3, "1993-12-10"}, + {1, 4, ""}, + {2, 0, "Half-Life"}, + {2, 4, "✓"}, + } + for _, tt := range tests { + if got := gameCellText(games, tt.row, tt.col); got != tt.want { + t.Errorf("gameCellText(games, %d, %d) = %q, want %q", tt.row, tt.col, got, tt.want) + } + } +} diff --git a/cmd/gui/validate.go b/cmd/gui/validate.go new file mode 100644 index 0000000..e7261a2 --- /dev/null +++ b/cmd/gui/validate.go @@ -0,0 +1,16 @@ +package main + +import "errors" + +// validateLogin checks the login form before it's sent to the server, so a +// blank field shows an immediate, specific error instead of an HTTP +// round trip followed by a generic one. +func validateLogin(email, password string) error { + if email == "" { + return errors.New("email is required") + } + if password == "" { + return errors.New("password is required") + } + return nil +} diff --git a/cmd/gui/validate_test.go b/cmd/gui/validate_test.go new file mode 100644 index 0000000..3906f75 --- /dev/null +++ b/cmd/gui/validate_test.go @@ -0,0 +1,23 @@ +package main + +import "testing" + +func TestValidateLogin(t *testing.T) { + tests := []struct { + name, email, password string + wantErr bool + }{ + {"valid", "a@example.com", "secret", false}, + {"empty email", "", "secret", true}, + {"empty password", "a@example.com", "", true}, + {"both empty", "", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateLogin(tt.email, tt.password) + if (err != nil) != tt.wantErr { + t.Errorf("validateLogin(%q, %q) error = %v, wantErr %v", tt.email, tt.password, err, tt.wantErr) + } + }) + } +} diff --git a/cmd/tui/main.go b/cmd/tui/main.go index d40ac05..937c6eb 100644 --- a/cmd/tui/main.go +++ b/cmd/tui/main.go @@ -1,150 +1,20 @@ package main import ( - "encoding/json" "fmt" - "net/http" "os" - "strings" - "time" - "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" + + "completed/internal/client" ) -// Styles -var ( - titleStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(lipgloss.Color("#FAFAFA")). - Background(lipgloss.Color("#7D56F4")). - Padding(0, 1) - - itemStyle = lipgloss.NewStyle(). - PaddingLeft(2) - - scoreStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFD700")) // Gold color - - errorStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FF5555")). - Bold(true) -) - -type Game struct { - ID int64 `json:"id"` - Name string `json:"name"` - PlatformID int64 `json:"platform_id"` - Score int32 `json:"score"` - ReleaseYear string `json:"release_year"` - Finished string `json:"finished,omitempty"` -} - -type GamesResponse struct { - Body []Game `json:"body"` -} - -type Model struct { - spinner spinner.Model - loading bool - err error - games []Game - quitting bool -} - -func InitialModel() Model { - s := spinner.New() - s.Spinner = spinner.Dot - s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) - return Model{spinner: s, loading: true} -} - -type loadedMsg []Game -type errMsg error - -func fetchGames() tea.Msg { - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Get("http://localhost:8080/games") - if err != nil { - return errMsg(err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return errMsg(fmt.Errorf("status code: %d", resp.StatusCode)) - } - - var data GamesResponse - if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { - return errMsg(err) - } - - return loadedMsg(data.Body) -} - -func (m Model) Init() tea.Cmd { - return tea.Batch(m.spinner.Tick, fetchGames) -} - -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.KeyMsg: - if msg.String() == "q" || msg.String() == "ctrl+c" { - m.quitting = true - return m, tea.Quit - } - case spinner.TickMsg: - var cmd tea.Cmd - m.spinner, cmd = m.spinner.Update(msg) - return m, cmd - case loadedMsg: - m.loading = false - m.games = msg - return m, nil - case errMsg: - m.loading = false - m.err = msg - return m, nil - } - return m, nil -} - -func (m Model) View() string { - if m.quitting { - return "Bye!\n" - } - - s := titleStyle.Render("My Game Collection") + "\n\n" - - if m.loading { - s += fmt.Sprintf("%s Loading games...", m.spinner.View()) - } else if m.err != nil { - s += errorStyle.Render(fmt.Sprintf("Error: %v", m.err)) - s += "\nMake sure the API is running on localhost:8080" - } else { - if len(m.games) == 0 { - s += "No games found." - } else { - for _, g := range m.games { - score := scoreStyle.Render(fmt.Sprintf("★ %d", g.Score)) - line := fmt.Sprintf("• %s (%s) - %s", g.Name, g.ReleaseYear, score) - if g.Finished != "" { - line += " [✓ Finished]" - } - s += itemStyle.Render(line) + "\n" - } - } - } - - s += strings.Repeat("\n", 2) + lipgloss.NewStyle().Foreground(lipgloss.Color("#555")).Render("Press 'q' to quit") - return s -} - func main() { - p := tea.NewProgram(InitialModel()) + c := client.New(os.Getenv("COMPLETED_API_URL")) + + p := tea.NewProgram(NewModel(c)) if _, err := p.Run(); err != nil { - fmt.Printf("Alas, there's been an error: %v", err) + fmt.Fprintf(os.Stderr, "Alas, there's been an error: %v\n", err) os.Exit(1) } } diff --git a/cmd/tui/model.go b/cmd/tui/model.go new file mode 100644 index 0000000..cda432b --- /dev/null +++ b/cmd/tui/model.go @@ -0,0 +1,298 @@ +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/spinner" + "github.com/charmbracelet/bubbles/table" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "completed/internal/client" + "completed/internal/model" +) + +// screen is which part of the app is currently showing. The model only +// ever does one thing at a time, so a single enum (rather than a flag per +// feature) keeps Update's branching simple. +type screen int + +const ( + screenLogin screen = iota + screenLoading + screenGames +) + +// focusField is which of the two login inputs has keyboard focus. +type focusField int + +const ( + focusEmail focusField = iota + focusPassword +) + +var ( + titleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#FAFAFA")). + Background(lipgloss.Color("#7D56F4")). + Padding(0, 1) + + errorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FF5555")). + Bold(true) + + helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#555")) + + focusedInputStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")) +) + +// Model is the TUI's whole state. It's a small base: log in, see your +// games in a scrollable table, refresh. Adding/editing games is for a +// later pass; the point here is a working, tested skeleton the rest of +// the TUI can grow from. +type Model struct { + client *client.Client + + screen screen + err error + + // Login screen. + emailInput textinput.Model + passwordInput textinput.Model + focus focusField + + // Loading screen (used for both the initial post-login fetch and a + // manual refresh). + spinner spinner.Model + + // Games screen. + table table.Model + user model.User +} + +// NewModel builds the initial Model, showing the login screen. +func NewModel(c *client.Client) Model { + email := textinput.New() + email.Placeholder = "email" + email.Focus() + email.CharLimit = 254 + + password := textinput.New() + password.Placeholder = "password" + password.EchoMode = textinput.EchoPassword + password.CharLimit = 254 + + s := spinner.New() + s.Spinner = spinner.Dot + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205")) + + return Model{ + client: c, + screen: screenLogin, + emailInput: email, + passwordInput: password, + spinner: s, + table: newGamesTable(nil), + } +} + +func newGamesTable(games []model.Game) table.Model { + columns := []table.Column{ + {Title: "Name", Width: 30}, + {Title: "Platform", Width: 16}, + {Title: "Score", Width: 7}, + {Title: "Released", Width: 12}, + {Title: "Finished", Width: 10}, + } + t := table.New( + table.WithColumns(columns), + table.WithRows(gamesToRows(games)), + table.WithFocused(true), + table.WithHeight(15), + ) + return t +} + +func gamesToRows(games []model.Game) []table.Row { + rows := make([]table.Row, len(games)) + for i, g := range games { + finished := "" + if g.Finished != "" { + finished = "✓" + } + rows[i] = table.Row{g.Name, g.PlatformName, fmt.Sprintf("%d", g.Score), g.ReleaseDate, finished} + } + return rows +} + +// --- Messages --- + +type loginSucceededMsg struct{ user model.User } +type loginFailedMsg struct{ err error } +type gamesLoadedMsg struct{ games []model.Game } +type gamesLoadFailedMsg struct{ err error } + +// --- Commands --- + +func doLogin(c *client.Client, email, password string) tea.Cmd { + return func() tea.Msg { + resp, err := c.Login(context.Background(), email, password) + if err != nil { + return loginFailedMsg{err: err} + } + return loginSucceededMsg{user: resp.User} + } +} + +func doListGames(c *client.Client) tea.Cmd { + return func() tea.Msg { + games, err := c.ListGames(context.Background()) + if err != nil { + return gamesLoadFailedMsg{err: err} + } + return gamesLoadedMsg{games: games} + } +} + +// --- bubbletea.Model --- + +func (m Model) Init() tea.Cmd { + return tea.Batch(m.spinner.Tick, textinput.Blink) +} + +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.KeyMsg: + return m.updateKey(msg) + + case spinner.TickMsg: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + + case loginSucceededMsg: + m.user = msg.user + m.err = nil + m.screen = screenLoading + return m, doListGames(m.client) + + case loginFailedMsg: + m.err = msg.err + return m, nil + + case gamesLoadedMsg: + m.table = newGamesTable(msg.games) + m.err = nil + m.screen = screenGames + return m, nil + + case gamesLoadFailedMsg: + m.err = msg.err + m.screen = screenGames + return m, nil + } + return m, nil +} + +func (m Model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + if msg.String() == "ctrl+c" { + return m, tea.Quit + } + + switch m.screen { + case screenLogin: + return m.updateLoginKey(msg) + case screenGames: + if msg.String() == "q" { + return m, tea.Quit + } + if msg.String() == "r" { + m.screen = screenLoading + m.err = nil + return m, doListGames(m.client) + } + var cmd tea.Cmd + m.table, cmd = m.table.Update(msg) + return m, cmd + } + return m, nil +} + +func (m Model) updateLoginKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + switch msg.String() { + case "tab", "shift+tab", "down", "up": + if m.focus == focusEmail { + m.focus = focusPassword + m.emailInput.Blur() + m.passwordInput.Focus() + } else { + m.focus = focusEmail + m.passwordInput.Blur() + m.emailInput.Focus() + } + return m, nil + + case "enter": + m.err = nil + m.screen = screenLoading + return m, doLogin(m.client, m.emailInput.Value(), m.passwordInput.Value()) + } + + var cmd tea.Cmd + if m.focus == focusEmail { + m.emailInput, cmd = m.emailInput.Update(msg) + } else { + m.passwordInput, cmd = m.passwordInput.Update(msg) + } + return m, cmd +} + +func (m Model) View() string { + switch m.screen { + case screenLogin: + return m.viewLogin() + case screenLoading: + return fmt.Sprintf("%s Loading...\n", m.spinner.View()) + case screenGames: + return m.viewGames() + } + return "" +} + +func (m Model) viewLogin() string { + var b strings.Builder + b.WriteString(titleStyle.Render("Completed") + "\n\n") + + emailLabel, passwordLabel := "Email", "Password" + if m.focus == focusEmail { + emailLabel = focusedInputStyle.Render(emailLabel) + } else { + passwordLabel = focusedInputStyle.Render(passwordLabel) + } + b.WriteString(emailLabel + "\n" + m.emailInput.View() + "\n\n") + b.WriteString(passwordLabel + "\n" + m.passwordInput.View() + "\n\n") + + if m.err != nil { + b.WriteString(errorStyle.Render("Error: "+m.err.Error()) + "\n\n") + } + + b.WriteString(helpStyle.Render("tab: switch field · enter: log in · ctrl+c: quit")) + return b.String() +} + +func (m Model) viewGames() string { + var b strings.Builder + b.WriteString(titleStyle.Render("Completed") + " " + m.user.Name + "\n\n") + + if m.err != nil { + b.WriteString(errorStyle.Render("Error: "+m.err.Error()) + "\n\n") + } + + b.WriteString(m.table.View() + "\n\n") + b.WriteString(helpStyle.Render("r: refresh · q: quit")) + return b.String() +} diff --git a/cmd/tui/model_test.go b/cmd/tui/model_test.go new file mode 100644 index 0000000..d2c350b --- /dev/null +++ b/cmd/tui/model_test.go @@ -0,0 +1,179 @@ +package main + +import ( + "errors" + "testing" + + tea "github.com/charmbracelet/bubbletea" + + "completed/internal/client" + "completed/internal/model" +) + +// key builds the tea.KeyMsg Update expects for a named key, matching what +// bubbletea itself constructs from terminal input. +func key(s string) tea.KeyMsg { + switch s { + case "enter": + return tea.KeyMsg{Type: tea.KeyEnter} + case "tab": + return tea.KeyMsg{Type: tea.KeyTab} + case "ctrl+c": + return tea.KeyMsg{Type: tea.KeyCtrlC} + default: + return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)} + } +} + +func update(t *testing.T, m Model, msg tea.Msg) Model { + t.Helper() + newModel, _ := m.Update(msg) + m, ok := newModel.(Model) + if !ok { + t.Fatalf("Update returned %T, want Model", newModel) + } + return m +} + +func TestTypingFillsFocusedField(t *testing.T) { + m := NewModel(client.New("")) + + m = update(t, m, key("a")) + m = update(t, m, key("b")) + if got := m.emailInput.Value(); got != "ab" { + t.Errorf("emailInput.Value() = %q, want %q", got, "ab") + } + if got := m.passwordInput.Value(); got != "" { + t.Errorf("passwordInput.Value() = %q, want empty while email is focused", got) + } + + m = update(t, m, key("tab")) + m = update(t, m, key("c")) + if got := m.passwordInput.Value(); got != "c" { + t.Errorf("passwordInput.Value() = %q, want %q after tab switches focus", got, "c") + } +} + +func TestEnterOnLoginStartsLoading(t *testing.T) { + m := NewModel(client.New("")) + m.emailInput.SetValue("a@example.com") + m.passwordInput.SetValue("secret") + + newModel, cmd := m.Update(key("enter")) + m = newModel.(Model) + + if m.screen != screenLoading { + t.Errorf("screen = %v, want screenLoading", m.screen) + } + if cmd == nil { + t.Error("expected a command to perform the login request, got nil") + } +} + +func TestLoginSucceededTransitionsToLoadingGames(t *testing.T) { + m := NewModel(client.New("")) + m.screen = screenLoading + + newModel, cmd := m.Update(loginSucceededMsg{user: model.User{Name: "Alice"}}) + m = newModel.(Model) + + if m.screen != screenLoading { + t.Errorf("screen = %v, want to stay screenLoading while games load", m.screen) + } + if m.user.Name != "Alice" { + t.Errorf("user.Name = %q, want Alice", m.user.Name) + } + if cmd == nil { + t.Error("expected a command to fetch games, got nil") + } +} + +func TestLoginFailedStaysOnLoginWithError(t *testing.T) { + m := NewModel(client.New("")) + m.screen = screenLoading + wantErr := errors.New("invalid email or password") + + m = update(t, m, loginFailedMsg{err: wantErr}) + + if m.err != wantErr { + t.Errorf("err = %v, want %v", m.err, wantErr) + } +} + +func TestGamesLoadedPopulatesTableAndSwitchesScreen(t *testing.T) { + m := NewModel(client.New("")) + m.screen = screenLoading + games := []model.Game{ + {ID: "g1", Name: "Doom", PlatformName: "PC", Score: 95, ReleaseDate: "1993-12-10"}, + {ID: "g2", Name: "Half-Life", PlatformName: "PC", Score: 96, ReleaseDate: "1998-11-19", Finished: "1999-01-01"}, + } + + m = update(t, m, gamesLoadedMsg{games: games}) + + if m.screen != screenGames { + t.Errorf("screen = %v, want screenGames", m.screen) + } + rows := m.table.Rows() + if len(rows) != 2 { + t.Fatalf("table has %d rows, want 2", len(rows)) + } + if rows[0][0] != "Doom" || rows[0][4] != "" { + t.Errorf("row 0 = %v, want Doom unfinished", rows[0]) + } + if rows[1][0] != "Half-Life" || rows[1][4] != "✓" { + t.Errorf("row 1 = %v, want Half-Life finished", rows[1]) + } +} + +func TestGamesLoadFailedShowsErrorOnGamesScreen(t *testing.T) { + m := NewModel(client.New("")) + m.screen = screenLoading + wantErr := errors.New("network error") + + m = update(t, m, gamesLoadFailedMsg{err: wantErr}) + + if m.screen != screenGames { + t.Errorf("screen = %v, want screenGames (so 'r' can retry)", m.screen) + } + if m.err != wantErr { + t.Errorf("err = %v, want %v", m.err, wantErr) + } +} + +func TestRefreshOnGamesScreenReloads(t *testing.T) { + m := NewModel(client.New("")) + m.screen = screenGames + m.table = newGamesTable([]model.Game{{ID: "g1", Name: "Doom"}}) + + newModel, cmd := m.Update(key("r")) + m = newModel.(Model) + + if m.screen != screenLoading { + t.Errorf("screen = %v, want screenLoading", m.screen) + } + if cmd == nil { + t.Error("expected a command to refetch games, got nil") + } +} + +func TestQuitOnGamesScreen(t *testing.T) { + m := NewModel(client.New("")) + m.screen = screenGames + + _, cmd := m.Update(key("q")) + if cmd == nil { + t.Fatal("expected tea.Quit, got nil") + } +} + +func TestCtrlCQuitsFromAnyScreen(t *testing.T) { + for _, s := range []screen{screenLogin, screenLoading, screenGames} { + m := NewModel(client.New("")) + m.screen = s + + _, cmd := m.Update(key("ctrl+c")) + if cmd == nil { + t.Errorf("screen %v: expected tea.Quit on ctrl+c, got nil", s) + } + } +} diff --git a/go.mod b/go.mod index 069d649..a16da97 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module completed go 1.25.4 require ( + fyne.io/fyne/v2 v2.8.1 github.com/charmbracelet/bubbles v0.21.0 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/danielgtaylor/huma/v2 v2.34.1 - github.com/gen2brain/raylib-go/raylib v0.55.1 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.7.6 @@ -17,35 +17,60 @@ require ( ) require ( + fyne.io/systray v1.12.3-0.20260810170012-af4e8e793ec4 // indirect + github.com/BurntSushi/toml v1.6.0 // indirect + github.com/FyshOS/fancyfs v0.0.1 // indirect + github.com/anthonynsimon/bild v0.14.0 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.10.1 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/ebitengine/purego v0.7.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fyne-io/gl-js v0.2.1-0.20260315212741-029c47fd27e8 // indirect + github.com/fyne-io/glfw-js v0.4.0 // indirect + github.com/fyne-io/image v0.1.1 // indirect + github.com/fyne-io/oksvg v0.2.0 // indirect + github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 // indirect + github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1.0.20260707082822-2a407d02d01a // indirect + github.com/go-text/render v0.2.1 // indirect + github.com/go-text/typesetting v0.3.4 // indirect + github.com/godbus/dbus/v5 v5.2.2 // indirect + github.com/hack-pad/go-indexeddb v0.3.2 // indirect + github.com/hack-pad/safejs v0.1.0 // indirect github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/kr/text v0.2.0 // indirect + github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect + github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.16.0 // indirect + github.com/rymdport/portal v0.4.2 // indirect + github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect + github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/yuin/goldmark v1.8.2 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/image v0.24.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect diff --git a/go.sum b/go.sum index e7a55fa..709d757 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,23 @@ +fyne.io/fyne/v2 v2.8.1 h1:EztGuE2W3Qhd0cWVmU+h5rkzNezUD1To6UqsoLQYUIM= +fyne.io/fyne/v2 v2.8.1/go.mod h1:kpeuFrClm0fiAgJYr2soTfwKMT5rzNcSKzmgGjxvHOY= +fyne.io/systray v1.12.3-0.20260810170012-af4e8e793ec4 h1:149/+Wa5EsLLXfyj2pdTmvnQf2VIlgCIwSjcCTHYhIo= +fyne.io/systray v1.12.3-0.20260810170012-af4e8e793ec4/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/FyshOS/fancyfs v0.0.1 h1:kgvm7VvwOMLkYTqSflplp62SlMVWQ2uAoHw9CXwXHYg= +github.com/FyshOS/fancyfs v0.0.1/go.mod h1:S5SHVz/5R72iCXOxCqdcyTPSlg3JxNd0gaHyGBSrY8A= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync= +github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= +github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= @@ -16,13 +30,16 @@ github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7 github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danielgtaylor/huma/v2 v2.34.1 h1:EmOJAbzEGfy0wAq/QMQ1YKfEMBEfE94xdBRLPBP0gwQ= github.com/danielgtaylor/huma/v2 v2.34.1/go.mod h1:ynwJgLk8iGVgoaipi5tgwIQ5yoFNmiu+QdhU7CEEmhk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -38,24 +55,46 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/ebitengine/purego v0.7.1 h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA= -github.com/ebitengine/purego v0.7.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gen2brain/raylib-go/raylib v0.55.1 h1:1rdc10WvvYjtj7qijHnV9T38/WuvlT6IIL+PaZ6cNA8= -github.com/gen2brain/raylib-go/raylib v0.55.1/go.mod h1:BaY76bZk7nw1/kVOSQObPY1v1iwVE1KHAGMfvI6oK1Q= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fyne-io/gl-js v0.2.1-0.20260315212741-029c47fd27e8 h1:0kdPD/GEntpWmZEK5Zu/xE6Tr37jYCVDf9QP8lA/QK8= +github.com/fyne-io/gl-js v0.2.1-0.20260315212741-029c47fd27e8/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI= +github.com/fyne-io/glfw-js v0.4.0 h1:I9hREBeFyI10cNIqbMKYb1PRidyPDgwob8o2la9SfQo= +github.com/fyne-io/glfw-js v0.4.0/go.mod h1:SDchsFZh4n7nVuBoiowOhOgIBdz+qUQVeC1w9fe2yVU= +github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA= +github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM= +github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8= +github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI= +github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276 h1:IO5P06Pcj9K04d+l4nrf3c2U56+dAotIFG6u4P1wAHI= +github.com/go-gl/gl v0.0.0-20260331235117-4566fea9a276/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1.0.20260707082822-2a407d02d01a h1:HWK0MBggT/T6YH7VffE10xBIhqeTq8JzIUPJXrRy87g= +github.com/go-gl/glfw/v3.4/glfw v0.1.0-pre.1.0.20260707082822-2a407d02d01a/go.mod h1:T5Dn0JwIJOX1euPZ/iT4tq6nFYtmukjcYa7937HuYK8= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-text/render v0.2.1 h1:qwHhxqGUjjg4L0XyJWj7M7bpY75NZM+kBpv2Yfw5mcg= +github.com/go-text/render v0.2.1/go.mod h1:HCCAq8MUlm/WRcXshBb4K/n+IkjeXQ1c2Ba+yICSm0A= +github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU= +github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY= +github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos= +github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= +github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= +github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= +github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= +github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= +github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -66,6 +105,10 @@ github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE= +github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M= +github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -84,8 +127,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -98,6 +141,10 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= +github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -107,11 +154,16 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU= +github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE= +github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ= +github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -123,6 +175,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= +github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= @@ -137,6 +191,8 @@ golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= +golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= diff --git a/internal/api/auth_routes.go b/internal/api/auth_routes.go index 4e66c1c..7441b5d 100644 --- a/internal/api/auth_routes.go +++ b/internal/api/auth_routes.go @@ -65,6 +65,7 @@ func newSession(ctx context.Context, queries db.Querier, userID uuid.UUID) (stri func registerAuthRoutes(api huma.API, queries db.Querier) { huma.Register(api, huma.Operation{ OperationID: "register", + Tags: []string{"Auth"}, Method: http.MethodPost, Path: "/api/auth/register", Summary: "Register a new account", @@ -100,6 +101,7 @@ func registerAuthRoutes(api huma.API, queries db.Querier) { huma.Register(api, huma.Operation{ OperationID: "login", + Tags: []string{"Auth"}, Method: http.MethodPost, Path: "/api/auth/login", Summary: "Log in to an existing account", @@ -133,6 +135,7 @@ func registerAuthRoutes(api huma.API, queries db.Querier) { logoutOp := huma.Operation{ OperationID: "logout", + Tags: []string{"Auth"}, Method: http.MethodPost, Path: "/api/auth/logout", Summary: "Log out of the current session", @@ -149,6 +152,7 @@ func registerAuthRoutes(api huma.API, queries db.Querier) { meOp := huma.Operation{ OperationID: "get-me", + Tags: []string{"Auth"}, Method: http.MethodGet, Path: "/api/auth/me", Summary: "Get the current user", diff --git a/internal/api/routes.go b/internal/api/routes.go index 1773fbe..9d5b996 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -17,6 +17,7 @@ func RegisterRoutes(api huma.API, queries db.Querier) { // Root/Hello Route huma.Register(api, huma.Operation{ OperationID: "get-hello", + Tags: []string{"Misc"}, Method: http.MethodGet, Path: "/hello", Summary: "Say Hello", @@ -52,6 +53,7 @@ func RegisterRoutes(api huma.API, queries db.Querier) { func registerPlatformRoutes(api huma.API, queries db.Querier) { listOp := huma.Operation{ OperationID: "list-platforms", + Tags: []string{"Platforms"}, Method: http.MethodGet, Path: "/api/platforms", Summary: "List Platforms", @@ -71,6 +73,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) { getOp := huma.Operation{ OperationID: "get-platform", + Tags: []string{"Platforms"}, Method: http.MethodGet, Path: "/api/platforms/{id}", Summary: "Get Platform", @@ -88,6 +91,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) { createOp := huma.Operation{ OperationID: "create-platform", + Tags: []string{"Platforms"}, Method: http.MethodPost, Path: "/api/platforms", Summary: "Create Platform", @@ -103,6 +107,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) { updateOp := huma.Operation{ OperationID: "update-platform", + Tags: []string{"Platforms"}, Method: http.MethodPut, Path: "/api/platforms/{id}", Summary: "Update Platform", @@ -124,6 +129,7 @@ func registerPlatformRoutes(api huma.API, queries db.Querier) { deleteOp := huma.Operation{ OperationID: "delete-platform", + Tags: []string{"Platforms"}, Method: http.MethodDelete, Path: "/api/platforms/{id}", Summary: "Delete Platform", @@ -185,6 +191,7 @@ func parseDate(s string) (pgtype.Date, error) { func registerGameRoutes(api huma.API, queries db.Querier) { listOp := huma.Operation{ OperationID: "list-games", + Tags: []string{"Games"}, Method: http.MethodGet, Path: "/api/games", Summary: "List Games", @@ -212,6 +219,7 @@ func registerGameRoutes(api huma.API, queries db.Querier) { getOp := huma.Operation{ OperationID: "get-game", + Tags: []string{"Games"}, Method: http.MethodGet, Path: "/api/games/{id}", Summary: "Get Game", @@ -237,6 +245,7 @@ func registerGameRoutes(api huma.API, queries db.Querier) { createOp := huma.Operation{ OperationID: "create-game", + Tags: []string{"Games"}, Method: http.MethodPost, Path: "/api/games", Summary: "Create Game", @@ -280,6 +289,7 @@ func registerGameRoutes(api huma.API, queries db.Querier) { updateOp := huma.Operation{ OperationID: "update-game", + Tags: []string{"Games"}, Method: http.MethodPut, Path: "/api/games/{id}", Summary: "Update Game", @@ -327,6 +337,7 @@ func registerGameRoutes(api huma.API, queries db.Querier) { deleteOp := huma.Operation{ OperationID: "delete-game", + Tags: []string{"Games"}, Method: http.MethodDelete, Path: "/api/games/{id}", Summary: "Delete Game", diff --git a/internal/client/client.go b/internal/client/client.go new file mode 100644 index 0000000..59c82b4 --- /dev/null +++ b/internal/client/client.go @@ -0,0 +1,218 @@ +// Package client is a thin, typed HTTP client for the Completed API. It's +// the shared base for the TUI and the Fyne GUI: both hold a *Client and +// call its methods instead of each hand-rolling net/http calls, so request +// building, auth, and error handling live in one tested place. +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "completed/internal/model" +) + +// DefaultBaseURL is used when no other base URL is configured. It matches +// cmd/api's own default port. +const DefaultBaseURL = "http://localhost:8080" + +// Client talks to the Completed API. The zero value is not usable; create +// one with New. A Client is safe for concurrent use once Token is no +// longer being written concurrently with reads - in practice, callers set +// it only right after Login/Register, before starting concurrent calls. +type Client struct { + BaseURL string + Token string + + httpClient *http.Client +} + +// New returns a Client pointed at baseURL. An empty baseURL is replaced +// with DefaultBaseURL. +func New(baseURL string) *Client { + if baseURL == "" { + baseURL = DefaultBaseURL + } + return &Client{ + BaseURL: baseURL, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } +} + +// APIError is returned for any non-2xx response. It carries the status +// code so callers can distinguish, say, a 401 (show "wrong password") from +// a 500 (show "something went wrong"). +type APIError struct { + Status int + Title string + Detail string +} + +func (e *APIError) Error() string { + if e.Detail != "" { + return e.Detail + } + if e.Title != "" { + return e.Title + } + return fmt.Sprintf("request failed with status %d", e.Status) +} + +// huma's error body shape; see internal/api's ErrorModel (from the huma +// package) in api/openapi.json. +type errorBody struct { + Title string `json:"title"` + Status int `json:"status"` + Detail string `json:"detail"` +} + +// do sends an HTTP request and decodes a JSON response into out (if out is +// non-nil). It attaches the bearer token, if set, and turns any non-2xx +// response into an *APIError. +func (c *Client) do(ctx context.Context, method, path string, body, out any) error { + var bodyReader io.Reader + if body != nil { + data, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("encode request body: %w", err) + } + bodyReader = bytes.NewReader(data) + } + + req, err := http.NewRequestWithContext(ctx, method, c.BaseURL+path, bodyReader) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + if c.Token != "" { + req.Header.Set("Authorization", "Bearer "+c.Token) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("%s %s: %w", method, path, err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read response: %w", err) + } + + if resp.StatusCode >= 300 { + var eb errorBody + _ = json.Unmarshal(data, &eb) // best-effort; fall back to status alone + return &APIError{Status: resp.StatusCode, Title: eb.Title, Detail: eb.Detail} + } + + if out != nil && len(data) > 0 { + if err := json.Unmarshal(data, out); err != nil { + return fmt.Errorf("decode response: %w", err) + } + } + return nil +} + +// --- Auth --- + +func (c *Client) Register(ctx context.Context, email, password, name string) (model.AuthResponse, error) { + var resp model.AuthResponse + err := c.do(ctx, http.MethodPost, "/api/auth/register", model.RegisterInput{ + Email: email, Password: password, Name: name, + }, &resp) + if err == nil { + c.Token = resp.Token + } + return resp, err +} + +func (c *Client) Login(ctx context.Context, email, password string) (model.AuthResponse, error) { + var resp model.AuthResponse + err := c.do(ctx, http.MethodPost, "/api/auth/login", model.LoginInput{ + Email: email, Password: password, + }, &resp) + if err == nil { + c.Token = resp.Token + } + return resp, err +} + +func (c *Client) Logout(ctx context.Context) error { + err := c.do(ctx, http.MethodPost, "/api/auth/logout", nil, nil) + c.Token = "" + return err +} + +func (c *Client) Me(ctx context.Context) (model.User, error) { + var u model.User + err := c.do(ctx, http.MethodGet, "/api/auth/me", nil, &u) + return u, err +} + +// --- Platforms --- + +func (c *Client) ListPlatforms(ctx context.Context) ([]model.Platform, error) { + var platforms []model.Platform + err := c.do(ctx, http.MethodGet, "/api/platforms", nil, &platforms) + return platforms, err +} + +func (c *Client) GetPlatform(ctx context.Context, id string) (model.Platform, error) { + var p model.Platform + err := c.do(ctx, http.MethodGet, "/api/platforms/"+id, nil, &p) + return p, err +} + +// CreatePlatform requires the current user to be an admin; see +// internal/api/middleware.go's addAdminAuth. +func (c *Client) CreatePlatform(ctx context.Context, name string) (model.Platform, error) { + var p model.Platform + err := c.do(ctx, http.MethodPost, "/api/platforms", model.PlatformInput{Name: name}, &p) + return p, err +} + +func (c *Client) UpdatePlatform(ctx context.Context, id, name string) (model.Platform, error) { + var p model.Platform + err := c.do(ctx, http.MethodPut, "/api/platforms/"+id, model.PlatformInput{Name: name}, &p) + return p, err +} + +func (c *Client) DeletePlatform(ctx context.Context, id string) error { + return c.do(ctx, http.MethodDelete, "/api/platforms/"+id, nil, nil) +} + +// --- Games --- + +func (c *Client) ListGames(ctx context.Context) ([]model.Game, error) { + var games []model.Game + err := c.do(ctx, http.MethodGet, "/api/games", nil, &games) + return games, err +} + +func (c *Client) GetGame(ctx context.Context, id string) (model.Game, error) { + var g model.Game + err := c.do(ctx, http.MethodGet, "/api/games/"+id, nil, &g) + return g, err +} + +func (c *Client) CreateGame(ctx context.Context, in model.GameInput) (model.Game, error) { + var g model.Game + err := c.do(ctx, http.MethodPost, "/api/games", in, &g) + return g, err +} + +func (c *Client) UpdateGame(ctx context.Context, id string, in model.GameInput) (model.Game, error) { + var g model.Game + err := c.do(ctx, http.MethodPut, "/api/games/"+id, in, &g) + return g, err +} + +func (c *Client) DeleteGame(ctx context.Context, id string) error { + return c.do(ctx, http.MethodDelete, "/api/games/"+id, nil, nil) +} diff --git a/internal/client/client_test.go b/internal/client/client_test.go new file mode 100644 index 0000000..d79ec73 --- /dev/null +++ b/internal/client/client_test.go @@ -0,0 +1,186 @@ +package client + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "completed/internal/model" +) + +func newTestServer(t *testing.T, handler http.HandlerFunc) (*Client, *httptest.Server) { + t.Helper() + srv := httptest.NewServer(handler) + t.Cleanup(srv.Close) + return New(srv.URL), srv +} + +func writeJSON(t *testing.T, w http.ResponseWriter, status int, v any) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(v); err != nil { + t.Fatalf("encode response: %v", err) + } +} + +func TestLoginSuccessStoresToken(t *testing.T) { + c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/api/auth/login" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + var in model.LoginInput + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + t.Fatalf("decode body: %v", err) + } + if in.Email != "alice@example.com" || in.Password != "secret" { + t.Fatalf("unexpected credentials: %+v", in) + } + writeJSON(t, w, http.StatusOK, model.AuthResponse{ + Token: "tok-123", + User: model.User{ID: "u1", Email: in.Email, Name: "Alice"}, + }) + }) + + resp, err := c.Login(t.Context(), "alice@example.com", "secret") + if err != nil { + t.Fatalf("Login: %v", err) + } + if resp.Token != "tok-123" { + t.Errorf("Token = %q, want tok-123", resp.Token) + } + if c.Token != "tok-123" { + t.Errorf("Client.Token = %q, want tok-123 (Login must store it)", c.Token) + } +} + +func TestLoginFailureDoesNotStoreToken(t *testing.T) { + c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, http.StatusUnauthorized, map[string]any{ + "title": "Unauthorized", "status": 401, "detail": "invalid email or password", + }) + }) + c.Token = "" // sanity + + _, err := c.Login(t.Context(), "alice@example.com", "wrong") + if err == nil { + t.Fatal("expected an error, got nil") + } + apiErr, ok := err.(*APIError) + if !ok { + t.Fatalf("expected *APIError, got %T: %v", err, err) + } + if apiErr.Status != http.StatusUnauthorized { + t.Errorf("Status = %d, want 401", apiErr.Status) + } + if apiErr.Error() != "invalid email or password" { + t.Errorf("Error() = %q, want the detail message", apiErr.Error()) + } + if c.Token != "" { + t.Errorf("Token = %q, want empty after a failed login", c.Token) + } +} + +func TestListGamesSendsBearerToken(t *testing.T) { + c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if got := r.Header.Get("Authorization"); got != "Bearer tok-abc" { + t.Errorf("Authorization header = %q, want %q", got, "Bearer tok-abc") + } + writeJSON(t, w, http.StatusOK, []model.Game{ + {ID: "g1", Name: "Doom", PlatformName: "PC", Score: 95, ReleaseDate: "1993-12-10"}, + }) + }) + c.Token = "tok-abc" + + games, err := c.ListGames(t.Context()) + if err != nil { + t.Fatalf("ListGames: %v", err) + } + if len(games) != 1 || games[0].Name != "Doom" { + t.Errorf("games = %+v, want one game named Doom", games) + } +} + +func TestListGamesUnauthorized(t *testing.T) { + c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, http.StatusUnauthorized, map[string]any{ + "title": "Unauthorized", "status": 401, "detail": "authentication required", + }) + }) + // No token set. + + _, err := c.ListGames(t.Context()) + apiErr, ok := err.(*APIError) + if !ok { + t.Fatalf("expected *APIError, got %T: %v", err, err) + } + if apiErr.Status != http.StatusUnauthorized { + t.Errorf("Status = %d, want 401", apiErr.Status) + } +} + +func TestCreateGameSendsBodyAndParsesResponse(t *testing.T) { + c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost || r.URL.Path != "/api/games" { + t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) + } + var in model.GameInput + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + t.Fatalf("decode body: %v", err) + } + if in.Name != "Half-Life" || in.Score != 96 { + t.Fatalf("unexpected body: %+v", in) + } + writeJSON(t, w, http.StatusOK, model.Game{ + ID: "g2", Name: in.Name, PlatformID: in.PlatformID, Score: in.Score, ReleaseDate: in.ReleaseDate, + }) + }) + + g, err := c.CreateGame(t.Context(), model.GameInput{ + Name: "Half-Life", PlatformID: "p1", Score: 96, ReleaseDate: "1998-11-19", + }) + if err != nil { + t.Fatalf("CreateGame: %v", err) + } + if g.ID != "g2" { + t.Errorf("ID = %q, want g2", g.ID) + } +} + +func TestCreatePlatformForbidden(t *testing.T) { + c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, http.StatusForbidden, map[string]any{ + "title": "Forbidden", "status": 403, "detail": "admin access required", + }) + }) + c.Token = "tok-non-admin" + + _, err := c.CreatePlatform(t.Context(), "PC") + apiErr, ok := err.(*APIError) + if !ok { + t.Fatalf("expected *APIError, got %T: %v", err, err) + } + if apiErr.Status != http.StatusForbidden { + t.Errorf("Status = %d, want 403", apiErr.Status) + } +} + +func TestLogoutClearsTokenEvenOnError(t *testing.T) { + c, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + }) + c.Token = "tok-abc" + + _ = c.Logout(t.Context()) + if c.Token != "" { + t.Errorf("Token = %q, want empty after Logout regardless of the response", c.Token) + } +} + +func TestDefaultBaseURL(t *testing.T) { + c := New("") + if c.BaseURL != DefaultBaseURL { + t.Errorf("BaseURL = %q, want %q", c.BaseURL, DefaultBaseURL) + } +} diff --git a/web/.editorconfig b/web/.editorconfig new file mode 100644 index 0000000..f166060 --- /dev/null +++ b/web/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single +ij_typescript_use_double_quotes = false + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..854acd5 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,44 @@ +# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/mcp.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings +__screenshots__/ + +# System files +.DS_Store +Thumbs.db diff --git a/web/.prettierrc b/web/.prettierrc new file mode 100644 index 0000000..d6c16d7 --- /dev/null +++ b/web/.prettierrc @@ -0,0 +1,12 @@ +{ + "printWidth": 100, + "singleQuote": true, + "overrides": [ + { + "files": "*.html", + "options": { + "parser": "angular" + } + } + ] +} diff --git a/web/.vscode/extensions.json b/web/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/web/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/web/.vscode/launch.json b/web/.vscode/launch.json new file mode 100644 index 0000000..925af83 --- /dev/null +++ b/web/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + }, + { + "name": "ng test", + "type": "chrome", + "request": "launch", + "preLaunchTask": "npm: test", + "url": "http://localhost:9876/debug.html" + } + ] +} diff --git a/web/.vscode/tasks.json b/web/.vscode/tasks.json new file mode 100644 index 0000000..244306f --- /dev/null +++ b/web/.vscode/tasks.json @@ -0,0 +1,42 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + }, + { + "type": "npm", + "script": "test", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "Changes detected" + }, + "endsPattern": { + "regexp": "bundle generation (complete|failed)" + } + } + } + } + ] +} diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..83c5fd5 --- /dev/null +++ b/web/README.md @@ -0,0 +1,59 @@ +# Web + +This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 22.2.1. + +## Development server + +To start a local development server, run: + +```bash +ng serve +``` + +Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. + +## Code scaffolding + +Angular CLI includes powerful code scaffolding tools. To generate a new component, run: + +```bash +ng generate component component-name +``` + +For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: + +```bash +ng generate --help +``` + +## Building + +To build the project run: + +```bash +ng build +``` + +This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. + +## Running unit tests + +To execute unit tests with the [Vitest](https://vitest.dev/) test runner, use the following command: + +```bash +ng test +``` + +## Running end-to-end tests + +For end-to-end (e2e) testing, run: + +```bash +ng e2e +``` + +Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. + +## Additional Resources + +For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/web/angular.json b/web/angular.json new file mode 100644 index 0000000..20fa820 --- /dev/null +++ b/web/angular.json @@ -0,0 +1,71 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "packageManager": "npm" + }, + "newProjectRoot": "projects", + "projects": { + "web": { + "projectType": "application", + "schematics": {}, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": ["src/material-theme.scss", "src/styles.css"] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "1MB" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "web:build:production" + }, + "development": { + "buildTarget": "web:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test" + } + } + } + } +} diff --git a/web/ng-openapi-gen.json b/web/ng-openapi-gen.json new file mode 100644 index 0000000..91643f7 --- /dev/null +++ b/web/ng-openapi-gen.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/ng-openapi-gen/ng-openapi-gen-schema.json", + "input": "../api/openapi.json", + "output": "src/app/api", + "ignoreUnusedModels": false, + "services": true +} diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..371b290 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,7131 @@ +{ + "name": "web", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.0", + "dependencies": { + "@angular/animations": "^22.2.1", + "@angular/cdk": "^22.2.1", + "@angular/common": "^22.2.0", + "@angular/compiler": "^22.2.0", + "@angular/core": "^22.2.0", + "@angular/forms": "^22.2.0", + "@angular/material": "^22.2.1", + "@angular/platform-browser": "^22.2.0", + "@angular/router": "^22.2.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^22.2.1", + "@angular/cli": "^22.2.1", + "@angular/compiler-cli": "^22.2.0", + "jsdom": "^30.0.0", + "ng-openapi-gen": "^1.1.0", + "prettier": "^3.8.1", + "typescript": "~6.0.2", + "vitest": "^5.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.2202.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2202.1.tgz", + "integrity": "sha512-WGRAxFj+R9LC8HuK3DnDgxHo/XrgfklNQeNgjq/phTEVq5ZdWxc8kDoPA4uQz4vF+HY2eycAAtOPfMvj2TBEiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.2.1", + "rxjs": "7.8.2" + }, + "bin": { + "architect": "bin/cli.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.2.1.tgz", + "integrity": "sha512-2oiuWVbUg7pE9kh2grp+MF2ynys+tMWZ/mmoYlqWrhMS2n5oV304ir0/IvvCCSGz9qsbL2A1vZhnlI2UbTSNiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.20.0", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.7", + "rxjs": "7.8.2", + "source-map": "0.8.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^5.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.2.1.tgz", + "integrity": "sha512-frHhFwu2TEfTZX5jfAYL2bztuG5mGggfJhiM9bo5/QtLAi49LtRbJxIktsu6vjJdujxeB2TKD7KqnXwNYgd41Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.2.1", + "jsonc-parser": "3.3.1", + "magic-string": "1.4.1", + "ora": "9.4.1", + "rxjs": "7.8.2" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/animations": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-22.2.1.tgz", + "integrity": "sha512-vTxZN6uL/m61lv7ZCvfmQ3VuFb4o/tniGLpc3SpkNHE6MLM8WN0IjfNBRLMYMovd1uUBSPwhcG4Llcmucr9dmw==", + "deprecated": "@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/core": "22.2.1" + } + }, + "node_modules/@angular/build": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.2.1.tgz", + "integrity": "sha512-VA8EkK5Y6sJbBFyn2x5jzd3KByh29DDpI7n6hucT0sK6YkMJEvsojPUWet9ifwF3OL+ZrGvxYlAHyZGKVlqw0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.2202.1", + "@babel/core": "8.0.5", + "@inquirer/confirm": "6.3.2", + "@parcel/watcher": "2.6.0", + "@vitejs/plugin-basic-ssl": "2.3.0", + "beasties": "0.5.4", + "browserslist": "^4.26.0", + "chokidar": "5.0.0", + "esbuild": "0.28.2", + "https-proxy-agent": "9.1.0", + "jsonc-parser": "3.3.1", + "listr2": "11.1.0", + "magic-string": "1.4.1", + "mrmime": "2.0.1", + "oxc-parser": "0.150.0", + "parse5-html-rewriting-stream": "8.0.1", + "picomatch": "4.0.7", + "piscina": "5.3.2", + "rolldown": "1.2.8", + "sass": "1.104.1", + "sass-embedded": "1.104.1", + "semver": "7.8.5", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.17", + "vite": "8.3.0", + "xxhash-wasm": "1.1.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.5.6" + }, + "peerDependencies": { + "@angular/compiler": "^22.0.0", + "@angular/compiler-cli": "^22.0.0", + "@angular/core": "^22.0.0", + "@angular/localize": "^22.0.0", + "@angular/platform-browser": "^22.0.0", + "@angular/platform-server": "^22.0.0", + "@angular/service-worker": "^22.0.0", + "@angular/ssr": "^22.2.1", + "istanbul-lib-instrument": "^6.0.0", + "karma": "^6.4.0", + "less": "^4.2.0", + "ng-packagr": "^22.0.0", + "postcss": "^8.4.0", + "rollup": "^4.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=6.0 <6.1", + "vitest": "^4.0.8 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, + "@angular/localize": { + "optional": true + }, + "@angular/platform-browser": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "istanbul-lib-instrument": { + "optional": true + }, + "karma": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "rollup": { + "optional": true + }, + "tailwindcss": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/vite": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@angular/cdk": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-22.2.1.tgz", + "integrity": "sha512-0vjlqYBbLif1EwHhKz2r4thTDvtRI2CKi2js9TzPRD0n4VlFUnAXaaW47s5TCCObIoYun6Bo1zZ0uPgwb+qEMw==", + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^22.0.0 || ^23.0.0", + "@angular/core": "^22.0.0 || ^23.0.0", + "@angular/forms": "^22.0.0 || ^23.0.0", + "@angular/platform-browser": "^22.0.0 || ^23.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.2.1.tgz", + "integrity": "sha512-tKMP68SZP+A9YCSRg0c+2e/+yWp7qiFrjCuQAomHK7owNaxHnLigAbra+8QA4xmWHoGaQk89NMmUPyA1BsBp8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.2202.1", + "@angular-devkit/core": "22.2.1", + "@angular-devkit/schematics": "22.2.1", + "@inquirer/prompts": "8.7.2", + "@listr2/prompt-adapter-inquirer": "4.2.7", + "@modelcontextprotocol/server": "2.0.0", + "@schematics/angular": "22.2.1", + "jsonc-parser": "3.3.1", + "listr2": "11.1.0", + "npm-package-arg": "14.0.0", + "parse5-html-rewriting-stream": "8.0.1", + "semver": "7.8.5", + "yargs": "18.1.0", + "zod": "4.6.5" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.2.1.tgz", + "integrity": "sha512-OqBH3EY4i8e3PvnYDugZGipSs1HWs2ZrqoClndKtFDS9mQULSSuLoxjG9nF1J/c+yUqhuyZlHjkXWS0n32pEfw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/core": "22.2.1", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.2.1.tgz", + "integrity": "sha512-KRSz0c/T5eEiFa+F1l9OppAebJj9pT8kU3ArBMlxR3v/vzhpCfuQsFVQ4xEFC7dEm3zZIdv9ASKAP6ocNC4D/Q==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.2.1.tgz", + "integrity": "sha512-Nh8sCBl0JF6x+H7/1hemY76JeTRC66EfMULT4B3ryF9srRSLYc6X6RfPRtYvrIyjgr6h3jZgrNHlV76wiJaOcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "8.0.1", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^5.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^18.0.0" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/compiler": "22.2.1", + "typescript": ">=6.0 <6.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.1.tgz", + "integrity": "sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/generator": "^8.0.0", + "@babel/helper-compilation-targets": "^8.0.0", + "@babel/helpers": "^8.0.0", + "@babel/parser": "^8.0.0", + "@babel/template": "^8.0.0", + "@babel/traverse": "^8.0.0", + "@babel/types": "^8.0.0", + "@types/gensync": "^1.0.5", + "convert-source-map": "^2.0.0", + "empathic": "^2.0.1", + "gensync": "^1.0.0-beta.2", + "import-meta-resolve": "^4.2.0", + "json5": "^2.2.3", + "obug": "^2.1.1", + "semver": "^7.7.3" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/core": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.2.1.tgz", + "integrity": "sha512-a8WlYTRiaMPz1KvLE5xjeWrLyZUuQX8bh6Ak7zajUci9iWsTUQefschjSwP1WKlin3JhM3XjmsTEDyZvSLIfRg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/compiler": "22.2.1", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } + } + }, + "node_modules/@angular/forms": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.2.1.tgz", + "integrity": "sha512-xHM+PhrvYDJ+edPxh6PWhkE99tXWbsMcxdXCWF6QcyByX+bsW2rVlNZ7dumOsUle9MYa8Yj0lZFACUWbwFw+4w==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0", + "zod": "^4.0.10" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/common": "22.2.1", + "@angular/core": "22.2.1", + "@angular/platform-browser": "22.2.1", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-22.2.1.tgz", + "integrity": "sha512-qw+/aJBV4lqqoJgv1VBWbMBVnE7431SX0Mew1j7PO+s1UcTushbG7CIAEo1hwmRq3kyo4Qqch8+uzFq4R631HQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/cdk": "22.2.1", + "@angular/common": "^22.0.0 || ^23.0.0", + "@angular/core": "^22.0.0 || ^23.0.0", + "@angular/forms": "^22.0.0 || ^23.0.0", + "@angular/platform-browser": "^22.0.0 || ^23.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.2.1.tgz", + "integrity": "sha512-KQ3oguemaSqyNP00yqEvPouY7NC4Lji+6S8yfjaWaUZjSth/GckpeP/Fo9HtMqd04y6MZ6lEb2hbQtTqFcSdEQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/animations": "22.2.1", + "@angular/common": "22.2.1", + "@angular/core": "22.2.1" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/router": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.2.1.tgz", + "integrity": "sha512-HJfJ4yA1znKd9YFtbDOJfKmD12x0PhS7LkmYdWlJRu2WkQZRBVrdGKdmbvV5UvrqCRllVWKpzw1TIZJM69rlcw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "@angular/common": "22.2.1", + "@angular/core": "22.2.1", + "@angular/platform-browser": "22.2.1", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.2.1", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.2.1.tgz", + "integrity": "sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + }, + "peerDependencies": { + "@types/json-schema": "^7.0.15" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.1.3.tgz", + "integrity": "sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.4.2", + "@csstools/css-color-parser": "^4.2.5", + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.4.tgz", + "integrity": "sha512-YnVzxDxLaqL0t7Q4wfcgHZjg55Wm6y5EaO7Bhu3q9J7wq/8iw3PhsnWiOQhYekx4VexnbkgWpTT6n+QIV7kczw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.1.0", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-8.0.6.tgz", + "integrity": "sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^8.0.6", + "js-tokens": "^10.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-8.0.5.tgz", + "integrity": "sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/core": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-8.0.5.tgz", + "integrity": "sha512-2/oWkgTbBYoqioCWAE4XJobOrzwxTDa5/XjDP3tJ1BhDr/owcd9qnXBp4xc3/2G5X4bvXM04nrxbRJxFcXAxFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/generator": "^8.0.5", + "@babel/helper-compilation-targets": "^8.0.5", + "@babel/helpers": "^8.0.5", + "@babel/parser": "^8.0.5", + "@babel/template": "^8.0.0", + "@babel/traverse": "^8.0.5", + "@babel/types": "^8.0.5", + "@types/gensync": "^1.0.5", + "convert-source-map": "^2.0.0", + "empathic": "^2.0.1", + "gensync": "^1.0.0-beta.2", + "import-meta-resolve": "^4.2.0", + "json5": "^2.2.3", + "obug": "^2.1.1", + "verkit": "^0.3.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.6.tgz", + "integrity": "sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.6", + "@babel/types": "^8.0.6", + "@jridgewell/gen-mapping": "0.4.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.31", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.4.0-beta.0.tgz", + "integrity": "sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0-beta.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-8.0.6.tgz", + "integrity": "sha512-HTrtZ4+iA0+kzdbCgvdSk1U4pEl3BvpGnlUbualCGNbrVhhQvhgBzurTUmqpWyvyHbWYWaIfdx2mOlL9LJjsew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^8.0.5", + "@babel/helper-validator-option": "^8.0.0", + "browserslist": "^4.24.0", + "flru": "1.0.2", + "verkit": "^0.3.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-8.0.6.tgz", + "integrity": "sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.6.tgz", + "integrity": "sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.6.tgz", + "integrity": "sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-8.0.0.tgz", + "integrity": "sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helpers": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-8.0.5.tgz", + "integrity": "sha512-fQtPOXjYOYv85PIdwotp2TJGVYOycX0PQq+l844fFAxOULtBy8BVF35GyeueX0r4KvDthqPH5xAI1clQPk/2uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.5" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/parser": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.6.tgz", + "integrity": "sha512-LpGDIYJAzc3Y3PT9x+FxBrGYu2PlWxLmyN7SwPTzaX0LSwEmFJBsRnPBqmaXF2r1v+Zhz6lNiEJ+7yKtM5+Ugg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/template": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-8.0.0.tgz", + "integrity": "sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.0", + "@babel/parser": "^8.0.0", + "@babel/types": "^8.0.0" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/traverse": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-8.0.6.tgz", + "integrity": "sha512-ilYV+qLStGzSKLipJlETdvdtpK3rAuwfCmCoGKv15MU4EoeIuLkespSNoNLFGOPSciEHx9TLWtN5mv0fz5w5mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^8.0.6", + "@babel/generator": "^8.0.6", + "@babel/helper-globals": "^8.0.6", + "@babel/parser": "^8.0.6", + "@babel/template": "^8.0.0", + "@babel/types": "^8.0.6", + "obug": "^2.1.1" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/types": { + "version": "8.0.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.6.tgz", + "integrity": "sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.6", + "@babel/helper-validator-identifier": "^8.0.6" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.16.0.tgz", + "integrity": "sha512-FWa0sPlqYGJgpTs6OxBRcL/AW4JT0OIO+W1ajerluoTVh8CV0B7XM1qsNCRgnkSCkRCgOICKHRQlY17jrsm0+Q==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.2.tgz", + "integrity": "sha512-grhRy3OKmniaAEKXMjua5z/EODX0MSqBGjunw8+j/3HQjOnahs2AGhvEOIYVUWcU6ScApbhLhVrQTX8XqrMrow==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.3.tgz", + "integrity": "sha512-iex20d8CHVkyvg6B7UKV7uHnI2Bqo9g+EFfT9E0y+GvTvhZ/DwONJ+9aKb1dlqm0ZiGsL5RXjp0fCoJYnkeDjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.6.tgz", + "integrity": "sha512-iiPQ3iRWwnJkeEn6RIu6SJPr7hYrLz6XZ9s/QZl+2/LI5KQVjpl2fdmDSZKuD4xP6GMmMPgHFFXg6k1Wkz0Trg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.2", + "@csstools/css-calc": "^3.4.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.2", + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.2.tgz", + "integrity": "sha512-40cSKyMvK+tq4qz6Awrlye2WGuOKt3FwPgtGg6KTfbHOWNw+Rk1rzbAtZnZ6IBhsY491HLRnDXwoyBAijmmILA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.2" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.15.tgz", + "integrity": "sha512-J0u7HkVl2nzSlhsiTOp4AmwcUQ3D+mGEEKfBy/7To5/y7F2OHwyLrXfrhR0SMgr4p5Lo+eaMVSeai24zUcBIxA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.2.tgz", + "integrity": "sha512-OoKoR0f76dCY666JlcbhmVTs2drYj1GUXZTYTcbUgJjh9Nv41aFfZ21bPQTERm5+L5cBDo466NltB2lplS5GBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.16.0.tgz", + "integrity": "sha512-IcpW84uEn3N7ETtNZMlxKhfl6Pec8rUNGOTBtWbK1FKhJxIFAptZyVrvVRVBimAJxJCgc3PxepxkdWWG4DVzfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@harperfast/extended-iterable": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", + "integrity": "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.8.tgz", + "integrity": "sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.5.tgz", + "integrity": "sha512-bRt8J8m+Fot9CXv+zNQGXUq2ET0MggR1fPz7v6edN6MFYmsbfGnMmkmWZJEegMKqrAC8ej/o1sqisHZXZJMAfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.3.2.tgz", + "integrity": "sha512-Xvr/0HggjddPtGppuqVmxhTw+Hr8PvsZ/k0HmOEaAqQEt80OITNkFWnsdNmyT0/eM4Ab+iJLx2R8rctlEyfSVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-12.0.3.tgz", + "integrity": "sha512-wsSy0sznmXwkty+2PzZwx00Cazc/E0r0B7mAzdGROz2Ct+DFZXaK7WDjGZvgjRldxH5ZhFVfF2lgkYrqgOw2KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.3.3.tgz", + "integrity": "sha512-YsKkS2q63IiLtaDK/9nqzdComN97SDQrmKiyNggN+ceP4ty+Z6VwyTz3FpjeUWeW1Efss2xHFKCC9sx7hnrsxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/external-editor": "^3.0.5", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.5.tgz", + "integrity": "sha512-uHuXLmXW+TtIfT/9vSBotypAkqn1n34Ul+CLGPos/xANyO4Ff5xZzkYhbKR4NEcfVK4a9mHQOpwVZzluSHFRGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.5.tgz", + "integrity": "sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.9.tgz", + "integrity": "sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.6.tgz", + "integrity": "sha512-HtcJhB2QFVXbLuJ5S3syhNbTUVxYvwqV4VRBDkQceBloC9bmTViUoRFP5PbSaDZb3HzfPmpuU/gG4ybVBz4FHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.2.3.tgz", + "integrity": "sha512-6Yuwh1NGSbu1Lo4N1EWjXs1jKRntLg/ZCwhmeorEHde90v1XxAozdbd4Iu30eOQLW+6h1hp2O9ujNfLSbTPJnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.2.2.tgz", + "integrity": "sha512-W9zYdyzogK+6110mqwaSJWCBu2yA5Q/OfnGSjjZB1bNpHlmUozXxTl0+QOZBNeVd6Qo81/qT75gW05gLAtITxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.7.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.7.2.tgz", + "integrity": "sha512-QoRB4wFIjgH5iOhSjoIKMkTvSHDuV+O3OITlIqAYO0oK5x364GJILXiMBvlPiE+klg7Xx9tq5XVqQHcGUDYYPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.5", + "@inquirer/confirm": "^6.3.2", + "@inquirer/editor": "^5.3.3", + "@inquirer/expand": "^5.1.5", + "@inquirer/input": "^5.1.6", + "@inquirer/number": "^4.2.3", + "@inquirer/password": "^5.2.2", + "@inquirer/rawlist": "^5.3.5", + "@inquirer/search": "^4.3.3", + "@inquirer/select": "^5.2.5" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.5.tgz", + "integrity": "sha512-1oHky1ONfCOwNrnkQGDE1oaSij/3fI6HFMSf2H/WsGO2lEyDX9My82iggITSy9ddSZ8yk8j9v41OI0fVoSIoaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.3.3.tgz", + "integrity": "sha512-fyuIU1Nbpvwlikjg3gXwJFDI11+EFjqQ7P+iByfmivIKQ1vmaykNrD/vy5unHuUqUpsOsnvJ25//tPF7E/RBRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.5.tgz", + "integrity": "sha512-9kc15hr8r/kI+3DO/xLog5nOzTz1jqsHXa6JBFzmQKhkoJ8Slda1I1L/uD8ZSZ9tF1yp79wwXe7mclvX1rqR2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.8", + "@inquirer/core": "^12.0.3", + "@inquirer/figures": "^2.0.9", + "@inquirer/type": "4.1.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.1.1.tgz", + "integrity": "sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-4.2.7.tgz", + "integrity": "sha512-k180vxSv2KqmnDxoato6pCqEJsov4U5C0EVmldZkAdNeVBxY48A/LM6TCNPGIzDK/iZSfsZP0Mw+gbWc5hXLyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=22.13.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 9", + "listr2": "11.1.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.5.6.tgz", + "integrity": "sha512-mY5FG4TjPAkY4P0w+OhHaUka5mDh2TX2WKYIwuKzJ1zeW3VvRgxdam/lGJTquI+bthTx5CSHDW+BAQCnNAzkEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.5.6.tgz", + "integrity": "sha512-foa+pwitysO8k+xhs7psBFfTKnVgR69NlZRRTHaFVDqphh7AdGpLeyRzKw/ofatr/sN6TiHRRW6mmop0ZrrppQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.5.6.tgz", + "integrity": "sha512-QR4YRyR5h5Z8eGXrNQjiyo2NNDfqi3tCc9dQG5Is1blCt+qWw1ZoBWhlWAr5d+jshkifMIJjVHzHGKbkKzF8Tw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.5.6.tgz", + "integrity": "sha512-HmiyFFdJa38s1heCMSooSPaBSFTHJ3C+ERPp28xAPlDX1YiALJVOgbry065nXd8Y7KISWjnw05zpG1RX8IfftA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.5.6.tgz", + "integrity": "sha512-ADzCuCF2cTNiX9kDScqcz1fjnAkxPpQNneV3KFTdV3wWtVlI2sTGzySoMTgDpinkMMFj1NTJlxA6XR8fwc4hlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.5.6.tgz", + "integrity": "sha512-J7A9aEQsQiv0TYtBGL7NDIPp2lOS8nnl+zm4sWZm1xlsTTaQ4PgD096Adzdrk27rw3UxCkDXdCUa4ax41oztBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.5.6.tgz", + "integrity": "sha512-1g7G0knRX2iV/voDu54yxrGqw5Dk0w2oIYb7dgJq8IkOi+m7wbD8Q3QpPFjh0C01G58S88dqGn03len6UPCXsg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", + "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", + "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.1.1.tgz", + "integrity": "sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.1.1", + "@napi-rs/nice-android-arm64": "1.1.1", + "@napi-rs/nice-darwin-arm64": "1.1.1", + "@napi-rs/nice-darwin-x64": "1.1.1", + "@napi-rs/nice-freebsd-x64": "1.1.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.1.1", + "@napi-rs/nice-linux-arm64-gnu": "1.1.1", + "@napi-rs/nice-linux-arm64-musl": "1.1.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.1.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.1.1", + "@napi-rs/nice-linux-s390x-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-gnu": "1.1.1", + "@napi-rs/nice-linux-x64-musl": "1.1.1", + "@napi-rs/nice-openharmony-arm64": "1.1.1", + "@napi-rs/nice-win32-arm64-msvc": "1.1.1", + "@napi-rs/nice-win32-ia32-msvc": "1.1.1", + "@napi-rs/nice-win32-x64-msvc": "1.1.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.1.1.tgz", + "integrity": "sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.1.1.tgz", + "integrity": "sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.1.1.tgz", + "integrity": "sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.1.1.tgz", + "integrity": "sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.1.1.tgz", + "integrity": "sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.1.1.tgz", + "integrity": "sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.1.1.tgz", + "integrity": "sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.1.1.tgz", + "integrity": "sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.1.1.tgz", + "integrity": "sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.1.1.tgz", + "integrity": "sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.1.1.tgz", + "integrity": "sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.1.1.tgz", + "integrity": "sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.1.1.tgz", + "integrity": "sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-openharmony-arm64": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-openharmony-arm64/-/nice-openharmony-arm64-1.1.1.tgz", + "integrity": "sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.1.1.tgz", + "integrity": "sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.1.1.tgz", + "integrity": "sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.1.1.tgz", + "integrity": "sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.150.0.tgz", + "integrity": "sha512-oQef2Zu4Prz1KLKznz3HqZzU9uVoA5PMoDZuuLmqms7hKmKSAPzlaMnLllJq3t+rgKmfJJ2siPrpZfFrW06btw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.150.0.tgz", + "integrity": "sha512-B6ofpoFiAUwIZ0MJ2IgHPvZK8FAtL4qzSZRwOkAKIfxEobE1mQC8nDdiFZj7pUaJiVUVCsfkMsBJKedzO8rZvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.150.0.tgz", + "integrity": "sha512-J+9IHKzx/bSz1JetOfD4zKXSK9sOm4/a7+0qomJODcTL6JsRXGeV/ZdkAPSIDydfFmWzYCraMlQEosSZEyBYkQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.150.0.tgz", + "integrity": "sha512-v6IPfcAcSYrBWXV5Tce1DmxDMXLhEYpIIWiRFPJopgCVscZdLaV4MRQOUxvL3usQlw+yrwYOjBFB9IwBx+jUiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.150.0.tgz", + "integrity": "sha512-AoR/4jD02HET0KO0yNT4nbTE+XJUxiO9jB1X/ycEjUE3WrIJ3IjV/Vf+gskhWLcqqeXfvNnkDtBR7epllAm88A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.150.0.tgz", + "integrity": "sha512-A/hycCFjLUrCmLoL5O/vBp40ohlaXO1Ta3v5hqYZxicYFs129wZmgZJggcW4mYGYbZebQLhup+N8JjeQl8qwyw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.150.0.tgz", + "integrity": "sha512-pbqahg1Pkz7J4RKmXm30s/iQu99hv64ayXCu8P4p75HcEH5azOdR4eGqiB6lFGkFR3mMpzaYsSKLkS8oJbjmeQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.150.0.tgz", + "integrity": "sha512-HV11aRbBQGwqv8bEo+K/6qr88uB4fEe1mhXncMhlVCrj+WpBSraxrUzHaPVmeQRU6x6x8v/3DuCbbo93rpp+fw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.150.0.tgz", + "integrity": "sha512-k6pVkJqALwtEuP4zukVhGRhdIy4+ofChUIUUsAHHyqDZlNsknmel3JjbggrJiWGaes6h/dIcWmEXsKW4SmK9oQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.150.0.tgz", + "integrity": "sha512-tEFg39mw/rHO5n8GK2DR4ZMFfsPQZtnQIPbsIpjoQRomJc/2tRfsCSmCT6gNit+NZGoeJG5aH9ATDH5bf0WnoQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.150.0.tgz", + "integrity": "sha512-0JO7IFkoek6HqV589Menx3hJySNXi6quRhO4tq2P7kpEHKEXoDATnoPVUpn5B7gDH2KLkdo751N0uIrGJFCTCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.150.0.tgz", + "integrity": "sha512-7XPzREnyAS5wHU9aB+49Uaqgoo1gVCklHc3DRlc3fJy2tXD/eAJFN1QFz/crj+Ulup5P1+axNREF+/RGaB/IRA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.150.0.tgz", + "integrity": "sha512-7n7ZxcbRWDFaTdyj8M8p0O9OfzoMKm8O6syBAIgcSutbOALq0Bb9G52Me+XH0anMLZV+NgXc726gqgG2q39vcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.150.0.tgz", + "integrity": "sha512-Vx0GSA9ZCRTUiczoEQnIIyXkMvtEY8uc6IiwLQtMe5ci2sXPqjrry0Ek5fsPP1k2kCzkML3ow9UOOEgnEDi7Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.150.0.tgz", + "integrity": "sha512-ii4/9m3viDLssMnfXU7/Pni/3nYplApuGayceX4qsVGaqqQJCx3tHIH9M/HWIeSty5i8LjS21zPYT6lVIkwLxw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.150.0.tgz", + "integrity": "sha512-ZtoxX5rez36ZWkwtiEhjkCPnlTPoQ2I7lIL0IYZ1yjxMYohbvoOjBmUIZzrf9W/HouONq0omIfTeCWEY+R380w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.150.0.tgz", + "integrity": "sha512-VqeRb5JX/bKYBrff7TUDvJyKY0914UzuCkuXIGxJS4FUmvMNfqkCbc7Sq90iMz9DlmAtlggwaBYQ9eg3h3ltiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.150.0.tgz", + "integrity": "sha512-EPqJfeZ4Pgg2BJSsCV8GozxV0FDltRzpVtGVa1r2noJu1iu+opOw3gtBWXwIo9odCT/MdFfyHxdy0/CRqs3OqA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.150.0.tgz", + "integrity": "sha512-n5YMzbqwPQqozbkrexi0lNZrUz5cnPHalYpFWe6Dau7AmKIWNo+y24IwzDuZEQtEdUHuKhXmMMih/MPjOI+CMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.150.0.tgz", + "integrity": "sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.6.0.tgz", + "integrity": "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.6.0", + "@parcel/watcher-darwin-arm64": "2.6.0", + "@parcel/watcher-darwin-x64": "2.6.0", + "@parcel/watcher-freebsd-x64": "2.6.0", + "@parcel/watcher-linux-arm-glibc": "2.6.0", + "@parcel/watcher-linux-arm-musl": "2.6.0", + "@parcel/watcher-linux-arm64-glibc": "2.6.0", + "@parcel/watcher-linux-arm64-musl": "2.6.0", + "@parcel/watcher-linux-x64-glibc": "2.6.0", + "@parcel/watcher-linux-x64-musl": "2.6.0", + "@parcel/watcher-win32-arm64": "2.6.0", + "@parcel/watcher-win32-x64": "2.6.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.6.0.tgz", + "integrity": "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.6.0.tgz", + "integrity": "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.6.0.tgz", + "integrity": "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.6.0.tgz", + "integrity": "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.6.0.tgz", + "integrity": "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.6.0.tgz", + "integrity": "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.6.0.tgz", + "integrity": "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.6.0.tgz", + "integrity": "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.6.0.tgz", + "integrity": "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.6.0.tgz", + "integrity": "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.6.0.tgz", + "integrity": "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.6.0.tgz", + "integrity": "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@schematics/angular": { + "version": "22.2.1", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.2.1.tgz", + "integrity": "sha512-L4Nl8D43znCzXwze9PtbQhG1GKlhWq9ip8Hz+e3LHgG3WpT5Tlr7aVsyHDqb9Qetuy0Mpd4tXE15STtGdA897w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "22.2.1", + "@angular-devkit/schematics": "22.2.1", + "jsonc-parser": "3.3.1", + "oxc-parser": "0.150.0", + "typescript": "6.0.3" + }, + "engines": { + "node": "^22.22.3 || ^24.15.0 || >=26.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/gensync": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/gensync/-/gensync-1.0.5.tgz", + "integrity": "sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.3.0.tgz", + "integrity": "sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.3", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/beasties": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.5.4.tgz", + "integrity": "sha512-8/lOOjT+PElYCnaUbmhgd3Uad5R1VTQ4Wg0k4rK8bX04m5WEqHNmnlLy3Q/lJ7dk25XA+72gayzc/skTfKKpcA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^7.0.0", + "css-what": "^8.0.0", + "dom-serializer": "^3.0.0", + "domhandler": "^6.0.0", + "htmlparser2": "^12.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3", + "postcss-safe-parser": "^7.0.1" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/boolbase": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz", + "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/browserslist": { + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-6.1.1.tgz", + "integrity": "sha512-06p9vyLahLa4zkGcgsGxU6iEkSOiuI4fhCH6Emhe2lPAcoUv73n72DnODsnHA+5wwXGnV0n9M9/qOQJSjYhFhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^9.0.0", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/colorjs.io": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.7.1.tgz", + "integrity": "sha512-LY7OHnJZxHwT5UlzNa9bbhHHDbzB6yE5+3MIPwJEQKRvSCt/T4G7epsj+9j2BExUIIfXFIJGsIXUKdfrK9Q5tA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz", + "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^2.0.0", + "css-what": "^8.0.0", + "domhandler": "^6.0.1", + "domutils": "^4.0.2", + "nth-check": "^3.0.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz", + "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz", + "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz", + "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/domhandler": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz", + "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^3.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz", + "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^3.0.0", + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.444", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz", + "integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.1.0.tgz", + "integrity": "sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eol": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.10.0.tgz", + "integrity": "sha512-+w3ktYrOphcIqC1XKmhQYvM+o2uxgQFiimL7B6JPZJlWVxf7Lno9e/JWLPIgbHo7DoZ+b7jsf/NzrUcNe6ZTZQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ryanve" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/flru": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz", + "integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fs-extra": { + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz", + "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hosted-git-info": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz", + "integrity": "sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-7.0.0.tgz", + "integrity": "sha512-UikN5yr7xsCDAq87Or5or0PAlD3HJJOKVzM05az588WnpDJ4Ux7a2A53Qi6gofGg2/EtvF/H4hCi/TXfCW4Y6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/htmlparser2": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-12.0.0.tgz", + "integrity": "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^3.0.0", + "domhandler": "^6.0.0", + "domutils": "^4.0.2", + "entities": "^8.0.0" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/immutable": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", + "integrity": "sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.1.tgz", + "integrity": "sha512-FahmoPK5vbPc+jxV1iErMHmAZypCZ942NHF4+qqaWAuvaKKTBZxawnmAtrbGWLU7MtlxfqIP0qw6aSI+aWGtLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^7.0.0", + "@asamuzakjp/dom-selector": "^9.2.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.13", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^7.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "tough-cookie": "^6.0.2", + "undici": "^8.10.2", + "w3c-xmlserializer": "^6.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/listr2": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-11.1.0.tgz", + "integrity": "sha512-xsuczF6mWkEo/E80QHaEokQzKmTy7m80KcLRnkUt9wrcePres2w7dbQi5zOsWtDUg8Dac4gPFyu4EJ/IvKwTOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^6.1.1", + "log-update": "^8.0.0", + "wrap-ansi": "^10.0.1" + }, + "engines": { + "node": ">=22.13.0" + } + }, + "node_modules/lmdb": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.5.6.tgz", + "integrity": "sha512-j3uE8ReKNyUWDjhfEFSJqE/1DLtfTR5Z8yFzVHvBjAk37wNg7HdScjcv8ttPHRvrdgPQMPWxFFI0SsdBzI5lBw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@harperfast/extended-iterable": "^1.0.3", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.5.6", + "@lmdb/lmdb-darwin-x64": "3.5.6", + "@lmdb/lmdb-linux-arm": "3.5.6", + "@lmdb/lmdb-linux-arm64": "3.5.6", + "@lmdb/lmdb-linux-x64": "3.5.6", + "@lmdb/lmdb-win32-arm64": "3.5.6", + "@lmdb/lmdb-win32-x64": "3.5.6" + } + }, + "node_modules/lmdb/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-8.0.0.tgz", + "integrity": "sha512-lddSgOt3bPASrylL54ZSpy8nBHns+vBVSoILlVOx+dei300pnLRN958rj/EdlVLKuWlSESU3qdnDZdAI7FXYGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.3.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^9.0.0", + "string-width": "^8.2.0", + "strip-ansi": "^7.2.0", + "wrap-ansi": "^10.0.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", + "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.12.1.tgz", + "integrity": "sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ng-openapi-gen": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ng-openapi-gen/-/ng-openapi-gen-1.1.0.tgz", + "integrity": "sha512-18Nujl6hPuacDfDJlw+II++w9IRrG6aHP90GLzuZOv9yB03yyoC2Dgd8Z5sXqVDRvgQxeWPwdXpsGvxyqhJDfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^14.2.1", + "argparse": "^2.0.1", + "eol": "^0.10.0", + "fs-extra": "^11.4.1", + "handlebars": "^4.7.9", + "jsesc": "^3.1.0", + "lodash": "^4.18.1", + "openapi-types": "^12.1.3" + }, + "bin": { + "ng-openapi-gen": "lib/index.js" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "rxjs": ">=6.5.0" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm-package-arg": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-14.0.0.tgz", + "integrity": "sha512-69XQh3k+dtGa1p+7RaR57IuG3rCko96xr/nUfN4yDYBXbTYICiWcOpsFKLN2GtGE9cyIljE+f1exnaYt9MvM+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^10.1.0", + "proc-log": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^8.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/nth-check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz", + "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^2.0.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/ora": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.4.1.tgz", + "integrity": "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.2.tgz", + "integrity": "sha512-BY+qgDArmYpvvmij141xbkB4Cw8RFLBQM5Zj4MODFb1SyXBRE/xO4Xj/QWbR7aUEBxbkjuNdsFVCL2TmbYrlNg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/oxc-parser": { + "version": "0.150.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.150.0.tgz", + "integrity": "sha512-zwajKw1GUa57IOdafIS7/yOObGF2NihwIfZfs5yJTRprMSkiwhWOc/k+n8bwnUPbHr2/XsjfBO8KyTPaW3VpWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.150.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.150.0", + "@oxc-parser/binding-android-arm64": "0.150.0", + "@oxc-parser/binding-darwin-arm64": "0.150.0", + "@oxc-parser/binding-darwin-x64": "0.150.0", + "@oxc-parser/binding-freebsd-x64": "0.150.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.150.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.150.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.150.0", + "@oxc-parser/binding-linux-arm64-musl": "0.150.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.150.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.150.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.150.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.150.0", + "@oxc-parser/binding-linux-x64-gnu": "0.150.0", + "@oxc-parser/binding-linux-x64-musl": "0.150.0", + "@oxc-parser/binding-openharmony-arm64": "0.150.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.150.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.150.0", + "@oxc-parser/binding-win32-x64-msvc": "0.150.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-8.0.1.tgz", + "integrity": "sha512-NaRku2aMpUN1Sh1Gyk1KWUh2A7EJx2c6qYzvwsPtqhoHoaURshdrceYK3LunVCm3WHhm6FS7Vcczbvdh3/UIVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0", + "parse5": "^8.0.0", + "parse5-sax-parser": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/piscina": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.3.2.tgz", + "integrity": "sha512-vv7l/mM7B9WgrhDaudIqv1x6v4LOBRYYmpWGdWushhpbCmVHp11sUB2v4hj1CAeTGXVxuZFz6xOU1RalAiPKfQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.x" + }, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-safe-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.1.0.tgz", + "integrity": "sha512-1WzZxRLaAFwEh6Do+zyGpjWV3nGJNxxhuh7Ubu/q1ICImMgZJnLwhoaEpRbG4pJppp2Y1ncL9ffA2f+LhrefQg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/prettier": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-7.0.0.tgz", + "integrity": "sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rolldown": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.149.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.104.1.tgz", + "integrity": "sha512-yDA+1aIG3EHgN4V/BvuhCvu61FF6hEd4e+9DxikUm9U0CAGuvdIZ/UYy7qbOxjhbbWQraoyLlMuzdRGOSV5Bmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-embedded": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.104.1.tgz", + "integrity": "sha512-7LxbtPJ1MWKVbhzOnGx1sHTkHTcHVJ9hbgVsU29p/8gY1VeXt19fcJFr5xsx2f6zfrrR3FeOcYMt0uYDLAxpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bufbuild/protobuf": "^2.5.0", + "colorjs.io": "^0.7.0", + "immutable": "^5.1.5", + "rxjs": "^7.4.0", + "supports-color": "^8.1.1", + "sync-child-process": "^1.0.2", + "varint": "^6.0.0" + }, + "bin": { + "sass": "dist/bin/sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "sass-embedded-all-unknown": "1.104.1", + "sass-embedded-android-arm": "1.104.1", + "sass-embedded-android-arm64": "1.104.1", + "sass-embedded-android-riscv64": "1.104.1", + "sass-embedded-android-x64": "1.104.1", + "sass-embedded-darwin-arm64": "1.104.1", + "sass-embedded-darwin-x64": "1.104.1", + "sass-embedded-linux-arm": "1.104.1", + "sass-embedded-linux-arm64": "1.104.1", + "sass-embedded-linux-musl-arm": "1.104.1", + "sass-embedded-linux-musl-arm64": "1.104.1", + "sass-embedded-linux-musl-riscv64": "1.104.1", + "sass-embedded-linux-musl-x64": "1.104.1", + "sass-embedded-linux-riscv64": "1.104.1", + "sass-embedded-linux-x64": "1.104.1", + "sass-embedded-unknown-all": "1.104.1", + "sass-embedded-win32-arm64": "1.104.1", + "sass-embedded-win32-x64": "1.104.1" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.104.1.tgz", + "integrity": "sha512-toyMCyUKkT6DsYlHuKZGjd8T6WltaW4TGX07F7HnF4tNJM5m/8VaVsd74zrq3QSPqGE9JjxIDK4NrNc/QY9pkA==", + "cpu": [ + "!arm", + "!arm64", + "!riscv64", + "!x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "sass": "1.104.1" + } + }, + "node_modules/sass-embedded-android-arm": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.104.1.tgz", + "integrity": "sha512-g/XPAJYAzSSQZFP4xCdGD0HLKB4mf24cJ/+5Nls8rMool3jNkBv9+AFO4EV0b+RxN8rLcEsMccH1CR64yIFYcQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-arm64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.104.1.tgz", + "integrity": "sha512-yW3BVW4YFpN2vu2cVQ+Pz5JrRTvoIz36aCdo+J/Mo1A9gEN8eVPV/AL7aeZ803YdKgIitd3qw3pY4wpmT6TXNg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-riscv64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.104.1.tgz", + "integrity": "sha512-ZYY8oMKQS7Q/6FBoC8w3T16O3MucDe3xv8iKym41Nht5uvLCGGpm4fC4OF5nlv7wQkJv8oactYddGG3Wd0Rqkg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-android-x64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.104.1.tgz", + "integrity": "sha512-5WZKC4lSfjMIjzZdQUlYQUAV+UiatBhD8w28U7cYLLj4/dMgEPOBd4g3VMv0BwpgH2JOaycMS42dXq29W2bcDg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-arm64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.104.1.tgz", + "integrity": "sha512-oatuHzn0NsEg0d9Dcze6CuWUPvuETtMxRK4cv0eRAg7p6R0vHnv5GZRyWG6gEy3nc/ha/L+GtTasWVU82f4jrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-darwin-x64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.104.1.tgz", + "integrity": "sha512-gql/5sHGzUTVwuHfM3QLCDeb6NYwen+LLj22gz81C2NKHF/4nUtDyh4N+fURWdT7J0/EhtMOd0+FLML3sGNP7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.104.1.tgz", + "integrity": "sha512-WfmshPJOp9vUKxgp8wZ2ncaKBbyrlW9J51M2aUQHWECjMNxTULnGOHWSmD7FViXv4zwn1Co5FIFQUipW7RCxtA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": "glibc", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-arm64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.104.1.tgz", + "integrity": "sha512-sVWUKXNTu8sP3X95Df0xo/t49Ce5IseTIMqXHxhFBKVmx7gf7kBWNLK0wH5ym+4khc+B5nNJPgq4IxpelG8tMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": "glibc", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.104.1.tgz", + "integrity": "sha512-k0cQ/CxBzFVYiQEObg6CpF6CUglkdEhi/DZgZXQbwC27kbGGY+fatmIs/cFPJL2z1ulACwoAr86RQUFzYvw5eg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": "musl", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-arm64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.104.1.tgz", + "integrity": "sha512-H7b+LzZ/w8pwNExInM8HDHraqvxCMdUc5ZRGU/aSJJhZ9HjeRKBjF3GxaisVHnx7o18j1l4VQ5cLBL9tTmNzZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": "musl", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-riscv64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.104.1.tgz", + "integrity": "sha512-9DYdn4wfrkidjxJVtEd2iCx8HKdZe+uF82GDmrYEZhgu4NjVE6a4fQyW7OLwzn3erZMHKBaObBOH1d37DnXI1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": "musl", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-musl-x64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.104.1.tgz", + "integrity": "sha512-5xz+nAr01b6yaRdrGVgE7lNvm1lGFjwNdUvnz+teidvHCbD5oTcbhrRVrnDoLHHvq5n3Jve9TzWaaHhezaEfHA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": "musl", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-riscv64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.104.1.tgz", + "integrity": "sha512-oWryPJWTXK5adtlz/FD7wd2kS4I0CEgp6mWLUVb4oMFURMLz9bA9fO87D7lc4SV++lds7T51PU1kykCbOvNHMg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": "glibc", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-linux-x64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.104.1.tgz", + "integrity": "sha512-j/sX/yKTx/wJKepC5/ISlR6kPeSuHKThf76D2za5i2VnbHg0MN19jQq3WMorowR3kRghqt6azs3awogG+e7IRA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": "glibc", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-unknown-all": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.104.1.tgz", + "integrity": "sha512-RaMsYZB5mRioL4qhoI7T5mmNb2OunakbPTJW2cuu3MeIULnGoxo7Wd5QKFxeQvF7sSQL57RuaQ+q9pH+/pQ9Fg==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "!android", + "!darwin", + "!linux", + "!win32" + ], + "dependencies": { + "sass": "1.104.1" + } + }, + "node_modules/sass-embedded-win32-arm64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.104.1.tgz", + "integrity": "sha512-fS6DL7lcnTPclqGLF4xRt8egZlFY9cU23d57ufbWqaUbCFu9JTu0+8qB4jMxfx1la6byi3GoSTfbnNOLfWv8sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-embedded-win32-x64": { + "version": "1.104.1", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.104.1.tgz", + "integrity": "sha512-/c4N2A93Ckms6DuRFVftKYISuI1tyC8gmlVJ8WaWFzgCBN/Swq2rARJIBUNeuxlmxjmYXfd3+2f/u887K63wbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-9.0.1.tgz", + "integrity": "sha512-aBY19bn/XA+hKOKX0qL7Z0UfdBsbQvc9hn993U8ALUjzxCvDcuZqLoRXjGJrUARWAlwMnsRVD9sw2AFNOYvalA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.2.tgz", + "integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/sync-child-process": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-child-process/-/sync-child-process-1.0.2.tgz", + "integrity": "sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sync-message-port": "^1.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/sync-message-port": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", + "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/tinybench": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", + "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tldts": { + "version": "7.4.16", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.16.tgz", + "integrity": "sha512-QwBER5KMR86IIjpIiO7H/Z3IMJPsZ1A6RKPAqzTTgOyUQUSt9FdnKcqhTaJmkY6HVrgouZHZR0ncK5QxvmnQeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.16" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.16", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.16.tgz", + "integrity": "sha512-MDolfaSJtlSK5Y0A1xl3277ekubZwobpBjugknDizI9O5Rm60a1m8k4ICK+MRsCDzPygT81mp3BBf5RKDlFRfA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-8.0.0.tgz", + "integrity": "sha512-SCv6OOV6Xj2/3cXy3dGmADluJTNcL3o7hZAglNPTe+WYuEuvxgJzxPrSDLZhF+CwyQOubqgecjMmTJGMVLWjYQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/varint": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", + "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/verkit": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.3.2.tgz", + "integrity": "sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vite": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite/node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/vitest": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.3", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-6.0.0.tgz", + "integrity": "sha512-4Nsy8K5Tr6SPDH9jhKJOHf7ChDrc1zufZTVSF7x72hwuEXBqxqk9G6cK+K2NRUtB3iELRJqjXb4JPDMBjMTl2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.2", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.2.tgz", + "integrity": "sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.2.tgz", + "integrity": "sha512-6OLRNZqRntVGBm26ghZ5eZhCuWWyOkRCSn8XsOIuq5stpCE/tjLbdozd93naILZig0lmD8ANfRwnhn0DLeXGlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz", + "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..3b09d1b --- /dev/null +++ b/web/package.json @@ -0,0 +1,36 @@ +{ + "name": "web", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test" + }, + "private": true, + "packageManager": "npm@11.19.1", + "dependencies": { + "@angular/animations": "^22.2.1", + "@angular/cdk": "^22.2.1", + "@angular/common": "^22.2.0", + "@angular/compiler": "^22.2.0", + "@angular/core": "^22.2.0", + "@angular/forms": "^22.2.0", + "@angular/material": "^22.2.1", + "@angular/platform-browser": "^22.2.0", + "@angular/router": "^22.2.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@angular/build": "^22.2.1", + "@angular/cli": "^22.2.1", + "@angular/compiler-cli": "^22.2.0", + "jsdom": "^30.0.0", + "ng-openapi-gen": "^1.1.0", + "prettier": "^3.8.1", + "typescript": "~6.0.2", + "vitest": "^5.0.0" + } +} diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..57614f9c967596fad0a3989bec2b1deff33034f6 GIT binary patch literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( literal 0 HcmV?d00001 diff --git a/web/src/app/api/api-configuration.ts b/web/src/app/api/api-configuration.ts new file mode 100644 index 0000000..6e2dc7b --- /dev/null +++ b/web/src/app/api/api-configuration.ts @@ -0,0 +1,27 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { Injectable } from '@angular/core'; + +/** + * Provides the ApiConfiguration configuration object with a given root URL. + */ +export function provideApiConfiguration(rootUrl: string) { + var config = new ApiConfiguration(); + config.rootUrl = rootUrl; + return { + provide: ApiConfiguration, + useValue: config + }; +} + +/** + * Global configuration + */ +@Injectable({ + providedIn: 'root', +}) +export class ApiConfiguration { + rootUrl: string = ''; +} + diff --git a/web/src/app/api/api.ts b/web/src/app/api/api.ts new file mode 100644 index 0000000..ece177f --- /dev/null +++ b/web/src/app/api/api.ts @@ -0,0 +1,64 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { Injectable } from '@angular/core'; +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable, firstValueFrom } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { ApiConfiguration } from './api-configuration'; +import { StrictHttpResponse } from './strict-http-response'; + +export type ApiFnOptional = (http: HttpClient, rootUrl: string, params?: P, context?: HttpContext) => Observable>; +export type ApiFnRequired = (http: HttpClient, rootUrl: string, params: P, context?: HttpContext) => Observable>; + +/** + * Helper service to call API functions directly + */ +@Injectable({ providedIn: 'root' }) +export class Api { + constructor( + private config: ApiConfiguration, + private http: HttpClient + ) { + } + + private _rootUrl?: string; + + /** + * Returns the root url for API operations. If not set directly here, + * will fallback to `ApiConfiguration.rootUrl`. + */ + get rootUrl(): string { + return this._rootUrl || this.config.rootUrl; + } + + /** + * Sets the root URL for API operations + */ + set rootUrl(rootUrl: string) { + this._rootUrl = rootUrl; + } + + /** + * Executes an API call, returning the response body only + */ + invoke(fn: ApiFnRequired, params: P, context?: HttpContext): Promise; + invoke(fn: ApiFnOptional, params?: P, context?: HttpContext): Promise; + async invoke(fn: ApiFnRequired | ApiFnOptional, params: P, context?: HttpContext): Promise { + const resp = this.invoke$Response(fn, params, context); + return (await resp).body; + } + + /** + * Executes an API call, returning the entire response + */ + invoke$Response(fn: ApiFnRequired, params: P, context?: HttpContext): Promise>; + invoke$Response(fn: ApiFnOptional, params?: P, context?: HttpContext): Promise>; + invoke$Response(fn: ApiFnRequired | ApiFnOptional, params: P, context?: HttpContext): Promise> { + const obs = fn(this.http, this.rootUrl, params, context) + .pipe( + filter(r => r instanceof HttpResponse), + map(r => r as StrictHttpResponse)); + return firstValueFrom(obs); + } +} diff --git a/web/src/app/api/base-service.ts b/web/src/app/api/base-service.ts new file mode 100644 index 0000000..fcebae4 --- /dev/null +++ b/web/src/app/api/base-service.ts @@ -0,0 +1,35 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { ApiConfiguration } from './api-configuration'; + +/** + * Base class for services + */ +@Injectable() +export class BaseService { + constructor( + protected config: ApiConfiguration, + protected http: HttpClient + ) { + } + + private _rootUrl?: string; + + /** + * Returns the root url for all operations in this service. If not set directly in this + * service, will fallback to `ApiConfiguration.rootUrl`. + */ + get rootUrl(): string { + return this._rootUrl || this.config.rootUrl; + } + + /** + * Sets the root URL for API operations in this service. + */ + set rootUrl(rootUrl: string) { + this._rootUrl = rootUrl; + } +} diff --git a/web/src/app/api/fn/auth/get-me.ts b/web/src/app/api/fn/auth/get-me.ts new file mode 100644 index 0000000..a620614 --- /dev/null +++ b/web/src/app/api/fn/auth/get-me.ts @@ -0,0 +1,30 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { User } from '../../models/user'; + +export interface GetMe$Params { +} + +export function getMe(http: HttpClient, rootUrl: string, params?: GetMe$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, getMe.PATH, 'get'); + if (params) { + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse; + }) + ); +} + +getMe.PATH = '/api/auth/me'; diff --git a/web/src/app/api/fn/auth/login.ts b/web/src/app/api/fn/auth/login.ts new file mode 100644 index 0000000..0537217 --- /dev/null +++ b/web/src/app/api/fn/auth/login.ts @@ -0,0 +1,33 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { AuthResponse } from '../../models/auth-response'; +import { LoginInput } from '../../models/login-input'; + +export interface Login$Params { + body: LoginInput +} + +export function login(http: HttpClient, rootUrl: string, params: Login$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, login.PATH, 'post'); + if (params) { + rb.body(params.body, 'application/json'); + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse; + }) + ); +} + +login.PATH = '/api/auth/login'; diff --git a/web/src/app/api/fn/auth/logout.ts b/web/src/app/api/fn/auth/logout.ts new file mode 100644 index 0000000..9bee403 --- /dev/null +++ b/web/src/app/api/fn/auth/logout.ts @@ -0,0 +1,29 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + + +export interface Logout$Params { +} + +export function logout(http: HttpClient, rootUrl: string, params?: Logout$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, logout.PATH, 'post'); + if (params) { + } + + return http.request( + rb.build({ responseType: 'text', accept: '*/*', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return (r as HttpResponse).clone({ body: undefined }) as StrictHttpResponse; + }) + ); +} + +logout.PATH = '/api/auth/logout'; diff --git a/web/src/app/api/fn/auth/register.ts b/web/src/app/api/fn/auth/register.ts new file mode 100644 index 0000000..c3cfb25 --- /dev/null +++ b/web/src/app/api/fn/auth/register.ts @@ -0,0 +1,33 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { AuthResponse } from '../../models/auth-response'; +import { RegisterInput } from '../../models/register-input'; + +export interface Register$Params { + body: RegisterInput +} + +export function register(http: HttpClient, rootUrl: string, params: Register$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, register.PATH, 'post'); + if (params) { + rb.body(params.body, 'application/json'); + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse; + }) + ); +} + +register.PATH = '/api/auth/register'; diff --git a/web/src/app/api/fn/games/create-game.ts b/web/src/app/api/fn/games/create-game.ts new file mode 100644 index 0000000..0e59729 --- /dev/null +++ b/web/src/app/api/fn/games/create-game.ts @@ -0,0 +1,33 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { Game } from '../../models/game'; +import { GameInput } from '../../models/game-input'; + +export interface CreateGame$Params { + body: GameInput +} + +export function createGame(http: HttpClient, rootUrl: string, params: CreateGame$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, createGame.PATH, 'post'); + if (params) { + rb.body(params.body, 'application/json'); + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse; + }) + ); +} + +createGame.PATH = '/api/games'; diff --git a/web/src/app/api/fn/games/delete-game.ts b/web/src/app/api/fn/games/delete-game.ts new file mode 100644 index 0000000..dfe7b0b --- /dev/null +++ b/web/src/app/api/fn/games/delete-game.ts @@ -0,0 +1,31 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + + +export interface DeleteGame$Params { + id: string; +} + +export function deleteGame(http: HttpClient, rootUrl: string, params: DeleteGame$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, deleteGame.PATH, 'delete'); + if (params) { + rb.path('id', params.id, {}); + } + + return http.request( + rb.build({ responseType: 'text', accept: '*/*', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return (r as HttpResponse).clone({ body: undefined }) as StrictHttpResponse; + }) + ); +} + +deleteGame.PATH = '/api/games/{id}'; diff --git a/web/src/app/api/fn/games/get-game.ts b/web/src/app/api/fn/games/get-game.ts new file mode 100644 index 0000000..7d7227b --- /dev/null +++ b/web/src/app/api/fn/games/get-game.ts @@ -0,0 +1,32 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { Game } from '../../models/game'; + +export interface GetGame$Params { + id: string; +} + +export function getGame(http: HttpClient, rootUrl: string, params: GetGame$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, getGame.PATH, 'get'); + if (params) { + rb.path('id', params.id, {}); + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse; + }) + ); +} + +getGame.PATH = '/api/games/{id}'; diff --git a/web/src/app/api/fn/games/list-games.ts b/web/src/app/api/fn/games/list-games.ts new file mode 100644 index 0000000..3a0deea --- /dev/null +++ b/web/src/app/api/fn/games/list-games.ts @@ -0,0 +1,30 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { Game } from '../../models/game'; + +export interface ListGames$Params { +} + +export function listGames(http: HttpClient, rootUrl: string, params?: ListGames$Params, context?: HttpContext): Observable | null)>> { + const rb = new RequestBuilder(rootUrl, listGames.PATH, 'get'); + if (params) { + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse<(Array | null)>; + }) + ); +} + +listGames.PATH = '/api/games'; diff --git a/web/src/app/api/fn/games/update-game.ts b/web/src/app/api/fn/games/update-game.ts new file mode 100644 index 0000000..88f8f8e --- /dev/null +++ b/web/src/app/api/fn/games/update-game.ts @@ -0,0 +1,35 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { Game } from '../../models/game'; +import { GameInput } from '../../models/game-input'; + +export interface UpdateGame$Params { + id: string; + body: GameInput +} + +export function updateGame(http: HttpClient, rootUrl: string, params: UpdateGame$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, updateGame.PATH, 'put'); + if (params) { + rb.path('id', params.id, {}); + rb.body(params.body, 'application/json'); + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse; + }) + ); +} + +updateGame.PATH = '/api/games/{id}'; diff --git a/web/src/app/api/fn/misc/get-hello.ts b/web/src/app/api/fn/misc/get-hello.ts new file mode 100644 index 0000000..2808467 --- /dev/null +++ b/web/src/app/api/fn/misc/get-hello.ts @@ -0,0 +1,30 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { GetHelloResponse } from '../../models/get-hello-response'; + +export interface GetHello$Params { +} + +export function getHello(http: HttpClient, rootUrl: string, params?: GetHello$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, getHello.PATH, 'get'); + if (params) { + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse; + }) + ); +} + +getHello.PATH = '/hello'; diff --git a/web/src/app/api/fn/platforms/create-platform.ts b/web/src/app/api/fn/platforms/create-platform.ts new file mode 100644 index 0000000..33ef755 --- /dev/null +++ b/web/src/app/api/fn/platforms/create-platform.ts @@ -0,0 +1,33 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { Platform } from '../../models/platform'; +import { PlatformInput } from '../../models/platform-input'; + +export interface CreatePlatform$Params { + body: PlatformInput +} + +export function createPlatform(http: HttpClient, rootUrl: string, params: CreatePlatform$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, createPlatform.PATH, 'post'); + if (params) { + rb.body(params.body, 'application/json'); + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse; + }) + ); +} + +createPlatform.PATH = '/api/platforms'; diff --git a/web/src/app/api/fn/platforms/delete-platform.ts b/web/src/app/api/fn/platforms/delete-platform.ts new file mode 100644 index 0000000..fe922e8 --- /dev/null +++ b/web/src/app/api/fn/platforms/delete-platform.ts @@ -0,0 +1,31 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + + +export interface DeletePlatform$Params { + id: string; +} + +export function deletePlatform(http: HttpClient, rootUrl: string, params: DeletePlatform$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, deletePlatform.PATH, 'delete'); + if (params) { + rb.path('id', params.id, {}); + } + + return http.request( + rb.build({ responseType: 'text', accept: '*/*', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return (r as HttpResponse).clone({ body: undefined }) as StrictHttpResponse; + }) + ); +} + +deletePlatform.PATH = '/api/platforms/{id}'; diff --git a/web/src/app/api/fn/platforms/get-platform.ts b/web/src/app/api/fn/platforms/get-platform.ts new file mode 100644 index 0000000..a6cd9d5 --- /dev/null +++ b/web/src/app/api/fn/platforms/get-platform.ts @@ -0,0 +1,32 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { Platform } from '../../models/platform'; + +export interface GetPlatform$Params { + id: string; +} + +export function getPlatform(http: HttpClient, rootUrl: string, params: GetPlatform$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, getPlatform.PATH, 'get'); + if (params) { + rb.path('id', params.id, {}); + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse; + }) + ); +} + +getPlatform.PATH = '/api/platforms/{id}'; diff --git a/web/src/app/api/fn/platforms/list-platforms.ts b/web/src/app/api/fn/platforms/list-platforms.ts new file mode 100644 index 0000000..d4df6ec --- /dev/null +++ b/web/src/app/api/fn/platforms/list-platforms.ts @@ -0,0 +1,30 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { Platform } from '../../models/platform'; + +export interface ListPlatforms$Params { +} + +export function listPlatforms(http: HttpClient, rootUrl: string, params?: ListPlatforms$Params, context?: HttpContext): Observable | null)>> { + const rb = new RequestBuilder(rootUrl, listPlatforms.PATH, 'get'); + if (params) { + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse<(Array | null)>; + }) + ); +} + +listPlatforms.PATH = '/api/platforms'; diff --git a/web/src/app/api/fn/platforms/update-platform.ts b/web/src/app/api/fn/platforms/update-platform.ts new file mode 100644 index 0000000..fcb211a --- /dev/null +++ b/web/src/app/api/fn/platforms/update-platform.ts @@ -0,0 +1,35 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext, HttpResponse } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { filter, map } from 'rxjs/operators'; +import { StrictHttpResponse } from '../../strict-http-response'; +import { RequestBuilder } from '../../request-builder'; + +import { Platform } from '../../models/platform'; +import { PlatformInput } from '../../models/platform-input'; + +export interface UpdatePlatform$Params { + id: string; + body: PlatformInput +} + +export function updatePlatform(http: HttpClient, rootUrl: string, params: UpdatePlatform$Params, context?: HttpContext): Observable> { + const rb = new RequestBuilder(rootUrl, updatePlatform.PATH, 'put'); + if (params) { + rb.path('id', params.id, {}); + rb.body(params.body, 'application/json'); + } + + return http.request( + rb.build({ responseType: 'json', accept: 'application/json', context }) + ).pipe( + filter((r: any): r is HttpResponse => r instanceof HttpResponse), + map((r: HttpResponse) => { + return r as StrictHttpResponse; + }) + ); +} + +updatePlatform.PATH = '/api/platforms/{id}'; diff --git a/web/src/app/api/functions.ts b/web/src/app/api/functions.ts new file mode 100644 index 0000000..98cf7e5 --- /dev/null +++ b/web/src/app/api/functions.ts @@ -0,0 +1,33 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export type { Login$Params as Login$Params } from './fn/auth/login'; +export { login as login } from './fn/auth/login'; +export type { Logout$Params as Logout$Params } from './fn/auth/logout'; +export { logout as logout } from './fn/auth/logout'; +export type { GetMe$Params as GetMe$Params } from './fn/auth/get-me'; +export { getMe as getMe } from './fn/auth/get-me'; +export type { Register$Params as Register$Params } from './fn/auth/register'; +export { register as register } from './fn/auth/register'; +export type { ListGames$Params as ListGames$Params } from './fn/games/list-games'; +export { listGames as listGames } from './fn/games/list-games'; +export type { CreateGame$Params as CreateGame$Params } from './fn/games/create-game'; +export { createGame as createGame } from './fn/games/create-game'; +export type { GetGame$Params as GetGame$Params } from './fn/games/get-game'; +export { getGame as getGame } from './fn/games/get-game'; +export type { UpdateGame$Params as UpdateGame$Params } from './fn/games/update-game'; +export { updateGame as updateGame } from './fn/games/update-game'; +export type { DeleteGame$Params as DeleteGame$Params } from './fn/games/delete-game'; +export { deleteGame as deleteGame } from './fn/games/delete-game'; +export type { ListPlatforms$Params as ListPlatforms$Params } from './fn/platforms/list-platforms'; +export { listPlatforms as listPlatforms } from './fn/platforms/list-platforms'; +export type { CreatePlatform$Params as CreatePlatform$Params } from './fn/platforms/create-platform'; +export { createPlatform as createPlatform } from './fn/platforms/create-platform'; +export type { GetPlatform$Params as GetPlatform$Params } from './fn/platforms/get-platform'; +export { getPlatform as getPlatform } from './fn/platforms/get-platform'; +export type { UpdatePlatform$Params as UpdatePlatform$Params } from './fn/platforms/update-platform'; +export { updatePlatform as updatePlatform } from './fn/platforms/update-platform'; +export type { DeletePlatform$Params as DeletePlatform$Params } from './fn/platforms/delete-platform'; +export { deletePlatform as deletePlatform } from './fn/platforms/delete-platform'; +export type { GetHello$Params as GetHello$Params } from './fn/misc/get-hello'; +export { getHello as getHello } from './fn/misc/get-hello'; diff --git a/web/src/app/api/models.ts b/web/src/app/api/models.ts new file mode 100644 index 0000000..3f81675 --- /dev/null +++ b/web/src/app/api/models.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export type { AuthResponse } from './models/auth-response'; +export type { ErrorDetail } from './models/error-detail'; +export type { ErrorModel } from './models/error-model'; +export type { Game } from './models/game'; +export type { GameInput } from './models/game-input'; +export type { GetHelloResponse } from './models/get-hello-response'; +export type { LoginInput } from './models/login-input'; +export type { Platform } from './models/platform'; +export type { PlatformInput } from './models/platform-input'; +export type { RegisterInput } from './models/register-input'; +export type { User } from './models/user'; diff --git a/web/src/app/api/models/auth-response.ts b/web/src/app/api/models/auth-response.ts new file mode 100644 index 0000000..174a92a --- /dev/null +++ b/web/src/app/api/models/auth-response.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { User } from '../models/user'; +export interface AuthResponse { + + /** + * A URL to the JSON Schema for this object. + */ + '$schema'?: string; + token: string; + user: User; +} diff --git a/web/src/app/api/models/error-detail.ts b/web/src/app/api/models/error-detail.ts new file mode 100644 index 0000000..f35752a --- /dev/null +++ b/web/src/app/api/models/error-detail.ts @@ -0,0 +1,20 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export interface ErrorDetail { + + /** + * Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id' + */ + location?: string; + + /** + * Error message text + */ + message?: string; + + /** + * The value at the given location + */ + value?: any; +} diff --git a/web/src/app/api/models/error-model.ts b/web/src/app/api/models/error-model.ts new file mode 100644 index 0000000..6a329f9 --- /dev/null +++ b/web/src/app/api/models/error-model.ts @@ -0,0 +1,41 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { ErrorDetail } from '../models/error-detail'; +export interface ErrorModel { + + /** + * A URL to the JSON Schema for this object. + */ + '$schema'?: string; + + /** + * A human-readable explanation specific to this occurrence of the problem. + */ + detail?: string; + + /** + * Optional list of individual error details + */ + errors?: (Array | null); + + /** + * A URI reference that identifies the specific occurrence of the problem. + */ + instance?: string; + + /** + * HTTP status code + */ + status?: number; + + /** + * A short, human-readable summary of the problem type. This value should not change between occurrences of the error. + */ + title?: string; + + /** + * A URI reference to human-readable documentation for the error. + */ + type?: string; +} diff --git a/web/src/app/api/models/game-input.ts b/web/src/app/api/models/game-input.ts new file mode 100644 index 0000000..1b5824a --- /dev/null +++ b/web/src/app/api/models/game-input.ts @@ -0,0 +1,23 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export interface GameInput { + + /** + * A URL to the JSON Schema for this object. + */ + '$schema'?: string; + + /** + * YYYY-MM-DD (optional) + */ + finished?: string; + name: string; + platform_id: string; + + /** + * YYYY-MM-DD + */ + release_date: string; + score: number; +} diff --git a/web/src/app/api/models/game.ts b/web/src/app/api/models/game.ts new file mode 100644 index 0000000..34284fd --- /dev/null +++ b/web/src/app/api/models/game.ts @@ -0,0 +1,25 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export interface Game { + + /** + * A URL to the JSON Schema for this object. + */ + '$schema'?: string; + + /** + * YYYY-MM-DD (optional) + */ + finished?: string; + id: string; + name: string; + platform_id: string; + platform_name: string; + + /** + * YYYY-MM-DD + */ + release_date: string; + score: number; +} diff --git a/web/src/app/api/models/get-hello-response.ts b/web/src/app/api/models/get-hello-response.ts new file mode 100644 index 0000000..5eece06 --- /dev/null +++ b/web/src/app/api/models/get-hello-response.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export interface GetHelloResponse { + + /** + * A URL to the JSON Schema for this object. + */ + '$schema'?: string; + message: string; +} diff --git a/web/src/app/api/models/login-input.ts b/web/src/app/api/models/login-input.ts new file mode 100644 index 0000000..8694782 --- /dev/null +++ b/web/src/app/api/models/login-input.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export interface LoginInput { + + /** + * A URL to the JSON Schema for this object. + */ + '$schema'?: string; + email: string; + password: string; +} diff --git a/web/src/app/api/models/platform-input.ts b/web/src/app/api/models/platform-input.ts new file mode 100644 index 0000000..3f2adbe --- /dev/null +++ b/web/src/app/api/models/platform-input.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export interface PlatformInput { + + /** + * A URL to the JSON Schema for this object. + */ + '$schema'?: string; + + /** + * Name of the platform + */ + name: string; +} diff --git a/web/src/app/api/models/platform.ts b/web/src/app/api/models/platform.ts new file mode 100644 index 0000000..8e0e43d --- /dev/null +++ b/web/src/app/api/models/platform.ts @@ -0,0 +1,12 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export interface Platform { + + /** + * A URL to the JSON Schema for this object. + */ + '$schema'?: string; + id: string; + name: string; +} diff --git a/web/src/app/api/models/register-input.ts b/web/src/app/api/models/register-input.ts new file mode 100644 index 0000000..fe08469 --- /dev/null +++ b/web/src/app/api/models/register-input.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export interface RegisterInput { + + /** + * A URL to the JSON Schema for this object. + */ + '$schema'?: string; + email: string; + name: string; + password: string; +} diff --git a/web/src/app/api/models/user.ts b/web/src/app/api/models/user.ts new file mode 100644 index 0000000..c974c31 --- /dev/null +++ b/web/src/app/api/models/user.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export interface User { + + /** + * A URL to the JSON Schema for this object. + */ + '$schema'?: string; + bio?: string; + email: string; + id: string; + is_admin: boolean; + name: string; +} diff --git a/web/src/app/api/request-builder.ts b/web/src/app/api/request-builder.ts new file mode 100644 index 0000000..9e28788 --- /dev/null +++ b/web/src/app/api/request-builder.ts @@ -0,0 +1,369 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpRequest, HttpParameterCodec, HttpParams, HttpHeaders, HttpContext } from '@angular/common/http'; + +/** + * Custom parameter codec to correctly handle the plus sign in parameter + * values. See https://github.com/angular/angular/issues/18261 + */ +class ParameterCodec implements HttpParameterCodec { + encodeKey(key: string): string { + return encodeURIComponent(key); + } + + encodeValue(value: string): string { + return encodeURIComponent(value); + } + + decodeKey(key: string): string { + return decodeURIComponent(key); + } + + decodeValue(value: string): string { + return decodeURIComponent(value); + } +} +const ParameterCodecInstance = new ParameterCodec(); + +/** + * Defines the options for appending a parameter + */ +interface ParameterOptions { + style?: string; + explode?: boolean; +} + +/** + * Base class for a parameter + */ +abstract class Parameter { + constructor(public name: string, public value: any, public options: ParameterOptions, defaultStyle: string, defaultExplode: boolean) { + this.options = options || {}; + if (this.options.style === null || this.options.style === undefined) { + this.options.style = defaultStyle; + } + if (this.options.explode === null || this.options.explode === undefined) { + this.options.explode = defaultExplode; + } + } + + serializeValue(value: any, separator = ','): string { + if (value === null || value === undefined) { + return ''; + } else if (value instanceof Array) { + return value.map(v => this.serializeValue(v).split(separator).join(encodeURIComponent(separator))).join(separator); + } else if (typeof value === 'object') { + const array: string[] = []; + for (const key of Object.keys(value)) { + let propVal = value[key]; + if (propVal !== null && propVal !== undefined) { + propVal = this.serializeValue(propVal).split(separator).join(encodeURIComponent(separator)); + if (this.options.explode) { + array.push(`${key}=${propVal}`); + } else { + array.push(key); + array.push(propVal); + } + } + } + return array.join(separator); + } else { + return String(value); + } + } +} + +/** + * A parameter in the operation path + */ +class PathParameter extends Parameter { + constructor(name: string, value: any, options: ParameterOptions) { + super(name, value, options, 'simple', false); + } + + append(path: string): string { + let value = this.value; + if (value === null || value === undefined) { + value = ''; + } + let prefix = this.options.style === 'label' ? '.' : ''; + let separator = this.options.explode ? prefix === '' ? ',' : prefix : ','; + let alreadySerialized = false; + if (this.options.style === 'matrix') { + // The parameter name is just used as prefix, except in some cases... + prefix = `;${this.name}=`; + if (this.options.explode && typeof value === 'object') { + prefix = ';'; + if (value instanceof Array) { + // For arrays we have to repeat the name for each element + value = value.map(v => `${this.name}=${this.serializeValue(v, ';')}`); + value = value.join(';'); + alreadySerialized = true; + } else { + // For objects we have to put each the key / value pairs + value = this.serializeValue(value, ';'); + alreadySerialized = true + } + } + } + value = prefix + (alreadySerialized ? value : this.serializeValue(value, separator)); + // Replace both the plain variable and the corresponding variant taking in the prefix and explode into account + path = path.replace(`{${this.name}}`, value); + path = path.replace(`{${prefix}${this.name}${this.options.explode ? '*' : ''}}`, value); + return path; + } + + // @ts-ignore + serializeValue(value: any, separator = ','): string { + var result = typeof value === 'string' ? encodeURIComponent(value) : super.serializeValue(value, separator); + result = result.replace(/%3D/g, '='); + result = result.replace(/%3B/g, ';'); + result = result.replace(/%2C/g, ','); + return result; + } +} + +/** + * A parameter in the query + */ +class QueryParameter extends Parameter { + constructor(name: string, value: any, options: ParameterOptions) { + super(name, value, options, 'form', true); + } + + append(params: HttpParams): HttpParams { + if (this.value instanceof Array) { + // Array serialization + if (this.options.explode) { + for (const v of this.value) { + params = params.append(this.name, this.serializeValue(v)); + } + } else { + const separator = this.options.style === 'spaceDelimited' + ? ' ' : this.options.style === 'pipeDelimited' + ? '|' : ','; + return params.append(this.name, this.serializeValue(this.value, separator)); + } + } else if (this.value !== null && typeof this.value === 'object') { + // Object serialization + if (this.options.style === 'deepObject') { + // Append a parameter for each key, in the form `name[key]` + for (const key of Object.keys(this.value)) { + const propVal = this.value[key]; + if (propVal !== null && propVal !== undefined) { + params = params.append(`${this.name}[${key}]`, this.serializeValue(propVal)); + } + } + } else if (this.options.explode) { + // Append a parameter for each key without using the parameter name + for (const key of Object.keys(this.value)) { + const propVal = this.value[key]; + if (propVal !== null && propVal !== undefined) { + params = params.append(key, this.serializeValue(propVal)); + } + } + } else { + // Append a single parameter whose values are a comma-separated list of key,value,key,value... + const array: any[] = []; + for (const key of Object.keys(this.value)) { + const propVal = this.value[key]; + if (propVal !== null && propVal !== undefined) { + array.push(key); + array.push(propVal); + } + } + params = params.append(this.name, this.serializeValue(array)); + } + } else if (this.value !== null && this.value !== undefined) { + // Plain value + params = params.append(this.name, this.serializeValue(this.value)); + } + return params; + } +} + +/** + * A parameter in the HTTP request header + */ +class HeaderParameter extends Parameter { + constructor(name: string, value: any, options: ParameterOptions) { + super(name, value, options, 'simple', false); + } + + append(headers: HttpHeaders): HttpHeaders { + if (this.value !== null && this.value !== undefined) { + if (this.value instanceof Array) { + for (const v of this.value) { + headers = headers.append(this.name, this.serializeValue(v)); + } + } else { + headers = headers.append(this.name, this.serializeValue(this.value)); + } + } + return headers; + } +} + +/** + * Helper to build http requests from parameters + */ +export class RequestBuilder { + + private _path = new Map(); + private _query = new Map(); + private _header = new Map(); + _bodyContent: any | null; + _bodyContentType?: string; + + constructor( + public rootUrl: string, + public operationPath: string, + public method: string) { + } + + /** + * Sets a path parameter + */ + path(name: string, value: any, options?: ParameterOptions): void { + this._path.set(name, new PathParameter(name, value, options || {})); + } + + /** + * Sets a query parameter + */ + query(name: string, value: any, options?: ParameterOptions): void { + this._query.set(name, new QueryParameter(name, value, options || {})); + } + + /** + * Sets a header parameter + */ + header(name: string, value: any, options?: ParameterOptions): void { + this._header.set(name, new HeaderParameter(name, value, options || {})); + } + + /** + * Sets the body content, along with the content type + */ + body(value: any, contentType = 'application/json'): void { + if (value instanceof Blob) { + this._bodyContentType = value.type; + } else { + this._bodyContentType = contentType; + } + if (this._bodyContentType === 'application/x-www-form-urlencoded' && value !== null && typeof value === 'object') { + // Handle URL-encoded data + const pairs: Array<[string, string]> = []; + for (const key of Object.keys(value)) { + let val = value[key]; + if (!(val instanceof Array)) { + val = [val]; + } + for (const v of val) { + const formValue = this.formDataValue(v); + if (formValue !== null) { + pairs.push([key, formValue]); + } + } + } + this._bodyContent = pairs.map(p => `${encodeURIComponent(p[0])}=${encodeURIComponent(p[1])}`).join('&'); + } else if (this._bodyContentType === 'multipart/form-data') { + // Handle multipart form data + const formData = new FormData(); + if (value !== null && value !== undefined) { + for (const key of Object.keys(value)) { + const val = value[key]; + if (val instanceof Array) { + for (const v of val) { + const toAppend = this.formDataValue(v); + if (toAppend !== null) { + formData.append(key, toAppend); + } + } + } else { + const toAppend = this.formDataValue(val); + if (toAppend !== null) { + formData.set(key, toAppend); + } + } + } + } + this._bodyContent = formData; + } else { + // The body is the plain content + this._bodyContent = value; + } + } + + private formDataValue(value: any): any { + if (value === null || value === undefined) { + return null; + } + if (value instanceof Blob) { + return value; + } + if (typeof value === 'object') { + return new Blob([JSON.stringify(value)], {type: 'application/json'}) + } + return String(value); + } + + /** + * Builds the request with the current set parameters + */ + build(options?: { + /** Which content types to accept */ + accept?: string; + + /** The expected response type */ + responseType?: 'json' | 'text' | 'blob' | 'arraybuffer'; + + /** Whether to report progress on uploads / downloads */ + reportProgress?: boolean; + + /** Allow passing HttpContext for HttpClient */ + context?: HttpContext; + }): HttpRequest { + + options = options || {}; + + // Path parameters + let path = this.operationPath; + for (const pathParam of this._path.values()) { + path = pathParam.append(path); + } + const url = this.rootUrl + path; + + // Query parameters + let httpParams = new HttpParams({ + encoder: ParameterCodecInstance + }); + for (const queryParam of this._query.values()) { + httpParams = queryParam.append(httpParams); + } + + // Header parameters + let httpHeaders = new HttpHeaders(); + if (options.accept) { + httpHeaders = httpHeaders.append('Accept', options.accept); + } + for (const headerParam of this._header.values()) { + httpHeaders = headerParam.append(httpHeaders); + } + + // Request content headers + if (this._bodyContentType && !(this._bodyContent instanceof FormData)) { + httpHeaders = httpHeaders.set('Content-Type', this._bodyContentType); + } + + // Perform the request + return new HttpRequest(this.method.toUpperCase(), url, this._bodyContent, { + params: httpParams, + headers: httpHeaders, + responseType: options.responseType, + reportProgress: options.reportProgress, + context: options.context + }); + } +} diff --git a/web/src/app/api/services.ts b/web/src/app/api/services.ts new file mode 100644 index 0000000..c40f9fc --- /dev/null +++ b/web/src/app/api/services.ts @@ -0,0 +1,7 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +export { AuthService } from './services/auth.service'; +export { GamesService } from './services/games.service'; +export { PlatformsService } from './services/platforms.service'; +export { MiscService } from './services/misc.service'; diff --git a/web/src/app/api/services/auth.service.ts b/web/src/app/api/services/auth.service.ts new file mode 100644 index 0000000..5cc8618 --- /dev/null +++ b/web/src/app/api/services/auth.service.ts @@ -0,0 +1,161 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; + +import { BaseService } from '../base-service'; +import { ApiConfiguration } from '../api-configuration'; +import { StrictHttpResponse } from '../strict-http-response'; + +import { AuthResponse } from '../models/auth-response'; +import { getMe } from '../fn/auth/get-me'; +import { GetMe$Params } from '../fn/auth/get-me'; +import { login } from '../fn/auth/login'; +import { Login$Params } from '../fn/auth/login'; +import { logout } from '../fn/auth/logout'; +import { Logout$Params } from '../fn/auth/logout'; +import { register } from '../fn/auth/register'; +import { Register$Params } from '../fn/auth/register'; +import { User } from '../models/user'; + +@Injectable({ providedIn: 'root' }) +export class AuthService extends BaseService { + constructor(config: ApiConfiguration, http: HttpClient) { + super(config, http); + } + + /** Path part for operation `login()` */ + static readonly LoginPath = '/api/auth/login'; + + /** + * Log in to an existing account. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `login()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + login$Response(params: Login$Params, context?: HttpContext): Promise> { + const obs = login(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Log in to an existing account. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `login$Response()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + login(params: Login$Params, context?: HttpContext): Promise { + const resp = this.login$Response(params, context); + return resp.then((r: StrictHttpResponse): AuthResponse => r.body); + } + + /** Path part for operation `logout()` */ + static readonly LogoutPath = '/api/auth/logout'; + + /** + * Log out of the current session. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `logout()` instead. + * + * This method doesn't expect any request body. + */ + logout$Response(params?: Logout$Params, context?: HttpContext): Promise> { + const obs = logout(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Log out of the current session. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `logout$Response()` instead. + * + * This method doesn't expect any request body. + */ + logout(params?: Logout$Params, context?: HttpContext): Promise { + const resp = this.logout$Response(params, context); + return resp.then((r: StrictHttpResponse): void => r.body); + } + + /** Path part for operation `getMe()` */ + static readonly GetMePath = '/api/auth/me'; + + /** + * Get the current user. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `getMe()` instead. + * + * This method doesn't expect any request body. + */ + getMe$Response(params?: GetMe$Params, context?: HttpContext): Promise> { + const obs = getMe(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Get the current user. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `getMe$Response()` instead. + * + * This method doesn't expect any request body. + */ + getMe(params?: GetMe$Params, context?: HttpContext): Promise { + const resp = this.getMe$Response(params, context); + return resp.then((r: StrictHttpResponse): User => r.body); + } + + /** Path part for operation `register()` */ + static readonly RegisterPath = '/api/auth/register'; + + /** + * Register a new account. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `register()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + register$Response(params: Register$Params, context?: HttpContext): Promise> { + const obs = register(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Register a new account. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `register$Response()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + register(params: Register$Params, context?: HttpContext): Promise { + const resp = this.register$Response(params, context); + return resp.then((r: StrictHttpResponse): AuthResponse => r.body); + } + +} diff --git a/web/src/app/api/services/games.service.ts b/web/src/app/api/services/games.service.ts new file mode 100644 index 0000000..4d412d2 --- /dev/null +++ b/web/src/app/api/services/games.service.ts @@ -0,0 +1,195 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; + +import { BaseService } from '../base-service'; +import { ApiConfiguration } from '../api-configuration'; +import { StrictHttpResponse } from '../strict-http-response'; + +import { createGame } from '../fn/games/create-game'; +import { CreateGame$Params } from '../fn/games/create-game'; +import { deleteGame } from '../fn/games/delete-game'; +import { DeleteGame$Params } from '../fn/games/delete-game'; +import { Game } from '../models/game'; +import { getGame } from '../fn/games/get-game'; +import { GetGame$Params } from '../fn/games/get-game'; +import { listGames } from '../fn/games/list-games'; +import { ListGames$Params } from '../fn/games/list-games'; +import { updateGame } from '../fn/games/update-game'; +import { UpdateGame$Params } from '../fn/games/update-game'; + +@Injectable({ providedIn: 'root' }) +export class GamesService extends BaseService { + constructor(config: ApiConfiguration, http: HttpClient) { + super(config, http); + } + + /** Path part for operation `listGames()` */ + static readonly ListGamesPath = '/api/games'; + + /** + * List Games. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `listGames()` instead. + * + * This method doesn't expect any request body. + */ + listGames$Response(params?: ListGames$Params, context?: HttpContext): Promise | null)>> { + const obs = listGames(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * List Games. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `listGames$Response()` instead. + * + * This method doesn't expect any request body. + */ + listGames(params?: ListGames$Params, context?: HttpContext): Promise<(Array | null)> { + const resp = this.listGames$Response(params, context); + return resp.then((r: StrictHttpResponse<(Array | null)>): (Array | null) => r.body); + } + + /** Path part for operation `createGame()` */ + static readonly CreateGamePath = '/api/games'; + + /** + * Create Game. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `createGame()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + createGame$Response(params: CreateGame$Params, context?: HttpContext): Promise> { + const obs = createGame(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Create Game. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `createGame$Response()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + createGame(params: CreateGame$Params, context?: HttpContext): Promise { + const resp = this.createGame$Response(params, context); + return resp.then((r: StrictHttpResponse): Game => r.body); + } + + /** Path part for operation `getGame()` */ + static readonly GetGamePath = '/api/games/{id}'; + + /** + * Get Game. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `getGame()` instead. + * + * This method doesn't expect any request body. + */ + getGame$Response(params: GetGame$Params, context?: HttpContext): Promise> { + const obs = getGame(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Get Game. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `getGame$Response()` instead. + * + * This method doesn't expect any request body. + */ + getGame(params: GetGame$Params, context?: HttpContext): Promise { + const resp = this.getGame$Response(params, context); + return resp.then((r: StrictHttpResponse): Game => r.body); + } + + /** Path part for operation `updateGame()` */ + static readonly UpdateGamePath = '/api/games/{id}'; + + /** + * Update Game. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `updateGame()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + updateGame$Response(params: UpdateGame$Params, context?: HttpContext): Promise> { + const obs = updateGame(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Update Game. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `updateGame$Response()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + updateGame(params: UpdateGame$Params, context?: HttpContext): Promise { + const resp = this.updateGame$Response(params, context); + return resp.then((r: StrictHttpResponse): Game => r.body); + } + + /** Path part for operation `deleteGame()` */ + static readonly DeleteGamePath = '/api/games/{id}'; + + /** + * Delete Game. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `deleteGame()` instead. + * + * This method doesn't expect any request body. + */ + deleteGame$Response(params: DeleteGame$Params, context?: HttpContext): Promise> { + const obs = deleteGame(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Delete Game. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `deleteGame$Response()` instead. + * + * This method doesn't expect any request body. + */ + deleteGame(params: DeleteGame$Params, context?: HttpContext): Promise { + const resp = this.deleteGame$Response(params, context); + return resp.then((r: StrictHttpResponse): void => r.body); + } + +} diff --git a/web/src/app/api/services/misc.service.ts b/web/src/app/api/services/misc.service.ts new file mode 100644 index 0000000..71c69be --- /dev/null +++ b/web/src/app/api/services/misc.service.ts @@ -0,0 +1,55 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; + +import { BaseService } from '../base-service'; +import { ApiConfiguration } from '../api-configuration'; +import { StrictHttpResponse } from '../strict-http-response'; + +import { GetHelloResponse } from '../models/get-hello-response'; +import { getHello } from '../fn/misc/get-hello'; +import { GetHello$Params } from '../fn/misc/get-hello'; + +@Injectable({ providedIn: 'root' }) +export class MiscService extends BaseService { + constructor(config: ApiConfiguration, http: HttpClient) { + super(config, http); + } + + /** Path part for operation `getHello()` */ + static readonly GetHelloPath = '/hello'; + + /** + * Say Hello. + * + * Returns a friendly hello message. + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `getHello()` instead. + * + * This method doesn't expect any request body. + */ + getHello$Response(params?: GetHello$Params, context?: HttpContext): Promise> { + const obs = getHello(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Say Hello. + * + * Returns a friendly hello message. + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `getHello$Response()` instead. + * + * This method doesn't expect any request body. + */ + getHello(params?: GetHello$Params, context?: HttpContext): Promise { + const resp = this.getHello$Response(params, context); + return resp.then((r: StrictHttpResponse): GetHelloResponse => r.body); + } + +} diff --git a/web/src/app/api/services/platforms.service.ts b/web/src/app/api/services/platforms.service.ts new file mode 100644 index 0000000..691f7b5 --- /dev/null +++ b/web/src/app/api/services/platforms.service.ts @@ -0,0 +1,195 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpClient, HttpContext } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; + +import { BaseService } from '../base-service'; +import { ApiConfiguration } from '../api-configuration'; +import { StrictHttpResponse } from '../strict-http-response'; + +import { createPlatform } from '../fn/platforms/create-platform'; +import { CreatePlatform$Params } from '../fn/platforms/create-platform'; +import { deletePlatform } from '../fn/platforms/delete-platform'; +import { DeletePlatform$Params } from '../fn/platforms/delete-platform'; +import { getPlatform } from '../fn/platforms/get-platform'; +import { GetPlatform$Params } from '../fn/platforms/get-platform'; +import { listPlatforms } from '../fn/platforms/list-platforms'; +import { ListPlatforms$Params } from '../fn/platforms/list-platforms'; +import { Platform } from '../models/platform'; +import { updatePlatform } from '../fn/platforms/update-platform'; +import { UpdatePlatform$Params } from '../fn/platforms/update-platform'; + +@Injectable({ providedIn: 'root' }) +export class PlatformsService extends BaseService { + constructor(config: ApiConfiguration, http: HttpClient) { + super(config, http); + } + + /** Path part for operation `listPlatforms()` */ + static readonly ListPlatformsPath = '/api/platforms'; + + /** + * List Platforms. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `listPlatforms()` instead. + * + * This method doesn't expect any request body. + */ + listPlatforms$Response(params?: ListPlatforms$Params, context?: HttpContext): Promise | null)>> { + const obs = listPlatforms(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * List Platforms. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `listPlatforms$Response()` instead. + * + * This method doesn't expect any request body. + */ + listPlatforms(params?: ListPlatforms$Params, context?: HttpContext): Promise<(Array | null)> { + const resp = this.listPlatforms$Response(params, context); + return resp.then((r: StrictHttpResponse<(Array | null)>): (Array | null) => r.body); + } + + /** Path part for operation `createPlatform()` */ + static readonly CreatePlatformPath = '/api/platforms'; + + /** + * Create Platform. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `createPlatform()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + createPlatform$Response(params: CreatePlatform$Params, context?: HttpContext): Promise> { + const obs = createPlatform(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Create Platform. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `createPlatform$Response()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + createPlatform(params: CreatePlatform$Params, context?: HttpContext): Promise { + const resp = this.createPlatform$Response(params, context); + return resp.then((r: StrictHttpResponse): Platform => r.body); + } + + /** Path part for operation `getPlatform()` */ + static readonly GetPlatformPath = '/api/platforms/{id}'; + + /** + * Get Platform. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `getPlatform()` instead. + * + * This method doesn't expect any request body. + */ + getPlatform$Response(params: GetPlatform$Params, context?: HttpContext): Promise> { + const obs = getPlatform(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Get Platform. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `getPlatform$Response()` instead. + * + * This method doesn't expect any request body. + */ + getPlatform(params: GetPlatform$Params, context?: HttpContext): Promise { + const resp = this.getPlatform$Response(params, context); + return resp.then((r: StrictHttpResponse): Platform => r.body); + } + + /** Path part for operation `updatePlatform()` */ + static readonly UpdatePlatformPath = '/api/platforms/{id}'; + + /** + * Update Platform. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `updatePlatform()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + updatePlatform$Response(params: UpdatePlatform$Params, context?: HttpContext): Promise> { + const obs = updatePlatform(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Update Platform. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `updatePlatform$Response()` instead. + * + * This method sends `application/json` and handles request body of type `application/json`. + */ + updatePlatform(params: UpdatePlatform$Params, context?: HttpContext): Promise { + const resp = this.updatePlatform$Response(params, context); + return resp.then((r: StrictHttpResponse): Platform => r.body); + } + + /** Path part for operation `deletePlatform()` */ + static readonly DeletePlatformPath = '/api/platforms/{id}'; + + /** + * Delete Platform. + * + * + * + * This method provides access to the full `HttpResponse`, allowing access to response headers. + * To access only the response body, use `deletePlatform()` instead. + * + * This method doesn't expect any request body. + */ + deletePlatform$Response(params: DeletePlatform$Params, context?: HttpContext): Promise> { + const obs = deletePlatform(this.http, this.rootUrl, params, context); + return firstValueFrom(obs); + } + + /** + * Delete Platform. + * + * + * + * This method provides access only to the response body. + * To access the full response (for headers, for example), `deletePlatform$Response()` instead. + * + * This method doesn't expect any request body. + */ + deletePlatform(params: DeletePlatform$Params, context?: HttpContext): Promise { + const resp = this.deletePlatform$Response(params, context); + return resp.then((r: StrictHttpResponse): void => r.body); + } + +} diff --git a/web/src/app/api/strict-http-response.ts b/web/src/app/api/strict-http-response.ts new file mode 100644 index 0000000..174f119 --- /dev/null +++ b/web/src/app/api/strict-http-response.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +/* Code generated by ng-openapi-gen DO NOT EDIT. */ + +import { HttpResponse } from '@angular/common/http'; + +/** + * Constrains the http response to not have the body defined as `T | null`, but `T` only. + */ +export type StrictHttpResponse = HttpResponse & { + readonly body: T; +} diff --git a/web/src/app/app.config.ts b/web/src/app/app.config.ts new file mode 100644 index 0000000..b811d48 --- /dev/null +++ b/web/src/app/app.config.ts @@ -0,0 +1,24 @@ +import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { provideRouter } from '@angular/router'; + +import { routes } from './app.routes'; +import { authInterceptor } from './core/auth-interceptor'; +import { provideApiConfiguration } from './api/api-configuration'; + +// The API's base URL. There's no server-side config to read it from at +// runtime in this small base (a plain SPA build), so it's a build-time +// constant; swap to Angular's fileReplacements if a later build needs a +// different URL per environment. +const API_BASE_URL = 'http://localhost:8080'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideAnimationsAsync(), + provideRouter(routes), + provideHttpClient(withInterceptors([authInterceptor])), + provideApiConfiguration(API_BASE_URL), + ], +}; diff --git a/web/src/app/app.routes.ts b/web/src/app/app.routes.ts new file mode 100644 index 0000000..6ea995f --- /dev/null +++ b/web/src/app/app.routes.ts @@ -0,0 +1,16 @@ +import { Routes } from '@angular/router'; + +import { authGuard } from './core/auth-guard'; + +export const routes: Routes = [ + { path: '', pathMatch: 'full', redirectTo: 'games' }, + { + path: 'login', + loadComponent: () => import('./features/login/login').then((m) => m.Login), + }, + { + path: 'games', + loadComponent: () => import('./features/games/games').then((m) => m.Games), + canActivate: [authGuard], + }, +]; diff --git a/web/src/app/app.spec.ts b/web/src/app/app.spec.ts new file mode 100644 index 0000000..c16155b --- /dev/null +++ b/web/src/app/app.spec.ts @@ -0,0 +1,18 @@ +import { TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; + +import { App } from './app'; + +describe('App', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [App], + providers: [provideRouter([])], + }).compileComponents(); + }); + + it('should create the app', () => { + const fixture = TestBed.createComponent(App); + expect(fixture.componentInstance).toBeTruthy(); + }); +}); diff --git a/web/src/app/app.ts b/web/src/app/app.ts new file mode 100644 index 0000000..759d8dc --- /dev/null +++ b/web/src/app/app.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + imports: [RouterOutlet], + selector: 'app-root', + template: '', +}) +export class App {} diff --git a/web/src/app/core/auth-guard.spec.ts b/web/src/app/core/auth-guard.spec.ts new file mode 100644 index 0000000..285ea8c --- /dev/null +++ b/web/src/app/core/auth-guard.spec.ts @@ -0,0 +1,43 @@ +import { TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { Router, UrlTree } from '@angular/router'; +import { provideRouter } from '@angular/router'; + +import { authGuard } from './auth-guard'; +import { Session } from './session'; +import { provideApiConfiguration } from '../api/api-configuration'; + +describe('authGuard', () => { + let session: Session; + let router: Router; + + beforeEach(() => { + localStorage.clear(); + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + provideApiConfiguration('http://api.test'), + provideRouter([]), + ], + }); + session = TestBed.inject(Session); + router = TestBed.inject(Router); + }); + + function runGuard() { + return TestBed.runInInjectionContext(() => authGuard({} as any, {} as any)); + } + + it('allows navigation when authenticated', () => { + session.token.set('tok-abc'); + expect(runGuard()).toBe(true); + }); + + it('redirects to /login when not authenticated', () => { + const result = runGuard(); + expect(result).not.toBe(true); + expect((result as UrlTree).toString()).toBe(router.createUrlTree(['/login']).toString()); + }); +}); diff --git a/web/src/app/core/auth-guard.ts b/web/src/app/core/auth-guard.ts new file mode 100644 index 0000000..abd4fcf --- /dev/null +++ b/web/src/app/core/auth-guard.ts @@ -0,0 +1,12 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; + +import { Session } from './session'; + +/** Redirects to /login unless a session token is present. */ +export const authGuard: CanActivateFn = () => { + if (inject(Session).isAuthenticated()) { + return true; + } + return inject(Router).createUrlTree(['/login']); +}; diff --git a/web/src/app/core/auth-interceptor.spec.ts b/web/src/app/core/auth-interceptor.spec.ts new file mode 100644 index 0000000..198ed1e --- /dev/null +++ b/web/src/app/core/auth-interceptor.spec.ts @@ -0,0 +1,45 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; + +import { authInterceptor } from './auth-interceptor'; +import { Session } from './session'; +import { provideApiConfiguration } from '../api/api-configuration'; + +describe('authInterceptor', () => { + let http: HttpClient; + let httpMock: HttpTestingController; + let session: Session; + + beforeEach(() => { + localStorage.clear(); + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptors([authInterceptor])), + provideHttpClientTesting(), + provideApiConfiguration('http://api.test'), + ], + }); + http = TestBed.inject(HttpClient); + httpMock = TestBed.inject(HttpTestingController); + session = TestBed.inject(Session); + }); + + afterEach(() => httpMock.verify()); + + it('attaches the bearer token when a session is present', () => { + session.token.set('tok-abc'); + + http.get('/whatever').subscribe(); + + const req = httpMock.expectOne('/whatever'); + expect(req.request.headers.get('Authorization')).toBe('Bearer tok-abc'); + }); + + it('sends no Authorization header when logged out', () => { + http.get('/whatever').subscribe(); + + const req = httpMock.expectOne('/whatever'); + expect(req.request.headers.has('Authorization')).toBe(false); + }); +}); diff --git a/web/src/app/core/auth-interceptor.ts b/web/src/app/core/auth-interceptor.ts new file mode 100644 index 0000000..c359e61 --- /dev/null +++ b/web/src/app/core/auth-interceptor.ts @@ -0,0 +1,13 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; + +import { Session } from './session'; + +/** Attaches the stored session token to every outgoing API request. */ +export const authInterceptor: HttpInterceptorFn = (req, next) => { + const token = inject(Session).token(); + if (!token) { + return next(req); + } + return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })); +}; diff --git a/web/src/app/core/session.spec.ts b/web/src/app/core/session.spec.ts new file mode 100644 index 0000000..0be420e --- /dev/null +++ b/web/src/app/core/session.spec.ts @@ -0,0 +1,90 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideHttpClient } from '@angular/common/http'; + +import { Session } from './session'; +import { provideApiConfiguration } from '../api/api-configuration'; + +describe('Session', () => { + let service: Session; + let httpMock: HttpTestingController; + + beforeEach(() => { + localStorage.clear(); + TestBed.configureTestingModule({ + providers: [provideHttpClient(), provideHttpClientTesting(), provideApiConfiguration('http://api.test')], + }); + service = TestBed.inject(Session); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('starts logged out with no stored token', () => { + expect(service.isAuthenticated()).toBe(false); + expect(service.user()).toBeNull(); + }); + + it('restores the token from a previous session', () => { + localStorage.setItem('completed.token', 'tok-123'); + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [provideHttpClient(), provideHttpClientTesting(), provideApiConfiguration('http://api.test')], + }); + const restored = TestBed.inject(Session); + expect(restored.isAuthenticated()).toBe(true); + }); + + it('login stores the token and user, and persists to localStorage', async () => { + const promise = service.login('alice@example.com', 'secret'); + + const req = httpMock.expectOne('http://api.test/api/auth/login'); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ email: 'alice@example.com', password: 'secret' }); + req.flush({ token: 'tok-abc', user: { id: 'u1', email: 'alice@example.com', name: 'Alice', is_admin: false } }); + + await promise; + + expect(service.token()).toBe('tok-abc'); + expect(service.user()?.name).toBe('Alice'); + expect(service.isAdmin()).toBe(false); + expect(localStorage.getItem('completed.token')).toBe('tok-abc'); + }); + + it('login failure leaves the session logged out', async () => { + const promise = service.login('alice@example.com', 'wrong'); + + const req = httpMock.expectOne('http://api.test/api/auth/login'); + req.flush({ title: 'Unauthorized', status: 401, detail: 'invalid email or password' }, { status: 401, statusText: 'Unauthorized' }); + + await expect(promise).rejects.toBeTruthy(); + expect(service.isAuthenticated()).toBe(false); + }); + + it('logout clears the session even if the request fails', async () => { + service.token.set('tok-abc'); + service.user.set({ id: 'u1', email: 'a@example.com', name: 'Alice', is_admin: false }); + + const promise = service.logout(); + const req = httpMock.expectOne('http://api.test/api/auth/logout'); + req.flush(null, { status: 500, statusText: 'Internal Server Error' }); + + await expect(promise).rejects.toBeTruthy(); + expect(service.isAuthenticated()).toBe(false); + expect(service.user()).toBeNull(); + expect(localStorage.getItem('completed.token')).toBeNull(); + }); + + it('restore() clears an invalid stored token instead of leaving a broken session', async () => { + service.token.set('tok-stale'); + + const promise = service.restore(); + const req = httpMock.expectOne('http://api.test/api/auth/me'); + req.flush({ title: 'Unauthorized', status: 401, detail: 'session expired' }, { status: 401, statusText: 'Unauthorized' }); + + await promise; + expect(service.isAuthenticated()).toBe(false); + }); +}); diff --git a/web/src/app/core/session.ts b/web/src/app/core/session.ts new file mode 100644 index 0000000..70114a5 --- /dev/null +++ b/web/src/app/core/session.ts @@ -0,0 +1,92 @@ +import { Service, computed, inject, signal } from '@angular/core'; + +import { AuthService } from '../api/services/auth.service'; +import { User } from '../api/models/user'; + +/** Where the token is kept so a page refresh doesn't log the user out. */ +const STORAGE_KEY = 'completed.token'; + +/** + * Holds the current login state (token + user) and is the one place that + * talks to the generated AuthService. Components read `user()` / + * `isAdmin()` and call `login`/`register`/`logout`; nothing else touches + * localStorage or the auth endpoints directly. + */ +@Service() +export class Session { + private readonly auth = inject(AuthService); + + readonly user = signal(null); + readonly token = signal(this.readStoredToken()); + readonly isAuthenticated = computed(() => this.token() !== null); + readonly isAdmin = computed(() => this.user()?.is_admin ?? false); + + // A stored token with no user yet (e.g. after a page refresh) is + // resolved lazily by whoever needs `user()` first - in this small base, + // the games page calls `restore()` on init. + + private readStoredToken(): string | null { + try { + return localStorage.getItem(STORAGE_KEY); + } catch { + // localStorage can throw in some privacy modes; fall back to + // "logged out" rather than crashing the app. + return null; + } + } + + private storeToken(token: string | null): void { + try { + if (token) { + localStorage.setItem(STORAGE_KEY, token); + } else { + localStorage.removeItem(STORAGE_KEY); + } + } catch { + // Same as above: a storage failure shouldn't break login/logout. + } + } + + /** Re-fetches the current user for a token restored from storage. */ + async restore(): Promise { + if (!this.token() || this.user()) { + return; + } + try { + const user = await this.auth.getMe(); + this.user.set(user); + } catch { + // The stored token is no longer valid (expired/revoked); drop it so + // the app shows the login screen instead of a broken "logged in". + this.clear(); + } + } + + async login(email: string, password: string): Promise { + const resp = await this.auth.login({ body: { email, password } }); + this.token.set(resp.token); + this.user.set(resp.user); + this.storeToken(resp.token); + } + + async register(email: string, password: string, name: string): Promise { + const resp = await this.auth.register({ body: { email, password, name } }); + this.token.set(resp.token); + this.user.set(resp.user); + this.storeToken(resp.token); + } + + async logout(): Promise { + try { + await this.auth.logout(); + } finally { + this.clear(); + } + } + + private clear(): void { + this.token.set(null); + this.user.set(null); + this.storeToken(null); + } +} diff --git a/web/src/app/features/games/games.css b/web/src/app/features/games/games.css new file mode 100644 index 0000000..e666a24 --- /dev/null +++ b/web/src/app/features/games/games.css @@ -0,0 +1,26 @@ +.spacer { + flex: 1 1 auto; +} + +.user-name { + margin-right: 1rem; + opacity: 0.8; +} + +.content { + padding: 1.5rem; +} + +.games-table { + width: 100%; +} + +.centered { + display: flex; + justify-content: center; + padding-top: 3rem; +} + +.error { + color: var(--mat-sys-error, #b3261e); +} diff --git a/web/src/app/features/games/games.html b/web/src/app/features/games/games.html new file mode 100644 index 0000000..9ace228 --- /dev/null +++ b/web/src/app/features/games/games.html @@ -0,0 +1,51 @@ + + Completed + + @if (session.user()) { + {{ session.user()!.name }} + } + + + + +
+ @if (loading()) { +
+ } @else if (error()) { +

{{ error() }}

+ } @else if (games().length === 0) { +

No games yet.

+ } @else { + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name{{ g.name }}Platform{{ g.platform_name }}Score{{ g.score }}Released{{ g.release_date }}Finished{{ g.finished ? '✓' : '' }}
+ } +
diff --git a/web/src/app/features/games/games.spec.ts b/web/src/app/features/games/games.spec.ts new file mode 100644 index 0000000..06f37d2 --- /dev/null +++ b/web/src/app/features/games/games.spec.ts @@ -0,0 +1,109 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { Router, provideRouter } from '@angular/router'; + +import { Games } from './games'; +import { Session } from '../../core/session'; +import { provideApiConfiguration } from '../../api/api-configuration'; + +describe('Games', () => { + let component: Games; + let fixture: ComponentFixture; + let httpMock: HttpTestingController; + let session: Session; + let router: Router; + + beforeEach(async () => { + localStorage.clear(); + await TestBed.configureTestingModule({ + imports: [Games], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + provideApiConfiguration('http://api.test'), + provideRouter([]), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(Games); + component = fixture.componentInstance; + httpMock = TestBed.inject(HttpTestingController); + session = TestBed.inject(Session); + router = TestBed.inject(Router); + }); + + afterEach(() => httpMock.verify()); + + // Setting both token and user means `session.restore()` (called by + // ngOnInit) finds a user already present and skips its own HTTP call, + // so each test only has to deal with the request it's actually testing. + function logIn(): void { + session.token.set('tok-abc'); + session.user.set({ id: 'u1', email: 'alice@example.com', name: 'Alice', is_admin: false }); + } + + // ngOnInit awaits session.restore() before calling refresh(), and + // restore() returns early (no HTTP call) once a user is already set - + // but that early return is still through an async function, so it still + // takes one microtask tick before ngOnInit resumes far enough to + // actually dispatch the /api/games request. + async function flushMicrotask(): Promise { + await Promise.resolve(); + } + + it('loads games on init', async () => { + logIn(); + const initPromise = component.ngOnInit(); + await flushMicrotask(); + + const req = httpMock.expectOne('http://api.test/api/games'); + req.flush([{ id: 'g1', name: 'Doom', platform_id: 'p1', platform_name: 'PC', score: 95, release_date: '1993-12-10' }]); + await initPromise; + + expect(component.games()).toHaveLength(1); + expect(component.games()[0].name).toBe('Doom'); + expect(component.error()).toBeNull(); + expect(component.loading()).toBe(false); + }); + + it('shows an error when the request fails', async () => { + logIn(); + const initPromise = component.ngOnInit(); + await flushMicrotask(); + + const req = httpMock.expectOne('http://api.test/api/games'); + req.flush({ title: 'Internal Server Error', status: 500 }, { status: 500, statusText: 'Internal Server Error' }); + await initPromise; + + expect(component.error()).toContain('Could not load games'); + expect(component.games()).toHaveLength(0); + }); + + it('refresh() re-fetches the list', async () => { + logIn(); + const initPromise = component.ngOnInit(); + await flushMicrotask(); + httpMock.expectOne('http://api.test/api/games').flush([]); + await initPromise; + + const refreshPromise = component.refresh(); + const req = httpMock.expectOne('http://api.test/api/games'); + req.flush([{ id: 'g1', name: 'Half-Life', platform_id: 'p1', platform_name: 'PC', score: 96, release_date: '1998-11-19' }]); + await refreshPromise; + + expect(component.games()).toHaveLength(1); + }); + + it('logout() clears the session and navigates to /login', async () => { + logIn(); + const navigateSpy = vi.spyOn(router, 'navigateByUrl'); + + const logoutPromise = component.logout(); + httpMock.expectOne('http://api.test/api/auth/logout').flush(null); + await logoutPromise; + + expect(session.isAuthenticated()).toBe(false); + expect(navigateSpy).toHaveBeenCalledWith('/login'); + }); +}); diff --git a/web/src/app/features/games/games.ts b/web/src/app/features/games/games.ts new file mode 100644 index 0000000..ec6c906 --- /dev/null +++ b/web/src/app/features/games/games.ts @@ -0,0 +1,58 @@ +import { Component, OnInit, signal } from '@angular/core'; +import { Router } from '@angular/router'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatTableModule } from '@angular/material/table'; +import { MatToolbarModule } from '@angular/material/toolbar'; + +import { GamesService } from '../../api/services/games.service'; +import { Game } from '../../api/models/game'; +import { Session } from '../../core/session'; + +@Component({ + imports: [ + MatButtonModule, + MatIconModule, + MatProgressSpinnerModule, + MatTableModule, + MatToolbarModule, + ], + selector: 'app-games', + styleUrl: './games.css', + templateUrl: './games.html', +}) +export class Games implements OnInit { + readonly columns = ['name', 'platform_name', 'score', 'release_date', 'finished']; + readonly games = signal([]); + readonly loading = signal(false); + readonly error = signal(null); + + constructor( + readonly session: Session, + private readonly gamesService: GamesService, + private readonly router: Router, + ) {} + + async ngOnInit(): Promise { + await this.session.restore(); + await this.refresh(); + } + + async refresh(): Promise { + this.loading.set(true); + this.error.set(null); + try { + this.games.set((await this.gamesService.listGames()) ?? []); + } catch { + this.error.set('Could not load games. Check your connection and try again.'); + } finally { + this.loading.set(false); + } + } + + async logout(): Promise { + await this.session.logout(); + this.router.navigateByUrl('/login'); + } +} diff --git a/web/src/app/features/login/login.css b/web/src/app/features/login/login.css new file mode 100644 index 0000000..ebb7c56 --- /dev/null +++ b/web/src/app/features/login/login.css @@ -0,0 +1,18 @@ +.login-page { + display: flex; + justify-content: center; + padding-top: 10vh; +} + +.login-card { + width: 100%; + max-width: 360px; +} + +.full-width { + width: 100%; +} + +.error { + color: var(--mat-sys-error, #b3261e); +} diff --git a/web/src/app/features/login/login.html b/web/src/app/features/login/login.html new file mode 100644 index 0000000..8934bfd --- /dev/null +++ b/web/src/app/features/login/login.html @@ -0,0 +1,32 @@ + diff --git a/web/src/app/features/login/login.spec.ts b/web/src/app/features/login/login.spec.ts new file mode 100644 index 0000000..845116a --- /dev/null +++ b/web/src/app/features/login/login.spec.ts @@ -0,0 +1,84 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { Router, provideRouter } from '@angular/router'; + +import { Login } from './login'; +import { provideApiConfiguration } from '../../api/api-configuration'; + +describe('Login', () => { + let component: Login; + let fixture: ComponentFixture; + let httpMock: HttpTestingController; + let router: Router; + + beforeEach(async () => { + localStorage.clear(); + await TestBed.configureTestingModule({ + imports: [Login], + providers: [ + provideHttpClient(), + provideHttpClientTesting(), + provideApiConfiguration('http://api.test'), + provideRouter([]), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(Login); + component = fixture.componentInstance; + httpMock = TestBed.inject(HttpTestingController); + router = TestBed.inject(Router); + await fixture.whenStable(); + }); + + afterEach(() => httpMock.verify()); + + it('is invalid with empty fields', () => { + expect(component.form.invalid).toBe(true); + }); + + it('is valid with a well-formed email and non-empty password', () => { + component.form.setValue({ email: 'alice@example.com', password: 'secret' }); + expect(component.form.valid).toBe(true); + }); + + it('rejects a malformed email', () => { + component.form.setValue({ email: 'not-an-email', password: 'secret' }); + expect(component.form.valid).toBe(false); + }); + + it('on success, logs in and navigates to /games', async () => { + const navigateSpy = vi.spyOn(router, 'navigateByUrl'); + component.form.setValue({ email: 'alice@example.com', password: 'secret' }); + + const submitPromise = component.submit(); + const req = httpMock.expectOne('http://api.test/api/auth/login'); + req.flush({ token: 'tok-abc', user: { id: 'u1', email: 'alice@example.com', name: 'Alice', is_admin: false } }); + await submitPromise; + + expect(navigateSpy).toHaveBeenCalledWith('/games'); + expect(component.error()).toBeNull(); + }); + + it('on failure, shows an error and does not navigate', async () => { + const navigateSpy = vi.spyOn(router, 'navigateByUrl'); + component.form.setValue({ email: 'alice@example.com', password: 'wrong' }); + + const submitPromise = component.submit(); + const req = httpMock.expectOne('http://api.test/api/auth/login'); + req.flush( + { title: 'Unauthorized', status: 401, detail: 'invalid email or password' }, + { status: 401, statusText: 'Unauthorized' }, + ); + await submitPromise; + + expect(navigateSpy).not.toHaveBeenCalled(); + expect(component.error()).toBe('invalid email or password'); + }); + + it('does nothing when the form is invalid', async () => { + component.form.setValue({ email: '', password: '' }); + await component.submit(); + httpMock.expectNone('http://api.test/api/auth/login'); + }); +}); diff --git a/web/src/app/features/login/login.ts b/web/src/app/features/login/login.ts new file mode 100644 index 0000000..193c6f2 --- /dev/null +++ b/web/src/app/features/login/login.ts @@ -0,0 +1,68 @@ +import { Component, signal } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +import { Session } from '../../core/session'; + +@Component({ + imports: [ + ReactiveFormsModule, + MatButtonModule, + MatCardModule, + MatFormFieldModule, + MatInputModule, + MatProgressSpinnerModule, + ], + selector: 'app-login', + styleUrl: './login.css', + templateUrl: './login.html', +}) +export class Login { + readonly form = new FormBuilder().group({ + email: ['', [Validators.required, Validators.email]], + password: ['', Validators.required], + }); + + readonly submitting = signal(false); + readonly error = signal(null); + + constructor( + private readonly session: Session, + private readonly router: Router, + ) {} + + async submit(): Promise { + if (this.form.invalid || this.submitting()) { + return; + } + const { email, password } = this.form.getRawValue(); + + this.submitting.set(true); + this.error.set(null); + try { + await this.session.login(email!, password!); + this.router.navigateByUrl('/games'); + } catch (err) { + this.error.set(errorMessage(err)); + } finally { + this.submitting.set(false); + } + } +} + +/** Pulls a server-provided message out of the generated client's error + * shape, falling back to a generic one for network failures etc. */ +function errorMessage(err: unknown): string { + if (err && typeof err === 'object' && 'error' in err) { + const body = (err as { error?: unknown }).error; + if (body && typeof body === 'object' && 'detail' in body && typeof (body as any).detail === 'string') { + return (body as any).detail; + } + } + return 'Could not log in. Check your connection and try again.'; +} diff --git a/web/src/index.html b/web/src/index.html new file mode 100644 index 0000000..2271996 --- /dev/null +++ b/web/src/index.html @@ -0,0 +1,23 @@ + + + + + Completed + + + + + + + + + + + + diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..5df75f9 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,6 @@ +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { App } from './app/app'; + +bootstrapApplication(App, appConfig) + .catch((err) => console.error(err)); diff --git a/web/src/material-theme.scss b/web/src/material-theme.scss new file mode 100644 index 0000000..2ccd444 --- /dev/null +++ b/web/src/material-theme.scss @@ -0,0 +1,38 @@ +// Include theming for Angular Material with `mat.theme()`. +// This Sass mixin will define CSS variables that are used for styling Angular Material +// components according to the Material 3 design spec. +// Learn more about theming and how to use it for your application's +// custom components at https://material.angular.dev/guide/theming +@use '@angular/material' as mat; + +html { + height: 100%; + @include mat.theme( + ( + color: ( + primary: mat.$azure-palette, + tertiary: mat.$blue-palette, + ), + typography: Roboto, + density: 0, + ) + ); +} + +body { + // Default the application to a light color theme. This can be changed to + // `dark` to enable the dark color theme, or to `light dark` to defer to the + // user's system settings. + color-scheme: light; + + // Set a default background, font and text colors for the application using + // Angular Material's system-level CSS variables. Learn more about these + // variables at https://material.angular.dev/guide/system-variables + background-color: var(--mat-sys-surface); + color: var(--mat-sys-on-surface); + font: var(--mat-sys-body-medium); + + // Reset the user agent margin. + margin: 0; + height: 100%; +} diff --git a/web/src/styles.css b/web/src/styles.css new file mode 100644 index 0000000..90d4ee0 --- /dev/null +++ b/web/src/styles.css @@ -0,0 +1 @@ +/* You can add global styles to this file, and also import other style files */ diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100644 index 0000000..cb151e1 --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.spec.ts" + ] +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..d2fbb9c --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,31 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "compileOnSave": false, + "compilerOptions": { + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "isolatedModules": true, + "experimentalDecorators": true, + "importHelpers": true, + "target": "ES2022", + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/web/tsconfig.spec.json b/web/tsconfig.spec.json new file mode 100644 index 0000000..9c8efb9 --- /dev/null +++ b/web/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ +/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [ + "vitest/globals" + ] + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.spec.ts" + ] +}