diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6056ed25cb..83c8dbde64 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,6 +10,10 @@ updates: interval: "weekly" allow: - dependency-type: "direct" + - package-ecosystem: "npm" + directory: "/playground" + schedule: + interval: "weekly" - package-ecosystem: "cargo" directory: "/crates" schedule: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 142080868b..b618cc131f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -63,6 +63,101 @@ jobs: steps: - run: echo "supported_pythons=$SUPPORTED_PYTHONS" + playground-changes: + name: Detect playground changes + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + changed: ${{ steps.changes.outputs.changed }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Check playground sources + id: changes + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before }} + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" || -z "$BASE_SHA" || "$BASE_SHA" =~ ^0+$ ]]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + elif git diff --quiet "$BASE_SHA" "$GITHUB_SHA" -- \ + playground \ + 'pkg/cli/playground*' \ + pkg/cli/root.go \ + cmd/cog \ + .goreleaser.yaml \ + mise.toml \ + mise.lock \ + go.mod \ + go.sum \ + .github/workflows/ci.yaml \ + .github/workflows/release-build.yaml; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + + fmt-playground: + name: Format playground + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-ci-${{ github.job }} + - name: Check formatting + run: mise run fmt:playground + + lint-playground: + name: Lint playground + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-ci-${{ github.job }} + - name: Lint + run: mise run lint:playground + + test-playground: + name: Test playground + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-ci-${{ github.job }} + - name: Test + run: mise run test:playground + + playground-check: + name: Check playground + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-ci-${{ github.job }} + - name: Type check + run: mise run typecheck:playground + - name: Build embedded assets + run: mise run --force build:playground + - name: Test embedded assets + run: go test -tags playground_assets ./pkg/cli -run '^TestPlaygroundServesUI$' + - name: Upload playground assets + uses: actions/upload-artifact@v6 + with: + name: PlaygroundAssets + path: pkg/cli/playground + # ============================================================================= # Version Check - Validates VERSION.txt format and sync # ============================================================================= @@ -183,6 +278,7 @@ jobs: build-cog: name: Build cog CLI + needs: [playground-changes, playground-check] runs-on: ubuntu-latest timeout-minutes: 15 steps: @@ -193,6 +289,11 @@ jobs: with: version: 2026.4.27 cache_key_prefix: mise-ci-${{ github.job }} + - name: Download verified playground assets + uses: actions/download-artifact@v8 + with: + name: PlaygroundAssets + path: pkg/cli/playground - name: Get version from VERSION.txt id: version run: echo "version=$(tr -d '[:space:]' < VERSION.txt)" >> "$GITHUB_OUTPUT" @@ -532,6 +633,65 @@ jobs: - name: Test coglet-python bindings run: uvx nox -s coglet -p ${{ matrix.python-version }} + test-playground-e2e: + name: Playground Playwright integration tests (Chromium) + needs: + - playground-changes + - fmt-playground + - lint-playground + - test-playground + - playground-check + - build-cog + - build-sdk + - build-rust + if: needs.playground-changes.outputs.changed == 'true' + runs-on: ubuntu-latest-16-cores + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Login to Docker Hub + uses: docker/login-action@v4 + if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request' + with: + registry: index.docker.io + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Download artifacts + uses: actions/download-artifact@v8 + with: + path: dist + merge-multiple: true + - name: Install cog binary + run: | + cp dist/cog ./cog + chmod +x ./cog + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-ci-${{ github.job }} + - name: Install playground dependencies and Chromium + run: | + mise run playground:install + pnpm --dir playground exec playwright install --with-deps chromium + - name: Run browser integration tests + env: + BUILDKIT_PROGRESS: quiet + COG_BINARY: ${{ github.workspace }}/cog + COG_REGISTRY_HOST: ghcr.io/replicate/cog + COG_SDK_WHEEL: ${{ github.workspace }}/dist + COGLET_WHEEL: ${{ github.workspace }}/dist + run: pnpm --dir playground run test:e2e + - name: Upload browser test report + if: failure() + uses: actions/upload-artifact@v6 + with: + name: PlaygroundPlaywrightReport + path: | + playground/playwright-report + playground/test-results + # Compute integration test shards dynamically. # Slow tests (tagged with [short] skip) are distributed round-robin first, # then remaining tests fill in. This ensures slow tests don't pile up on one runner. @@ -696,6 +856,11 @@ jobs: name: CI Complete needs: - setup + - playground-changes + - fmt-playground + - lint-playground + - test-playground + - playground-check - version-check - build-cog - build-sdk @@ -716,6 +881,7 @@ jobs: - test-rust - test-python - test-coglet-python + - test-playground-e2e - integration-shards - test-integration if: always() @@ -725,6 +891,11 @@ jobs: - name: Check job results run: | echo "Job results:" + echo " playground-changes: ${{ needs.playground-changes.result }}" + echo " fmt-playground: ${{ needs.fmt-playground.result }}" + echo " lint-playground: ${{ needs.lint-playground.result }}" + echo " test-playground: ${{ needs.test-playground.result }}" + echo " playground-check: ${{ needs.playground-check.result }}" echo " version-check: ${{ needs.version-check.result }}" echo " build-cog: ${{ needs.build-cog.result }}" echo " build-sdk: ${{ needs.build-sdk.result }}" @@ -745,41 +916,55 @@ jobs: echo " test-rust: ${{ needs.test-rust.result }}" echo " test-python: ${{ needs.test-python.result }}" echo " test-coglet-python: ${{ needs.test-coglet-python.result }}" + echo " test-playground-e2e: ${{ needs.test-playground-e2e.result }}" echo " integration-shards: ${{ needs.integration-shards.result }}" echo " test-integration: ${{ needs.test-integration.result }}" - # Fail if any required job failed. - # lint-rust-advisories is excluded: it uses continue-on-error because - # advisory DB updates shouldn't block unrelated PRs. FAILED=false - for result in \ - "${{ needs.setup.result }}" \ - "${{ needs.version-check.result }}" \ - "${{ needs.build-cog.result }}" \ - "${{ needs.build-sdk.result }}" \ - "${{ needs.build-rust.result }}" \ - "${{ needs.fmt-go.result }}" \ - "${{ needs.fmt-rust.result }}" \ - "${{ needs.fmt-python.result }}" \ - "${{ needs.check-llm-docs.result }}" \ - "${{ needs.check-stubs.result }}" \ - "${{ needs.lint-go.result }}" \ - "${{ needs.lint-rust.result }}" \ - "${{ needs.lint-rust-deny.result }}" \ - "${{ needs.lint-python.result }}" \ - "${{ needs.lint-docs.result }}" \ - "${{ needs.test-go.result }}" \ - "${{ needs.fuzz-go.result }}" \ - "${{ needs.test-rust.result }}" \ - "${{ needs.test-python.result }}" \ - "${{ needs.test-coglet-python.result }}" \ - "${{ needs.integration-shards.result }}" \ - "${{ needs.test-integration.result }}" - do - if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then + check_success() { + if [ "$2" != "success" ]; then + echo "::error::$1 finished with unexpected result: $2" FAILED=true fi - done + } + + # lint-rust-advisories is informational and intentionally excluded. + check_success setup "${{ needs.setup.result }}" + check_success playground-changes "${{ needs.playground-changes.result }}" + check_success fmt-playground "${{ needs.fmt-playground.result }}" + check_success lint-playground "${{ needs.lint-playground.result }}" + check_success test-playground "${{ needs.test-playground.result }}" + check_success playground-check "${{ needs.playground-check.result }}" + check_success version-check "${{ needs.version-check.result }}" + check_success build-cog "${{ needs.build-cog.result }}" + check_success build-sdk "${{ needs.build-sdk.result }}" + check_success build-rust "${{ needs.build-rust.result }}" + check_success fmt-go "${{ needs.fmt-go.result }}" + check_success fmt-rust "${{ needs.fmt-rust.result }}" + check_success fmt-python "${{ needs.fmt-python.result }}" + check_success check-llm-docs "${{ needs.check-llm-docs.result }}" + check_success check-stubs "${{ needs.check-stubs.result }}" + check_success lint-go "${{ needs.lint-go.result }}" + check_success lint-rust "${{ needs.lint-rust.result }}" + check_success lint-rust-deny "${{ needs.lint-rust-deny.result }}" + check_success lint-python "${{ needs.lint-python.result }}" + check_success lint-docs "${{ needs.lint-docs.result }}" + check_success test-go "${{ needs.test-go.result }}" + check_success fuzz-go "${{ needs.fuzz-go.result }}" + check_success test-rust "${{ needs.test-rust.result }}" + check_success test-python "${{ needs.test-python.result }}" + check_success test-coglet-python "${{ needs.test-coglet-python.result }}" + check_success integration-shards "${{ needs.integration-shards.result }}" + check_success test-integration "${{ needs.test-integration.result }}" + + if [ "${{ needs.playground-changes.outputs.changed }}" = "true" ]; then + check_success test-playground-e2e "${{ needs.test-playground-e2e.result }}" + else + if [ "${{ needs.test-playground-e2e.result }}" != "skipped" ]; then + echo "::error::Playground integration tests should be skipped when playground sources are unchanged" + FAILED=true + fi + fi if [ "$FAILED" = "true" ]; then echo "::error::Some jobs failed or were cancelled" diff --git a/.github/workflows/release-build.yaml b/.github/workflows/release-build.yaml index 015bc34dc2..fa27317f3a 100644 --- a/.github/workflows/release-build.yaml +++ b/.github/workflows/release-build.yaml @@ -25,7 +25,7 @@ on: tags: ["v[0-9]+.[0-9]+.[0-9]+*"] permissions: - contents: write + contents: read env: CARGO_TERM_COLOR: always @@ -248,6 +248,25 @@ jobs: name: sdk-dist path: dist/* + build-playground: + name: Build playground assets + needs: verify-tag + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v6 + - uses: jdx/mise-action@v4 + with: + version: 2026.4.27 + cache_key_prefix: mise-rel-${{ github.job }} + - name: Build embedded playground assets + run: mise run --force build:playground + - name: Upload playground assets + uses: actions/upload-artifact@v6 + with: + name: playground-assets + path: pkg/cli/playground + build-coglet-wheels: name: Build coglet wheel (${{ matrix.target }}) needs: verify-tag @@ -298,7 +317,9 @@ jobs: create-release: name: Create release - needs: [verify-tag, build-sdk, build-coglet-wheels] + permissions: + contents: write + needs: [verify-tag, build-sdk, build-coglet-wheels, build-playground] # macOS arm64 runner: native clang for darwin targets, zig for linux targets. # CGo required for go-tree-sitter (static Python schema parser). runs-on: macos-14 @@ -327,6 +348,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Download verified playground assets + uses: actions/download-artifact@v8 + with: + name: playground-assets + path: pkg/cli/playground + # Goreleaser builds CLI binaries and creates draft release - name: Build CLI and create draft release uses: goreleaser/goreleaser-action@v7 diff --git a/.gitignore b/.gitignore index 4c1c6b6b9d..35ee93229c 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,10 @@ target # Generated docs /site + +# Playground frontend +/playground/node_modules/ +/playground/coverage/ +/playground/playwright-report/ +/playground/test-results/ +/pkg/cli/playground/ diff --git a/.golangci.yaml b/.golangci.yaml index 17b97cfad8..0d795ff98a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -64,6 +64,8 @@ linters: - weakCond exclusions: generated: strict + paths: + - playground/node_modules rules: # Exclude some linters from running on test files - path: '(.+)_test\.go' diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 33a9dd13cc..bdb5eb5b14 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -18,6 +18,8 @@ builds: - amd64 - arm64 main: ./cmd/cog + tags: + - playground_assets ldflags: # COG_VERSION (from VERSION.txt via mise build:cog or CI) overrides git-derived version for snapshots. # For tagged releases, COG_VERSION is unset and envOrDefault falls back to .Version (from tag). diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index 18d9b90fc6..5396b71f52 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -7,6 +7,8 @@ ignores: - "site/**" - "docs/llms.txt" - "CLAUDE.md" + - "playground/node_modules/**" + - "playground/coverage/**" config: default: false diff --git a/architecture/06-cli.md b/architecture/06-cli.md index b47a67ee61..81bff1b912 100644 --- a/architecture/06-cli.md +++ b/architecture/06-cli.md @@ -6,15 +6,16 @@ The Cog CLI is a Go binary that provides commands for the full model lifecycle: ## Commands Overview -| Command | Job To Be Done | -| ----------- | ------------------------------------- | -| `cog init` | Bootstrap a new model project | -| `cog build` | Create a container image | -| `cog run` | Run a prediction in a container | -| `cog exec` | Run arbitrary commands in a container | -| `cog serve` | Start HTTP server in a container | -| `cog push` | Deploy to Replicate | -| `cog login` | Authenticate with Replicate | +| Command | Job To Be Done | +| ---------------- | ------------------------------------- | +| `cog init` | Bootstrap a new model project | +| `cog build` | Create a container image | +| `cog run` | Run a prediction in a container | +| `cog exec` | Run arbitrary commands in a container | +| `cog serve` | Start HTTP server in a container | +| `cog playground` | Open a local UI for a model API | +| `cog push` | Deploy to Replicate | +| `cog login` | Authenticate with Replicate | ## Development Commands @@ -98,6 +99,21 @@ Builds the image (if needed) and starts a container running the [Container Runti **Code**: `pkg/cli/serve.go` +### cog playground + +**Job**: Explore and call a running Cog HTTP API from a browser. + +```bash +cog serve -p 8393 +cog playground --target http://localhost:8393 +``` + +The command serves an embedded React application and reverse-proxies its requests to the selected model API. Each browser tab is an independent workspace: every request carries that tab's target, and the proxy keeps no shared current-target state, so tabs can use the same or different models concurrently. The UI and proxy accept only loopback connections, even when `--host 0.0.0.0` is used so a container can deliver webhooks. The proxy snapshots upstream response headers into encoded metadata so the request inspector can show model values without including or merging playground security and transport headers. Webhook URLs contain an opaque per-prediction token, which also isolates concurrent tab subscriptions, and payloads are relayed to the browser over server-sent events. + +The proxy is intentionally user-directed: this is a local development tool for APIs selected by the user, not a remotely hosted gateway. CLI and release builds compile `playground/` into ignored assets under `pkg/cli/playground/`, then embed them in the Go binary. Ordinary Go tests use a build-time stub, so a clean Go checkout does not require Node; binaries built without the asset tag reject the playground command with build instructions. + +**Code**: `pkg/cli/` owns the server and embedded assets; `playground/` owns the frontend source. + ## Build Commands ### cog build @@ -219,6 +235,7 @@ pkg/cli/ ├── predict.go # prediction execution and legacy cog predict ├── exec.go # cog exec ├── serve.go # cog serve +├── playground.go # cog playground and local proxy ├── push.go # cog push ├── login.go # cog login └── init.go # cog init diff --git a/docs/cli.md b/docs/cli.md index eacd27e5d3..304a17674e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -175,6 +175,49 @@ cog login [flags] --token-stdin Pass login token on stdin instead of opening a browser. You can find your Replicate login token at https://replicate.com/auth/token ``` +## `cog playground` + +Open a browser playground for talking to a running model. + +Starts a local web server that serves a schema-driven UI (a Postman-like tool +for Cog models). Point it at any running Cog HTTP API -- for example one started +with 'cog serve' -- and the playground reflects that model's inputs and outputs +from its OpenAPI schema in real time. + +Requests are reverse-proxied through this server, so the target API does not +need to set CORS headers. The server also hosts a webhook sink so async +predictions can be observed in the browser. + +Async/webhook testing against a containerized model requires the webhook URL to +be reachable from inside the container. On Docker Desktop the default +'host.docker.internal' works once the server listens on a reachable interface +(e.g. --host 0.0.0.0). + +``` +cog playground [flags] +``` + +**Examples** + +``` + # Start a model API in one terminal + cog serve -p 8393 + + # Open the playground pointing at it + cog playground --target http://localhost:8393 +``` + +**Options** + +``` + -h, --help help for playground + --host string Address to bind (use 0.0.0.0 to receive webhooks from containers) (default "127.0.0.1") + --no-open Do not open the browser automatically + -p, --port int Port to listen on (0 picks a free port) + --target string Default target model API URL (default "http://localhost:8393") + --webhook-host string Hostname the model uses to reach this server for webhooks (default "host.docker.internal") +``` + ## `cog push` Build a Docker image from cog.yaml and push it to a container registry. diff --git a/docs/llms.txt b/docs/llms.txt index e8419e39a8..2e1044e373 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -421,6 +421,49 @@ cog login [flags] --token-stdin Pass login token on stdin instead of opening a browser. You can find your Replicate login token at https://replicate.com/auth/token ``` +## `cog playground` + +Open a browser playground for talking to a running model. + +Starts a local web server that serves a schema-driven UI (a Postman-like tool +for Cog models). Point it at any running Cog HTTP API -- for example one started +with 'cog serve' -- and the playground reflects that model's inputs and outputs +from its OpenAPI schema in real time. + +Requests are reverse-proxied through this server, so the target API does not +need to set CORS headers. The server also hosts a webhook sink so async +predictions can be observed in the browser. + +Async/webhook testing against a containerized model requires the webhook URL to +be reachable from inside the container. On Docker Desktop the default +'host.docker.internal' works once the server listens on a reachable interface +(e.g. --host 0.0.0.0). + +``` +cog playground [flags] +``` + +**Examples** + +``` + # Start a model API in one terminal + cog serve -p 8393 + + # Open the playground pointing at it + cog playground --target http://localhost:8393 +``` + +**Options** + +``` + -h, --help help for playground + --host string Address to bind (use 0.0.0.0 to receive webhooks from containers) (default "127.0.0.1") + --no-open Do not open the browser automatically + -p, --port int Port to listen on (0 picks a free port) + --target string Default target model API URL (default "http://localhost:8393") + --webhook-host string Hostname the model uses to reach this server for webhooks (default "host.docker.internal") +``` + ## `cog push` Build a Docker image from cog.yaml and push it to a container registry. diff --git a/mise.lock b/mise.lock index 3fe3fb9ec2..f26db49f81 100644 --- a/mise.lock +++ b/mise.lock @@ -16,10 +16,18 @@ url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/carg checksum = "sha256:0e8c2aa59128612c90d9e09c02204e912f29a5b8d9a64671b94608cbe09e064f" url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-unknown-linux-musl.tar.gz" +[tools."aqua:EmbarkStudios/cargo-deny"."platforms.linux-x64-baseline"] +checksum = "sha256:0e8c2aa59128612c90d9e09c02204e912f29a5b8d9a64671b94608cbe09e064f" +url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-unknown-linux-musl.tar.gz" + [tools."aqua:EmbarkStudios/cargo-deny"."platforms.linux-x64-musl"] checksum = "sha256:0e8c2aa59128612c90d9e09c02204e912f29a5b8d9a64671b94608cbe09e064f" url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-unknown-linux-musl.tar.gz" +[tools."aqua:EmbarkStudios/cargo-deny"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:0e8c2aa59128612c90d9e09c02204e912f29a5b8d9a64671b94608cbe09e064f" +url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-unknown-linux-musl.tar.gz" + [tools."aqua:EmbarkStudios/cargo-deny"."platforms.macos-arm64"] checksum = "sha256:a22f2023c06f3eefd099a5d42dd828fd4fa74d1e1c167bd1dbc3cf59ad62ded0" url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-aarch64-apple-darwin.tar.gz" @@ -28,10 +36,18 @@ url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/carg checksum = "sha256:c42163655413f7e872638cd8c4345a327b512ef0ab99109e9cced691b95af5fb" url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-apple-darwin.tar.gz" +[tools."aqua:EmbarkStudios/cargo-deny"."platforms.macos-x64-baseline"] +checksum = "sha256:c42163655413f7e872638cd8c4345a327b512ef0ab99109e9cced691b95af5fb" +url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-apple-darwin.tar.gz" + [tools."aqua:EmbarkStudios/cargo-deny"."platforms.windows-x64"] checksum = "sha256:413f0e2ce780d0d14ba8e9339f9fb033419a8a971ec7714faec518e4a664bdb0" url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-pc-windows-msvc.tar.gz" +[tools."aqua:EmbarkStudios/cargo-deny"."platforms.windows-x64-baseline"] +checksum = "sha256:413f0e2ce780d0d14ba8e9339f9fb033419a8a971ec7714faec518e4a664bdb0" +url = "https://github.com/EmbarkStudios/cargo-deny/releases/download/0.19.0/cargo-deny-0.19.0-x86_64-pc-windows-msvc.tar.gz" + [[tools."aqua:golangci/golangci-lint"]] version = "2.10.1" backend = "aqua:golangci/golangci-lint" @@ -51,11 +67,21 @@ checksum = "sha256:dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09 url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-amd64.tar.gz" provenance = "github-attestations" +[tools."aqua:golangci/golangci-lint"."platforms.linux-x64-baseline"] +checksum = "sha256:dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99" +url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-amd64.tar.gz" +provenance = "github-attestations" + [tools."aqua:golangci/golangci-lint"."platforms.linux-x64-musl"] checksum = "sha256:dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99" url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-amd64.tar.gz" provenance = "github-attestations" +[tools."aqua:golangci/golangci-lint"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:dfa775874cf0561b404a02a8f4481fc69b28091da95aa697259820d429b09c99" +url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-linux-amd64.tar.gz" +provenance = "github-attestations" + [tools."aqua:golangci/golangci-lint"."platforms.macos-arm64"] checksum = "sha256:03bfadf67e52b441b7ec21305e501c717df93c959836d66c7f97312654acb297" url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-darwin-arm64.tar.gz" @@ -66,11 +92,21 @@ checksum = "sha256:66fb0da81b8033b477f97eea420d4b46b230ca172b8bb87c6610109f3772b url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-darwin-amd64.tar.gz" provenance = "github-attestations" +[tools."aqua:golangci/golangci-lint"."platforms.macos-x64-baseline"] +checksum = "sha256:66fb0da81b8033b477f97eea420d4b46b230ca172b8bb87c6610109f3772b6b6" +url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-darwin-amd64.tar.gz" +provenance = "github-attestations" + [tools."aqua:golangci/golangci-lint"."platforms.windows-x64"] checksum = "sha256:c60c87695e79db8e320f0e5be885059859de52bb5ee5f11be5577828570bc2a3" url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-windows-amd64.zip" provenance = "github-attestations" +[tools."aqua:golangci/golangci-lint"."platforms.windows-x64-baseline"] +checksum = "sha256:c60c87695e79db8e320f0e5be885059859de52bb5ee5f11be5577828570bc2a3" +url = "https://github.com/golangci/golangci-lint/releases/download/v2.10.1/golangci-lint-2.10.1-windows-amd64.zip" +provenance = "github-attestations" + [[tools."aqua:gotestyourself/gotestsum"]] version = "1.13.0" backend = "aqua:gotestyourself/gotestsum" @@ -87,10 +123,18 @@ url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/got checksum = "sha256:11ccddeaf708ef228889f9fe2f68291a75b27013ddfc3b18156e094f5f40e8ee" url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz" +[tools."aqua:gotestyourself/gotestsum"."platforms.linux-x64-baseline"] +checksum = "sha256:11ccddeaf708ef228889f9fe2f68291a75b27013ddfc3b18156e094f5f40e8ee" +url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz" + [tools."aqua:gotestyourself/gotestsum"."platforms.linux-x64-musl"] checksum = "sha256:11ccddeaf708ef228889f9fe2f68291a75b27013ddfc3b18156e094f5f40e8ee" url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz" +[tools."aqua:gotestyourself/gotestsum"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:11ccddeaf708ef228889f9fe2f68291a75b27013ddfc3b18156e094f5f40e8ee" +url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz" + [tools."aqua:gotestyourself/gotestsum"."platforms.macos-arm64"] checksum = "sha256:509cb27aef747f48faf9bce424f59dcf79572c905204b990ee935bbfcc7fa0e9" url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_darwin_arm64.tar.gz" @@ -99,10 +143,18 @@ url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/got checksum = "sha256:99529350f4c7b780b1efc543ca0d9721b09f0a4228f0efa9281261f58fefa05a" url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_darwin_amd64.tar.gz" +[tools."aqua:gotestyourself/gotestsum"."platforms.macos-x64-baseline"] +checksum = "sha256:99529350f4c7b780b1efc543ca0d9721b09f0a4228f0efa9281261f58fefa05a" +url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_darwin_amd64.tar.gz" + [tools."aqua:gotestyourself/gotestsum"."platforms.windows-x64"] checksum = "sha256:fd5a6dc69e46a0970593e70d85a7e75f16714e9c61d6d72ccc324eb82df5bb8a" url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_windows_amd64.tar.gz" +[tools."aqua:gotestyourself/gotestsum"."platforms.windows-x64-baseline"] +checksum = "sha256:fd5a6dc69e46a0970593e70d85a7e75f16714e9c61d6d72ccc324eb82df5bb8a" +url = "https://github.com/gotestyourself/gotestsum/releases/download/v1.13.0/gotestsum_1.13.0_windows_amd64.tar.gz" + [[tools."aqua:jqlang/jq"]] version = "1.8.1" backend = "aqua:jqlang/jq" @@ -122,11 +174,21 @@ checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2 url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" provenance = "github-attestations" +[tools."aqua:jqlang/jq"."platforms.linux-x64-baseline"] +checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d" +url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" +provenance = "github-attestations" + [tools."aqua:jqlang/jq"."platforms.linux-x64-musl"] checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" provenance = "github-attestations" +[tools."aqua:jqlang/jq"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:020468de7539ce70ef1bceaf7cde2e8c4f2ca6c3afb84642aabc5c97d9fc2a0d" +url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-linux-amd64" +provenance = "github-attestations" + [tools."aqua:jqlang/jq"."platforms.macos-arm64"] checksum = "sha256:a9fe3ea2f86dfc72f6728417521ec9067b343277152b114f4e98d8cb0e263603" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-arm64" @@ -137,11 +199,21 @@ checksum = "sha256:e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-amd64" provenance = "github-attestations" +[tools."aqua:jqlang/jq"."platforms.macos-x64-baseline"] +checksum = "sha256:e80dbe0d2a2597e3c11c404f03337b981d74b4a8504b70586c354b7697a7c27f" +url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-macos-amd64" +provenance = "github-attestations" + [tools."aqua:jqlang/jq"."platforms.windows-x64"] checksum = "sha256:23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334" url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-windows-amd64.exe" provenance = "github-attestations" +[tools."aqua:jqlang/jq"."platforms.windows-x64-baseline"] +checksum = "sha256:23cb60a1354eed6bcc8d9b9735e8c7b388cd1fdcb75726b93bc299ef22dd9334" +url = "https://github.com/jqlang/jq/releases/download/jq-1.8.1/jq-windows-amd64.exe" +provenance = "github-attestations" + [[tools."aqua:mitsuhiko/insta"]] version = "1.46.0" backend = "aqua:mitsuhiko/insta" @@ -150,10 +222,18 @@ backend = "aqua:mitsuhiko/insta" checksum = "sha256:592f7f8ddd465f0b8f2e7121bbe6bace97b475330b7f3c0ac62e504c1de6b967" url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-unknown-linux-musl.tar.xz" +[tools."aqua:mitsuhiko/insta"."platforms.linux-x64-baseline"] +checksum = "sha256:592f7f8ddd465f0b8f2e7121bbe6bace97b475330b7f3c0ac62e504c1de6b967" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-unknown-linux-musl.tar.xz" + [tools."aqua:mitsuhiko/insta"."platforms.linux-x64-musl"] checksum = "sha256:592f7f8ddd465f0b8f2e7121bbe6bace97b475330b7f3c0ac62e504c1de6b967" url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-unknown-linux-musl.tar.xz" +[tools."aqua:mitsuhiko/insta"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:592f7f8ddd465f0b8f2e7121bbe6bace97b475330b7f3c0ac62e504c1de6b967" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-unknown-linux-musl.tar.xz" + [tools."aqua:mitsuhiko/insta"."platforms.macos-arm64"] checksum = "sha256:c32a785806a7b329330fefced808c0ba7017416c8a7ea24c0a8363ad66d1aeed" url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-aarch64-apple-darwin.tar.xz" @@ -162,10 +242,18 @@ url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-a checksum = "sha256:4a2e8bc9b3e7591fd96580cbb4c79cae062060f9482719bb32bc3932eff08fba" url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-apple-darwin.tar.xz" +[tools."aqua:mitsuhiko/insta"."platforms.macos-x64-baseline"] +checksum = "sha256:4a2e8bc9b3e7591fd96580cbb4c79cae062060f9482719bb32bc3932eff08fba" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-apple-darwin.tar.xz" + [tools."aqua:mitsuhiko/insta"."platforms.windows-x64"] checksum = "sha256:d13a207264e10644d6995bdb332d7cc7353ffc53a0199f4e20376923016247ab" url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-pc-windows-msvc.zip" +[tools."aqua:mitsuhiko/insta"."platforms.windows-x64-baseline"] +checksum = "sha256:d13a207264e10644d6995bdb332d7cc7353ffc53a0199f4e20376923016247ab" +url = "https://github.com/mitsuhiko/insta/releases/download/1.46.0/cargo-insta-x86_64-pc-windows-msvc.zip" + [[tools."aqua:rust-cross/cargo-zigbuild"]] version = "0.20.1" backend = "aqua:rust-cross/cargo-zigbuild" @@ -182,10 +270,18 @@ url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/ca checksum = "sha256:742ed281f15fef6eaf49535ac10ffe98fb57913d0c4c88d6888d794043c05618" url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.x86_64-unknown-linux-musl.tar.gz" +[tools."aqua:rust-cross/cargo-zigbuild"."platforms.linux-x64-baseline"] +checksum = "sha256:742ed281f15fef6eaf49535ac10ffe98fb57913d0c4c88d6888d794043c05618" +url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.x86_64-unknown-linux-musl.tar.gz" + [tools."aqua:rust-cross/cargo-zigbuild"."platforms.linux-x64-musl"] checksum = "sha256:742ed281f15fef6eaf49535ac10ffe98fb57913d0c4c88d6888d794043c05618" url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.x86_64-unknown-linux-musl.tar.gz" +[tools."aqua:rust-cross/cargo-zigbuild"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:742ed281f15fef6eaf49535ac10ffe98fb57913d0c4c88d6888d794043c05618" +url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.x86_64-unknown-linux-musl.tar.gz" + [tools."aqua:rust-cross/cargo-zigbuild"."platforms.macos-arm64"] checksum = "sha256:d5c7ac2e6f25fb76083dff84640cdf679c5da858b9c97631555c3185be2fbf35" url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.apple-darwin.tar.gz" @@ -194,10 +290,18 @@ url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/ca checksum = "sha256:d5c7ac2e6f25fb76083dff84640cdf679c5da858b9c97631555c3185be2fbf35" url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.apple-darwin.tar.gz" +[tools."aqua:rust-cross/cargo-zigbuild"."platforms.macos-x64-baseline"] +checksum = "sha256:d5c7ac2e6f25fb76083dff84640cdf679c5da858b9c97631555c3185be2fbf35" +url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.apple-darwin.tar.gz" + [tools."aqua:rust-cross/cargo-zigbuild"."platforms.windows-x64"] checksum = "sha256:b0bb728ba068ee61342f40a2124b3d8d234af8f716dd416b7c1f794dfeb4e478" url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.windows-x64.zip" +[tools."aqua:rust-cross/cargo-zigbuild"."platforms.windows-x64-baseline"] +checksum = "sha256:b0bb728ba068ee61342f40a2124b3d8d234af8f716dd416b7c1f794dfeb4e478" +url = "https://github.com/rust-cross/cargo-zigbuild/releases/download/v0.20.1/cargo-zigbuild-v0.20.1.windows-x64.zip" + [[tools."aqua:rust-lang/rustup"]] version = "1.28.2" backend = "aqua:rust-lang/rustup" @@ -214,10 +318,18 @@ url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-unknown-linux- checksum = "sha256:20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init" +[tools."aqua:rust-lang/rustup"."platforms.linux-x64-baseline"] +checksum = "sha256:20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init" + [tools."aqua:rust-lang/rustup"."platforms.linux-x64-musl"] checksum = "sha256:e6599a1c7be58a2d8eaca66a80e0dc006d87bbcf780a58b7343d6e14c1605cb2" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-musl/rustup-init" +[tools."aqua:rust-lang/rustup"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:e6599a1c7be58a2d8eaca66a80e0dc006d87bbcf780a58b7343d6e14c1605cb2" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-musl/rustup-init" + [tools."aqua:rust-lang/rustup"."platforms.macos-arm64"] checksum = "sha256:20ef5516c31b1ac2290084199ba77dbbcaa1406c45c1d978ca68558ef5964ef5" url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-apple-darwin/rustup-init" @@ -226,10 +338,18 @@ url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-apple-darwin/r checksum = "sha256:9c331076f62b4d0edeae63d9d1c9442d5fe39b37b05025ec8d41c5ed35486496" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-apple-darwin/rustup-init" +[tools."aqua:rust-lang/rustup"."platforms.macos-x64-baseline"] +checksum = "sha256:9c331076f62b4d0edeae63d9d1c9442d5fe39b37b05025ec8d41c5ed35486496" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-apple-darwin/rustup-init" + [tools."aqua:rust-lang/rustup"."platforms.windows-x64"] checksum = "sha256:88d8258dcf6ae4f7a80c7d1088e1f36fa7025a1cfd1343731b4ee6f385121fc0" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-pc-windows-msvc/rustup-init.exe" +[tools."aqua:rust-lang/rustup"."platforms.windows-x64-baseline"] +checksum = "sha256:88d8258dcf6ae4f7a80c7d1088e1f36fa7025a1cfd1343731b4ee6f385121fc0" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-pc-windows-msvc/rustup-init.exe" + [[tools."aqua:rust-lang/rustup/rustup-init"]] version = "1.28.2" backend = "aqua:rust-lang/rustup/rustup-init" @@ -246,10 +366,18 @@ url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-unknown-linux- checksum = "sha256:20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init" +[tools."aqua:rust-lang/rustup/rustup-init"."platforms.linux-x64-baseline"] +checksum = "sha256:20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-gnu/rustup-init" + [tools."aqua:rust-lang/rustup/rustup-init"."platforms.linux-x64-musl"] checksum = "sha256:e6599a1c7be58a2d8eaca66a80e0dc006d87bbcf780a58b7343d6e14c1605cb2" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-musl/rustup-init" +[tools."aqua:rust-lang/rustup/rustup-init"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:e6599a1c7be58a2d8eaca66a80e0dc006d87bbcf780a58b7343d6e14c1605cb2" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-unknown-linux-musl/rustup-init" + [tools."aqua:rust-lang/rustup/rustup-init"."platforms.macos-arm64"] checksum = "sha256:20ef5516c31b1ac2290084199ba77dbbcaa1406c45c1d978ca68558ef5964ef5" url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-apple-darwin/rustup-init" @@ -258,10 +386,18 @@ url = "https://static.rust-lang.org/rustup/archive/1.28.2/aarch64-apple-darwin/r checksum = "sha256:9c331076f62b4d0edeae63d9d1c9442d5fe39b37b05025ec8d41c5ed35486496" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-apple-darwin/rustup-init" +[tools."aqua:rust-lang/rustup/rustup-init"."platforms.macos-x64-baseline"] +checksum = "sha256:9c331076f62b4d0edeae63d9d1c9442d5fe39b37b05025ec8d41c5ed35486496" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-apple-darwin/rustup-init" + [tools."aqua:rust-lang/rustup/rustup-init"."platforms.windows-x64"] checksum = "sha256:88d8258dcf6ae4f7a80c7d1088e1f36fa7025a1cfd1343731b4ee6f385121fc0" url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-pc-windows-msvc/rustup-init.exe" +[tools."aqua:rust-lang/rustup/rustup-init"."platforms.windows-x64-baseline"] +checksum = "sha256:88d8258dcf6ae4f7a80c7d1088e1f36fa7025a1cfd1343731b4ee6f385121fc0" +url = "https://static.rust-lang.org/rustup/archive/1.28.2/x86_64-pc-windows-msvc/rustup-init.exe" + [[tools."aqua:vektra/mockery"]] version = "3.7.0" backend = "aqua:vektra/mockery" @@ -278,10 +414,18 @@ url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_ checksum = "sha256:5e10597d9741d7b8cda699f044b24c9e8b51467dce413e79516d7cd9373c2896" url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Linux_x86_64.tar.gz" +[tools."aqua:vektra/mockery"."platforms.linux-x64-baseline"] +checksum = "sha256:5e10597d9741d7b8cda699f044b24c9e8b51467dce413e79516d7cd9373c2896" +url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Linux_x86_64.tar.gz" + [tools."aqua:vektra/mockery"."platforms.linux-x64-musl"] checksum = "sha256:5e10597d9741d7b8cda699f044b24c9e8b51467dce413e79516d7cd9373c2896" url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Linux_x86_64.tar.gz" +[tools."aqua:vektra/mockery"."platforms.linux-x64-musl-baseline"] +checksum = "sha256:5e10597d9741d7b8cda699f044b24c9e8b51467dce413e79516d7cd9373c2896" +url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Linux_x86_64.tar.gz" + [tools."aqua:vektra/mockery"."platforms.macos-arm64"] checksum = "sha256:c0a8ea54d602c071775d1692321d385903ccc80bd2d1ae9f230539a195643d7d" url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Darwin_arm64.tar.gz" @@ -290,10 +434,18 @@ url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_ checksum = "sha256:36e7357fd160689a9a8ae57726e099761159845e2749ba31091589853a5a894d" url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Darwin_x86_64.tar.gz" +[tools."aqua:vektra/mockery"."platforms.macos-x64-baseline"] +checksum = "sha256:36e7357fd160689a9a8ae57726e099761159845e2749ba31091589853a5a894d" +url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Darwin_x86_64.tar.gz" + [tools."aqua:vektra/mockery"."platforms.windows-x64"] checksum = "sha256:76d524ad1740cd02ed621d90015f538fdb53cf6cd4a1ad4d289db017fb69bd0e" url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Windows_x86_64.tar.gz" +[tools."aqua:vektra/mockery"."platforms.windows-x64-baseline"] +checksum = "sha256:76d524ad1740cd02ed621d90015f538fdb53cf6cd4a1ad4d289db017fb69bd0e" +url = "https://github.com/vektra/mockery/releases/download/v3.7.0/mockery_3.7.0_Windows_x86_64.tar.gz" + [[tools.cargo-binstall]] version = "1.16.6" backend = "aqua:cargo-bins/cargo-binstall" @@ -310,10 +462,18 @@ url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/ca checksum = "sha256:3225eea8041c30d7462761a481883e3aa8fe31c58def4b6c8dd91b7c80973df0" url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-unknown-linux-musl.tgz" +[tools.cargo-binstall."platforms.linux-x64-baseline"] +checksum = "sha256:3225eea8041c30d7462761a481883e3aa8fe31c58def4b6c8dd91b7c80973df0" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-unknown-linux-musl.tgz" + [tools.cargo-binstall."platforms.linux-x64-musl"] checksum = "sha256:3225eea8041c30d7462761a481883e3aa8fe31c58def4b6c8dd91b7c80973df0" url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-unknown-linux-musl.tgz" +[tools.cargo-binstall."platforms.linux-x64-musl-baseline"] +checksum = "sha256:3225eea8041c30d7462761a481883e3aa8fe31c58def4b6c8dd91b7c80973df0" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-unknown-linux-musl.tgz" + [tools.cargo-binstall."platforms.macos-arm64"] checksum = "sha256:30543b378b96fbddabee1edfaccde7914dd2f851f02c560de859f81a21ab665b" url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-aarch64-apple-darwin.zip" @@ -322,10 +482,18 @@ url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/ca checksum = "sha256:633dc2f381f7000d8ba3c02eb24b2f290bf0154372bafe8d8094d777f129f21d" url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-apple-darwin.zip" +[tools.cargo-binstall."platforms.macos-x64-baseline"] +checksum = "sha256:633dc2f381f7000d8ba3c02eb24b2f290bf0154372bafe8d8094d777f129f21d" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-apple-darwin.zip" + [tools.cargo-binstall."platforms.windows-x64"] checksum = "sha256:fca962c3d12ae6192280111074db073c15abad3ba162a1a5a2af0f6f01872114" url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-pc-windows-msvc.zip" +[tools.cargo-binstall."platforms.windows-x64-baseline"] +checksum = "sha256:fca962c3d12ae6192280111074db073c15abad3ba162a1a5a2af0f6f01872114" +url = "https://github.com/cargo-bins/cargo-binstall/releases/download/v1.16.6/cargo-binstall-x86_64-pc-windows-msvc.zip" + [[tools."cargo:cargo-nextest"]] version = "0.9.120" backend = "cargo:cargo-nextest" @@ -350,10 +518,18 @@ url = "https://dl.google.com/go/go1.25.6.linux-arm64.tar.gz" checksum = "sha256:f022b6aad78e362bcba9b0b94d09ad58c5a70c6ba3b7582905fababf5fe0181a" url = "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz" +[tools.go."platforms.linux-x64-baseline"] +checksum = "sha256:f022b6aad78e362bcba9b0b94d09ad58c5a70c6ba3b7582905fababf5fe0181a" +url = "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz" + [tools.go."platforms.linux-x64-musl"] checksum = "sha256:f022b6aad78e362bcba9b0b94d09ad58c5a70c6ba3b7582905fababf5fe0181a" url = "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz" +[tools.go."platforms.linux-x64-musl-baseline"] +checksum = "sha256:f022b6aad78e362bcba9b0b94d09ad58c5a70c6ba3b7582905fababf5fe0181a" +url = "https://dl.google.com/go/go1.25.6.linux-amd64.tar.gz" + [tools.go."platforms.macos-arm64"] checksum = "sha256:984521ae978a5377c7d782fd2dd953291840d7d3d0bd95781a1f32f16d94a006" url = "https://dl.google.com/go/go1.25.6.darwin-arm64.tar.gz" @@ -362,18 +538,78 @@ url = "https://dl.google.com/go/go1.25.6.darwin-arm64.tar.gz" checksum = "sha256:e2b5b237f5c262931b8e280ac4b8363f156e19bfad5270c099998932819670b7" url = "https://dl.google.com/go/go1.25.6.darwin-amd64.tar.gz" +[tools.go."platforms.macos-x64-baseline"] +checksum = "sha256:e2b5b237f5c262931b8e280ac4b8363f156e19bfad5270c099998932819670b7" +url = "https://dl.google.com/go/go1.25.6.darwin-amd64.tar.gz" + [tools.go."platforms.windows-x64"] checksum = "sha256:19b4733b727ba5c611b5656187f3ac367d278d64c3d4199a845e39c0fdac5335" url = "https://dl.google.com/go/go1.25.6.windows-amd64.zip" +[tools.go."platforms.windows-x64-baseline"] +checksum = "sha256:19b4733b727ba5c611b5656187f3ac367d278d64c3d4199a845e39c0fdac5335" +url = "https://dl.google.com/go/go1.25.6.windows-amd64.zip" + [[tools."go:golang.org/x/tools/cmd/goimports"]] version = "0.44.0" backend = "go:golang.org/x/tools/cmd/goimports" +[[tools.node]] +version = "24.18.0" +backend = "core:node" + +[tools.node."platforms.linux-arm64"] +checksum = "sha256:6b4484c2190274175df9aa8f28e2d758a819cb1c1fe6ab481e2f95b463ab8508" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-arm64.tar.gz" + +[tools.node."platforms.linux-arm64-musl"] +checksum = "sha256:b1c6c2dc31b46dd8fb2322f4fe75b07e775c5120bc37251deeea28f529d4567b" +url = "https://unofficial-builds.nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-arm64-musl.tar.gz" + +[tools.node."platforms.linux-x64"] +checksum = "sha256:783130984963db7ba9cbd01089eaf2c2efb055c7c1693c943174b967b3050cb8" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-baseline"] +checksum = "sha256:783130984963db7ba9cbd01089eaf2c2efb055c7c1693c943174b967b3050cb8" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-linux-x64.tar.gz" + +[tools.node."platforms.linux-x64-musl"] +checksum = "sha256:ea58409911e141ec6b19d9178efa2d9185a13295005b1cbf5521b3157eed1d95" +url = "https://unofficial-builds.nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-x64-musl.tar.gz" + +[tools.node."platforms.linux-x64-musl-baseline"] +checksum = "sha256:ea58409911e141ec6b19d9178efa2d9185a13295005b1cbf5521b3157eed1d95" +url = "https://unofficial-builds.nodejs.org/download/release/v24.18.0/node-v24.18.0-linux-x64-musl.tar.gz" + +[tools.node."platforms.macos-arm64"] +checksum = "sha256:e1a97e14c99c803e96c7339403282ea05a499c32f8d83defe9ef5ec66f979ed1" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-darwin-arm64.tar.gz" + +[tools.node."platforms.macos-x64"] +checksum = "sha256:dfd0dbd3e721503434df7b7205e719f61b3a3a31b2bcf9729b8b91fea240f080" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-darwin-x64.tar.gz" + +[tools.node."platforms.macos-x64-baseline"] +checksum = "sha256:dfd0dbd3e721503434df7b7205e719f61b3a3a31b2bcf9729b8b91fea240f080" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-darwin-x64.tar.gz" + +[tools.node."platforms.windows-x64"] +checksum = "sha256:0ae68406b42d7725661da979b1403ec9926da205c6770827f33aac9d8f26e821" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-win-x64.zip" + +[tools.node."platforms.windows-x64-baseline"] +checksum = "sha256:0ae68406b42d7725661da979b1403ec9926da205c6770827f33aac9d8f26e821" +url = "https://nodejs.org/dist/v24.18.0/node-v24.18.0-win-x64.zip" + [[tools."npm:markdownlint-cli2"]] version = "0.22.0" backend = "npm:markdownlint-cli2" +[[tools."npm:pnpm"]] +version = "11.11.0" +backend = "npm:pnpm" + [[tools."npm:prettier"]] version = "3.6.2" backend = "npm:prettier" @@ -402,10 +638,18 @@ url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-aarch64- checksum = "sha256:2fe394f493318551f271277a2228e56b31ca69a4842483e5709af4548f342bc3" url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-unknown-linux-musl.tar.gz" +[tools.ruff."platforms.linux-x64-baseline"] +checksum = "sha256:2fe394f493318551f271277a2228e56b31ca69a4842483e5709af4548f342bc3" +url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-unknown-linux-musl.tar.gz" + [tools.ruff."platforms.linux-x64-musl"] checksum = "sha256:2fe394f493318551f271277a2228e56b31ca69a4842483e5709af4548f342bc3" url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-unknown-linux-musl.tar.gz" +[tools.ruff."platforms.linux-x64-musl-baseline"] +checksum = "sha256:2fe394f493318551f271277a2228e56b31ca69a4842483e5709af4548f342bc3" +url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-unknown-linux-musl.tar.gz" + [tools.ruff."platforms.macos-arm64"] checksum = "sha256:067d1a90da8add55614eff91990425883a092d8279d9e503258ff8be0f8e9c18" url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-aarch64-apple-darwin.tar.gz" @@ -414,10 +658,26 @@ url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-aarch64- checksum = "sha256:69e424a42ac3a7c6c7032ad96deb757c35c93c848b6ea329a3f4c605e6d89ef9" url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-apple-darwin.tar.gz" +[tools.ruff."platforms.macos-x64-baseline"] +checksum = "sha256:69e424a42ac3a7c6c7032ad96deb757c35c93c848b6ea329a3f4c605e6d89ef9" +url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-apple-darwin.tar.gz" + [tools.ruff."platforms.windows-x64"] checksum = "sha256:d2af4376053458f283d74980b49dc0d61d3ef9d9b8684c5b25bd64b73e11634a" url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-pc-windows-msvc.zip" +[tools.ruff."platforms.windows-x64-baseline"] +checksum = "sha256:d2af4376053458f283d74980b49dc0d61d3ef9d9b8684c5b25bd64b73e11634a" +url = "https://github.com/astral-sh/ruff/releases/download/0.14.13/ruff-x86_64-pc-windows-msvc.zip" + +[[tools.rust]] +version = "1.93.0" +backend = "core:rust" + +[tools.rust.options] +components = "clippy,rust-analyzer,rustfmt" +targets = "aarch64-apple-darwin,aarch64-unknown-linux-gnu,x86_64-unknown-linux-gnu" + [[tools.rust]] version = "1.93.0" backend = "core:rust" @@ -438,10 +698,18 @@ url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-aarch64-unkno checksum = "sha256:870e0f634d740df2a543dc977d68cae9bb073f1e70da93278b78622962a51189" url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-unknown-linux-musl.tar.gz" +[tools.ty."platforms.linux-x64-baseline"] +checksum = "sha256:870e0f634d740df2a543dc977d68cae9bb073f1e70da93278b78622962a51189" +url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-unknown-linux-musl.tar.gz" + [tools.ty."platforms.linux-x64-musl"] checksum = "sha256:870e0f634d740df2a543dc977d68cae9bb073f1e70da93278b78622962a51189" url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-unknown-linux-musl.tar.gz" +[tools.ty."platforms.linux-x64-musl-baseline"] +checksum = "sha256:870e0f634d740df2a543dc977d68cae9bb073f1e70da93278b78622962a51189" +url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-unknown-linux-musl.tar.gz" + [tools.ty."platforms.macos-arm64"] checksum = "sha256:5c256844da0401534908535d3c744ffb28db823e2e21ebc2aa45fa4b0599f6f5" url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-aarch64-apple-darwin.tar.gz" @@ -450,10 +718,18 @@ url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-aarch64-apple checksum = "sha256:eb839e72d317e381ad7c5804d61a6098b041b0b7bd0a207f429b8bfe9f445be0" url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-apple-darwin.tar.gz" +[tools.ty."platforms.macos-x64-baseline"] +checksum = "sha256:eb839e72d317e381ad7c5804d61a6098b041b0b7bd0a207f429b8bfe9f445be0" +url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-apple-darwin.tar.gz" + [tools.ty."platforms.windows-x64"] checksum = "sha256:d7cd33adf0113decee579fbf31f58b55e44195571043b22ed0e653bf60062d4a" url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-pc-windows-msvc.zip" +[tools.ty."platforms.windows-x64-baseline"] +checksum = "sha256:d7cd33adf0113decee579fbf31f58b55e44195571043b22ed0e653bf60062d4a" +url = "https://github.com/astral-sh/ty/releases/download/0.0.10/ty-x86_64-pc-windows-msvc.zip" + [[tools.uv]] version = "0.9.26" backend = "aqua:astral-sh/uv" @@ -473,11 +749,21 @@ checksum = "sha256:708b752876aeeb753257e1d55470569789e465684c1d3bc1760db26360b6c url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-musl.tar.gz" provenance = "github-attestations" +[tools.uv."platforms.linux-x64-baseline"] +checksum = "sha256:708b752876aeeb753257e1d55470569789e465684c1d3bc1760db26360b6c28b" +url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-musl.tar.gz" +provenance = "github-attestations" + [tools.uv."platforms.linux-x64-musl"] checksum = "sha256:708b752876aeeb753257e1d55470569789e465684c1d3bc1760db26360b6c28b" url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-musl.tar.gz" provenance = "github-attestations" +[tools.uv."platforms.linux-x64-musl-baseline"] +checksum = "sha256:708b752876aeeb753257e1d55470569789e465684c1d3bc1760db26360b6c28b" +url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-unknown-linux-musl.tar.gz" +provenance = "github-attestations" + [tools.uv."platforms.macos-arm64"] checksum = "sha256:fcf0a9ea6599c6ae28a4c854ac6da76f2c889354d7c36ce136ef071f7ab9721f" url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-aarch64-apple-darwin.tar.gz" @@ -488,11 +774,21 @@ checksum = "sha256:171eb8c518313e157c5b4cec7b4f743bc6bab1bd23e09b646679a02d096a0 url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-apple-darwin.tar.gz" provenance = "github-attestations" +[tools.uv."platforms.macos-x64-baseline"] +checksum = "sha256:171eb8c518313e157c5b4cec7b4f743bc6bab1bd23e09b646679a02d096a047f" +url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-apple-darwin.tar.gz" +provenance = "github-attestations" + [tools.uv."platforms.windows-x64"] checksum = "sha256:eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036" url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-pc-windows-msvc.zip" provenance = "github-attestations" +[tools.uv."platforms.windows-x64-baseline"] +checksum = "sha256:eb02fd95d8e0eed462b4a67ecdd320d865b38c560bffcda9a0b87ec944bdf036" +url = "https://github.com/astral-sh/uv/releases/download/0.9.26/uv-x86_64-pc-windows-msvc.zip" +provenance = "github-attestations" + [[tools.zig]] version = "0.15.2" backend = "core:zig" @@ -509,10 +805,18 @@ url = "https://ziglang.org/download/0.15.2/zig-aarch64-linux-0.15.2.tar.xz" checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" +[tools.zig."platforms.linux-x64-baseline"] +checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" + [tools.zig."platforms.linux-x64-musl"] checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" +[tools.zig."platforms.linux-x64-musl-baseline"] +checksum = "sha256:02aa270f183da276e5b5920b1dac44a63f1a49e55050ebde3aecc9eb82f93239" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-linux-0.15.2.tar.xz" + [tools.zig."platforms.macos-arm64"] checksum = "sha256:3cc2bab367e185cdfb27501c4b30b1b0653c28d9f73df8dc91488e66ece5fa6b" url = "https://ziglang.org/download/0.15.2/zig-aarch64-macos-0.15.2.tar.xz" @@ -522,6 +826,14 @@ provenance = "minisign" checksum = "sha256:375b6909fc1495d16fc2c7db9538f707456bfc3373b14ee83fdd3e22b3d43f7f" url = "https://ziglang.org/download/0.15.2/zig-x86_64-macos-0.15.2.tar.xz" +[tools.zig."platforms.macos-x64-baseline"] +checksum = "sha256:375b6909fc1495d16fc2c7db9538f707456bfc3373b14ee83fdd3e22b3d43f7f" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-macos-0.15.2.tar.xz" + [tools.zig."platforms.windows-x64"] checksum = "sha256:3a0ed1e8799a2f8ce2a6e6290a9ff22e6906f8227865911fb7ddedc3cc14cb0c" url = "https://ziglang.org/download/0.15.2/zig-x86_64-windows-0.15.2.zip" + +[tools.zig."platforms.windows-x64-baseline"] +checksum = "sha256:3a0ed1e8799a2f8ce2a6e6290a9ff22e6906f8227865911fb7ddedc3cc14cb0c" +url = "https://ziglang.org/download/0.15.2/zig-x86_64-windows-0.15.2.zip" diff --git a/mise.toml b/mise.toml index bace737d06..8a15051cf2 100644 --- a/mise.toml +++ b/mise.toml @@ -33,6 +33,8 @@ config_roots = ["."] [tools] go = "latest" +node = "24.18.0" +"npm:pnpm" = "11.11.0" uv = "0.9.26" "pipx:nox" = { version = "2025.11.12", uvx = true, uvx_args = "--python-preference=managed -p 3.13" } "rust" = { version = "1.93.0", components = "rustfmt,clippy,rust-analyzer", targets = "x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,aarch64-apple-darwin" } @@ -148,7 +150,8 @@ echo "Installed $DEST/cog -> $BINARY" [tasks."build:cog"] description = "Build cog CLI (development)" -sources = ["cmd/**/*.go", "pkg/**/*.go", "go.mod", "go.sum", "VERSION.txt"] +depends = ["build:playground"] +sources = ["cmd/**/*.go", "pkg/**/*.go", "pkg/cli/playground/**", ".goreleaser.yaml", "go.mod", "go.sum", "mise.toml", "mise.lock", "VERSION.txt"] outputs = ["dist/go/*/cog"] run = """ #!/usr/bin/env bash @@ -158,8 +161,27 @@ set -e GOFLAGS=-buildvcs=false go run github.com/goreleaser/goreleaser/v2@latest build --clean --snapshot --single-target --id cog --output cog """ +[tasks."playground:install"] +description = "Install playground dependencies" +dir = "playground" +sources = ["package.json", "pnpm-lock.yaml", "../mise.toml", "../mise.lock"] +outputs = ["node_modules/.cog-playground-installed"] +run = "pnpm install --frozen-lockfile && touch node_modules/.cog-playground-installed" + +[tasks."build:playground"] +description = "Build embedded playground assets" +depends = ["playground:install"] +dir = "playground" +sources = ["src/**/*", "scripts/**/*", "index.html", "package.json", "pnpm-lock.yaml", "tsconfig.json", "vite.config.ts", "../mise.toml", "../mise.lock"] +outputs = ["../pkg/cli/playground/index.html", "../pkg/cli/playground/THIRD_PARTY_LICENSES.md", "../pkg/cli/playground/assets/*"] +run = [ + "pnpm run build", + "test -s ../pkg/cli/playground/THIRD_PARTY_LICENSES.md", +] + [tasks."build:cog:release"] description = "Build cog CLI (release)" +depends = ["build:playground"] run = "go run github.com/goreleaser/goreleaser/v2@latest build --clean --single-target --id cog --output cog" [tasks."build:rust"] @@ -292,6 +314,7 @@ set -e mise run test:go mise run test:rust mise run test:python +mise run test:playground if [ "${INTEGRATION_TESTS:-}" = "1" ]; then mise run test:integration fi @@ -310,6 +333,27 @@ description = "Run Python SDK tests (latest supported Python)" depends = ["build:coglet:wheel"] run = "nox -s tests -p 3.13" +[tasks."test:playground"] +description = "Run playground tests with coverage" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run test:coverage" + +[tasks."test:playground:e2e"] +description = "Run playground browser tests against a real Cog server" +depends = ["build:cog", "build:sdk", "build:coglet:wheel:linux-x64", "playground:install"] +dir = "playground" +run = """ +#!/usr/bin/env bash +set -euo pipefail +ROOT="$(cd .. && pwd)" +BINARY="${COG_BINARY:-$(ls "$ROOT"/dist/go/*/cog | head -1)}" +COG_BINARY="$BINARY" \ +COG_SDK_WHEEL="${COG_SDK_WHEEL:-$ROOT/dist}" \ +COGLET_WHEEL="${COGLET_WHEEL:-$ROOT/dist}" \ +pnpm run test:e2e +""" + [tasks."test:python:all"] description = "Run Python SDK tests on all supported Python versions" depends = ["build:coglet:wheel"] @@ -388,7 +432,7 @@ fi [tasks.fmt] alias = ["format", "fmt:check", "format:check"] description = "Check formatting for all languages (non-destructive)" -run = [{ tasks = ["fmt:go", "fmt:rust", "fmt:python", "fmt:docs"] }] +run = [{ tasks = ["fmt:go", "fmt:rust", "fmt:python", "fmt:playground", "fmt:docs"] }] [tasks."fmt:fix"] alias = "format:fix" @@ -398,6 +442,7 @@ run = [ "fmt:go:fix", "fmt:rust:fix", "fmt:python:fix", + "fmt:playground:fix", "fmt:docs:fix", ] }, ] @@ -437,6 +482,19 @@ run = "ruff format --check ." description = "Fix Python formatting" run = "ruff format ." +[tasks."fmt:playground"] +alias = "fmt:playground:check" +description = "Check playground formatting" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run fmt:check" + +[tasks."fmt:playground:fix"] +description = "Fix playground formatting" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run fmt" + # Docs formatting [tasks."fmt:docs"] alias = "fmt:docs:check" @@ -454,7 +512,7 @@ run = ["bash ./tools/format-markdown.sh --write", "markdownlint-cli2 --fix"] [tasks.lint] alias = "lint:check" description = "Run linters for all languages (non-destructive)" -run = [{ tasks = ["lint:go", "lint:rust", "lint:python", "lint:docs"] }] +run = [{ tasks = ["lint:go", "lint:rust", "lint:python", "lint:playground", "lint:docs"] }] [tasks."lint:fix"] description = "Fix lint issues for all languages" @@ -463,6 +521,7 @@ run = [ "lint:go:fix", "lint:rust:fix", "lint:python:fix", + "lint:playground:fix", "lint:docs:fix", ] }, ] @@ -520,13 +579,26 @@ mise run typecheck:python description = "Fix Python lint issues" run = "ruff check --fix ." +[tasks."lint:playground"] +alias = "lint:playground:check" +description = "Lint playground code" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run lint" + +[tasks."lint:playground:fix"] +description = "Fix playground lint issues" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run lint:fix" + # ============================================================================= # Typecheck tasks # ============================================================================= [tasks.typecheck] description = "Run type checking for all languages" -run = [{ tasks = ["typecheck:rust", "typecheck:python"] }] +run = [{ tasks = ["typecheck:rust", "typecheck:python", "typecheck:playground"] }] [tasks."typecheck:rust"] description = "Type check Rust code (cargo check)" @@ -536,6 +608,12 @@ run = "cargo check --manifest-path crates/Cargo.toml --workspace" description = "Type check Python code" run = "nox -s typecheck" +[tasks."typecheck:playground"] +description = "Type check playground code" +depends = ["playground:install"] +dir = "playground" +run = "pnpm run typecheck" + # ============================================================================= # Generate tasks # ============================================================================= diff --git a/pkg/cli/playground.go b/pkg/cli/playground.go new file mode 100644 index 0000000000..e2f6aa834a --- /dev/null +++ b/pkg/cli/playground.go @@ -0,0 +1,529 @@ +package cli + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "net" + "net/http" + "net/http/httputil" + "net/url" + "os" + "os/exec" + "os/signal" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/spf13/cobra" + + "github.com/replicate/cog/pkg/global" + "github.com/replicate/cog/pkg/util/console" +) + +const ( + // maxWebhookBody caps a single webhook payload relayed to the browser. + maxWebhookBody = 10 * 1024 * 1024 + // maxPlaygroundHeaderMetadata keeps inspector metadata below browser header limits. + maxPlaygroundHeaderMetadata = 32 * 1024 + // playgroundUpstreamHeaders identifies model response headers for the request inspector. + playgroundUpstreamHeaders = "X-Cog-Upstream-Headers" +) + +var ( + playgroundPort = 0 + playgroundTarget = "http://localhost:8393" + playgroundHost = "127.0.0.1" + playgroundWebhookHost = "host.docker.internal" + playgroundNoOpen = false +) + +func newPlaygroundCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "playground", + Short: "Open a browser playground for talking to a running model", + Long: `Open a browser playground for talking to a running model. + +Starts a local web server that serves a schema-driven UI (a Postman-like tool +for Cog models). Point it at any running Cog HTTP API -- for example one started +with 'cog serve' -- and the playground reflects that model's inputs and outputs +from its OpenAPI schema in real time. + +Requests are reverse-proxied through this server, so the target API does not +need to set CORS headers. The server also hosts a webhook sink so async +predictions can be observed in the browser. + +Async/webhook testing against a containerized model requires the webhook URL to +be reachable from inside the container. On Docker Desktop the default +'host.docker.internal' works once the server listens on a reachable interface +(e.g. --host 0.0.0.0).`, + Example: ` # Start a model API in one terminal + cog serve -p 8393 + + # Open the playground pointing at it + cog playground --target http://localhost:8393`, + RunE: cmdPlayground, + Args: cobra.MaximumNArgs(0), + SuggestFor: []string{"ui", "gui"}, + } + + cmd.Flags().IntVarP(&playgroundPort, "port", "p", playgroundPort, "Port to listen on (0 picks a free port)") + cmd.Flags().StringVar(&playgroundTarget, "target", playgroundTarget, "Default target model API URL") + cmd.Flags().StringVar(&playgroundHost, "host", playgroundHost, "Address to bind (use 0.0.0.0 to receive webhooks from containers)") + cmd.Flags().StringVar(&playgroundWebhookHost, "webhook-host", playgroundWebhookHost, "Hostname the model uses to reach this server for webhooks") + cmd.Flags().BoolVar(&playgroundNoOpen, "no-open", playgroundNoOpen, "Do not open the browser automatically") + + return cmd +} + +// playgroundServer holds the runtime state for a playground instance. +type playgroundServer struct { + hub *eventHub + webhookBase string + defaultTarget string +} + +// newPlaygroundServer builds a playground server with an initialized event hub. +func newPlaygroundServer(webhookBase, defaultTarget string) *playgroundServer { + return &playgroundServer{ + hub: newEventHub(), + webhookBase: webhookBase, + defaultTarget: defaultTarget, + } +} + +func cmdPlayground(cmd *cobra.Command, _ []string) error { + if !playgroundAssetsBuilt { + return errors.New("playground assets are not included; build Cog with 'mise run build:cog'") + } + + ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM) + defer stop() + + uiFS, err := fs.Sub(playgroundUI, "playground") + if err != nil { + return fmt.Errorf("loading playground assets: %w", err) + } + + ln, err := net.Listen("tcp", playgroundAddress(playgroundHost, playgroundPort)) + if err != nil { + return fmt.Errorf("starting playground server: %w", err) + } + port := ln.Addr().(*net.TCPAddr).Port + + srvState := newPlaygroundServer( + "http://"+playgroundAddress(playgroundWebhookHost, port), + playgroundTarget, + ) + + mux := srvState.routes(uiFS) + + browserHost := playgroundBrowserHost(playgroundHost) + uiURL := "http://" + playgroundAddress(browserHost, port) + "/" + console.Infof("Cog playground running at %s", uiURL) + console.Info("Press Ctrl+C to stop.") + if !playgroundNoOpen { + maybeOpenBrowser(uiURL) + } + + srv := &http.Server{ + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 5 * time.Minute, + IdleTimeout: 60 * time.Second, + MaxHeaderBytes: 64 * 1024, + BaseContext: func(net.Listener) context.Context { return ctx }, + } + + return servePlayground(ctx, srv, ln, 5*time.Second) +} + +func servePlayground(ctx context.Context, srv *http.Server, ln net.Listener, timeout time.Duration) error { + serveErr := make(chan error, 1) + go func() { serveErr <- srv.Serve(ln) }() + + select { + case err := <-serveErr: + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil + case <-ctx.Done(): + shutdownCtx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + if err := srv.Shutdown(shutdownCtx); err != nil { + _ = srv.Close() + <-serveErr + return fmt.Errorf("shutting down playground server: %w", err) + } + if err := <-serveErr; err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + return nil + } +} + +func playgroundAddress(host string, port int) string { + return net.JoinHostPort(normalizePlaygroundHost(host), strconv.Itoa(port)) +} + +func playgroundBrowserHost(host string) string { + host = normalizePlaygroundHost(host) + if host == "" { + return "127.0.0.1" + } + ip := net.ParseIP(host) + if ip == nil || !ip.IsUnspecified() { + return host + } + if ip.To4() == nil { + return "::1" + } + return "127.0.0.1" +} + +func normalizePlaygroundHost(host string) string { + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + return host[1 : len(host)-1] + } + return host +} + +// routes builds the HTTP handler: static UI, the reverse proxy, and the webhook +// sink + event relay. +func (s *playgroundServer) routes(uiFS fs.FS) http.Handler { + mux := http.NewServeMux() + mux.Handle("/", http.FileServerFS(uiFS)) + mux.HandleFunc("/proxy/", handlePlaygroundProxy) + mux.HandleFunc("/webhook/", s.handleWebhook) + mux.HandleFunc("/events", s.handleEvents) + mux.HandleFunc("/config", s.handleConfig) + return protectPlayground(mux) +} + +// protectPlayground keeps the UI and user-directed proxy private even when the +// server listens on 0.0.0.0 for container webhook callbacks. Remote webhook +// deliveries are the only requests exempt from the browser-origin checks. +func protectPlayground(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + setPlaygroundSecurityHeaders(w, r.URL.Path) + if strings.HasPrefix(r.URL.Path, "/webhook/") { + next.ServeHTTP(w, r) + return + } + if !isLoopbackRemote(r.RemoteAddr) || !isLoopbackHost(r.Host) || isCrossSiteRequest(r) { + http.Error(w, "playground UI and proxy are only available from this browser", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + }) +} + +func setPlaygroundSecurityHeaders(w http.ResponseWriter, path string) { + csp := "default-src 'self'; connect-src 'self'; img-src 'self' data: http: https:; media-src 'self' data: http: https:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'" + if strings.HasPrefix(path, "/assets/validation.worker-") && strings.HasSuffix(path, ".js") { + // Ajv compiles dynamic model schemas. Confine the required code generation + // to this network-isolated worker rather than relaxing the page policy. + csp = "default-src 'none'; script-src 'self' 'unsafe-eval'; connect-src 'none'; base-uri 'none'; form-action 'none'" + } + w.Header().Set("Content-Security-Policy", csp) + w.Header().Set("Referrer-Policy", "no-referrer") + w.Header().Set("X-Content-Type-Options", "nosniff") + w.Header().Set("X-Frame-Options", "DENY") +} + +func isLoopbackHost(hostport string) bool { + host := hostport + if parsed, _, err := net.SplitHostPort(hostport); err == nil { + host = parsed + } + host = strings.Trim(host, "[]") + if strings.EqualFold(host, "localhost") { + return true + } + ip := net.ParseIP(host) + return ip != nil && ip.IsLoopback() +} + +func isCrossSiteRequest(r *http.Request) bool { + if strings.EqualFold(r.Header.Get("Sec-Fetch-Site"), "cross-site") { + return true + } + origin := r.Header.Get("Origin") + if origin == "" { + return false + } + parsed, err := url.Parse(origin) + return err != nil || !strings.EqualFold(parsed.Host, r.Host) || (parsed.Scheme != "http" && parsed.Scheme != "https") +} + +func isLoopbackRemote(remoteAddr string) bool { + host, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + return false + } + ip := net.ParseIP(host) + return ip != nil && ip.IsLoopback() +} + +// handleConfig reports runtime configuration the UI needs, notably the webhook +// base URL the model should call back on. +func (s *playgroundServer) handleConfig(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]string{ + "target": s.defaultTarget, + "webhookBase": s.webhookBase, + "cogVersion": global.Version, + }) +} + +// handleWebhook receives a webhook delivery from a model and relays its body to +// any browser subscribed to the matching token's event stream. +func (s *playgroundServer) handleWebhook(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.Header().Set("Allow", http.MethodPost) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + token := strings.TrimPrefix(r.URL.Path, "/webhook/") + if token == "" || strings.Contains(token, "/") || len(token) > 128 { + http.Error(w, "missing token", http.StatusNotFound) + return + } + if !s.hub.hasSubscribers(token) { + http.Error(w, "no browser is ready to receive this webhook", http.StatusServiceUnavailable) + return + } + body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, maxWebhookBody)) + if err != nil { + var maxBytesError *http.MaxBytesError + if errors.As(err, &maxBytesError) { + http.Error(w, "webhook body too large", http.StatusRequestEntityTooLarge) + return + } + http.Error(w, "cannot read webhook body", http.StatusBadRequest) + return + } + if !s.hub.publish(token, body) { + http.Error(w, "no browser is ready to receive this webhook", http.StatusServiceUnavailable) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprint(w, "{}") +} + +// handleEvents streams relayed webhook payloads to the browser over SSE. +func (s *playgroundServer) handleEvents(w http.ResponseWriter, r *http.Request) { + token := r.URL.Query().Get("token") + if token == "" { + http.Error(w, "missing token", http.StatusBadRequest) + return + } + flusher, ok := w.(http.Flusher) + if !ok { + http.Error(w, "streaming unsupported", http.StatusInternalServerError) + return + } + + // Subscribe before flushing headers so the caller is guaranteed to be + // receiving by the time it observes the response (no missed events). + ch := s.hub.subscribe(token) + defer s.hub.unsubscribe(token, ch) + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + flusher.Flush() + + keepAlive := time.NewTicker(15 * time.Second) + defer keepAlive.Stop() + ctx := r.Context() + for { + select { + case <-ctx.Done(): + return + case msg := <-ch: + writeSSEData(w, msg) + flusher.Flush() + case <-keepAlive.C: + _, _ = fmt.Fprint(w, ": keep-alive\n\n") + flusher.Flush() + } + } +} + +// writeSSEData emits a payload as a single SSE event, prefixing every line with +// "data: ". This preserves embedded newlines without letting them terminate the +// event early or inject additional SSE fields (e.g. a spoofed "event:" line). +func writeSSEData(w io.Writer, msg []byte) { + normalized := strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(string(msg)) + for line := range strings.SplitSeq(normalized, "\n") { + _, _ = fmt.Fprintf(w, "data: %s\n", line) + } + _, _ = fmt.Fprint(w, "\n") +} + +// handlePlaygroundProxy reverse-proxies /proxy/* to the target model API. The +// target origin is taken from the X-Cog-Target header set by the playground UI. +// Proxying keeps the browser same-origin, sidestepping CORS, and streams SSE +// responses through unbuffered. +func handlePlaygroundProxy(w http.ResponseWriter, r *http.Request) { + rawTarget := r.Header.Get("X-Cog-Target") + if rawTarget == "" { + writeProxyError(w, http.StatusBadRequest, "no target API set") + return + } + + target, err := url.Parse(strings.TrimRight(rawTarget, "/")) + if err != nil || target.Host == "" || target.User != nil || target.Fragment != "" || (target.Scheme != "http" && target.Scheme != "https") { + writeProxyError(w, http.StatusBadRequest, "invalid target API URL") + return + } + + proxy := &httputil.ReverseProxy{ + FlushInterval: -1, // flush immediately so SSE streams in real time + Rewrite: func(pr *httputil.ProxyRequest) { + // Forward the path after /proxy. + escapedPath := strings.TrimPrefix(pr.In.URL.EscapedPath(), "/proxy") + if escapedPath == "" { + escapedPath = "/" + } + path, err := url.PathUnescape(escapedPath) + if err != nil { + path = strings.TrimPrefix(pr.In.URL.Path, "/proxy") + } + pr.Out.URL.Path = path + pr.Out.URL.RawPath = escapedPath + pr.SetURL(target) + pr.Out.Host = target.Host + pr.Out.Header.Del("X-Cog-Target") + pr.Out.Header.Del("Authorization") + pr.Out.Header.Del("Cookie") + pr.Out.Header.Del("Origin") + pr.Out.Header.Del("Referer") + }, + ModifyResponse: func(resp *http.Response) error { + resp.Header.Del("Clear-Site-Data") + resp.Header.Del("Service-Worker-Allowed") + resp.Header.Del("Set-Cookie") + // Drop absolute redirects so the browser cannot be pivoted off the + // intended target (fetch uses redirect: "manual" as a second line). + if resp.StatusCode >= 300 && resp.StatusCode < 400 { + if location := resp.Header.Get("Location"); location != "" { + if parsed, err := url.Parse(location); err != nil || parsed.IsAbs() { + resp.Header.Del("Location") + } + } + } + upstreamHeaders := resp.Header.Clone() + upstreamHeaders.Del(playgroundUpstreamHeaders) + resp.Header.Del(playgroundUpstreamHeaders) + encodedHeaders, err := json.Marshal(upstreamHeaders) + if err != nil { + return fmt.Errorf("encode upstream response headers: %w", err) + } + metadata := base64.RawURLEncoding.EncodeToString(encodedHeaders) + if len(metadata) <= maxPlaygroundHeaderMetadata { + resp.Header.Set(playgroundUpstreamHeaders, metadata) + } + return nil + }, + ErrorHandler: func(w http.ResponseWriter, _ *http.Request, err error) { + writeProxyError(w, http.StatusBadGateway, "cannot reach target API: "+err.Error()) + }, + } + // The proxy target is user-specified by design (a local model API); SSRF to + // it is the intended behavior of this dev tool, not a vulnerability. + proxy.ServeHTTP(w, r) //nolint:gosec // user-directed proxy target is intentional +} + +func writeProxyError(w http.ResponseWriter, status int, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + // message is server-controlled text; encode minimally as a JSON string. + _, _ = fmt.Fprintf(w, `{"error":%q}`, message) +} + +// maybeOpenBrowser best-effort opens a URL in the default browser. +func maybeOpenBrowser(target string) { + var cmd *exec.Cmd + switch runtime.GOOS { + case "darwin": + cmd = exec.Command("open", target) + case "windows": + cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", target) + default: + cmd = exec.Command("xdg-open", target) + } + if err := cmd.Start(); err == nil { + go func() { _ = cmd.Wait() }() + } +} + +// eventHub fans out relayed webhook payloads to browser SSE subscribers keyed +// by an opaque token. +type eventHub struct { + mu sync.Mutex + subs map[string]map[chan []byte]struct{} +} + +func newEventHub() *eventHub { + return &eventHub{subs: make(map[string]map[chan []byte]struct{})} +} + +func (h *eventHub) subscribe(token string) chan []byte { + h.mu.Lock() + defer h.mu.Unlock() + ch := make(chan []byte, 4) + if h.subs[token] == nil { + h.subs[token] = make(map[chan []byte]struct{}) + } + h.subs[token][ch] = struct{}{} + return ch +} + +func (h *eventHub) hasSubscribers(token string) bool { + h.mu.Lock() + defer h.mu.Unlock() + return len(h.subs[token]) > 0 +} + +func (h *eventHub) unsubscribe(token string, ch chan []byte) { + h.mu.Lock() + defer h.mu.Unlock() + subs := h.subs[token] + if subs == nil { + return + } + delete(subs, ch) + if len(subs) == 0 { + delete(h.subs, token) + } +} + +// publish delivers msg to every subscriber of token, best-effort. It reports +// whether at least one subscriber received it. Delivery is non-blocking: a +// subscriber whose buffer is momentarily full is skipped rather than stalling +// the webhook. The depth-buffered channels absorb any realistic burst for a +// single-user tool. +func (h *eventHub) publish(token string, msg []byte) bool { + h.mu.Lock() + defer h.mu.Unlock() + + delivered := false + for ch := range h.subs[token] { + select { + case ch <- msg: + delivered = true + default: + } + } + return delivered +} diff --git a/pkg/cli/playground_assets.go b/pkg/cli/playground_assets.go new file mode 100644 index 0000000000..97dcd472e5 --- /dev/null +++ b/pkg/cli/playground_assets.go @@ -0,0 +1,10 @@ +//go:build playground_assets + +package cli + +import "embed" + +//go:embed playground +var playgroundUI embed.FS + +const playgroundAssetsBuilt = true diff --git a/pkg/cli/playground_assets_stub.go b/pkg/cli/playground_assets_stub.go new file mode 100644 index 0000000000..ef129f9a4c --- /dev/null +++ b/pkg/cli/playground_assets_stub.go @@ -0,0 +1,11 @@ +//go:build !playground_assets + +package cli + +import "testing/fstest" + +var playgroundUI = fstest.MapFS{ + "playground/index.html": {Data: []byte("Cog Playground")}, +} + +const playgroundAssetsBuilt = false diff --git a/pkg/cli/playground_test.go b/pkg/cli/playground_test.go new file mode 100644 index 0000000000..85ec5325da --- /dev/null +++ b/pkg/cli/playground_test.go @@ -0,0 +1,633 @@ +package cli + +import ( + "bufio" + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "io/fs" + "net" + "net/http" + "net/http/httptest" + "net/url" + "regexp" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/replicate/cog/pkg/global" +) + +func newTestPlayground(t *testing.T) *httptest.Server { + t.Helper() + uiFS, err := fs.Sub(playgroundUI, "playground") + require.NoError(t, err) + s := newPlaygroundServer("http://wh.example/cb", "http://localhost:8393") + ts := httptest.NewServer(s.routes(uiFS)) + t.Cleanup(ts.Close) + return ts +} + +// echoServer reports the received path and (forwarded) query as JSON. +func echoServer(t *testing.T) *httptest.Server { + t.Helper() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"path":%q,"escaped_path":%q,"query":%q}`, r.URL.Path, r.URL.EscapedPath(), r.URL.RawQuery) + })) + t.Cleanup(ts.Close) + return ts +} + +func TestPlaygroundServesUI(t *testing.T) { + ts := newTestPlayground(t) + + resp, err := http.Get(ts.URL + "/") + require.NoError(t, err) + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Contains(t, string(body), "Cog Playground") + + assets := regexp.MustCompile(`(?:src|href)="(/?assets/[^"]+)"`).FindAllStringSubmatch(string(body), -1) + if !playgroundAssetsBuilt { + assert.Empty(t, assets, "stub index should not reference generated assets") + return + } + require.NotEmpty(t, assets, "generated index should reference bundled assets") + for _, match := range assets { + path := "/" + strings.TrimPrefix(match[1], "/") + assetResp, err := http.Get(ts.URL + path) + require.NoError(t, err, "requesting %s", path) + assetResp.Body.Close() + assert.Equal(t, http.StatusOK, assetResp.StatusCode, "%s should be served", path) + if strings.HasSuffix(path, ".css") { + assert.Contains(t, assetResp.Header.Get("Content-Type"), "text/css") + } else if strings.HasSuffix(path, ".js") { + assert.Contains(t, assetResp.Header.Get("Content-Type"), "javascript") + } + } +} + +func TestServePlaygroundWaitsForHandlers(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + started := make(chan struct{}) + canceled := make(chan struct{}) + release := make(chan struct{}) + srv := &http.Server{ + Handler: http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + close(started) + <-r.Context().Done() + close(canceled) + <-release + }), + BaseContext: func(net.Listener) context.Context { return ctx }, + } + serveDone := make(chan error, 1) + go func() { serveDone <- servePlayground(ctx, srv, ln, time.Second) }() + requestDone := make(chan struct{}) + go func() { + defer close(requestDone) + resp, requestErr := http.Get("http://" + ln.Addr().String()) + if requestErr == nil { + resp.Body.Close() + } + }() + + <-started + cancel() + <-canceled + select { + case err := <-serveDone: + require.FailNow(t, "server returned before the active handler finished", "%v", err) + default: + } + close(release) + require.NoError(t, <-serveDone) + <-requestDone +} + +func TestPlaygroundConfig(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Get(ts.URL + "/config") + require.NoError(t, err) + defer resp.Body.Close() + + var config map[string]string + require.NoError(t, json.NewDecoder(resp.Body).Decode(&config)) + assert.Equal(t, "http://wh.example/cb", config["webhookBase"]) + assert.Equal(t, "http://localhost:8393", config["target"]) + assert.Equal(t, global.Version, config["cogVersion"]) +} + +func TestPlaygroundRejectsRemoteUIAndProxyRequests(t *testing.T) { + uiFS, err := fs.Sub(playgroundUI, "playground") + require.NoError(t, err) + s := newPlaygroundServer("http://wh.example/cb", "") + + for _, path := range []string{"/", "/config", "/proxy/health-check"} { + req := httptest.NewRequest(http.MethodGet, "http://playground.example"+path, nil) + req.RemoteAddr = "192.0.2.10:1234" + resp := httptest.NewRecorder() + s.routes(uiFS).ServeHTTP(resp, req) + assert.Equal(t, http.StatusForbidden, resp.Code, path) + } +} + +func TestPlaygroundAllowsRemoteWebhookRequests(t *testing.T) { + uiFS, err := fs.Sub(playgroundUI, "playground") + require.NoError(t, err) + s := newPlaygroundServer("http://wh.example/cb", "") + ch := s.hub.subscribe("token") + defer s.hub.unsubscribe("token", ch) + + req := httptest.NewRequest(http.MethodPost, "http://playground.example/webhook/token", strings.NewReader(`{"status":"succeeded"}`)) + req.RemoteAddr = "192.0.2.10:1234" + resp := httptest.NewRecorder() + s.routes(uiFS).ServeHTTP(resp, req) + + assert.Equal(t, http.StatusOK, resp.Code) +} + +func TestPlaygroundProxyHeaderTarget(t *testing.T) { + ts := newTestPlayground(t) + stub := echoServer(t) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/openapi.json?foo=bar", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", stub.URL) + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var got map[string]string + require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) + assert.Equal(t, "/openapi.json", got["path"], "/proxy prefix should be stripped") + assert.Equal(t, "foo=bar", got["query"]) +} + +func TestPlaygroundProxyRejectsQueryTarget(t *testing.T) { + ts := newTestPlayground(t) + stub := echoServer(t) + + u := ts.URL + "/proxy/health-check?x=1&cog_target=" + url.QueryEscape(stub.URL) + resp, err := http.Get(u) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) +} + +func TestPlaygroundProxyPreservesEscapedPathSegments(t *testing.T) { + ts := newTestPlayground(t) + stub := echoServer(t) + + req, err := http.NewRequest(http.MethodPost, ts.URL+"/proxy/predictions/custom%2Fid/cancel", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", stub.URL+"/api") + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + var got map[string]string + require.NoError(t, json.NewDecoder(resp.Body).Decode(&got)) + assert.Equal(t, "/api/predictions/custom/id/cancel", got["path"]) + assert.Equal(t, "/api/predictions/custom%2Fid/cancel", got["escaped_path"]) +} + +func TestPlaygroundProxyForwardsRequestAndResponse(t *testing.T) { + ts := newTestPlayground(t) + stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + assert.Equal(t, http.MethodPut, r.Method) + assert.Equal(t, "/api/predictions/p1", r.URL.Path) + assert.Equal(t, `{"input":{"prompt":"hello"}}`, string(body)) + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Empty(t, r.Header.Get("X-Cog-Target")) + assert.Empty(t, r.Header.Get("Authorization")) + assert.Empty(t, r.Header.Get("Cookie")) + w.Header().Set("X-Upstream", "preserved") + w.Header().Set("X-Frame-Options", "SAMEORIGIN") + w.Header().Set("Set-Cookie", "session=upstream") + w.WriteHeader(http.StatusUnprocessableEntity) + _, _ = w.Write([]byte(`{"detail":"invalid input"}`)) + })) + t.Cleanup(stub.Close) + + req, err := http.NewRequest(http.MethodPut, ts.URL+"/proxy/predictions/p1", bytes.NewBufferString(`{"input":{"prompt":"hello"}}`)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer local-secret") + req.Header.Set("Cookie", "session=local-secret") + req.Header.Set("X-Cog-Target", stub.URL+"/api") + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode) + assert.Equal(t, "preserved", resp.Header.Get("X-Upstream")) + assert.Empty(t, resp.Header.Get("Set-Cookie")) + encodedHeaders, err := base64.RawURLEncoding.DecodeString(resp.Header.Get(playgroundUpstreamHeaders)) + require.NoError(t, err) + var upstreamHeaders http.Header + require.NoError(t, json.Unmarshal(encodedHeaders, &upstreamHeaders)) + assert.Equal(t, "preserved", upstreamHeaders.Get("X-Upstream")) + assert.Equal(t, "SAMEORIGIN", upstreamHeaders.Get("X-Frame-Options")) + assert.Empty(t, upstreamHeaders.Get("Set-Cookie")) + assert.JSONEq(t, `{"detail":"invalid input"}`, string(body)) +} + +func TestPlaygroundProxyRoutesConcurrentWorkspacesIndependently(t *testing.T) { + ts := newTestPlayground(t) + models := map[string]*httptest.Server{} + for _, name := range []string{"first", "second"} { + models[name] = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = fmt.Fprint(w, name) + })) + t.Cleanup(models[name].Close) + } + + errs := make(chan error, 40) + var requests sync.WaitGroup + for name, model := range models { + for range 20 { + requests.Go(func() { + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/identity", nil) + if err != nil { + errs <- err + return + } + req.Header.Set("X-Cog-Target", model.URL) + resp, err := http.DefaultClient.Do(req) + if err != nil { + errs <- err + return + } + body, readErr := io.ReadAll(resp.Body) + resp.Body.Close() + if readErr != nil { + errs <- readErr + return + } + if string(body) != name { + errs <- fmt.Errorf("request for %s reached %q", name, body) + } + }) + } + } + requests.Wait() + close(errs) + for err := range errs { + assert.NoError(t, err) + } +} + +func TestPlaygroundProxyOmitsOversizedHeaderMetadata(t *testing.T) { + ts := newTestPlayground(t) + stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("X-Large", strings.Repeat("x", maxPlaygroundHeaderMetadata)) + w.Header().Set(playgroundUpstreamHeaders, "spoofed") + w.WriteHeader(http.StatusNoContent) + })) + t.Cleanup(stub.Close) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/health-check", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", stub.URL) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, strings.Repeat("x", maxPlaygroundHeaderMetadata), resp.Header.Get("X-Large")) + assert.Empty(t, resp.Header.Get(playgroundUpstreamHeaders)) +} + +func TestPlaygroundProxyMissingTarget(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Get(ts.URL + "/proxy/health-check") + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) +} + +func TestPlaygroundProxyInvalidTarget(t *testing.T) { + ts := newTestPlayground(t) + for _, target := range []string{"ftp://example.com", "garbage", "", "http://user@example.com", "http://example.com/#fragment"} { + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/health-check", nil) + require.NoError(t, err) + if target != "" { + req.Header.Set("X-Cog-Target", target) + } + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "target %q should be rejected", target) + } +} + +func TestPlaygroundWebhookRejectsNonPost(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Get(ts.URL + "/webhook/token") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusMethodNotAllowed, resp.StatusCode) + assert.Equal(t, http.MethodPost, resp.Header.Get("Allow")) +} + +func TestPlaygroundRecognizesIPv6Loopback(t *testing.T) { + assert.True(t, isLoopbackRemote("[::1]:8080")) + assert.False(t, isLoopbackRemote("not-an-address")) + assert.True(t, isLoopbackHost("[::1]:8080")) + assert.True(t, isLoopbackHost("localhost:8080")) + assert.False(t, isLoopbackHost("playground.example:8080")) +} + +func TestPlaygroundFormatsIPv6Address(t *testing.T) { + assert.Equal(t, "[::1]:8080", playgroundAddress("::1", 8080)) + assert.Equal(t, "[::1]:8080", playgroundAddress("[::1]", 8080)) +} + +func TestPlaygroundUsesLoopbackBrowserHostForWildcard(t *testing.T) { + assert.Equal(t, "127.0.0.1", playgroundBrowserHost("0.0.0.0")) + assert.Equal(t, "::1", playgroundBrowserHost("::")) + assert.Equal(t, "::1", playgroundBrowserHost("[::]")) +} + +func TestPlaygroundRejectsCrossSiteBrowserRequests(t *testing.T) { + ts := newTestPlayground(t) + + for name, mutate := range map[string]func(*http.Request){ + "host": func(req *http.Request) { req.Host = "attacker.example" }, + "origin": func(req *http.Request) { + req.Header.Set("Origin", "https://attacker.example") + }, + "fetch metadata": func(req *http.Request) { + req.Header.Set("Sec-Fetch-Site", "cross-site") + }, + } { + t.Run(name, func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, ts.URL+"/config", nil) + require.NoError(t, err) + mutate(req) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + }) + } +} + +func TestPlaygroundSetsBrowserSecurityHeaders(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Get(ts.URL + "/") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Contains(t, resp.Header.Get("Content-Security-Policy"), "frame-ancestors 'none'") + assert.NotContains(t, resp.Header.Get("Content-Security-Policy"), "'unsafe-eval'") + assert.Equal(t, "no-referrer", resp.Header.Get("Referrer-Policy")) + assert.Equal(t, "nosniff", resp.Header.Get("X-Content-Type-Options")) + + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/assets/validation.worker-test.js", nil) + request.RemoteAddr = "127.0.0.1:1234" + request.Host = "127.0.0.1" + protectPlayground(http.NotFoundHandler()).ServeHTTP(recorder, request) + workerCSP := recorder.Header().Get("Content-Security-Policy") + assert.Contains(t, workerCSP, "script-src 'self' 'unsafe-eval'") + assert.Contains(t, workerCSP, "connect-src 'none'") +} + +func TestPlaygroundProxyUnreachableTarget(t *testing.T) { + ts := newTestPlayground(t) + dead := httptest.NewServer(http.NotFoundHandler()) + deadURL := dead.URL + dead.Close() // now refuses connections + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/health-check", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", deadURL) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadGateway, resp.StatusCode) +} + +func TestPlaygroundProxyStreamsSSE(t *testing.T) { + ts := newTestPlayground(t) + sse := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "text/event-stream") + w.WriteHeader(http.StatusOK) + w.(http.Flusher).Flush() + fmt.Fprint(w, "event: start\ndata: {\"a\":1}\n\n") + w.(http.Flusher).Flush() + })) + t.Cleanup(sse.Close) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/predictions", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", sse.URL) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, "text/event-stream", resp.Header.Get("Content-Type")) + body, _ := io.ReadAll(resp.Body) + assert.Contains(t, string(body), "event: start") + assert.Contains(t, string(body), `data: {"a":1}`) +} + +func TestPlaygroundWebhookRelay(t *testing.T) { + ts := newTestPlayground(t) + const token = "tok123" + + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, ts.URL+"/events?token="+token, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + require.Equal(t, "text/event-stream", resp.Header.Get("Content-Type")) + + // The subscription is registered before headers are flushed, so by now the + // hub has our channel; deliver a webhook and expect it relayed. + got := make(chan string, 1) + go func() { + scanner := bufio.NewScanner(resp.Body) + for scanner.Scan() { + if line := scanner.Text(); strings.HasPrefix(line, "data: ") { + got <- strings.TrimPrefix(line, "data: ") + return + } + } + }() + + whResp, err := http.Post(ts.URL+"/webhook/"+token, "application/json", + strings.NewReader(`{"status":"succeeded","id":"p1"}`)) + require.NoError(t, err) + whResp.Body.Close() + assert.Equal(t, http.StatusOK, whResp.StatusCode) + + select { + case data := <-got: + assert.JSONEq(t, `{"status":"succeeded","id":"p1"}`, data) + case <-time.After(2 * time.Second): + require.FailNow(t, "timed out waiting for relayed webhook event") + } +} + +// A payload containing newlines must be framed as one SSE event with a +// "data: " prefix per line, not terminate the event early or inject fields. +func TestPlaygroundWebhookRelayPreservesNewlines(t *testing.T) { + ts := newTestPlayground(t) + const token = "tok-nl" + + req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, ts.URL+"/events?token="+token, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + got := make(chan []string, 1) + go func() { + scanner := bufio.NewScanner(resp.Body) + var data []string + for scanner.Scan() { + line := scanner.Text() + if after, ok := strings.CutPrefix(line, "data: "); ok { + data = append(data, after) + } else if line == "" && len(data) > 0 { + got <- data + return + } + } + }() + + whResp, err := http.Post(ts.URL+"/webhook/"+token, "application/json", + strings.NewReader("{\"a\":1}\n{\"b\":2}")) + require.NoError(t, err) + whResp.Body.Close() + + select { + case data := <-got: + assert.Equal(t, []string{`{"a":1}`, `{"b":2}`}, data) + case <-time.After(2 * time.Second): + require.FailNow(t, "timed out waiting for relayed webhook event") + } +} + +func TestWriteSSEDataNormalizesLineEndings(t *testing.T) { + for name, input := range map[string]string{ + "LF": "first\nsecond", + "CRLF": "first\r\nsecond", + "CR": "first\rsecond", + } { + t.Run(name, func(t *testing.T) { + var output strings.Builder + writeSSEData(&output, []byte(input)) + assert.Equal(t, "data: first\ndata: second\n\n", output.String()) + }) + } +} + +func TestPlaygroundEventsMissingToken(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Get(ts.URL + "/events") + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) +} + +func TestPlaygroundWebhookMissingToken(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Post(ts.URL+"/webhook/", "application/json", strings.NewReader("{}")) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusNotFound, resp.StatusCode) +} + +func TestPlaygroundWebhookRejectsOversizedBody(t *testing.T) { + uiFS, err := fs.Sub(playgroundUI, "playground") + require.NoError(t, err) + // Keep a subscriber active so the handler reaches its body-size check. + s := newPlaygroundServer("http://wh.example/cb", "") + ch := s.hub.subscribe("token") + defer s.hub.unsubscribe("token", ch) + ts := httptest.NewServer(s.routes(uiFS)) + t.Cleanup(ts.Close) + resp, err := http.Post(ts.URL+"/webhook/token", "application/json", + strings.NewReader(strings.Repeat("x", maxWebhookBody+1))) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusRequestEntityTooLarge, resp.StatusCode) +} + +func TestPlaygroundWebhookRejectsMissingSubscriber(t *testing.T) { + ts := newTestPlayground(t) + resp, err := http.Post(ts.URL+"/webhook/token", "application/json", strings.NewReader("{}")) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode) +} + +func TestPlaygroundProxyStripsAbsoluteRedirectLocation(t *testing.T) { + ts := newTestPlayground(t) + stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Location", "http://169.254.169.254/latest/meta-data/") + w.WriteHeader(http.StatusFound) + })) + t.Cleanup(stub.Close) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/proxy/health-check", nil) + require.NoError(t, err) + req.Header.Set("X-Cog-Target", stub.URL) + // Do not follow redirects; we want the proxy response as the browser would with redirect:manual. + client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { + return http.ErrUseLastResponse + }} + resp, err := client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusFound, resp.StatusCode) + assert.Empty(t, resp.Header.Get("Location")) +} + +// A stalled subscriber (full buffer) must not block or deny delivery to a +// healthy one: publish is best-effort and non-blocking. +func TestEventHubPublishDeliversDespiteStalledSubscriber(t *testing.T) { + hub := newEventHub() + stalled := hub.subscribe("token") + healthy := hub.subscribe("token") + // Fill the stalled subscriber's buffer so any send to it would block. + for len(stalled) < cap(stalled) { + stalled <- []byte("fill") + } + + assert.True(t, hub.publish("token", []byte("msg")), "healthy subscriber should receive the message") + + select { + case msg := <-healthy: + assert.Equal(t, []byte("msg"), msg) + case <-time.After(time.Second): + require.FailNow(t, "healthy subscriber did not receive the message") + } +} + +func TestEventHubPublishReturnsFalseWithoutSubscribers(t *testing.T) { + hub := newEventHub() + assert.False(t, hub.publish("token", []byte("msg"))) +} diff --git a/pkg/cli/root.go b/pkg/cli/root.go index 6de4283905..794f437431 100644 --- a/pkg/cli/root.go +++ b/pkg/cli/root.go @@ -60,6 +60,7 @@ https://github.com/replicate/cog`, newPushCommand(), newExecCommand(), newServeCommand(), + newPlaygroundCommand(), newTrainCommand(), newWeightsCommand(), ) diff --git a/playground/.gitignore b/playground/.gitignore new file mode 100644 index 0000000000..a55b3fb16e --- /dev/null +++ b/playground/.gitignore @@ -0,0 +1,5 @@ +**/node_modules/ +**/coverage/ +**/playwright-report/ +**/test-results/ +*.log diff --git a/playground/.oxfmtrc.json b/playground/.oxfmtrc.json new file mode 100644 index 0000000000..600e409925 --- /dev/null +++ b/playground/.oxfmtrc.json @@ -0,0 +1,5 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 100, + "ignorePatterns": ["coverage", "playwright-report", "test-results"] +} diff --git a/playground/.oxlintrc.json b/playground/.oxlintrc.json new file mode 100644 index 0000000000..ee4fa674b3 --- /dev/null +++ b/playground/.oxlintrc.json @@ -0,0 +1,11 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "import", "vitest", "jsx-a11y"], + "rules": { + "vitest/require-mock-type-parameters": "off" + }, + "env": { + "browser": true + }, + "ignorePatterns": ["coverage", "playwright-report", "test-results"] +} diff --git a/playground/e2e/fixture/cog.yaml b/playground/e2e/fixture/cog.yaml new file mode 100644 index 0000000000..e19c4aea46 --- /dev/null +++ b/playground/e2e/fixture/cog.yaml @@ -0,0 +1,3 @@ +build: + python_version: "3.12" +run: "run.py:Runner" diff --git a/playground/e2e/fixture/run.py b/playground/e2e/fixture/run.py new file mode 100644 index 0000000000..fa5761b2cd --- /dev/null +++ b/playground/e2e/fixture/run.py @@ -0,0 +1,20 @@ +import time + +from cog import BaseRunner, ConcatenateIterator, Input, streaming + + +class Runner(BaseRunner): + @streaming + def run( + self, + text: str = Input( + description="Text to prefix with 'hello '", + min_length=2, + max_length=20, + regex=r"^[a-z ]+$", + ), + ) -> ConcatenateIterator[str]: + yield "hello " + if text == "slow": + time.sleep(2) + yield text diff --git a/playground/e2e/playground.spec.ts b/playground/e2e/playground.spec.ts new file mode 100644 index 0000000000..84429a46c2 --- /dev/null +++ b/playground/e2e/playground.spec.ts @@ -0,0 +1,352 @@ +import { expect, test, type Page } from "@playwright/test"; +import { createServer } from "node:http"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); + await expect(page.getByRole("heading", { name: "Cog Playground" })).toBeVisible(); + await expect(page.getByRole("textbox", { name: "text" })).toBeVisible({ timeout: 30_000 }); + await expect(page.getByRole("status").filter({ hasText: "ready" })).toBeVisible({ + timeout: 30_000, + }); +}); + +test("keeps Form and CodeMirror JSON input synchronized", async ({ page }) => { + const run = page.getByRole("button", { name: "Run" }); + await expect(run).toBeEnabled(); + await page.getByRole("textbox", { name: "text" }).fill("from form"); + await page.getByRole("tab", { name: "JSON" }).click(); + + const editor = page.getByRole("textbox", { name: "Prediction input JSON" }); + await expect(editor).toContainText('"text": "from form"'); + await editor.click(); + await page.keyboard.press("Tab"); + await expect(page.getByRole("button", { name: "Copy" })).toBeFocused(); + await replaceEditor(page, "Prediction input JSON", '{"text":"from json"}'); + await page.getByRole("button", { name: "Format" }).click(); + await expect(editor).toContainText('"text": "from json"'); + await expect(run).toBeEnabled(); + + await replaceEditor(page, "Prediction input JSON", "{"); + await expect(run).toBeEnabled(); + await page.getByRole("tab", { name: "Form" }).click(); + await expect(page.getByRole("textbox", { name: "text" })).toHaveValue("from json"); + + await page.getByRole("tab", { name: "JSON" }).click(); + await expect(page.getByRole("textbox", { name: "Prediction input JSON" })).toContainText( + '"text": "from json"', + ); + await expect(run).toBeEnabled(); +}); + +test("folds JSON objects in CodeMirror", async ({ page }) => { + await page.getByRole("tab", { name: "JSON" }).click(); + await replaceEditor( + page, + "Prediction input JSON", + '{\n "text": "folded",\n "metadata": {\n "count": 1\n }\n}', + ); + + const fold = page.locator('.json-input .cm-foldGutter [title="Fold line"]').first(); + await expect(fold).toBeVisible(); + await fold.click(); + await expect(page.locator(".json-input .cm-foldPlaceholder")).toBeVisible(); +}); + +test("validates Form and JSON input against OpenAPI before running", async ({ page }) => { + const run = page.getByRole("button", { name: "Run" }); + const text = page.getByRole("textbox", { name: "text" }); + await text.fill("123"); + await expect(run).toBeEnabled(); + await expect(text).not.toHaveAttribute("aria-invalid", "true"); + await expect(page.getByText("Does not match the required pattern.")).toHaveCount(0); + + await run.click(); + await expect(text).toHaveAttribute("aria-invalid", "true"); + await expect(page.getByText("Does not match the required pattern.").first()).toBeVisible(); + + await text.fill("valid input"); + await expect(run).toBeEnabled(); + await expect(text).not.toHaveAttribute("aria-invalid", "true"); + + await page.getByRole("tab", { name: "JSON" }).click(); + const editor = page.getByRole("textbox", { name: "Prediction input JSON" }); + await replaceEditor(page, "Prediction input JSON", '{"text":42}'); + await expect(run).toBeEnabled(); + await expect(editor).not.toHaveAttribute("aria-invalid", "true"); + + await run.click(); + await expect(editor).toHaveAttribute("aria-invalid", "true"); + await expect(page.getByText("Input does not match the OpenAPI schema.")).toBeVisible(); + + await replaceEditor(page, "Prediction input JSON", '{"text":"valid"}'); + await expect(run).toBeEnabled(); + await expect(editor).not.toHaveAttribute("aria-invalid", "true"); +}); + +test("runs a synchronous prediction and inspects its response", async ({ page }) => { + await page.getByRole("textbox", { name: "text" }).fill("sync"); + await page.getByRole("button", { name: "Sync", exact: true }).click(); + await page.getByRole("button", { name: "Run" }).click(); + + await expectPredictionStatus(page, "succeeded"); + await expect(predictionOutput(page)).toContainText("hello sync"); + + await page.getByRole("tab", { name: "Raw", exact: true }).click(); + const raw = page.getByRole("textbox", { name: "Raw prediction response" }); + await expect(raw).toHaveAttribute("aria-readonly", "true"); + await expect(raw).toHaveAttribute("contenteditable", "false"); + await expect(raw).toContainText('"status": "succeeded"'); + await expect(page.locator(".response-editor .cm-content span").first()).toBeVisible(); + + await page.getByRole("tab", { name: "Timeline" }).click(); + await expect(page.getByText(/POST \/predictions/)).toBeVisible(); + await page.getByRole("tab", { name: "Request" }).click(); + await expect(page.getByText("Prediction time")).toBeVisible(); + await expect(page.getByText("200", { exact: true })).toBeVisible(); + await page.getByText("Response headers", { exact: true }).click(); + const responseHeaders = page.getByLabel("Response headers", { exact: true }); + await expect(responseHeaders).toContainText("content-type"); + await expect(responseHeaders).not.toContainText("content-security-policy"); + await expect(responseHeaders).not.toContainText("x-frame-options"); +}); + +test("keeps model targets isolated across browser workspaces", async ({ context, page }) => { + const firstModel = await startTestModel("first model"); + const secondModel = await startTestModel("second model"); + const secondPage = await context.newPage(); + const thirdPage = await context.newPage(); + + try { + await Promise.all([secondPage.goto("/"), thirdPage.goto("/")]); + await Promise.all([ + connectTarget(page, firstModel.url), + connectTarget(secondPage, secondModel.url), + connectTarget(thirdPage, firstModel.url), + ]); + + await page.getByRole("textbox", { name: "text" }).fill("one"); + await secondPage.getByRole("textbox", { name: "text" }).fill("two"); + await thirdPage.getByRole("textbox", { name: "text" }).fill("three"); + await Promise.all([ + page.getByRole("button", { name: "Run" }).click(), + secondPage.getByRole("button", { name: "Run" }).click(), + thirdPage.getByRole("button", { name: "Run" }).click(), + ]); + + await expect(predictionOutput(page)).toContainText("first model: one"); + await expect(predictionOutput(secondPage)).toContainText("second model: two"); + await expect(predictionOutput(thirdPage)).toContainText("first model: three"); + } finally { + await Promise.all([secondPage.close(), thirdPage.close()]); + await Promise.all([firstModel.close(), secondModel.close()]); + } +}); + +test("shows progressive streaming output before completion", async ({ page }) => { + await page.getByRole("textbox", { name: "text" }).fill("slow"); + await page.getByRole("button", { name: "Stream", exact: true }).click(); + await page.getByRole("button", { name: "Run" }).click(); + + const output = predictionOutput(page); + await expect(output).toHaveAttribute("aria-busy", "true"); + await expectPredictionStatus(page, "processing"); + await expect(output).toContainText("hello", { timeout: 30_000 }); + await expect(output).not.toContainText("slow"); + + await expectPredictionStatus(page, "succeeded"); + await expect(output).toHaveAttribute("aria-busy", "false"); + await expect(output).toContainText("hello slow"); +}); + +test("stops a running prediction", async ({ page }) => { + await page.getByRole("textbox", { name: "text" }).fill("slow"); + await page.getByRole("textbox", { name: "Prediction ID" }).fill("playground-stop-id"); + await page.getByRole("button", { name: "Stream", exact: true }).click(); + await page.getByRole("button", { name: "Run" }).click(); + await expect(page.getByRole("button", { name: "Stop" })).toBeEnabled(); + await expect(predictionOutput(page)).toContainText("hello", { timeout: 30_000 }); + + const cancelResponse = page.waitForResponse( + (response) => + response.request().method() === "POST" && + new URL(response.url()).pathname === "/proxy/predictions/playground-stop-id/cancel", + ); + await page.getByRole("button", { name: "Stop" }).click(); + expect((await cancelResponse).ok()).toBe(true); + + await expectPredictionStatus(page, "canceled"); + await expect(page.getByRole("button", { name: "Run" })).toBeEnabled(); + await expect(page.getByRole("status").filter({ hasText: "ready" })).toBeVisible({ + timeout: 30_000, + }); +}); + +test("configures and receives an asynchronous webhook prediction", async ({ page }) => { + await page.getByRole("textbox", { name: "text" }).fill("webhook"); + await page.getByRole("button", { name: "Async", exact: true }).click(); + await expect(page.getByText(/Webhook: .*\/webhook\/\.\.\./)).toBeVisible(); + await page.getByRole("checkbox", { name: "output" }).click(); + await page.getByRole("checkbox", { name: "logs" }).click(); + await expect(page.getByRole("checkbox", { name: "completed" })).toHaveAttribute( + "aria-disabled", + "true", + ); + await page.getByRole("button", { name: "Run" }).click(); + + await expectPredictionStatus(page, "succeeded"); + await expect(predictionOutput(page)).toContainText("hello webhook"); + await page.getByRole("tab", { name: "Request" }).click(); + const requestBody = page.getByLabel("Request body", { exact: true }); + await expect(requestBody).toContainText("start"); + await expect(requestBody).toContainText("completed"); + await expect(requestBody).not.toContainText('"logs"'); +}); + +test("reconnects and opens the loaded schema", async ({ page }) => { + const target = page.getByRole("textbox", { name: "Target" }); + const targetURL = await target.inputValue(); + await target.fill(" "); + await expect(page.getByRole("button", { name: "Connect" })).toBeDisabled(); + await target.fill(`${targetURL}/`); + const schemaResponse = page.waitForResponse( + (response) => new URL(response.url()).pathname === "/proxy/openapi.json", + ); + await target.press("Enter"); + expect((await schemaResponse).ok()).toBe(true); + await expect(page.getByRole("status").filter({ hasText: "ready" })).toBeVisible(); + + const popupPromise = page.waitForEvent("popup"); + await page.getByRole("button", { name: "Schema" }).click(); + const popup = await popupPromise; + await popup.waitForLoadState(); + const schema = JSON.parse((await popup.locator("body").textContent()) ?? "") as { + openapi?: string; + }; + expect(schema.openapi).toMatch(/^3\./); +}); + +test("toggles the color theme", async ({ page }) => { + const root = page.locator("html"); + const initialTheme = await root.getAttribute("data-mode"); + const nextTheme = initialTheme === "dark" ? "light" : "dark"; + const toggleLabel = initialTheme === "dark" ? "Light" : "Dark"; + await page.getByRole("button", { name: toggleLabel }).click(); + await expect(root).toHaveAttribute("data-mode", nextTheme); +}); + +test("uses a custom prediction ID and resets the playground", async ({ page }) => { + await page.getByRole("textbox", { name: "text" }).fill("identified"); + await page.getByRole("textbox", { name: "Prediction ID" }).fill("playground-e2e-id"); + await page.getByRole("button", { name: "Sync", exact: true }).click(); + const predictionRequest = page.waitForRequest( + (request) => + request.method() === "PUT" && + new URL(request.url()).pathname === "/proxy/predictions/playground-e2e-id", + ); + await page.getByRole("button", { name: "Run" }).click(); + await predictionRequest; + await expectPredictionStatus(page, "succeeded"); + await expect(predictionOutput(page)).toContainText("hello identified"); + + await page.getByRole("button", { name: "Reset" }).click(); + await expect(page.getByRole("textbox", { name: "text" })).toHaveValue(""); + await expect(page.getByRole("textbox", { name: "Prediction ID" })).toHaveValue(""); + await expect(page.getByText("Run a prediction to see its output.")).toBeVisible(); +}); + +test("fits every primary control on a narrow viewport", async ({ page }) => { + await page.setViewportSize({ width: 320, height: 800 }); + + const widths = await page.evaluate(() => ({ + document: document.documentElement.scrollWidth, + viewport: document.documentElement.clientWidth, + })); + expect(widths.document).toBeLessThanOrEqual(widths.viewport); + await expect(page.getByRole("button", { name: "Run" })).toBeVisible(); + await expect(page.getByRole("tab", { name: "Form" })).toBeVisible(); + await expect(page.getByRole("tab", { name: "Output" })).toBeVisible(); +}); + +async function replaceEditor(page: Page, label: string, value: string): Promise { + const editor = page.getByRole("textbox", { name: label }); + await editor.click(); + await page.keyboard.press("ControlOrMeta+A"); + await page.keyboard.insertText(value); +} + +async function expectPredictionStatus(page: Page, status: string): Promise { + await expect(page.locator("#output-panel .response-title").getByRole("status")).toHaveText( + status, + { timeout: 60_000 }, + ); +} + +function predictionOutput(page: Page) { + return page.getByRole("region", { name: "Prediction output" }); +} + +async function connectTarget(page: Page, target: string): Promise { + const schema = page.waitForResponse( + (response) => + new URL(response.url()).pathname === "/proxy/openapi.json" && + response.request().headers()["x-cog-target"] === target, + ); + await page.getByRole("textbox", { name: "Target" }).fill(target); + await page.getByRole("button", { name: "Connect" }).click(); + expect((await schema).ok()).toBe(true); + await expect(page.getByRole("textbox", { name: "text" })).toBeVisible(); +} + +async function startTestModel(name: string): Promise<{ url: string; close: () => Promise }> { + const server = createServer((request, response) => { + response.setHeader("Content-Type", "application/json"); + if (request.url === "/health-check") { + response.end(JSON.stringify({ status: "READY" })); + return; + } + if (request.url === "/openapi.json") { + response.end( + JSON.stringify({ + components: { + schemas: { + Input: { + type: "object", + required: ["text"], + properties: { text: { type: "string" } }, + }, + }, + }, + paths: { "/predictions": { post: {} } }, + }), + ); + return; + } + if (request.url === "/predictions" && request.method === "POST") { + let body = ""; + request.setEncoding("utf8"); + request.on("data", (chunk) => (body += chunk)); + request.on("end", () => { + const prediction = JSON.parse(body) as { input: { text: string } }; + response.end( + JSON.stringify({ status: "succeeded", output: `${name}: ${prediction.input.text}` }), + ); + }); + return; + } + response.statusCode = 404; + response.end(JSON.stringify({ error: "not found" })); + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Test model did not bind a port"); + return { + url: `http://127.0.0.1:${address.port}`, + close: () => + new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ), + }; +} diff --git a/playground/e2e/serve.ts b/playground/e2e/serve.ts new file mode 100644 index 0000000000..f5425f2d21 --- /dev/null +++ b/playground/e2e/serve.ts @@ -0,0 +1,141 @@ +import { type ChildProcess, spawn } from "node:child_process"; +import { createServer } from "node:net"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const cog = process.env.COG_BINARY; +if (!cog) throw new Error("COG_BINARY must point to the built Cog CLI"); +const cogBinary = cog; + +const fixture = fileURLToPath(new URL("./fixture/", import.meta.url)); +const modelPort = await availablePort(); +const modelURL = `http://127.0.0.1:${modelPort}`; +const playgroundPort = Number(process.env.PLAYGROUND_E2E_PORT ?? 8400); +const playgroundURL = `http://127.0.0.1:${playgroundPort}`; +const children: ChildProcess[] = []; +const childFailures: Promise[] = []; +let stopping = false; + +function availablePort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + server.close((error) => { + if (error) reject(error); + else if (typeof address === "object" && address) resolve(address.port); + else reject(new Error("Could not allocate a model port")); + }); + }); + }); +} + +function start(name: string, args: string[], cwd: string): Promise { + const child = spawn(cogBinary, args, { + cwd, + env: { ...process.env, BUILDKIT_PROGRESS: "quiet" }, + stdio: ["ignore", "pipe", "pipe"], + }); + child.stdout?.on("data", (data: Buffer) => process.stdout.write(`[${name}] ${data}`)); + child.stderr?.on("data", (data: Buffer) => process.stderr.write(`[${name}] ${data}`)); + const failure = new Promise((_, reject) => { + child.once("error", reject); + child.once("exit", (code, signal) => { + if (!stopping) reject(new Error(`${name} exited unexpectedly (${code ?? signal})`)); + }); + }); + children.push(child); + childFailures.push(failure); + return failure; +} + +async function waitFor(url: string, timeout: number): Promise { + const deadline = Date.now() + timeout; + while (Date.now() < deadline) { + try { + const response = await fetch(url, { signal: AbortSignal.timeout(2000) }); + if (response.ok) return; + } catch { + // The process is still starting. + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error(`Timed out waiting for ${url}`); +} + +async function waitForModel(timeout: number): Promise { + const deadline = Date.now() + timeout; + while (Date.now() < deadline) { + try { + const response = await fetch(`${modelURL}/health-check`, { + signal: AbortSignal.timeout(2000), + }); + if (response.ok) { + const health = (await response.json()) as { status?: string }; + if (health.status === "READY" || health.status === "BUSY") return; + if (health.status === "SETUP_FAILED" || health.status === "DEFUNCT") { + throw new Error(`Model startup failed with status ${health.status}`); + } + } + } catch (error) { + if (error instanceof Error && error.message.startsWith("Model startup failed")) throw error; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error(`Timed out waiting for ${modelURL}`); +} + +async function stop(): Promise { + if (stopping) return; + stopping = true; + try { + await fetch(`${modelURL}/shutdown`, { + method: "POST", + signal: AbortSignal.timeout(5000), + }); + } catch { + // Fall back to process signals below. + } + for (const child of children) child.kill("SIGTERM"); + await new Promise((resolve) => setTimeout(resolve, 3000)); + for (const child of children) { + if (child.exitCode === null) child.kill("SIGKILL"); + } +} + +for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.once(signal, () => { + void stop().finally(() => process.exit(0)); + }); +} + +try { + // Enable Cog's host-gateway mapping so webhooks reach the host on Linux CI. + const modelFailure = start( + "model", + ["serve", "--port", String(modelPort), "--upload-url", "http://unused/"], + fixture, + ); + await Promise.race([waitForModel(12 * 60_000), modelFailure]); + const playgroundFailure = start( + "playground", + [ + "playground", + "--host", + "0.0.0.0", + "--port", + String(playgroundPort), + "--target", + modelURL, + "--no-open", + ], + fixture, + ); + await Promise.race([waitFor(playgroundURL, 30_000), playgroundFailure]); + await Promise.race(childFailures); +} catch (error) { + console.error(error); + await stop(); + process.exit(1); +} diff --git a/playground/index.html b/playground/index.html new file mode 100644 index 0000000000..67a622b658 --- /dev/null +++ b/playground/index.html @@ -0,0 +1,21 @@ + + + + + + Cog Playground + + + +
+ + + diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 0000000000..9445043c8f --- /dev/null +++ b/playground/package.json @@ -0,0 +1,56 @@ +{ + "name": "@replicate/cog-playground", + "private": true, + "type": "module", + "scripts": { + "build": "vite build && node ./scripts/merge-licenses.ts && pnpm run check:bundle-size", + "check:bundle-size": "node ./scripts/check-bundle-size.ts", + "dev": "vite", + "fmt": "oxfmt", + "fmt:check": "oxfmt --check", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "test:e2e": "playwright test", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@cloudflare/kumo": "^2.7.0", + "@codemirror/commands": "^6.10.4", + "@codemirror/lang-json": "^6.0.2", + "@codemirror/language": "^6.12.4", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.6", + "@lezer/highlight": "^1.2.3", + "@phosphor-icons/react": "^2.1.10", + "ajv": "^8.20.0", + "ajv-draft-04": "^1.0.0", + "ajv-formats": "^3.0.1", + "react": "^19.2.4", + "react-dom": "^19.2.4" + }, + "devDependencies": { + "@playwright/test": "^1.55.0", + "@tailwindcss/vite": "^4.1.17", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/node": "^24.10.1", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.2", + "@vitejs/plugin-react": "^5.1.2", + "@vitest/coverage-v8": "^4.0.8", + "jsdom": "^27.2.0", + "oxfmt": "^0.58.0", + "oxlint": "^1.50.0", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", + "vite": "^8.1.4", + "vitest": "^4.0.8" + }, + "engines": { + "node": "24.18.0" + }, + "packageManager": "pnpm@11.11.0" +} diff --git a/playground/playwright.config.ts b/playground/playwright.config.ts new file mode 100644 index 0000000000..e8fb004c00 --- /dev/null +++ b/playground/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from "@playwright/test"; + +const playgroundPort = Number(process.env.PLAYGROUND_E2E_PORT ?? 8400); +const playgroundURL = `http://127.0.0.1:${playgroundPort}`; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: false, + retries: process.env.CI ? 1 : 0, + reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "list", + timeout: 60_000, + use: { + baseURL: playgroundURL, + screenshot: "only-on-failure", + trace: "retain-on-failure", + video: "retain-on-failure", + }, + webServer: { + command: "node ./e2e/serve.ts", + url: playgroundURL, + reuseExistingServer: false, + timeout: 15 * 60_000, + gracefulShutdown: { signal: "SIGTERM", timeout: 10_000 }, + }, + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], +}); diff --git a/playground/pnpm-lock.yaml b/playground/pnpm-lock.yaml new file mode 100644 index 0000000000..fe15af9d2e --- /dev/null +++ b/playground/pnpm-lock.yaml @@ -0,0 +1,3345 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@cloudflare/kumo': + specifier: ^2.7.0 + version: 2.7.0(@date-fns/tz@1.5.0)(@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@codemirror/commands': + specifier: ^6.10.4 + version: 6.10.4 + '@codemirror/lang-json': + specifier: ^6.0.2 + version: 6.0.2 + '@codemirror/language': + specifier: ^6.12.4 + version: 6.12.4 + '@codemirror/state': + specifier: ^6.7.1 + version: 6.7.1 + '@codemirror/view': + specifier: ^6.43.6 + version: 6.43.6 + '@lezer/highlight': + specifier: ^1.2.3 + version: 1.2.3 + '@phosphor-icons/react': + specifier: ^2.1.10 + version: 2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + ajv: + specifier: ^8.20.0 + version: 8.20.0 + ajv-draft-04: + specifier: ^1.0.0 + version: 1.0.0(ajv@8.20.0) + ajv-formats: + specifier: ^3.0.1 + version: 3.0.1(ajv@8.20.0) + react: + specifier: ^19.2.4 + version: 19.2.7 + react-dom: + specifier: ^19.2.4 + version: 19.2.7(react@19.2.7) + devDependencies: + '@playwright/test': + specifier: ^1.55.0 + version: 1.61.1 + '@tailwindcss/vite': + specifier: ^4.1.17 + version: 4.3.2(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)) + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/node': + specifier: ^24.10.1 + version: 24.13.3 + '@types/react': + specifier: ^19.2.2 + version: 19.2.17 + '@types/react-dom': + specifier: ^19.2.2 + version: 19.2.3(@types/react@19.2.17) + '@vitejs/plugin-react': + specifier: ^5.1.2 + version: 5.2.0(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)) + '@vitest/coverage-v8': + specifier: ^4.0.8 + version: 4.1.10(vitest@4.1.10) + jsdom: + specifier: ^27.2.0 + version: 27.4.0 + oxfmt: + specifier: ^0.58.0 + version: 0.58.0 + oxlint: + specifier: ^1.50.0 + version: 1.73.0 + tailwindcss: + specifier: ^4.1.17 + version: 4.3.2 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^8.1.4 + version: 8.1.4(@types/node@24.13.3)(jiti@2.7.0) + vitest: + specifier: ^4.0.8 + version: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)) + +packages: + + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@4.1.2': + resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.29.7': + resolution: {integrity: sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.29.7': + resolution: {integrity: sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@base-ui/react@1.6.0': + resolution: {integrity: sha512-/jzjTWJYXhRFO45Bev9lc3cHbmjzCMpUqbMZ2AgKy/z25mY9B6shGSNcXcjQar9n5doM0KYW1W8fcFv2jZBuMw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@date-fns/tz': ^1.2.0 + '@types/react': ^17 || ^18 || ^19 + date-fns: ^4.0.0 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@date-fns/tz': + optional: true + '@types/react': + optional: true + date-fns: + optional: true + + '@base-ui/utils@0.3.1': + resolution: {integrity: sha512-gFFiltORVmW/N6IILTGxizP3PBpVpysqML1ALY5Vk0mH+7faVkCknOU31goYHN5Aoek2dkjxva1XOD2Ce9WuIg==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + + '@cloudflare/kumo@2.7.0': + resolution: {integrity: sha512-KniieEmj/ZwqffqmLIVW76JnMuG5aeKJrs1nmSMME3U2wFo4yEzQtbLrLujNR2xmiDed5qm2PKpO03BltQa2mw==, tarball: https://registry.npmjs.org/@cloudflare/kumo/-/kumo-2.7.0.tgz} + hasBin: true + peerDependencies: + '@phosphor-icons/react': ^2.1.10 + echarts: ^6.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + zod: ^4.0.0 + peerDependenciesMeta: + echarts: + optional: true + zod: + optional: true + + '@codemirror/commands@6.10.4': + resolution: {integrity: sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} + + '@codemirror/view@6.43.6': + resolution: {integrity: sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==} + + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.9': + resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.6': + resolution: {integrity: sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@date-fns/tz@1.5.0': + resolution: {integrity: sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==} + + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + 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 + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@marijn/find-cluster-break@1.0.3': + resolution: {integrity: sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.139.0': + resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + + '@oxfmt/binding-android-arm-eabi@0.58.0': + resolution: {integrity: sha512-Uz62sHduGGPftXtILGyxdSW4PX82rUg+rfdNqhsgxe881g4rIoXlIqmZQ6HVKcF4f+F8qMhdD03Bx5u7gmeTdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxfmt/binding-android-arm64@0.58.0': + resolution: {integrity: sha512-rD0lRaJp1b+9vw6X4A2dJWKukd6X8yxiicN4JxXcXayolmUypRZxk+lKR+fVOu5q/iYc0fh5fR4bgmfOfVlbaA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxfmt/binding-darwin-arm64@0.58.0': + resolution: {integrity: sha512-uzbPPk7O6M+w2K65vcQ1woga3wgP8zghjL1KOG5b6qJ8dvYHZJ1VShaslg2KOK6yQIwCQtcMCXqLBM6sqXUNTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxfmt/binding-darwin-x64@0.58.0': + resolution: {integrity: sha512-L0nKYDxU32oxeQqJj21W9SlIMnf81VZEhyah6iDvFhf5q0oynq498Fopth7blErUJVBpVtxQ98RMCfMPqpJX6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxfmt/binding-freebsd-x64@0.58.0': + resolution: {integrity: sha512-woNwfD58dC5PGS9LSLSD5JYfo/EFK5iG9vhDWkcCg3q78ag7KC8bpDqgvPHrMoXpx83OLXxoSOhu6z8FsVTHlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxfmt/binding-linux-arm-gnueabihf@0.58.0': + resolution: {integrity: sha512-Sqs8nMLxuQpY21NKJ1u4stPDmO5hskBCNNh2E3AdCfI1QqWtf4m+Qn4mGEIUO4KGmuq3SWc/SZ80uy5IiwTCDw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm-musleabihf@0.58.0': + resolution: {integrity: sha512-Vd4exzBI5B5hB9m22JiTQzIL23WvHo/Pe+sNXPNeBLXSP9swCBPKCEBRwKpmpQzYhlgYaCgfPcGXPKAJBRIiZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxfmt/binding-linux-arm64-gnu@0.58.0': + resolution: {integrity: sha512-bUWi5mHV+4Vi56RLHE1h6q/HHfwAIT3XoB9vJAVeRzfu5NriXM8y6eeJu0vlKa0C9kq2rq1sOWRClhdLHPocrg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-arm64-musl@0.58.0': + resolution: {integrity: sha512-2ZHxemzgHcjtktAuVUwSoyXmGo/t+aF5tS1ciPpPei4rhSyrz3JOqDosXXrmhN/yLUSzJjtuW7ToTWqfQpCj2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-ppc64-gnu@0.58.0': + resolution: {integrity: sha512-AwKkVwjVmFQ3bcO7j0McGYAqCKH2a326fswfofng/E8VewCT/raeeGQr4huVhY704deK8AWASSTlxzMj0eZc6Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-gnu@0.58.0': + resolution: {integrity: sha512-xsRpTxfUnJF8D3AUKko/qyWdjw4GZVHlCVFuGlzSCTeewLmykKINW8em1+wx+axsDVtJJcMtvsiaXggXxrlHgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-riscv64-musl@0.58.0': + resolution: {integrity: sha512-Z4AYOTcy7nYEIiXwD62PlerimyYRcfJOgUbQAEBjXz098kxKuERBlRntofGy69HHhe9E0TLVNMl1yspVNu+efw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-linux-s390x-gnu@0.58.0': + resolution: {integrity: sha512-A3nhhtZPC/TKVWOPj9q/H3p2znJDCcHWYlJBhWL8hGq/bFmBaNBHC8Np6E581yVq1w9Mi3rMDNzDalWvtUfJtQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-gnu@0.58.0': + resolution: {integrity: sha512-2g+tVkgwqphw8R4hgo+kF4oz8+P5RwVOtr9+irsC7uwEp0e9j7Crw8kDGKL20uYlLPD7g02DqA61mC/UNYx98A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxfmt/binding-linux-x64-musl@0.58.0': + resolution: {integrity: sha512-rc15P6AbyyB7426aN8AakLd02Trb3a6ML/mmfAQeVHJEfVofWLcWIrBdy6zDEY+DIaL/s8E4GGPboVw+oP3+EA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxfmt/binding-openharmony-arm64@0.58.0': + resolution: {integrity: sha512-ZWoTM27/HYPOh9iq86DAbhPu9nXb8qKvvGU/h8OfliyVUFAMMNTLDkGsWDKKnDqIkqvZ9+dXlgUOsH1LYO3O7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxfmt/binding-win32-arm64-msvc@0.58.0': + resolution: {integrity: sha512-LHZnqFXe2dEfkRI4XdZS/57nEOT/I4UCRX5IyM9v4GYW9XwQCjGe1IUK59SuKw3POwvcgWQ4pme2cYXmNqTNPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxfmt/binding-win32-ia32-msvc@0.58.0': + resolution: {integrity: sha512-mZKpg20TpheCJym1rarcZCUJeW1sSruw8zAAaCYWvuVfwIUDN1CXdrPU/JgCWReXTCTrEfCB8Wyo3hh9jSZ2EA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxfmt/binding-win32-x64-msvc@0.58.0': + resolution: {integrity: sha512-N/wUU4N5PZ2orBtI+Ko7MnMfYLfE7K91UrGMY/c/pYyHR3lA9kwst1XugkZx+92YcRh/Eo+iv2eTESSWXfiZPA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint/binding-android-arm-eabi@1.73.0': + resolution: {integrity: sha512-HZQRN/UMBu+Ut+/9MiAChkbP4qZqrNOWBcNI45vOT40GVhbGR0JgHB87L48D4iAqFQIdVmeQYtV9RF89AjTKkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@oxlint/binding-android-arm64@1.73.0': + resolution: {integrity: sha512-Gp+KJRylv2aW7thRpG5p1KTxZq4ZJFbWowrKzufNq9d3ssl3r3JviYV45/+p+7CN1Nv0zDd1e8Ex0b/HUDq4TQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@oxlint/binding-darwin-arm64@1.73.0': + resolution: {integrity: sha512-3de96NdtXhxERMjIz7wsp2HYMY6pMQycGxFWac2mFecAx6VeARF/IqFb1QIaqiCRIdfzBwzTed+pCTCoiS+CYA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@oxlint/binding-darwin-x64@1.73.0': + resolution: {integrity: sha512-5zx/uPW32TiaOeVY1dQ/H5iOf0K1HOdFKOJhLqGl4o63+i1fpzoqqu/mKtd7OFgFjNCdhlyTGgjVkQTZm1ELcg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@oxlint/binding-freebsd-x64@1.73.0': + resolution: {integrity: sha512-qNe4gKHaGnLuZJ8toUg90JAa0S2vTVvDw+0bRi3q1avXZXDT4u5mMeECf3nD4HYrbdn1O7dXqWut4onY/yx/Xg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + resolution: {integrity: sha512-cCehYh5hTbfShm/fxTD6wwrGUWIpvX+N5OxmAMhFhDeTGXvw+BeNj889tpxsFQ9ZLatQ6wImuY8tsKLZ+FMz7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm-musleabihf@1.73.0': + resolution: {integrity: sha512-d5j5GDU/2dMgjVhw7TQT9ITrsIr1Y02KEXKyVGIXUkD+KiaxE9TP65FS2ZdgTBemQvoRL+gSBdbrIm3cQIeacg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@oxlint/binding-linux-arm64-gnu@1.73.0': + resolution: {integrity: sha512-Eyf1SrP3+yR1DI3OJgOY2Pvrr9dWP9TK37xPaDYycwTtlGlI45erJAVIfH5/m/xosDt6BupJYEFi47bvbTuuyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-arm64-musl@1.73.0': + resolution: {integrity: sha512-IlT/OJApEDKaMmCooHuncgJZbbCe7T5QIWmTZBEtYscWvzPQuuEinVcid6kwQRVQOUdb7PUCz4jQHnaYXdfJXw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-ppc64-gnu@1.73.0': + resolution: {integrity: sha512-L+JYcb/vdg5fmcH08V6o0YYLU28cTH1SPNulwJdvK9NK49aXSkYy6oNpKBmddArVOXYqNepriDGiZ04G54kh1Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-gnu@1.73.0': + resolution: {integrity: sha512-Qtk0g3bKV6OwWjIm7R8kQN1uOZRKQt/MODK2a8QfkwhTpXBD53ozx5XLVWLGDQAVyp2otLW4D2wB98XfAfMPGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-riscv64-musl@1.73.0': + resolution: {integrity: sha512-wX0NQKZVxltkAOVmzFcpOaMpdaUvsq1Eqpx9tkAfl71UdkTlSo1R4AdAnGccR1Fm2+TzFgZ22CyyGuZ41RDr/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@oxlint/binding-linux-s390x-gnu@1.73.0': + resolution: {integrity: sha512-vPe7UGBMWyiLTtnqS4xxgMQFSFGmtQwhwCxuiw6lXygaO6bVt0D8dFVg8Xv05eaiN3ybC0HXXHUAohFMFvqoCQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-gnu@1.73.0': + resolution: {integrity: sha512-2CwIWr9cemFC/CbRBWZvuk5mffz6ObmfFkfcC/9rTQ7f+icNhYr2kOjf9Rt8lLvugvkdGDOmkoVoFFHh6ClCTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@oxlint/binding-linux-x64-musl@1.73.0': + resolution: {integrity: sha512-nDadfJgg7NBBxG0N560wOe7LLX5QiYp6qBaI7viuk5EUORFBktU/NfV0MbTqU3gTqQDCh4VyxKdo5VADxk9w8Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@oxlint/binding-openharmony-arm64@1.73.0': + resolution: {integrity: sha512-wGjJC+NLH9xP+IKGn9RDW94ojJR/wPbg5WCnQjj/oReaOtCQthr8ws1zICe77JFmo4ouUdeTHHZL/ESGiF6Pmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@oxlint/binding-win32-arm64-msvc@1.73.0': + resolution: {integrity: sha512-I7X47GPGljw225YUQ5SbC/rb1Kkdrd0yQf0x+hYxeKS6DpfjMbo9ccQPQ6LNY6BoJQ1sHhgDUGuMn5Vg5gHT6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@oxlint/binding-win32-ia32-msvc@1.73.0': + resolution: {integrity: sha512-5lWj+3h+74Fm1jYOO9qkJA4xkAlZA099DkXppuXsk7UpnpZLttsefrZU469vChGaG6hcSqrkKXQOvMTZtbjeNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + + '@oxlint/binding-win32-x64-msvc@1.73.0': + resolution: {integrity: sha512-WaNRvh4f6zY9CvUQk2YoA1O90ieWrIklI84+HXFr9Isjz9CSESrdqo/RtIYt4Dll/cAchqGDMehfaZd0vqEFZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@phosphor-icons/react@2.1.10': + resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} + engines: {node: '>=10'} + peerDependencies: + react: '>= 16.8' + react-dom: '>= 16.8' + + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + + '@rolldown/binding-android-arm64@1.1.5': + resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.5': + resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.5': + resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.5': + resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.5': + resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.5': + resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.5': + resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.5': + resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.5': + resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.5': + resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@shikijs/core@4.3.1': + resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.3.1': + resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.3.1': + resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.3.1': + resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.3.1': + resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} + engines: {node: '>=20'} + + '@shikijs/themes@4.3.1': + resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} + engines: {node: '>=20'} + + '@shikijs/types@4.3.1': + resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tabby_ai/hijri-converter@1.0.5': + resolution: {integrity: sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==} + engines: {node: '>=16.0.0'} + + '@tailwindcss/node@4.3.2': + resolution: {integrity: sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==} + + '@tailwindcss/oxide-android-arm64@4.3.2': + resolution: {integrity: sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.2': + resolution: {integrity: sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.2': + resolution: {integrity: sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.2': + resolution: {integrity: sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + resolution: {integrity: sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + resolution: {integrity: sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + resolution: {integrity: sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + resolution: {integrity: sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.2': + resolution: {integrity: sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.2': + resolution: {integrity: sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + resolution: {integrity: sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + resolution: {integrity: sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.2': + resolution: {integrity: sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.2': + resolution: {integrity: sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + + '@vitejs/plugin-react@5.2.0': + resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + '@vitest/coverage-v8@4.1.10': + resolution: {integrity: sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==} + peerDependencies: + '@vitest/browser': 4.1.10 + vitest: 4.1.10 + peerDependenciesMeta: + '@vitest/browser': + optional: true + + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-v8-to-istanbul@1.0.4: + resolution: {integrity: sha512-0bC0/4bTSrnwdhU3IsZDwEdojvuPrSg59OYZfKsLRtJZ0u8VBx9DebfqqG8bRdCC0I7vjgxmPi41P0lpkhJHtA==} + + baseline-browser-mapping@2.10.42: + resolution: {integrity: sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + browserslist@4.28.5: + resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001803: + resolution: {integrity: sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + cnfast@0.0.8: + resolution: {integrity: sha512-EjXKMfGfdwtV4AcNSQ6AwQaVzpC1B7IxeiwA3FlhTXz+YFlMKVi4c1JX9tgD2QOlahQXjB8KUXrBaYG+3v871Q==} + hasBin: true + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + + date-fns-jalali@4.1.0-0: + resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==} + + date-fns@4.4.0: + resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + electron-to-chromium@1.5.389: + resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + + enhanced-resolve@5.21.6: + resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} + engines: {node: '>=10.13.0'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + es-module-lexer@2.3.0: + resolution: {integrity: sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.3: + resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + framer-motion@12.42.2: + resolution: {integrity: sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.5.3: + resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + motion-dom@12.42.2: + resolution: {integrity: sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==} + + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + + motion@12.42.2: + resolution: {integrity: sha512-Atvv11yUKIid41cVrRBDVX5m8tF8kNpExRSlbpt6APClhDjtwQssgFHhQzejxw7/7YYbjHSPKBVbHo05BuJT5Q==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + oxfmt@0.58.0: + resolution: {integrity: sha512-8feG/7NVEHDVwc1OUpP6Pks+TnaDFUw2jLLFIMi5bcmmwxAX2wBQvjSzj62RRTYBf2Op1Wt8xbkmagmPTR5ETg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + + oxlint@1.73.0: + resolution: {integrity: sha512-u91G9TJzU6yqKWNZUYprQB07W7YvntZXaRxQ6CkoytepYhLWUXWsr1M8zUJ34VatNPuUAr3Z8GH+O2A331CluQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.24.0' + vite-plus: '*' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + vite-plus: + optional: true + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-day-picker@9.14.0: + resolution: {integrity: sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==} + engines: {node: '>=18'} + peerDependencies: + react: '>=16.8.0' + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-refresh@0.18.0: + resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} + engines: {node: '>=0.10.0'} + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + + rolldown@1.1.5: + resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + shiki@4.3.1: + resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} + engines: {node: '>=20'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tailwindcss@4.3.2: + resolution: {integrity: sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinypool@2.1.0: + resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + engines: {node: ^20.0.0 || >=22.0.0} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.8: + resolution: {integrity: sha512-c1P7u0EhACHj7lPy4MJm8iTFEU8+nB0LCtddH0fhP7noaVoXAqafMtOOeX+ulpuPBqnrRgRhw494RICT3mbhnw==} + + tldts@7.4.8: + resolution: {integrity: sha512-htwgN/8KRB3z3vnC0BOETVh2m499g5GmyTK9Wq5JBLX3FNz6tSBveAd+fQhzy9hkjif8vy2jwDMR1sGhLtZl2A==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@8.1.4: + resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@acemir/cssom@0.9.31': {} + + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@4.1.2': + dependencies: + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.5 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@base-ui/react@1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@base-ui/utils': 0.3.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@floating-ui/utils': 0.2.11 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@date-fns/tz': 1.5.0 + '@types/react': 19.2.17 + date-fns: 4.4.0 + + '@base-ui/utils@0.3.1(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@floating-ui/utils': 0.2.11 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + reselect: 5.2.0 + use-sync-external-store: 1.6.0(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + '@bcoe/v8-coverage@1.0.2': {} + + '@cloudflare/kumo@2.7.0(@date-fns/tz@1.5.0)(@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@base-ui/react': 1.6.0(@date-fns/tz@1.5.0)(@types/react@19.2.17)(date-fns@4.4.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@phosphor-icons/react': 2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@shikijs/langs': 4.3.1 + '@shikijs/themes': 4.3.1 + cnfast: 0.0.8 + d3-geo: 3.1.1 + motion: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react: 19.2.7 + react-day-picker: 9.14.0(react@19.2.7) + react-dom: 19.2.7(react@19.2.7) + shiki: 4.3.1 + transitivePeerDependencies: + - '@date-fns/tz' + - '@emotion/is-prop-valid' + - '@types/react' + - date-fns + + '@codemirror/commands@6.10.4': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + '@lezer/common': 1.5.2 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/json': 1.0.3 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.6 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/state@6.7.1': + dependencies: + '@marijn/find-cluster-break': 1.0.3 + + '@codemirror/view@6.43.6': + dependencies: + '@codemirror/state': 6.7.1 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.6(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@date-fns/tz@1.5.0': {} + + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@exodus/bytes@1.15.1': {} + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@floating-ui/utils@0.2.11': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lezer/common@1.5.2': {} + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@marijn/find-cluster-break@1.0.3': {} + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.139.0': {} + + '@oxfmt/binding-android-arm-eabi@0.58.0': + optional: true + + '@oxfmt/binding-android-arm64@0.58.0': + optional: true + + '@oxfmt/binding-darwin-arm64@0.58.0': + optional: true + + '@oxfmt/binding-darwin-x64@0.58.0': + optional: true + + '@oxfmt/binding-freebsd-x64@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm-gnueabihf@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm-musleabihf@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-arm64-musl@0.58.0': + optional: true + + '@oxfmt/binding-linux-ppc64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-riscv64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-riscv64-musl@0.58.0': + optional: true + + '@oxfmt/binding-linux-s390x-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-x64-gnu@0.58.0': + optional: true + + '@oxfmt/binding-linux-x64-musl@0.58.0': + optional: true + + '@oxfmt/binding-openharmony-arm64@0.58.0': + optional: true + + '@oxfmt/binding-win32-arm64-msvc@0.58.0': + optional: true + + '@oxfmt/binding-win32-ia32-msvc@0.58.0': + optional: true + + '@oxfmt/binding-win32-x64-msvc@0.58.0': + optional: true + + '@oxlint/binding-android-arm-eabi@1.73.0': + optional: true + + '@oxlint/binding-android-arm64@1.73.0': + optional: true + + '@oxlint/binding-darwin-arm64@1.73.0': + optional: true + + '@oxlint/binding-darwin-x64@1.73.0': + optional: true + + '@oxlint/binding-freebsd-x64@1.73.0': + optional: true + + '@oxlint/binding-linux-arm-gnueabihf@1.73.0': + optional: true + + '@oxlint/binding-linux-arm-musleabihf@1.73.0': + optional: true + + '@oxlint/binding-linux-arm64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-arm64-musl@1.73.0': + optional: true + + '@oxlint/binding-linux-ppc64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-riscv64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-riscv64-musl@1.73.0': + optional: true + + '@oxlint/binding-linux-s390x-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-x64-gnu@1.73.0': + optional: true + + '@oxlint/binding-linux-x64-musl@1.73.0': + optional: true + + '@oxlint/binding-openharmony-arm64@1.73.0': + optional: true + + '@oxlint/binding-win32-arm64-msvc@1.73.0': + optional: true + + '@oxlint/binding-win32-ia32-msvc@1.73.0': + optional: true + + '@oxlint/binding-win32-x64-msvc@1.73.0': + optional: true + + '@phosphor-icons/react@2.1.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + + '@rolldown/binding-android-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.5': + optional: true + + '@rolldown/binding-darwin-x64@1.1.5': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.5': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.5': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.5': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.5': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.5': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.5': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.5': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.3': {} + + '@rolldown/pluginutils@1.0.1': {} + + '@shikijs/core@4.3.1': + dependencies: + '@shikijs/primitive': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + + '@shikijs/primitive@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.3.1': + dependencies: + '@shikijs/types': 4.3.1 + + '@shikijs/types@4.3.1': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@standard-schema/spec@1.1.0': {} + + '@tabby_ai/hijri-converter@1.0.5': {} + + '@tailwindcss/node@4.3.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.6 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.2 + + '@tailwindcss/oxide-android-arm64@4.3.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.2': + optional: true + + '@tailwindcss/oxide@4.3.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-arm64': 4.3.2 + '@tailwindcss/oxide-darwin-x64': 4.3.2 + '@tailwindcss/oxide-freebsd-x64': 4.3.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.2 + '@tailwindcss/oxide-linux-x64-musl': 4.3.2 + '@tailwindcss/oxide-wasm32-wasi': 4.3.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.2 + + '@tailwindcss/vite@4.3.2(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.2 + '@tailwindcss/oxide': 4.3.2 + tailwindcss: 4.3.2 + vite: 8.1.4(@types/node@24.13.3)(jiti@2.7.0) + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + '@types/react-dom': 19.2.3(@types/react@19.2.17) + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.7 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/react-dom@19.2.3(@types/react@19.2.17)': + dependencies: + '@types/react': 19.2.17 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.3.3': {} + + '@vitejs/plugin-react@5.2.0(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0))': + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) + '@rolldown/pluginutils': 1.0.0-rc.3 + '@types/babel__core': 7.20.5 + react-refresh: 0.18.0 + vite: 8.1.4(@types/node@24.13.3)(jiti@2.7.0) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@4.1.10(vitest@4.1.10)': + dependencies: + '@bcoe/v8-coverage': 1.0.2 + '@vitest/utils': 4.1.10 + ast-v8-to-istanbul: 1.0.4 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-reports: 3.2.0 + magicast: 0.5.3 + obug: 2.1.3 + std-env: 4.2.0 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)) + + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.4(@types/node@24.13.3)(jiti@2.7.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + agent-base@7.1.4: {} + + ajv-draft-04@1.0.0(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + ast-v8-to-istanbul@1.0.4: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + estree-walker: 3.0.3 + js-tokens: 10.0.0 + + baseline-browser-mapping@2.10.42: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + browserslist@4.28.5: + dependencies: + baseline-browser-mapping: 2.10.42 + caniuse-lite: 1.0.30001803 + electron-to-chromium: 1.5.389 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.5) + + caniuse-lite@1.0.30001803: {} + + ccount@2.0.1: {} + + chai@6.2.2: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + cnfast@0.0.8: {} + + comma-separated-tokens@2.0.3: {} + + convert-source-map@2.0.0: {} + + crelt@1.0.7: {} + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.6(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.5.2 + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + + date-fns-jalali@4.1.0-0: {} + + date-fns@4.4.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + electron-to-chromium@1.5.389: {} + + enhanced-resolve@5.21.6: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@8.0.0: {} + + es-module-lexer@2.3.0: {} + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.3: {} + + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + + framer-motion@12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + motion-dom: 12.42.2 + motion-utils: 12.39.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + html-escaper@2.0.2: {} + + html-void-elements@3.0.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + indent-string@4.0.0: {} + + internmap@2.0.3: {} + + is-potential-custom-element-name@1.0.1: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jiti@2.7.0: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + jsdom@27.4.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@exodus/bytes': 1.15.1 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.21.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.5.3: + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdn-data@2.27.1: {} + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + min-indent@1.0.1: {} + + motion-dom@12.42.2: + dependencies: + motion-utils: 12.39.0 + + motion-utils@12.39.0: {} + + motion@12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + framer-motion: 12.42.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + ms@2.1.3: {} + + nanoid@3.3.15: {} + + node-releases@2.0.51: {} + + obug@2.1.3: {} + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + oxfmt@0.58.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.58.0 + '@oxfmt/binding-android-arm64': 0.58.0 + '@oxfmt/binding-darwin-arm64': 0.58.0 + '@oxfmt/binding-darwin-x64': 0.58.0 + '@oxfmt/binding-freebsd-x64': 0.58.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.58.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.58.0 + '@oxfmt/binding-linux-arm64-gnu': 0.58.0 + '@oxfmt/binding-linux-arm64-musl': 0.58.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.58.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.58.0 + '@oxfmt/binding-linux-riscv64-musl': 0.58.0 + '@oxfmt/binding-linux-s390x-gnu': 0.58.0 + '@oxfmt/binding-linux-x64-gnu': 0.58.0 + '@oxfmt/binding-linux-x64-musl': 0.58.0 + '@oxfmt/binding-openharmony-arm64': 0.58.0 + '@oxfmt/binding-win32-arm64-msvc': 0.58.0 + '@oxfmt/binding-win32-ia32-msvc': 0.58.0 + '@oxfmt/binding-win32-x64-msvc': 0.58.0 + + oxlint@1.73.0: + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.73.0 + '@oxlint/binding-android-arm64': 1.73.0 + '@oxlint/binding-darwin-arm64': 1.73.0 + '@oxlint/binding-darwin-x64': 1.73.0 + '@oxlint/binding-freebsd-x64': 1.73.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.73.0 + '@oxlint/binding-linux-arm-musleabihf': 1.73.0 + '@oxlint/binding-linux-arm64-gnu': 1.73.0 + '@oxlint/binding-linux-arm64-musl': 1.73.0 + '@oxlint/binding-linux-ppc64-gnu': 1.73.0 + '@oxlint/binding-linux-riscv64-gnu': 1.73.0 + '@oxlint/binding-linux-riscv64-musl': 1.73.0 + '@oxlint/binding-linux-s390x-gnu': 1.73.0 + '@oxlint/binding-linux-x64-gnu': 1.73.0 + '@oxlint/binding-linux-x64-musl': 1.73.0 + '@oxlint/binding-openharmony-arm64': 1.73.0 + '@oxlint/binding-win32-arm64-msvc': 1.73.0 + '@oxlint/binding-win32-ia32-msvc': 1.73.0 + '@oxlint/binding-win32-x64-msvc': 1.73.0 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.5: {} + + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + property-information@7.2.0: {} + + punycode@2.3.1: {} + + react-day-picker@9.14.0(react@19.2.7): + dependencies: + '@date-fns/tz': 1.5.0 + '@tabby_ai/hijri-converter': 1.0.5 + date-fns: 4.4.0 + date-fns-jalali: 4.1.0-0 + react: 19.2.7 + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react-is@17.0.2: {} + + react-refresh@0.18.0: {} + + react@19.2.7: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + require-from-string@2.0.2: {} + + reselect@5.2.0: {} + + rolldown@1.1.5: + dependencies: + '@oxc-project/types': 0.139.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.5 + '@rolldown/binding-darwin-arm64': 1.1.5 + '@rolldown/binding-darwin-x64': 1.1.5 + '@rolldown/binding-freebsd-x64': 1.1.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 + '@rolldown/binding-linux-arm64-gnu': 1.1.5 + '@rolldown/binding-linux-arm64-musl': 1.1.5 + '@rolldown/binding-linux-ppc64-gnu': 1.1.5 + '@rolldown/binding-linux-s390x-gnu': 1.1.5 + '@rolldown/binding-linux-x64-gnu': 1.1.5 + '@rolldown/binding-linux-x64-musl': 1.1.5 + '@rolldown/binding-openharmony-arm64': 1.1.5 + '@rolldown/binding-wasm32-wasi': 1.1.5 + '@rolldown/binding-win32-arm64-msvc': 1.1.5 + '@rolldown/binding-win32-x64-msvc': 1.1.5 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.8.5: {} + + shiki@4.3.1: + dependencies: + '@shikijs/core': 4.3.1 + '@shikijs/engine-javascript': 4.3.1 + '@shikijs/engine-oniguruma': 4.3.1 + '@shikijs/langs': 4.3.1 + '@shikijs/themes': 4.3.1 + '@shikijs/types': 4.3.1 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + style-mod@4.1.3: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + symbol-tree@3.2.4: {} + + tailwindcss@4.3.2: {} + + tapable@2.3.3: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + + tinypool@2.1.0: {} + + tinyrainbow@3.1.0: {} + + tldts-core@7.4.8: {} + + tldts@7.4.8: + dependencies: + tldts-core: 7.4.8 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.8 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + trim-lines@3.0.1: {} + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@7.18.2: {} + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + update-browserslist-db@1.2.3(browserslist@4.28.5): + dependencies: + browserslist: 4.28.5 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.6.0(react@19.2.7): + dependencies: + react: 19.2.7 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.16 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.13.3 + fsevents: 2.3.3 + jiti: 2.7.0 + + vitest@4.1.10(@types/node@24.13.3)(@vitest/coverage-v8@4.1.10)(jsdom@27.4.0)(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@24.13.3)(jiti@2.7.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.4(@types/node@24.13.3)(jiti@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + '@vitest/coverage-v8': 4.1.10(vitest@4.1.10) + jsdom: 27.4.0 + transitivePeerDependencies: + - msw + + w3c-keyname@2.2.8: {} + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + ws@8.21.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + zwitch@2.0.4: {} diff --git a/playground/scripts/check-bundle-size.ts b/playground/scripts/check-bundle-size.ts new file mode 100644 index 0000000000..fe95929c4f --- /dev/null +++ b/playground/scripts/check-bundle-size.ts @@ -0,0 +1,17 @@ +import { readdir, stat } from "node:fs/promises"; + +const assets = new URL("../../pkg/cli/playground/assets/", import.meta.url); +const maxJavaScriptBytes = 1024 * 1024; +const files = (await readdir(assets)).filter((file) => file.endsWith(".js")); +if (files.length === 0) throw new Error("No built playground JavaScript assets found"); +const sizes = await Promise.all( + files.map(async (file) => (await stat(new URL(file, assets))).size), +); +const total = sizes.reduce((sum, size) => sum + size, 0); + +console.log(`Playground JavaScript: ${(total / 1024).toFixed(1)} KiB`); +if (total > maxJavaScriptBytes) { + throw new Error( + `Playground JavaScript exceeds the ${maxJavaScriptBytes / 1024 / 1024} MiB JavaScript budget`, + ); +} diff --git a/playground/scripts/merge-licenses.ts b/playground/scripts/merge-licenses.ts new file mode 100644 index 0000000000..e31073da00 --- /dev/null +++ b/playground/scripts/merge-licenses.ts @@ -0,0 +1,29 @@ +import { readFileSync, unlinkSync, writeFileSync } from "node:fs"; + +// THIRD_PARTY_LICENSES.md is not used at runtime. Playground JS (React, editor +// deps, ajv in the validation worker, etc.) is bundled into assets embedded in +// the cog binary and redistributed to users; many OSS licenses require shipping +// those notices with the software. This file is that compliance record. +const outputDirectory = new URL("../../pkg/cli/playground/", import.meta.url); +const mainPath = new URL("THIRD_PARTY_LICENSES.md", outputDirectory); +const workerPath = new URL("WORKER_LICENSES.md", outputDirectory); +const main = readFileSync(mainPath, "utf8").trimEnd(); +const worker = readFileSync(workerPath, "utf8"); +const existing = new Set([...main.matchAll(/^## (.+?) - .+$/gm)].map((match) => match[1])); +const workerSections = worker + .split(/(?=^## )/m) + .slice(1) + .filter((section) => { + const name = /^## (.+?) - /m.exec(section)?.[1]; + return name && !existing.has(name); + }); +const merged = `${main}\n\n${workerSections.join("\n").trim()}\n`; + +for (const dependency of ["ajv", "ajv-draft-04", "ajv-formats", "fast-uri"]) { + if (!merged.includes(`## ${dependency} - `)) { + throw new Error(`Worker dependency ${dependency} is missing from THIRD_PARTY_LICENSES.md`); + } +} + +writeFileSync(mainPath, merged); +unlinkSync(workerPath); diff --git a/playground/scripts/worker-license-plugin.ts b/playground/scripts/worker-license-plugin.ts new file mode 100644 index 0000000000..d0ec51811b --- /dev/null +++ b/playground/scripts/worker-license-plugin.ts @@ -0,0 +1,74 @@ +import { readFileSync, readdirSync } from "node:fs"; + +import type { Plugin } from "vite"; + +type PackageLicense = { + license: string; + name: string; + text: string; + version: string; +}; + +/** Creates a Vite plugin that emits license notices for dependencies bundled into workers. */ +export function workerLicensePlugin(): Plugin { + return { + name: "worker-license-file", + /** Scans emitted worker chunks and writes a sorted Markdown license asset. */ + generateBundle(_options, bundle) { + const packages = new Map(); + for (const output of Object.values(bundle)) { + if (output.type !== "chunk") continue; + for (const moduleId of Object.keys(output.modules)) { + const packageRoot = packageRootForModule(moduleId); + if (!packageRoot || packages.has(packageRoot)) continue; + const license = readPackageLicense(packageRoot); + if (license) packages.set(packageRoot, license); + } + } + const sections = [...packages.values()] + .sort((left, right) => left.name.localeCompare(right.name)) + .map( + ({ license, name, text, version }) => + `## ${name} - ${version} (${license})\n\n${text.trim()}\n`, + ); + this.emitFile({ + type: "asset", + fileName: "WORKER_LICENSES.md", + source: `# Worker licenses\n\n${sections.join("\n")}`, + }); + }, + }; +} + +function packageRootForModule(moduleId: string): string | undefined { + const cleanId = moduleId.split("?", 1)[0]; + const marker = "/node_modules/"; + const start = cleanId.lastIndexOf(marker); + if (start < 0) return undefined; + const packagePath = cleanId.slice(start + marker.length).split("/"); + const name = packagePath[0].startsWith("@") + ? `${packagePath[0]}/${packagePath[1]}` + : packagePath[0]; + return cleanId.slice(0, start + marker.length) + name; +} + +function readPackageLicense(root: string): PackageLicense | undefined { + try { + const metadata = JSON.parse(readFileSync(`${root}/package.json`, "utf8")) as { + license?: string; + name?: string; + version?: string; + }; + if (!metadata.name || !metadata.version) return undefined; + const licenseFile = readdirSync(root).find((name) => /^licen[cs]e(?:\..+)?$/i.test(name)); + if (!licenseFile) return undefined; + return { + license: metadata.license ?? "Unknown", + name: metadata.name, + text: readFileSync(`${root}/${licenseFile}`, "utf8"), + version: metadata.version, + }; + } catch { + return undefined; + } +} diff --git a/playground/src/app/App.test.tsx b/playground/src/app/App.test.tsx new file mode 100644 index 0000000000..7e97a6980a --- /dev/null +++ b/playground/src/app/App.test.tsx @@ -0,0 +1,303 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { useEffect } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { createInputValidator } from "@/features/inputs/validation/inputValidation"; +import type { ValidationIssue } from "@/features/inputs/validation/inputValidation"; +import { deferred } from "@/test/deferred"; + +const run = vi.fn(); +const reset = vi.fn(); +const { mockValidateInput } = vi.hoisted(() => ({ mockValidateInput: vi.fn() })); + +vi.mock("@/features/connection/hooks/useConnection", () => { + const connection = { + target: "http://localhost:8393", + targetDraft: "http://localhost:8393", + setTargetDraft: vi.fn(), + connect: vi.fn(), + health: { status: "ready", version: { python_sdk: "0.21.0" } }, + schema: { + components: { + schemas: { + Input: { + type: "object", + required: ["prompt"], + properties: { + prompt: { type: "string" }, + }, + }, + Output: { type: "string" }, + }, + }, + paths: { "/predictions": { post: {} } }, + }, + schemaError: "", + webhookBase: "", + cogVersion: "0.21.1-dev+g0db4bffa", + capabilities: { + endpoint: "/predictions", + input: { + type: "object", + required: ["prompt"], + properties: { + prompt: { type: "string" }, + }, + }, + output: { type: "string" }, + streaming: false, + async: false, + }, + }; + return { useConnection: () => connection }; +}); + +vi.mock("@/features/predictions/hooks/usePrediction", () => ({ + usePrediction: () => ({ + running: false, + envelope: undefined, + output: undefined, + rawEvents: [], + error: "", + trace: undefined, + run, + stop: vi.fn(), + reset, + }), +})); + +vi.mock("@/features/inputs/validation/validateInput", () => ({ + validateInput: mockValidateInput, + disposeValidationWorker: vi.fn(), +})); + +vi.mock("@/features/inputs/components/InputForm", () => ({ + InputForm: ({ + onChange, + onValidityChange, + value, + }: { + onChange: (value: Record) => void; + onValidityChange: (valid: boolean) => void; + value: Record; + }) => { + const prompt = String(value.prompt ?? ""); + useEffect(() => onValidityChange(true), [onValidityChange]); + return ( +