diff --git a/AGENTS.md b/AGENTS.md index be1afe9fb..6c3d36da4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,8 @@ homarr/ ├── tooling/ │ ├── typescript/ # Base tsconfig │ └── github/ # CI setup action +├── tools/ +│ └── homarr-dev/ # Go CLI for local and PR Docker images ├── development/ # Dev docker-compose (Redis, MySQL, PostgreSQL) ├── e2e/ # E2E test specs └── Dockerfile # Multi-stage production build @@ -68,6 +70,9 @@ When modifying user-facing code, you MUST also update the corresponding document ## Monorepo Commands - `pnpm dev` — Next.js app only +- `pnpm dev:cli -- dev` — run the developer CLI without installing a global binary +- `pnpm db:seed` — seed default database data explicitly +- `pnpm docker:dev:up` — start the Redis development service in the background - `pnpm dev:docs` — Docusaurus docs site only - `pnpm turbo build` — build all packages - `pnpm turbo build --filter=@homarr/docs` — build docs only @@ -81,3 +86,7 @@ When modifying user-facing code, you MUST also update the corresponding document - UI: Mantine (not Tailwind) — Tailwind is only used in docs app - Icons: @tabler/icons-react - Docs app can import from `@homarr/definitions` for shared types +- Run `pnpm dev:cli -- dev` to browse local `homarr:*` images and remote PR images. +- Run `pnpm dev:cli -- build ` from a Homarr checkout to build `homarr:` with rebuild provenance. +- Run `pnpm dev:cli -- build --pr ` to build a PR locally from a temporary checkout. +- Run Go checks from `tools/homarr-dev` with `go test ./...` and `go vet ./...`. diff --git a/apps/docs/docs/advanced/command-line/development/index.mdx b/apps/docs/docs/advanced/command-line/development/index.mdx new file mode 100644 index 000000000..cdbffc29a --- /dev/null +++ b/apps/docs/docs/advanced/command-line/development/index.mdx @@ -0,0 +1,41 @@ +# Developer Docker CLI + +The host-side developer CLI builds and runs local Homarr images and pull-request previews. Run it directly from a checkout with `pnpm dev:cli -- ...`; no shell-specific setup script is required. It is separate from the recovery CLI included in production images. + +To install an optional `homarr` binary, run `pnpm dev:cli:install` and make your Go bin directory available on `PATH`. GitHub CLI authentication is only needed for pull-request features. + +## Browse development images + +```bash +pnpm dev:cli -- +``` + +The main screen combines local `homarr:*` images with open pull requests and their GHCR preview status. Select an image to start or stop it, use `R` to build a remote pull request locally in the background or rebuild a local image, or use `p` to force a remote preview pull. Press `d` to switch to the instance dashboard and `d` again to return without losing the browser state. The explicit `dev` and `dash` subcommands start on either screen. + +A locally built PR image is folded into its remote PR row only when its recorded revision exactly matches the current GitHub head SHA. The row is marked `◆ local`, and starting it prefers that local image without a registry pull. Outdated local builds remain separate rows. Navigation and dashboard switching remain available during background builds. + +## Build images + +```bash +pnpm dev:cli -- build +pnpm dev:cli -- build --pr [name] +``` + +The first command builds the current checkout as `homarr:`. The pull-request form uses a depth-one clone in a temporary directory and defaults to `homarr:pr-` when `name` is omitted. + +## Rebuild images + +```bash +pnpm dev:cli -- rebuild +``` + +Images built by this CLI record their source checkout, revision, and optional pull-request number. Rebuild uses that provenance to select the original checkout or create a fresh temporary pull-request checkout. + +## Run images + +```bash +homarr run +homarr run --pr +``` + +Add `--detach` to run in the background, `--demo` to enable demo data, or repeat `--env KEY=VALUE` to provide additional environment variables. diff --git a/apps/docs/docs/advanced/development/getting-started.mdx b/apps/docs/docs/advanced/development/getting-started.mdx index 7c16813ba..e8f68ea6d 100644 --- a/apps/docs/docs/advanced/development/getting-started.mdx +++ b/apps/docs/docs/advanced/development/getting-started.mdx @@ -17,8 +17,9 @@ This guide will help you set up your development environment to start contributi - **Node.js (lts)**: You can download it from the [official website](https://nodejs.org/). - **pnpm (latest)**: You can install it by running `npm install -g pnpm`. - **Git**: You can download it from the [official website](https://git-scm.com/). -- **Docker or Docker Desktop**: You can download it from the [official website](https://www.docker.com/). -- **A GitHub account**: You can create one on the [official website](https://github.com/). +- **Docker or Docker Desktop**: Required for the Redis service and the developer Docker CLI. +- **Go 1.25+**: Required only for the developer Docker CLI. You can download it from the [official website](https://go.dev/). +- **GitHub CLI**: Required only for pull-request image features. Install it from the [official website](https://cli.github.com/), then authenticate with `gh auth login`. ### Setting up the project @@ -34,11 +35,11 @@ This guide will help you set up your development environment to start contributi 4. Install the dependencies with `pnpm install`. -5. Start redis by running `pnpm run docker:dev`. This will run a docker-compose file with a redis service. +5. Create `.env` from `.env.example`. In a POSIX shell, run `cp .env.example .env`; in PowerShell, run `Copy-Item .env.example .env`. Set `DB_URL` to an absolute path for your SQLite file. -6. Copy the `.env.example` file to `.env` and replace `DB_URL` with the full path you want to use for the sqlite database file. For example `DB_URL='C:\\Dev\\homarr\\packages\\db\\db.sqlite'` +6. Start Redis in the background with `pnpm docker:dev:up` if your development workflow needs it. -7. Run the migrations to create the sqlite db file with `pnpm run db:migration:sqlite:run`. +7. Create or update the database with `pnpm db:migration:sqlite:run`. This also seeds the default data. To run the seed explicitly later, use `pnpm db:seed`. 8. Start the development server with `pnpm dev` and wait for a message that says `✓ Ready in 10.7s` @@ -49,8 +50,15 @@ This guide will help you set up your development environment to start contributi #### During development - `pnpm dev`: Start the development server. +- `pnpm dev:cli -- dev`: Browse and run local images and remote pull-request images. +- `pnpm dev:cli -- build `: Build the current checkout as `homarr:`. +- `pnpm dev:cli -- build --pr `: Build a pull request locally from a temporary checkout. +- `pnpm dev:cli -- rebuild `: Rebuild a local image from its recorded checkout or pull request. +- `pnpm dev:cli:install`: Optionally install the developer CLI as a `homarr` binary. - `pnpm cli`: Run cli commands. - `pnpm db:migration:sqlite:run`: Run the migrations to create or update the sqlite db file. +- `pnpm db:seed`: Explicitly seed the default database data. +- `pnpm docker:dev:up`: Start the Redis development service in the background. - `pnpm db:migration:sqlite:generate`: Generate a new migration file for the sqlite db. Also works with `mysql`. - `pnpm db:studio`: Open a visual studio for the database. - `pnpm package:new`: Create a new package in the monorepo. @@ -63,6 +71,7 @@ This guide will help you set up your development environment to start contributi - `pnpm typecheck`: Run the typescript type checker. #### Building the project + - `pnpm build`: Build the project for production. -- `docker build -t homarr .`: Build a docker image for the project. -- `docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY='your_64_character_hex_string' homarr:latest`: Run the created docker image. \ No newline at end of file +- `pnpm dev:cli -- build `: Build a `homarr:` image and record its checkout for later rebuilds. +- `docker run -p 7575:7575 -e SECRET_ENCRYPTION_KEY='your_64_character_hex_string' homarr:`: Run the created docker image. diff --git a/package.json b/package.json index 42736863f..705cae439 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,15 @@ "db:migration:postgresql:run": "pnpm -F db migration:postgresql:run", "db:migration:sqlite:generate": "pnpm -F db migration:sqlite:generate", "db:migration:sqlite:run": "pnpm -F db migration:sqlite:run", + "db:seed": "pnpm -F db seed", "db:push": "pnpm -F db push:sqlite", "db:studio": "pnpm -F db studio", "dev": "pnpm -F nextjs dev", + "dev:cli": "go -C tools/homarr-dev run .", + "dev:cli:install": "go -C tools/homarr-dev install .", "dev:docs": "pnpm -F @homarr/docs dev", "docker:dev": "docker compose -f ./development/development.docker-compose.yml up", + "docker:dev:up": "docker compose -f ./development/development.docker-compose.yml up -d redis", "format": "turbo format --continue -- --check", "format:fix": "turbo format --continue", "lint": "turbo lint --continue", diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index 88c6a7088..e2772fb94 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -442,7 +442,7 @@ export const integrationDefs = { name: "Beszel", secretKinds: [["username", "password"]], iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/beszel.svg", - category: ["healthMonitoring"], + category: ["beszel"], documentationUrl: createDocumentationLink("/docs/integrations/beszel"), defaultPort: 8090, }, @@ -565,6 +565,7 @@ export const integrationCategories = [ "smartHomeServer", "indexerManager", "healthMonitoring", + "beszel", "search", "mediaTranscoding", "networkController", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f848b67eb..ba5c8a1d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,14 +112,14 @@ catalogs: specifier: ^0.9.54 version: 0.9.54 '@semantic-release/changelog': - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^7.0.0 + version: 7.0.0 '@semantic-release/commit-analyzer': specifier: ^13.0.1 version: 13.0.1 '@semantic-release/git': - specifier: ^10.0.1 - version: 10.0.1 + specifier: ^11.0.1 + version: 11.0.1 '@semantic-release/github': specifier: ^12.0.9 version: 12.0.9 @@ -606,13 +606,13 @@ importers: version: 1.61.1 '@semantic-release/changelog': specifier: 'catalog:' - version: 6.0.3(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2)) + version: 7.0.0(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2)) '@semantic-release/commit-analyzer': specifier: 'catalog:' version: 13.0.1(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2))(supports-color@10.2.2) '@semantic-release/git': specifier: 'catalog:' - version: 10.0.1(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2))(supports-color@10.2.2) + version: 11.0.1(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2))(supports-color@10.2.2) '@semantic-release/github': specifier: 'catalog:' version: 12.0.9(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2))(supports-color@10.2.2) @@ -681,25 +681,25 @@ importers: dependencies: '@docusaurus/core': specifier: ^3.10.1 - version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/faster': specifier: ^3.10.1 - version: 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3) + version: 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3) '@docusaurus/plugin-content-docs': specifier: ^3.10.1 - version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/plugin-sitemap': specifier: ^3.10.1 - version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + version: 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/preset-classic': specifier: ^3.10.1 - version: 3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(supports-color@10.2.2)(uglify-js@3.19.3) + version: 3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/theme-common': specifier: ^3.10.1 - version: 3.10.1(693648dca07bcb0a42a6196192ee56b1) + version: 3.10.1(411712c28a3adb38d3c382c02b26255d) '@docusaurus/theme-mermaid': specifier: ^3.10.1 - version: 3.10.1(c336c9d98c1f4baf3a75901518d97959) + version: 3.10.1(59bf1c207f3351d61f3d6e4d32f816cb) '@homarr/definitions': specifier: workspace:^ version: link:../../packages/definitions @@ -714,7 +714,7 @@ importers: version: 0.9.54(@typescript/typescript6@6.0.2)(nprogress@0.2.0)(react@19.2.7)(supports-color@10.2.2)(tailwindcss@4.3.2)(zod@4.4.3) '@signalwire/docusaurus-plugin-llms-txt': specifier: ^1.2.2 - version: 1.2.2(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(supports-color@10.2.2) + version: 1.2.2(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(supports-color@10.2.2) '@tabler/icons-react': specifier: 'catalog:' version: 3.44.0(react@19.2.7) @@ -729,7 +729,7 @@ importers: version: 1.11.21 docusaurus-plugin-image-zoom: specifier: ^3.0.1 - version: 3.0.1(@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)) + version: 3.0.1(@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)) leader-line-new: specifier: ^1.1.9 version: 1.1.9 @@ -3012,7 +3012,6 @@ packages: '@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-bugfix-firefox-class-in-computed-class-key@7.29.7': resolution: {integrity: sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==} @@ -3577,7 +3576,6 @@ packages: '@bramus/specificity@2.4.2': resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} - hasBin: true '@chevrotain/types@11.1.2': resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} @@ -4098,7 +4096,6 @@ packages: '@docusaurus/core@3.10.1': resolution: {integrity: sha512-3pf2fXXw0eVk8WnC3T4LIigRDupcpvngpKo9Vy7mYyBhuddc0klDUuZAIfzMoK6z05pdlk6EFC/vBSX43+1O5w==} engines: {node: '>=20.0'} - hasBin: true peerDependencies: '@docusaurus/faster': '*' '@mdx-js/react': ^3.0.0 @@ -5022,7 +5019,6 @@ packages: '@grpc/proto-loader@0.7.13': resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==} engines: {node: '>=6'} - hasBin: true '@hapi/bourne@3.0.0': resolution: {integrity: sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==} @@ -6287,7 +6283,6 @@ packages: '@paralleldrive/cuid2@3.3.0': resolution: {integrity: sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw==} - hasBin: true '@parcel/watcher-android-arm64@2.4.1': resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==} @@ -6423,7 +6418,6 @@ packages: '@playwright/test@1.61.1': resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} engines: {node: '>=18'} - hasBin: true '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} @@ -6839,11 +6833,11 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@semantic-release/changelog@6.0.3': - resolution: {integrity: sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag==} - engines: {node: '>=14.17'} + '@semantic-release/changelog@7.0.0': + resolution: {integrity: sha512-TNPyag5db24o7jWjre7UwKB4EcL8oJxbRhnDQ7hmZRAYqzreAc6PgdxQuU3pppp5xQinYtiumL0iG8SSKvnlzg==} + engines: {node: ^22.22.2 || >=24.15} peerDependencies: - semantic-release: '>=18.0.0' + semantic-release: '>=20.1.0' '@semantic-release/commit-analyzer@13.0.1': resolution: {integrity: sha512-wdnBPHKkr9HhNhXOhZD5a2LNl91+hs8CC2vsAVYxtZH3y0dV3wKn+uZSN61rdJQZ8EGxzWB3inWocBHV9+u/CQ==} @@ -6851,19 +6845,15 @@ packages: peerDependencies: semantic-release: '>=20.1.0' - '@semantic-release/error@3.0.0': - resolution: {integrity: sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==} - engines: {node: '>=14.17'} - '@semantic-release/error@4.0.0': resolution: {integrity: sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==} engines: {node: '>=18'} - '@semantic-release/git@10.0.1': - resolution: {integrity: sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==} - engines: {node: '>=14.17'} + '@semantic-release/git@11.0.1': + resolution: {integrity: sha512-Zr8BUYCTZMc8V6wDKN2dpR7nJgewd9I6THL3ydLTnp3OEdTo1/4RBLNYaeRucYMsjMv+BXoCNfXA0NADj1kwhw==} + engines: {node: ^22.22.2 || >=24.15} peerDependencies: - semantic-release: '>=18.0.0' + semantic-release: '>=20.1.0' '@semantic-release/github@12.0.9': resolution: {integrity: sha512-ODIqb0V3QqndipryEEiaBxUQCFjvv7Oese5Dt4omMGa60YRNEW0Sx3K+zri0uac2Y6S9nOlMehciWIzvvRCTGQ==} @@ -7600,14 +7590,12 @@ packages: '@trpc/client@11.18.0': resolution: {integrity: sha512-wOqeg3Fvl25V1ZisQhUD3K8G60ZJDlSGJNSyeXrLH24xAo5w6GSR2Kzb1cSNY9Y+IQ2YZvYGZstBU+V/ulo/ow==} - hasBin: true peerDependencies: '@trpc/server': 11.18.0 typescript: '>=5.7.2' '@trpc/next@11.18.0': resolution: {integrity: sha512-ocwbruAWMGX9hY3HFg86X4jAcoF2v+xx+A2jDn72SbttRRG2hXR+XKPjrLc1dDJC0oi+/2DJEbL14+k1pyY5og==} - hasBin: true peerDependencies: '@tanstack/react-query': ^5.59.15 '@trpc/client': 11.18.0 @@ -7634,13 +7622,11 @@ packages: '@trpc/server@11.18.0': resolution: {integrity: sha512-JAvXOuNTxgXjIDfQaOvDq1j66LMNfDJUH1IU7Slfn8EvRv2EkH6ehu3A7zpYhjO0syHHiYg77v2lG2JFJgvw7Q==} - hasBin: true peerDependencies: typescript: '>=5.7.2' '@trpc/tanstack-react-query@11.18.0': resolution: {integrity: sha512-dm5xIlN0SEzJAbQ34EHdb1Kgd/zWZ63ZMviQEw3WCWCIPz8MjLKAhquncPcn+YxFUJa+Qi4qatdGFymmM4HmAQ==} - hasBin: true peerDependencies: '@tanstack/react-query': ^5.80.3 '@trpc/client': 11.18.0 @@ -7663,7 +7649,6 @@ packages: '@turbo/gen@2.10.4': resolution: {integrity: sha512-FP3XAL344PohE7yN8cR+SDLyhWIMk7kn+jCNlovzhXDQJBHjfn9Iv7GgMf1dnSTO7etYeOfe4v41Ix26bzYm+A==} - hasBin: true '@turbo/linux-64@2.10.4': resolution: {integrity: sha512-IzV1QovmwX7mfGnVinmE++2IB8tbeo38weltiuH5zNqwCTBjLs/DytyRKx+bmnhHdXIq9SheR8p0Nip/LBUPHg==} @@ -8051,7 +8036,6 @@ packages: '@typescript/typescript6@6.0.2': resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} - hasBin: true '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -8335,7 +8319,6 @@ packages: acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} - hasBin: true address@1.2.2: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} @@ -8526,7 +8509,6 @@ packages: autoprefixer@10.5.0: resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} - hasBin: true peerDependencies: postcss: ^8.1.0 @@ -8952,7 +8934,6 @@ packages: cli-highlight@2.1.11: resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} @@ -9089,7 +9070,6 @@ packages: concurrently@10.0.3: resolution: {integrity: sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==} engines: {node: '>=22'} - hasBin: true config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -9137,7 +9117,6 @@ packages: conventional-changelog-writer@8.0.0: resolution: {integrity: sha512-TQcoYGRatlAnT2qEWDON/XSfnVG38JzA7E0wcGScu7RElQBkg9WWgZd1peCWFcWDh1xfb2CfsrcvOn1bbSzztA==} engines: {node: '>=18'} - hasBin: true conventional-commits-filter@5.0.0: resolution: {integrity: sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==} @@ -9146,7 +9125,6 @@ packages: conventional-commits-parser@6.0.0: resolution: {integrity: sha512-TbsINLp48XeMXR8EvGjTnKGsZqBemisPoyWESlpRyR8lif0lcwzqz+NMtYSj1ooF/WYjSuu7wX0CtdeeMEQAmA==} engines: {node: '>=18'} - hasBin: true convert-hrtime@5.0.0: resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} @@ -9242,7 +9220,6 @@ packages: crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} - hasBin: true crc32-stream@6.0.0: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} @@ -9258,7 +9235,6 @@ packages: cross-env@10.1.0: resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} engines: {node: '>=20'} - hasBin: true cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} @@ -9366,7 +9342,6 @@ packages: cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} - hasBin: true cssnano-preset-advanced@6.1.2: resolution: {integrity: sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==} @@ -9687,7 +9662,6 @@ packages: detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} - hasBin: true detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} @@ -9789,7 +9763,6 @@ packages: dotenv-cli@11.0.0: resolution: {integrity: sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==} - hasBin: true dotenv-expand@12.0.3: resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} @@ -9805,7 +9778,6 @@ packages: drizzle-kit@0.31.10: resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} - hasBin: true drizzle-orm@0.45.2: resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} @@ -10036,22 +10008,18 @@ packages: esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} - hasBin: true esbuild@0.27.7: resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} - hasBin: true esbuild@0.28.0: resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} engines: {node: '>=18'} - hasBin: true esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} - hasBin: true escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -10167,6 +10135,10 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + execa@10.0.0: + resolution: {integrity: sha512-Cxl6MKxB1dr1H0FHmiizJ+lavKF7pV+fcDZFyqMB8d5m7qUPm/OtZYcD5vPWePKxSnTQ57KuBd9mtdZ3oNCvyQ==} + engines: {node: '>=22'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -10257,11 +10229,9 @@ packages: fast-xml-parser@5.8.0: resolution: {integrity: sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==} - hasBin: true fast-xml-parser@5.9.3: resolution: {integrity: sha512-brCNCeScma/kqa54J4PIDriSSSLssRkuYaUCpvHJulGc3HGI/xxKUCTDcYkAdqJsyb//ydpbxecjC3hB9+tb/g==} - hasBin: true fastify@5.9.0: resolution: {integrity: sha512-VMS5lE0zj+MZlJpQa3Qv5iGjfun0H2N7VRgoBwpcTNQ2bdIQpv7fDpb+HGteGbicBsGkzGS+X+hdx9mmrfWuHQ==} @@ -10532,7 +10502,6 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} @@ -10598,7 +10567,6 @@ packages: handlebars@4.7.8: resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} - hasBin: true has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} @@ -10854,6 +10822,10 @@ packages: resolution: {integrity: sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==} engines: {node: '>=18.18.0'} + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + hyperdyperid@1.2.0: resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} engines: {node: '>=10.18'} @@ -11237,7 +11209,6 @@ packages: jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} - hasBin: true joi@17.13.3: resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} @@ -11274,11 +11245,9 @@ packages: js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} - hasBin: true js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true jsbn@1.1.0: resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} @@ -11331,7 +11300,6 @@ packages: json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} - hasBin: true jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -11342,7 +11310,6 @@ packages: jsonpath-plus@10.4.0: resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} engines: {node: '>=18.0.0'} - hasBin: true jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -11658,12 +11625,10 @@ packages: marked@15.0.11: resolution: {integrity: sha512-1BEXAU2euRCG3xwgLVT1y0xbJEld1XOrmRJpUwRCcy7rxhSCwMrmEu9LXoPhHSCJG41V7YcQ2mjKRr5BA3ITIA==} engines: {node: '>= 18'} - hasBin: true marked@16.4.2: resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} engines: {node: '>= 20'} - hasBin: true math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} @@ -11671,7 +11636,6 @@ packages: mcp-handler@1.1.0: resolution: {integrity: sha512-MVCES7g18gcoZy+R/3v5nadkUMzMAWdos8jRl6DyljOKvd2/ZKDmwlCjL6zp4vo+7FeCXOYL1uWinHWlkKAAUg==} - hasBin: true peerDependencies: '@modelcontextprotocol/sdk': 1.26.0 next: '>=13.0.0' @@ -11943,12 +11907,10 @@ packages: mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} - hasBin: true mime@4.0.4: resolution: {integrity: sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==} engines: {node: '>=16'} - hasBin: true mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -12006,11 +11968,9 @@ packages: mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} - hasBin: true mpd-parser@1.4.0: resolution: {integrity: sha512-blbA7XpAaOdC/PR0Tu8GiUxlx6CpBuRowQPYV7lhi9Yr9G9+dkPXIUjqjzc/NCn/uZZPopluaJlIYntIEI/VLw==} - hasBin: true mrmime@2.0.0: resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} @@ -12046,7 +12006,6 @@ packages: mux.js@7.1.0: resolution: {integrity: sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==} engines: {node: '>=8', npm: '>=5'} - hasBin: true mysql2@3.22.6: resolution: {integrity: sha512-fPKmeDGUzvFP7bMD5SASlJ5zIgvCC4hbanTmhbUlEmhyrY1hR4Hi3xLOWgTd3luYjLVifx6uGvMNJ2m/LlqEpg==} @@ -12067,12 +12026,10 @@ packages: nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true nanoid@5.1.16: resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==} engines: {node: ^18 || >=20} - hasBin: true nanostores@1.3.0: resolution: {integrity: sha512-XPUa/jz+P1oJvN9VBxw4L9MtdFfaH3DAryqPssqhb2kXjmb9npz0dly6rCsgFWOPr4Yg9mTfM3MDZgZZ+7A3lA==} @@ -12184,7 +12141,6 @@ packages: node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} - hasBin: true node-loader@2.1.0: resolution: {integrity: sha512-OwjPkyh8+7jW8DMd/iq71uU1Sspufr/C2+c3t0p08J3CrM9ApZ4U53xuisNrDXOHyGi5OYHgtfmmh+aK9zJA6g==} @@ -12234,7 +12190,6 @@ packages: npm@11.6.2: resolution: {integrity: sha512-7iKzNfy8lWYs3zq4oFPa8EXZz5xt9gQNKJZau3B1ErLBb6bF7sBJ00x09485DOvRT2l5Gerbl3VlZNT57MxJVA==} engines: {node: ^20.17.0 || >=22.9.0} - hasBin: true bundledDependencies: - '@isaacs/string-locale-compare' - '@npmcli/arborist' @@ -12398,7 +12353,6 @@ packages: 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: '*' @@ -12411,7 +12365,6 @@ packages: 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: '*' @@ -12465,10 +12418,6 @@ packages: resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} engines: {node: '>=8'} - p-reduce@2.1.0: - resolution: {integrity: sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==} - engines: {node: '>=8'} - p-reduce@3.0.0: resolution: {integrity: sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==} engines: {node: '>=12'} @@ -12686,7 +12635,6 @@ packages: pino@10.3.1: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} - hasBin: true pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} @@ -12694,7 +12642,6 @@ packages: pkcs7@1.0.4: resolution: {integrity: sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==} - hasBin: true pkg-conf@2.1.0: resolution: {integrity: sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==} @@ -12711,12 +12658,10 @@ packages: 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 po-parser@2.1.1: resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==} @@ -13207,7 +13152,6 @@ packages: resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} engines: {node: '>=10'} deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. - hasBin: true pretty-error@4.0.0: resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==} @@ -13394,7 +13338,6 @@ packages: rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true react-dom@19.2.7: resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} @@ -13798,7 +13741,6 @@ packages: rolldown@1.1.4: resolution: {integrity: sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==} engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} @@ -13849,7 +13791,6 @@ packages: sass@1.101.0: resolution: {integrity: sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==} engines: {node: '>=20.19.0'} - hasBin: true sax@1.6.0: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} @@ -13893,7 +13834,6 @@ packages: semantic-release@25.0.5: resolution: {integrity: sha512-mn61SUJwtM8ThrWn2WmgLVpwVJeG/hPSupua1psdMoufmwRIPyvRLkRkL0JDXkP67OntlLWUYnBnfVc8EDO3/g==} engines: {node: ^22.14.0 || >= 24.10.0} - hasBin: true semver-diff@4.0.0: resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} @@ -13913,7 +13853,6 @@ packages: semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} - hasBin: true send@0.19.2: resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} @@ -14521,11 +14460,9 @@ packages: tldts@6.1.69: resolution: {integrity: sha512-Oh/CqRQ1NXNY7cy9NkTPUauOWiTro0jEYZTioGbOmcQh6EC45oribyIMJp0OJO3677r13tO6SKdWoGZUx2BDFw==} - hasBin: true tldts@7.0.12: resolution: {integrity: sha512-M9ZQBPp6FyqhMcl233vHYyYRkxXOA1SKGlnq13S0mJdUhRSwr2w6I8rlchPL73wBwRlyIZpFvpu2VcdSMWLYXw==} - hasBin: true tmp@0.2.7: resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} @@ -14574,7 +14511,6 @@ packages: tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -14616,7 +14552,6 @@ packages: resolution: {integrity: sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ==} engines: {node: ^18 || >=20} deprecated: unmaintained - hasBin: true peerDependencies: typescript: ^5.0.0 peerDependenciesMeta: @@ -14640,12 +14575,10 @@ packages: tsx@4.20.4: resolution: {integrity: sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==} engines: {node: '>=18.0.0'} - hasBin: true tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} - hasBin: true tsyringe@4.10.0: resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} @@ -14660,7 +14593,6 @@ packages: turbo@2.10.4: resolution: {integrity: sha512-GQpduILaKjoaGljw097ScsSyKTtZSY7cZ3bJktzfTkPMyCf3ShKLuXK2IaOEN2Plziml+ArR7WJ1m+V4VbnaKQ==} - hasBin: true tweetnacl@0.14.5: resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} @@ -14707,7 +14639,6 @@ packages: uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} - hasBin: true uint8array-extras@1.5.0: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} @@ -14828,7 +14759,6 @@ packages: update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -14965,7 +14895,6 @@ packages: 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 @@ -15008,7 +14937,6 @@ packages: 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 @@ -15052,7 +14980,6 @@ packages: vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} - hasBin: true peerDependencies: '@vue/composition-api': ^1.0.0-rc.1 vue: ^3.0.0-0 || ^2.6.0 @@ -15114,7 +15041,6 @@ packages: webpack-dev-server@5.2.4: resolution: {integrity: sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==} engines: {node: '>= 18.12.0'} - hasBin: true peerDependencies: webpack: ^5.0.0 webpack-cli: '*' @@ -15139,7 +15065,6 @@ packages: webpack@5.107.1: resolution: {integrity: sha512-mvdIWxj/H6QsfgDdH9djne3a5dYcmEmtsXGESkypaGN5jXjF/b+9KDlmTDQ2TKlFUeA2fI9Y65kihD30JOdB+Q==} engines: {node: '>=10.13.0'} - hasBin: true peerDependencies: webpack-cli: '*' peerDependenciesMeta: @@ -15186,10 +15111,13 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-command@0.1.0: + resolution: {integrity: sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA==} + engines: {node: '>=22'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} - hasBin: true which@4.0.0: resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} @@ -15199,7 +15127,6 @@ packages: why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} - hasBin: true widest-line@4.0.1: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} @@ -15275,7 +15202,6 @@ packages: xml-js@1.6.11: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} - hasBin: true xml-lexer@0.2.2: resolution: {integrity: sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w==} @@ -15319,7 +15245,6 @@ packages: yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} - hasBin: true yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} @@ -15357,6 +15282,10 @@ packages: resolution: {integrity: sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==} engines: {node: '>=18'} + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -17088,7 +17017,7 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/bundler@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/bundler@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) '@docusaurus/babel': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) @@ -17100,7 +17029,7 @@ snapshots: clean-css: 5.3.3 copy-webpack-plugin: 11.0.0(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) css-loader: 6.11.0(@rspack/core@1.7.11)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) - css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) + css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.32.0)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) cssnano: 6.1.2(postcss@8.5.16) file-loader: 6.2.0(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) html-minifier-terser: 7.2.0 @@ -17109,13 +17038,13 @@ snapshots: postcss: 8.5.16 postcss-loader: 7.3.4(@typescript/typescript6@6.0.2)(postcss@8.5.16)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) postcss-preset-env: 10.6.1(postcss@8.5.16) - terser-webpack-plugin: 5.6.0(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.0)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) + terser-webpack-plugin: 5.6.0(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) tslib: 2.8.1 url-loader: 4.1.1(file-loader@6.2.0(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)))(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) webpack: 5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3) webpackbar: 7.0.0(@rspack/core@1.7.11)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) optionalDependencies: - '@docusaurus/faster': 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3) + '@docusaurus/faster': 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3) transitivePeerDependencies: - '@minify-html/node' - '@parcel/css' @@ -17133,10 +17062,10 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: '@docusaurus/babel': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/bundler': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/bundler': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.1 '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) @@ -17181,7 +17110,7 @@ snapshots: webpack-dev-server: 5.2.4(debug@4.4.3(supports-color@10.2.2))(supports-color@10.2.2)(tslib@2.8.1)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)) webpack-merge: 6.0.1 optionalDependencies: - '@docusaurus/faster': 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3) + '@docusaurus/faster': 3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3) transitivePeerDependencies: - '@minify-html/node' - '@parcel/css' @@ -17211,10 +17140,10 @@ snapshots: postcss-sort-media-queries: 5.2.0(postcss@8.5.16) tslib: 2.8.1 - '@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3)': + '@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3)': dependencies: '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@rspack/core': 1.7.11(@swc/helpers@0.5.15) + '@rspack/core': 1.7.11 '@swc/core': 1.15.3 '@swc/html': 1.15.40 browserslist: 4.28.2 @@ -17312,13 +17241,13 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/plugin-content-blog@3.10.1(c336c9d98c1f4baf3a75901518d97959)': + '@docusaurus/plugin-content-blog@3.10.1(59bf1c207f3351d61f3d6e4d32f816cb)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.1 '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.1(693648dca07bcb0a42a6196192ee56b1) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.1(411712c28a3adb38d3c382c02b26255d) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) @@ -17360,13 +17289,13 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/plugin-content-docs@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.1 '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.1(693648dca07bcb0a42a6196192ee56b1) + '@docusaurus/theme-common': 3.10.1(411712c28a3adb38d3c382c02b26255d) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) @@ -17406,9 +17335,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-content-pages@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/plugin-content-pages@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) @@ -17442,9 +17371,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-css-cascade-layers@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/plugin-css-cascade-layers@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) @@ -17475,9 +17404,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-debug@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/plugin-debug@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) fs-extra: 11.2.0 @@ -17509,9 +17438,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-analytics@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/plugin-google-analytics@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) react: 19.2.7 @@ -17541,9 +17470,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-gtag@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/plugin-google-gtag@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@types/gtag.js': 0.0.20 @@ -17574,9 +17503,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-google-tag-manager@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/plugin-google-tag-manager@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) react: 19.2.7 @@ -17606,9 +17535,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-sitemap@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/plugin-sitemap@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.1 '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) @@ -17643,9 +17572,9 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/plugin-svgr@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/plugin-svgr@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) @@ -17679,22 +17608,22 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/preset-classic@3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/preset-classic@3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-content-blog': 3.10.1(c336c9d98c1f4baf3a75901518d97959) - '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-content-pages': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-css-cascade-layers': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-debug': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-google-analytics': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-google-gtag': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-google-tag-manager': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-sitemap': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-svgr': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/theme-classic': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.1(693648dca07bcb0a42a6196192ee56b1) - '@docusaurus/theme-search-algolia': 3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-content-blog': 3.10.1(59bf1c207f3351d61f3d6e4d32f816cb) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-content-pages': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-css-cascade-layers': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-debug': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-google-analytics': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-google-gtag': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-google-tag-manager': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-sitemap': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-svgr': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/theme-classic': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.1(411712c28a3adb38d3c382c02b26255d) + '@docusaurus/theme-search-algolia': 3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -17730,16 +17659,16 @@ snapshots: '@types/react': 19.2.17 react: 19.2.7 - '@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.1 '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-content-blog': 3.10.1(c336c9d98c1f4baf3a75901518d97959) - '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-content-pages': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.1(693648dca07bcb0a42a6196192ee56b1) + '@docusaurus/plugin-content-blog': 3.10.1(59bf1c207f3351d61f3d6e4d32f816cb) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-content-pages': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.1(411712c28a3adb38d3c382c02b26255d) '@docusaurus/theme-translations': 3.10.1 '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) @@ -17783,11 +17712,11 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-common@3.10.1(693648dca07bcb0a42a6196192ee56b1)': + '@docusaurus/theme-common@3.10.1(411712c28a3adb38d3c382c02b26255d)': dependencies: '@docusaurus/mdx-loader': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils-common': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@types/history': 4.7.11 @@ -17816,11 +17745,11 @@ snapshots: - uglify-js - webpack-cli - '@docusaurus/theme-mermaid@3.10.1(c336c9d98c1f4baf3a75901518d97959)': + '@docusaurus/theme-mermaid@3.10.1(59bf1c207f3351d61f3d6e4d32f816cb)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/module-type-aliases': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.1(693648dca07bcb0a42a6196192ee56b1) + '@docusaurus/theme-common': 3.10.1(411712c28a3adb38d3c382c02b26255d) '@docusaurus/types': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) mermaid: 11.15.0 @@ -17852,14 +17781,14 @@ snapshots: - utf-8-validate - webpack-cli - '@docusaurus/theme-search-algolia@3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(supports-color@10.2.2)(uglify-js@3.19.3)': + '@docusaurus/theme-search-algolia@3.10.1(@algolia/client-search@5.53.0)(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(supports-color@10.2.2)(uglify-js@3.19.3)': dependencies: '@algolia/autocomplete-core': 1.19.8(@algolia/client-search@5.53.0)(algoliasearch@5.53.0)(search-insights@2.17.3) '@docsearch/react': 4.6.3(@algolia/client-search@5.53.0)(@types/react@19.2.17)(algoliasearch@5.53.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3) - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/logger': 3.10.1 - '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) - '@docusaurus/theme-common': 3.10.1(693648dca07bcb0a42a6196192ee56b1) + '@docusaurus/plugin-content-docs': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/theme-common': 3.10.1(411712c28a3adb38d3c382c02b26255d) '@docusaurus/theme-translations': 3.10.1 '@docusaurus/utils': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) '@docusaurus/utils-validation': 3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) @@ -20189,13 +20118,11 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 1.7.11 '@rspack/binding-win32-x64-msvc': 1.7.11 - '@rspack/core@1.7.11(@swc/helpers@0.5.15)': + '@rspack/core@1.7.11': dependencies: '@module-federation/runtime-tools': 0.22.0 '@rspack/binding': 1.7.11 '@rspack/lite-tapable': 1.1.0 - optionalDependencies: - '@swc/helpers': 0.5.15 '@rspack/lite-tapable@1.1.0': {} @@ -20554,12 +20481,11 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@semantic-release/changelog@6.0.3(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2))': + '@semantic-release/changelog@7.0.0(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2))': dependencies: - '@semantic-release/error': 3.0.0 - aggregate-error: 3.1.0 - fs-extra: 11.2.0 - lodash: 4.18.1 + '@semantic-release/error': 4.0.0 + aggregate-error: 5.0.0 + lodash-es: 4.17.21 semantic-release: 25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2) '@semantic-release/commit-analyzer@13.0.1(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2))(supports-color@10.2.2)': @@ -20576,20 +20502,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@semantic-release/error@3.0.0': {} - '@semantic-release/error@4.0.0': {} - '@semantic-release/git@10.0.1(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2))(supports-color@10.2.2)': + '@semantic-release/git@11.0.1(semantic-release@25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2))(supports-color@10.2.2)': dependencies: - '@semantic-release/error': 3.0.0 - aggregate-error: 3.1.0 + '@semantic-release/error': 4.0.0 + aggregate-error: 5.0.0 debug: 4.4.3(supports-color@10.2.2) dir-glob: 3.0.1 - execa: 5.1.1 - lodash: 4.18.1 + execa: 10.0.0 + lodash-es: 4.17.21 micromatch: 4.0.8 - p-reduce: 2.1.0 + p-reduce: 3.0.0 semantic-release: 25.0.5(@typescript/typescript6@6.0.2)(supports-color@10.2.2) transitivePeerDependencies: - supports-color @@ -20658,9 +20582,9 @@ snapshots: '@sideway/pinpoint@2.0.0': {} - '@signalwire/docusaurus-plugin-llms-txt@1.2.2(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(supports-color@10.2.2)': + '@signalwire/docusaurus-plugin-llms-txt@1.2.2(@docusaurus/core@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(supports-color@10.2.2)': dependencies: - '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/core': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) fs-extra: 11.2.0 hast-util-select: 6.0.4 hast-util-to-html: 9.0.5 @@ -23139,10 +23063,10 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.4 optionalDependencies: - '@rspack/core': 1.7.11(@swc/helpers@0.5.15) + '@rspack/core': 1.7.11 webpack: 5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3) - css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.0)(lightningcss@1.32.0)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)): + css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.28.1)(lightningcss@1.32.0)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 cssnano: 6.1.2(postcss@8.5.16) @@ -23154,7 +23078,7 @@ snapshots: optionalDependencies: clean-css: 5.3.3 csso: 5.0.5 - esbuild: 0.28.0 + esbuild: 0.28.1 lightningcss: 1.32.0 css-prefers-color-scheme@10.0.0(postcss@8.5.16): @@ -23616,9 +23540,9 @@ snapshots: transitivePeerDependencies: - supports-color - docusaurus-plugin-image-zoom@3.0.1(@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)): + docusaurus-plugin-image-zoom@3.0.1(@docusaurus/theme-classic@3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3)): dependencies: - '@docusaurus/theme-classic': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(@swc/helpers@0.5.15)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) + '@docusaurus/theme-classic': 3.10.1(@docusaurus/faster@3.10.1(@docusaurus/types@3.10.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3))(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.16)(uglify-js@3.19.3))(@rspack/core@1.7.11)(@swc/core@1.15.3)(@swc/html@1.15.40)(@types/react@19.2.17)(@typescript/typescript6@6.0.2)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(debug@4.4.3(supports-color@10.2.2))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)(uglify-js@3.19.3) medium-zoom: 1.1.0 validate-peer-dependencies: 2.2.0 @@ -24049,6 +23973,22 @@ snapshots: dependencies: eventsource-parser: 3.1.0 + execa@10.0.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + path-key: 4.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + which-command: 0.1.0 + yoctocolors: 2.1.2 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -24943,7 +24883,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.3 optionalDependencies: - '@rspack/core': 1.7.11(@swc/helpers@0.5.15) + '@rspack/core': 1.7.11 webpack: 5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3) html-whitespace-sensitive-tag-names@3.0.1: {} @@ -25043,6 +24983,8 @@ snapshots: human-signals@8.0.0: {} + human-signals@8.0.1: {} + hyperdyperid@1.2.0: {} ical.js@2.2.1: {} @@ -26800,8 +26742,6 @@ snapshots: eventemitter3: 4.0.7 p-timeout: 3.2.0 - p-reduce@2.1.0: {} - p-reduce@3.0.0: {} p-retry@6.2.1: @@ -29126,25 +29066,6 @@ snapshots: type-fest: 2.19.0 unique-string: 3.0.0 - terser-webpack-plugin@5.6.0(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.0)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)): - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - jest-worker: 27.5.1 - schema-utils: 4.3.3 - terser: 5.46.1 - webpack: 5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3) - optionalDependencies: - '@swc/core': 1.15.3 - '@swc/html': 1.15.40 - clean-css: 5.3.3 - cssnano: 6.1.2(postcss@8.5.16) - csso: 5.0.5 - esbuild: 0.28.0 - html-minifier-terser: 7.2.0 - lightningcss: 1.32.0 - postcss: 8.5.16 - uglify-js: 3.19.3 - terser-webpack-plugin@5.6.0(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)(webpack@5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3)): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -29931,7 +29852,7 @@ snapshots: pretty-time: 1.1.0 std-env: 3.10.0 optionalDependencies: - '@rspack/core': 1.7.11(@swc/helpers@0.5.15) + '@rspack/core': 1.7.11 webpack: 5.107.1(@swc/core@1.15.3)(@swc/html@1.15.40)(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.16))(csso@5.0.5)(esbuild@0.28.1)(html-minifier-terser@7.2.0)(lightningcss@1.32.0)(postcss@8.5.16)(uglify-js@3.19.3) websocket-driver@0.7.5: @@ -29963,6 +29884,8 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-command@0.1.0: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -30125,6 +30048,8 @@ snapshots: yoctocolors@2.1.1: {} + yoctocolors@2.1.2: {} + zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 273604dd1..3770a15b4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -40,9 +40,9 @@ catalog: "@octokit/auth-app": ^8.2.0 "@paralleldrive/cuid2": ^3.3.0 "@playwright/test": ^1.61.1 - "@semantic-release/changelog": ^6.0.3 + "@semantic-release/changelog": ^7.0.0 "@semantic-release/commit-analyzer": ^13.0.1 - "@semantic-release/git": ^10.0.1 + "@semantic-release/git": ^11.0.1 "@semantic-release/github": ^12.0.9 "@semantic-release/npm": ^13.1.5 "@semantic-release/release-notes-generator": ^14.1.1 diff --git a/tools/homarr-dev/README.md b/tools/homarr-dev/README.md new file mode 100644 index 000000000..99fe89640 --- /dev/null +++ b/tools/homarr-dev/README.md @@ -0,0 +1,103 @@ +# Homarr Developer CLI + +Build and launch local Homarr images and GHCR pull-request builds, then manage them from responsive terminal dashboards. + +## Requirements + +- Docker or OrbStack +- Go 1.25+ to build from source +- GitHub CLI authenticated with `gh auth login` for pull-request features + +## Setup + +```sh +pnpm install +``` + +Create `.env` from `.env.example` and set `DB_URL` to an absolute path for the SQLite file before running the migration. In a POSIX shell, use `cp .env.example .env`; in PowerShell, use `Copy-Item .env.example .env`. +Then run `pnpm db:migration:sqlite:run` to create or update the database. The migration command also seeds a new database. Use `pnpm db:seed` explicitly when you need to seed an existing database. Start Redis separately with `pnpm docker:dev:up` when it is needed. +The examples below use `pnpm dev:cli -- ...`, which works from POSIX shells and PowerShell. If you prefer a global command, install it with `pnpm dev:cli:install` and add your Go bin directory to `PATH`. + +## Build + +```sh +pnpm dev:cli -- build feature +pnpm dev:cli -- build --pr 6441 +pnpm dev:cli -- rebuild feature +``` + +Checkout builds are tagged as `homarr:`. The source checkout and revision are recorded on the image so `homarr rebuild` and the development dashboard can rebuild it later. PR builds use a temporary checkout and default to `homarr:pr-`. + +## Launch + +```sh +pnpm dev:cli -- run dev +pnpm dev:cli -- run --detach dev +pnpm dev:cli -- run --pr 6441 +pnpm dev:cli -- run --pr 6441 --demo +pnpm dev:cli -- run --env FOO=bar --env FEATURE=true dev +``` + +PR launches always pull the latest GHCR tag before starting. Local tags remain local. +The first launch generates a per-user encryption key in the OS config directory and reuses it for later launches. Set `HOMARR_DEV_SECRET_ENCRYPTION_KEY` to a 64-character hexadecimal key to override it. + +The command reference below assumes the optional `homarr` installation. Without it, prefix each command with `pnpm dev:cli --` (for example, `pnpm dev:cli -- dev`). + +## Commands + +```text +homarr Interactive local and remote image browser +homarr dev Explicit alias for the main browser +homarr dash Start on the instance dashboard +homarr build Build this checkout as homarr: +homarr build --pr Build a PR locally from a temporary checkout +homarr rebuild Rebuild from recorded provenance +homarr list Script-friendly instance list +homarr logs Follow logs +homarr open Open in browser +homarr stop Stop +homarr restart Restart +homarr remove Force-remove +homarr doctor Check Docker and optional CLI integrations +``` + +## Development Browser + +```text +/ Filter by PR number, title, author, branch, CI, image, state, or port +↑/↓ or j/k Select +Enter or Space Start the matching local build, otherwise pull remote; stop when running +R Build a remote PR locally in the background or rebuild a local image +p Pull a remote PR image and redeploy, preserving port and volume +m Toggle demo mode for the next launch +o Open PR on GitHub +a Open running app +l Toggle inline logs for the selected running PR +f Follow or pause inline logs +Page Up/Down Scroll inline logs +b Show or hide bot PRs +r Refresh +d Switch to the instance dashboard +q Quit; during a pull, cancel it +``` + +Pull progress, layer completion, local build provenance, selected PR details, CI state, image availability, inline logs, and the final port remain inside the TUI. +Local PR images are folded into their remote PR row only when the recorded revision exactly matches the current GitHub head SHA. They are marked `◆ local` and preferred by Enter; outdated builds remain separate local rows. Navigation and the `d` dashboard toggle remain available while a local build runs. + +## Instance Dashboard + +```text +/ Filter by name, image, state, health, port, or URL +↑/↓ or j/k Select +l Toggle inline logs +f Follow or pause logs +Page Up/Down Scroll logs +s Stop +R Restart +x Remove with confirmation +o Open in browser +c Copy URL +r Refresh +d Switch to the development browser +q Quit +``` diff --git a/tools/homarr-dev/go.mod b/tools/homarr-dev/go.mod new file mode 100644 index 000000000..6a40e27cf --- /dev/null +++ b/tools/homarr-dev/go.mod @@ -0,0 +1,32 @@ +module github.com/homarr-labs/homarr/tools/homarr-dev + +go 1.25.0 + +require ( + charm.land/bubbles/v2 v2.1.1 + charm.land/bubbletea/v2 v2.0.8 + charm.land/lipgloss/v2 v2.0.5 + github.com/atotto/clipboard v0.1.4 + github.com/spf13/cobra v1.10.2 +) + +require ( + github.com/charmbracelet/colorprofile v0.4.3 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-runewidth v0.0.24 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/spf13/pflag v1.0.9 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect +) diff --git a/tools/homarr-dev/go.sum b/tools/homarr-dev/go.sum new file mode 100644 index 000000000..cf4f9f13b --- /dev/null +++ b/tools/homarr-dev/go.sum @@ -0,0 +1,56 @@ +charm.land/bubbles/v2 v2.1.1 h1:7r55WzBxpo/R3z98hGmY7KKPd3ET6vsf0Fb9sDHOV60= +charm.land/bubbles/v2 v2.1.1/go.mod h1:GE6M31gaWZVXzGw73OeuTTgy4lX+OtkH0E5ymnNsHxo= +charm.land/bubbletea/v2 v2.0.8 h1:SxTJMhCAI3lbPmy4SgX5LWZ24AdINr4I6UEqzZvYJuY= +charm.land/bubbletea/v2 v2.0.8/go.mod h1:2SkdgoTXluXJHOUwAoRlRXF/28vklb1rFl6GcgV1/ss= +charm.land/lipgloss/v2 v2.0.5 h1:kbNxgeeUOYv5J0YdpxFjfvf3dFvqH8Aci4zB6xqFtrY= +charm.land/lipgloss/v2 v2.0.5/go.mod h1:9oqhxt4yxIMe6q5A4kHr44DremZk7J9UNh74GlWa5nc= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7 h1:3FmWoGNWK4STvqg0O0Aeav2T7rodWJAPeF0QpH+8gFw= +github.com/charmbracelet/ultraviolet v0.0.0-20260703014108-f5a850f9c2b7/go.mod h1:f/jRa757WUmaOZrbPspXymbg/GnbF+rwe4OLsG7aXYo= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= +github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/tools/homarr-dev/internal/docker/docker.go b/tools/homarr-dev/internal/docker/docker.go new file mode 100644 index 000000000..da5f2fa92 --- /dev/null +++ b/tools/homarr-dev/internal/docker/docker.go @@ -0,0 +1,427 @@ +package docker + +import ( + "context" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" +) + +const developmentEncryptionKeyEnv = "HOMARR_DEV_SECRET_ENCRYPTION_KEY" + +type Container struct { + ID string + Name string + Image string + Status string + Ports string + State string +} + +func (c Container) HostPort() string { + for _, mapping := range strings.Split(c.Ports, ", ") { + if strings.HasSuffix(mapping, "->7575/tcp") { + parts := strings.Split(mapping, "->") + left := parts[0] + if i := strings.LastIndex(left, ":"); i >= 0 { + return left[i+1:] + } + return left + } + } + return "" +} + +func (c Container) Running() bool { + return c.State == "running" +} + +func List() ([]Container, error) { + out, err := exec.Command("docker", "ps", "-a", + "--filter", "name=homarr", + "--format", "{{json .}}").Output() + if err != nil { + return nil, err + } + + var containers []Container + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + if line == "" { + continue + } + var raw struct { + ID string + Names string + Image string + Status string + Ports string + State string + } + if err := json.Unmarshal([]byte(line), &raw); err != nil { + continue + } + containers = append(containers, Container{ + ID: raw.ID, + Name: raw.Names, + Image: raw.Image, + Status: raw.Status, + Ports: raw.Ports, + State: raw.State, + }) + } + filtered := containers[:0] + for _, c := range containers { + if c.Name == "homarr" || strings.HasPrefix(c.Name, "homarr_") { + filtered = append(filtered, c) + } + } + return filtered, nil +} + +func FindFreePort(start int) int { + if start > 65535 { + return 0 + } + used := usedDockerPorts() + start = max(start, 1) + for port := start; port <= 65535; port++ { + if used[port] { + continue + } + listener, err := net.Listen("tcp4", fmt.Sprintf("0.0.0.0:%d", port)) + if err == nil { + _ = listener.Close() + return port + } + } + return 0 +} + +func usedDockerPorts() map[int]bool { + out, err := exec.Command("docker", "ps", "--format", "{{.Ports}}").Output() + if err != nil { + return make(map[int]bool) + } + return parseUsedDockerPorts(out) +} + +func parseUsedDockerPorts(out []byte) map[int]bool { + used := make(map[int]bool) + for _, line := range strings.Split(string(out), "\n") { + for _, mapping := range strings.Split(line, ",") { + left, _, found := strings.Cut(mapping, "->") + if !found { + continue + } + colon := strings.LastIndex(left, ":") + if colon < 0 { + continue + } + ports := strings.SplitN(strings.TrimSpace(left[colon+1:]), "-", 2) + first, err := strconv.Atoi(ports[0]) + if err != nil { + continue + } + last := first + if len(ports) == 2 { + if parsed, err := strconv.Atoi(ports[1]); err == nil { + last = parsed + } + } + for port := first; port <= last; port++ { + used[port] = true + } + } + } + return used +} + +func IsRunning(name string) bool { + containers, _ := List() + for _, c := range containers { + if c.Name == name && c.Running() { + return true + } + } + return false +} + +type StartOptions struct { + Name string + Image string + Volume string + HostPort int + Env []string + Platform string + PullAlways bool + Daemon bool +} + +func Start(opts StartOptions) error { + encryptionKey, err := developmentEncryptionKey() + if err != nil { + return err + } + + if opts.PullAlways { + cmd := PullCommand(opts.Image, opts.Platform) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("pull %s: %w", opts.Image, err) + } + } + + _ = exec.Command("docker", "rm", "-f", opts.Name).Run() + + args := []string{"run", "--rm", "--name", opts.Name, + "-e", "SECRET_ENCRYPTION_KEY=" + encryptionKey, + } + if opts.Platform != "" { + args = append(args, "--platform", opts.Platform) + } + if opts.Daemon { + args = append(args, "-d") + } else { + args = append(args, "-it") + } + for _, env := range opts.Env { + args = append(args, "-e", env) + } + args = append(args, + "-v", opts.Volume+":/appdata", + "-v", "/var/run/docker.sock:/var/run/docker.sock:ro", + "-p", fmt.Sprintf("%d:7575", opts.HostPort), + opts.Image, + ) + + cmd := exec.Command("docker", args...) + if opts.Daemon { + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%w: %s", err, cleanOutput(out)) + } + return nil + } + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + stderr := &tailBuffer{} + cmd.Stderr = io.MultiWriter(os.Stderr, stderr) + if err := cmd.Run(); err != nil { + return fmt.Errorf("%w: %s", err, cleanOutput(stderr.data)) + } + return nil +} + +func developmentEncryptionKey() (string, error) { + if key := os.Getenv(developmentEncryptionKeyEnv); key != "" { + return validateDevelopmentEncryptionKey(key) + } + configDir, err := os.UserConfigDir() + if err != nil { + return "", fmt.Errorf("locate user config directory: %w", err) + } + return persistedDevelopmentEncryptionKey(configDir) +} + +func persistedDevelopmentEncryptionKey(configDir string) (string, error) { + dir := filepath.Join(configDir, "homarr-dev") + path := filepath.Join(dir, "secret-encryption-key") + if key, found, err := readDevelopmentEncryptionKey(path); found || err != nil { + return key, err + } + + if err := os.MkdirAll(dir, 0o700); err != nil { + return "", fmt.Errorf("create development config directory: %w", err) + } + lockPath := path + ".lock" + for attempts := 0; ; attempts++ { + if err := os.Mkdir(lockPath, 0o700); err == nil { + break + } else if !os.IsExist(err) { + return "", fmt.Errorf("lock development encryption key: %w", err) + } + if key, found, err := readDevelopmentEncryptionKey(path); found || err != nil { + return key, err + } + if attempts == 99 { + return "", fmt.Errorf("timed out waiting for development encryption key lock %q", lockPath) + } + time.Sleep(10 * time.Millisecond) + } + defer func() { _ = os.Remove(lockPath) }() + + if key, found, err := readDevelopmentEncryptionKey(path); found || err != nil { + return key, err + } + keyBytes := make([]byte, 32) + if _, err := rand.Read(keyBytes); err != nil { + return "", fmt.Errorf("generate development encryption key: %w", err) + } + key := hex.EncodeToString(keyBytes) + file, err := os.CreateTemp(dir, "secret-encryption-key-*.tmp") + if err != nil { + return "", fmt.Errorf("create development encryption key: %w", err) + } + tempPath := file.Name() + defer func() { _ = os.Remove(tempPath) }() + if _, err := file.WriteString(key + "\n"); err != nil { + _ = file.Close() + return "", fmt.Errorf("write development encryption key: %w", err) + } + if err := file.Close(); err != nil { + return "", fmt.Errorf("close development encryption key: %w", err) + } + if err := os.Chmod(tempPath, 0o600); err != nil { + return "", fmt.Errorf("secure development encryption key: %w", err) + } + if err := os.Rename(tempPath, path); err != nil { + return "", fmt.Errorf("persist development encryption key: %w", err) + } + return key, nil +} + +func readDevelopmentEncryptionKey(path string) (string, bool, error) { + data, err := os.ReadFile(path) + if os.IsNotExist(err) { + return "", false, nil + } + if err != nil { + return "", false, fmt.Errorf("read development encryption key: %w", err) + } + key, err := validateDevelopmentEncryptionKey(strings.TrimSpace(string(data))) + return key, true, err +} + +func validateDevelopmentEncryptionKey(key string) (string, error) { + decoded, err := hex.DecodeString(key) + if err != nil || len(decoded) != 32 { + return "", fmt.Errorf("development encryption key must contain exactly 64 hexadecimal characters") + } + return key, nil +} + +type tailBuffer struct { + data []byte +} + +func (b *tailBuffer) Write(p []byte) (int, error) { + const limit = 32 * 1024 + b.data = append(b.data, p...) + if len(b.data) > limit { + b.data = b.data[len(b.data)-limit:] + } + return len(p), nil +} + +func cleanOutput(out []byte) string { + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + cleaned := make([]string, 0, len(lines)) + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "WARNING: The requested image's platform") || isContainerID(line) { + continue + } + if line != "" { + cleaned = append(cleaned, line) + } + } + return strings.Join(cleaned, "\n") +} + +func isContainerID(value string) bool { + if len(value) != 64 { + return false + } + for _, r := range value { + if !(r >= '0' && r <= '9') && !(r >= 'a' && r <= 'f') { + return false + } + } + return true +} + +func PullCommand(image, platform string) *exec.Cmd { + return PullCommandContext(context.Background(), image, platform) +} + +func PullCommandContext(ctx context.Context, image, platform string) *exec.Cmd { + args := []string{"pull"} + if platform != "" { + args = append(args, "--platform", platform) + } + args = append(args, image) + cmd := exec.CommandContext(ctx, "docker", args...) + if platform != "" { + cmd.Env = append(os.Environ(), "DOCKER_DEFAULT_PLATFORM="+platform) + } + return cmd +} + +func IsPortConflict(err error) bool { + if err == nil { + return false + } + message := strings.ToLower(err.Error()) + return strings.Contains(message, "port is already allocated") || + strings.Contains(message, "address already in use") || + strings.Contains(message, "failed programming external connectivity") +} + +func Stop(name string) error { + cmd := exec.Command("docker", "stop", name) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out))) + } + return nil +} + +func Restart(name string) error { + cmd := exec.Command("docker", "restart", name) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out))) + } + return nil +} + +func Remove(name string) error { + cmd := exec.Command("docker", "rm", "-f", name) + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out))) + } + return nil +} + +func CheckImage(image string) (bool, error) { + cmd := exec.Command("docker", "manifest", "inspect", image) + out, err := cmd.CombinedOutput() + if err == nil { + return true, nil + } + message := strings.ToLower(string(out)) + if strings.Contains(message, "manifest unknown") || strings.Contains(message, "no such manifest") { + return false, nil + } + return false, fmt.Errorf("check image %s: %w: %s", image, err, strings.TrimSpace(string(out))) +} + +func FollowLogs(name string) error { + cmd := exec.Command("docker", "logs", "-f", "--tail", "100", name) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/tools/homarr-dev/internal/docker/docker_test.go b/tools/homarr-dev/internal/docker/docker_test.go new file mode 100644 index 000000000..3422c703d --- /dev/null +++ b/tools/homarr-dev/internal/docker/docker_test.go @@ -0,0 +1,125 @@ +package docker + +import ( + "encoding/hex" + "errors" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestContainerHostPort(t *testing.T) { + c := Container{Ports: "0.0.0.0:7576->7575/tcp, [::]:7576->7575/tcp"} + if got := c.HostPort(); got != "7576" { + t.Fatalf("HostPort() = %q, want 7576", got) + } +} + +func TestIsPortConflict(t *testing.T) { + for _, message := range []string{ + "Bind for 0.0.0.0:7575 failed: port is already allocated", + "driver failed programming external connectivity on endpoint", + } { + if !IsPortConflict(errors.New(message)) { + t.Fatalf("expected port conflict for %q", message) + } + } + if IsPortConflict(errors.New("manifest unknown")) { + t.Fatal("manifest error classified as port conflict") + } +} + +func TestParseUsedDockerPortsPreservesLineBoundaries(t *testing.T) { + ports := parseUsedDockerPorts([]byte("6379/tcp\n0.0.0.0:7575->7575/tcp, [::]:7575->7575/tcp\n0.0.0.0:8000-8002->8000-8002/tcp\n")) + for _, port := range []int{7575, 8000, 8001, 8002} { + if !ports[port] { + t.Fatalf("port %d was not detected", port) + } + } +} + +func TestFindFreePortRejectsOutOfRangeStart(t *testing.T) { + if port := FindFreePort(65536); port != 0 { + t.Fatalf("port = %d, want 0", port) + } +} + +func TestDevelopmentEncryptionKeyUsesEnvironmentOverride(t *testing.T) { + want := strings.Repeat("a", 64) + t.Setenv(developmentEncryptionKeyEnv, want) + got, err := developmentEncryptionKey() + if err != nil { + t.Fatal(err) + } + if got != want { + t.Fatalf("key = %q, want environment value", got) + } +} + +func TestPersistedDevelopmentEncryptionKeyIsReused(t *testing.T) { + configDir := t.TempDir() + first, err := persistedDevelopmentEncryptionKey(configDir) + if err != nil { + t.Fatal(err) + } + second, err := persistedDevelopmentEncryptionKey(configDir) + if err != nil { + t.Fatal(err) + } + if first != second { + t.Fatal("persisted key changed between reads") + } + decoded, err := hex.DecodeString(first) + if err != nil || len(decoded) != 32 { + t.Fatalf("generated key is not 32 bytes: %q", first) + } + info, err := os.Stat(filepath.Join(configDir, "homarr-dev", "secret-encryption-key")) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o600 { + t.Fatalf("key permissions = %o, want 600", info.Mode().Perm()) + } +} + +func TestPersistedDevelopmentEncryptionKeyRejectsInvalidValue(t *testing.T) { + configDir := t.TempDir() + dir := filepath.Join(configDir, "homarr-dev") + if err := os.Mkdir(dir, 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "secret-encryption-key"), []byte("invalid\n"), 0o600); err != nil { + t.Fatal(err) + } + if _, err := persistedDevelopmentEncryptionKey(configDir); err == nil { + t.Fatal("expected invalid persisted key to fail") + } +} + +func TestPersistedDevelopmentEncryptionKeyConcurrentCreation(t *testing.T) { + configDir := t.TempDir() + type result struct { + key string + err error + } + results := make(chan result, 20) + for range 20 { + go func() { + key, err := persistedDevelopmentEncryptionKey(configDir) + results <- result{key: key, err: err} + }() + } + var first string + for range 20 { + result := <-results + if result.err != nil { + t.Fatal(result.err) + } + if first == "" { + first = result.key + } else if result.key != first { + t.Fatal("concurrent callers received different keys") + } + } +} diff --git a/tools/homarr-dev/internal/docker/images.go b/tools/homarr-dev/internal/docker/images.go new file mode 100644 index 000000000..cb4f16e66 --- /dev/null +++ b/tools/homarr-dev/internal/docker/images.go @@ -0,0 +1,149 @@ +package docker + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" +) + +const ( + SourceLabel = "org.homarr.dev.source" + RevisionLabel = "org.homarr.dev.revision" + PRLabel = "org.homarr.dev.pr" +) + +var tagPattern = regexp.MustCompile(`^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$`) + +type Image struct { + ID string + Tag string + Source string + Revision string + PRNumber int +} + +func (image Image) Reference() string { + return "homarr:" + image.Tag +} + +type imageListRow struct { + ID string + Repository string + Tag string +} + +func parseImageList(output []byte) ([]Image, error) { + images := make([]Image, 0) + for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") { + if line == "" { + continue + } + var row imageListRow + if err := json.Unmarshal([]byte(line), &row); err != nil { + return nil, fmt.Errorf("parse local Docker image: %w", err) + } + if row.Repository == "homarr" && row.Tag != "" { + images = append(images, Image{ID: row.ID, Tag: row.Tag}) + } + } + return images, nil +} + +func ListLocalImages(ctx context.Context) ([]Image, error) { + out, err := exec.CommandContext(ctx, "docker", "image", "ls", "--filter", "reference=homarr:*", "--format", "{{json .}}").Output() + if err != nil { + return nil, fmt.Errorf("list local Homarr images: %w", err) + } + images, err := parseImageList(out) + if err != nil { + return nil, err + } + labelsByImage, err := inspectImageLabels(ctx, images) + if err != nil { + return nil, err + } + for index, labels := range labelsByImage { + images[index].Source = labels[SourceLabel] + images[index].Revision = labels[RevisionLabel] + images[index].PRNumber, _ = strconv.Atoi(labels[PRLabel]) + } + return images, nil +} + +func inspectImageLabels(ctx context.Context, images []Image) ([]map[string]string, error) { + if len(images) == 0 { + return nil, nil + } + args := []string{"image", "inspect", "--format", "{{json .Config.Labels}}"} + for _, image := range images { + args = append(args, image.Reference()) + } + out, err := exec.CommandContext(ctx, "docker", args...).Output() + if err != nil { + return nil, fmt.Errorf("inspect local Homarr images: %w", err) + } + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + if len(lines) != len(images) { + return nil, fmt.Errorf("inspect returned %d images, expected %d", len(lines), len(images)) + } + labelsByImage := make([]map[string]string, len(images)) + for index, line := range lines { + labelsByImage[index] = make(map[string]string) + if line == "null" { + continue + } + if err := json.Unmarshal([]byte(line), &labelsByImage[index]); err != nil { + return nil, fmt.Errorf("parse labels for %s: %w", images[index].Reference(), err) + } + } + return labelsByImage, nil +} + +type BuildOptions struct { + Context string + Tag string + Source string + Revision string + PRNumber int +} + +func BuildCommand(ctx context.Context, options BuildOptions) (*exec.Cmd, error) { + if !tagPattern.MatchString(options.Tag) { + return nil, fmt.Errorf("invalid image name %q", options.Tag) + } + contextPath, err := filepath.Abs(options.Context) + if err != nil { + return nil, fmt.Errorf("resolve build context: %w", err) + } + if info, err := os.Stat(filepath.Join(contextPath, "Dockerfile")); err != nil || info.IsDir() { + return nil, fmt.Errorf("%s does not contain a Dockerfile", contextPath) + } + args := []string{"build", "--label", SourceLabel + "=" + options.Source, "--label", RevisionLabel + "=" + options.Revision} + if options.PRNumber > 0 { + args = append(args, "--label", fmt.Sprintf("%s=%d", PRLabel, options.PRNumber)) + } + args = append(args, "-t", "homarr:"+options.Tag, ".") + command := exec.CommandContext(ctx, "docker", args...) + command.Dir = contextPath + return command, nil +} + +func BuildContext(ctx context.Context, options BuildOptions, stdout, stderr io.Writer) error { + command, err := BuildCommand(ctx, options) + if err != nil { + return err + } + command.Stdout = stdout + command.Stderr = stderr + if err := command.Run(); err != nil { + return fmt.Errorf("build homarr:%s: %w", options.Tag, err) + } + return nil +} diff --git a/tools/homarr-dev/internal/docker/images_test.go b/tools/homarr-dev/internal/docker/images_test.go new file mode 100644 index 000000000..559a9423a --- /dev/null +++ b/tools/homarr-dev/internal/docker/images_test.go @@ -0,0 +1,69 @@ +package docker + +import ( + "os" + "testing" +) + +func TestParseImageListFiltersHomarrTags(t *testing.T) { + output := []byte("{\"ID\":\"sha256:1\",\"Repository\":\"homarr\",\"Tag\":\"dev\"}\n{\"ID\":\"sha256:2\",\"Repository\":\"other\",\"Tag\":\"dev\"}\n") + images, err := parseImageList(output) + if err != nil { + t.Fatal(err) + } + if len(images) != 1 || images[0].Reference() != "homarr:dev" { + t.Fatalf("images = %#v, want homarr:dev", images) + } +} + +func TestInspectImageLabelsWithNoImages(t *testing.T) { + labels, err := inspectImageLabels(t.Context(), nil) + if err != nil { + t.Fatal(err) + } + if labels != nil { + t.Fatalf("labels = %#v, want nil", labels) + } +} + +func TestBuildCommandIncludesProvenance(t *testing.T) { + command, err := BuildCommand(t.Context(), BuildOptions{ + Context: "../../../..", + Tag: "feature", + Source: "/tmp/homarr", + Revision: "abc123", + PRNumber: 42, + }) + if err != nil { + t.Fatal(err) + } + want := []string{"docker", "build", "--label", SourceLabel + "=/tmp/homarr", "--label", RevisionLabel + "=abc123", "--label", PRLabel + "=42", "-t", "homarr:feature", "."} + if len(command.Args) != len(want) { + t.Fatalf("args = %v, want %v", command.Args, want) + } + for index := range want { + if command.Args[index] != want[index] { + t.Fatalf("args = %v, want %v", command.Args, want) + } + } +} + +func TestListLocalImagesIntegration(t *testing.T) { + wantTag := os.Getenv("HOMARR_TEST_LOCAL_IMAGE") + if wantTag == "" { + t.Skip("set HOMARR_TEST_LOCAL_IMAGE to a local homarr image tag") + } + images, err := ListLocalImages(t.Context()) + if err != nil { + t.Fatal(err) + } + for _, image := range images { + if image.Tag == wantTag { + if image.Source == "" || image.Revision == "" { + t.Fatalf("image %#v is missing provenance", image) + } + return + } + } + t.Fatalf("homarr:%s was not discovered", wantTag) +} diff --git a/tools/homarr-dev/internal/gh/gh.go b/tools/homarr-dev/internal/gh/gh.go new file mode 100644 index 000000000..c4e61e6fe --- /dev/null +++ b/tools/homarr-dev/internal/gh/gh.go @@ -0,0 +1,199 @@ +package gh + +import ( + "context" + "encoding/json" + "fmt" + "os/exec" + "strings" + "sync" + "time" +) + +const Repo = "homarr-labs/homarr" +const prCacheTTL = 5 * time.Minute + +var prListCache = struct { + sync.Mutex + entries map[int]cachedPRs +}{entries: make(map[int]cachedPRs)} + +type cachedPRs struct { + prs []PR + fetchedAt time.Time +} + +func isBot(login string) bool { + return strings.HasSuffix(login, "[bot]") || strings.HasPrefix(login, "app/") +} + +type PR struct { + Number int `json:"number"` + Title string `json:"title"` + Author string `json:"author"` + HeadRef string `json:"headRefName"` + HeadSHA string `json:"headRefOid"` + CIState string `json:"ciState"` + IsDraft bool `json:"isDraft"` + UpdatedAt string `json:"updatedAt"` +} + +type rawPR struct { + Number int `json:"number"` + Title string `json:"title"` + IsDraft bool `json:"isDraft"` + Author struct { + Login string `json:"login"` + } `json:"author"` + HeadRefName string `json:"headRefName"` + HeadRefOID string `json:"headRefOid"` + UpdatedAt string `json:"updatedAt"` + StatusCheckRollup []rawCheck `json:"statusCheckRollup"` +} + +type rawCheck struct { + Status string `json:"status"` + Conclusion string `json:"conclusion"` + State string `json:"state"` +} + +func rollupState(checks []rawCheck) string { + if len(checks) == 0 { + return "NONE" + } + failed, pending := 0, 0 + for _, c := range checks { + if c.State != "" { + switch c.State { + case "SUCCESS": + case "PENDING", "EXPECTED": + pending++ + default: + failed++ + } + continue + } + if c.Status != "COMPLETED" { + pending++ + continue + } + switch c.Conclusion { + case "SUCCESS", "SKIPPED", "NEUTRAL": + default: + failed++ + } + } + if failed > 0 { + return "FAILURE" + } + if pending > 0 { + return "PENDING" + } + return "SUCCESS" +} + +func ListPRs(ctx context.Context, limit int, includeBots bool) ([]PR, error) { + return listPRs(ctx, limit, includeBots, false) +} + +func RefreshPRs(ctx context.Context, limit int, includeBots bool) ([]PR, error) { + return listPRs(ctx, limit, includeBots, true) +} + +func listPRs(ctx context.Context, limit int, includeBots, refresh bool) ([]PR, error) { + cached, fetchedAt := cachedPRList(limit) + if !refresh && !fetchedAt.IsZero() && time.Since(fetchedAt) < prCacheTTL { + return filterPRs(cached, includeBots), nil + } + + out, err := exec.CommandContext(ctx, "gh", "pr", "list", + "--repo", Repo, + "--state", "open", + "--limit", fmt.Sprint(limit), + "--json", "number,title,author,headRefName,headRefOid,updatedAt,isDraft,statusCheckRollup", + ).CombinedOutput() + if err != nil { + err = commandError(err, out) + if len(cached) > 0 { + return filterPRs(cached, includeBots), fmt.Errorf("%w; showing cached PRs from %s", err, fetchedAt.Format(time.Kitchen)) + } + return nil, err + } + + var raws []rawPR + if err := json.Unmarshal(out, &raws); err != nil { + return nil, err + } + + prs := make([]PR, 0, len(raws)) + for _, r := range raws { + prs = append(prs, PR{ + Number: r.Number, + Title: r.Title, + Author: r.Author.Login, + HeadRef: r.HeadRefName, + HeadSHA: r.HeadRefOID, + CIState: rollupState(r.StatusCheckRollup), + IsDraft: r.IsDraft, + UpdatedAt: r.UpdatedAt, + }) + } + storePRList(limit, prs) + return filterPRs(prs, includeBots), nil +} + +func cachedPRList(limit int) ([]PR, time.Time) { + prListCache.Lock() + defer prListCache.Unlock() + entry := prListCache.entries[limit] + return append([]PR(nil), entry.prs...), entry.fetchedAt +} + +func storePRList(limit int, prs []PR) { + prListCache.Lock() + defer prListCache.Unlock() + prListCache.entries[limit] = cachedPRs{prs: append([]PR(nil), prs...), fetchedAt: time.Now()} +} + +func filterPRs(prs []PR, includeBots bool) []PR { + filtered := make([]PR, 0, len(prs)) + for _, pr := range prs { + if includeBots || !isBot(pr.Author) { + filtered = append(filtered, pr) + } + } + return filtered +} + +func commandError(err error, output []byte) error { + detail := strings.TrimSpace(string(output)) + if detail == "" { + return err + } + return fmt.Errorf("%s: %w", detail, err) +} + +func GetPR(ctx context.Context, number int) (*PR, error) { + out, err := exec.CommandContext(ctx, "gh", "pr", "view", fmt.Sprint(number), + "--repo", Repo, + "--json", "number,title,author,headRefName,headRefOid,updatedAt,isDraft,statusCheckRollup", + ).CombinedOutput() + if err != nil { + return nil, commandError(err, out) + } + + var r rawPR + if err := json.Unmarshal(out, &r); err != nil { + return nil, err + } + return &PR{ + Number: r.Number, + Title: r.Title, + Author: r.Author.Login, + HeadRef: r.HeadRefName, + HeadSHA: r.HeadRefOID, + CIState: rollupState(r.StatusCheckRollup), + IsDraft: r.IsDraft, + UpdatedAt: r.UpdatedAt, + }, nil +} diff --git a/tools/homarr-dev/internal/gh/gh_test.go b/tools/homarr-dev/internal/gh/gh_test.go new file mode 100644 index 000000000..64e14e20c --- /dev/null +++ b/tools/homarr-dev/internal/gh/gh_test.go @@ -0,0 +1,91 @@ +package gh + +import ( + "errors" + "os" + "strings" + "testing" + "time" +) + +func TestIsBot(t *testing.T) { + tests := []struct { + login string + want bool + }{ + {login: "app/homarr-renovate", want: true}, + {login: "dependabot[bot]", want: true}, + {login: "ajnart", want: false}, + } + + for _, test := range tests { + if got := isBot(test.login); got != test.want { + t.Errorf("isBot(%q) = %v, want %v", test.login, got, test.want) + } + } +} + +func TestRollupStateHandlesChecksAndStatuses(t *testing.T) { + tests := []struct { + name string + checks []rawCheck + want string + }{ + {name: "check success", checks: []rawCheck{{Status: "COMPLETED", Conclusion: "SUCCESS"}}, want: "SUCCESS"}, + {name: "status failure", checks: []rawCheck{{State: "FAILURE"}}, want: "FAILURE"}, + {name: "status pending", checks: []rawCheck{{State: "PENDING"}}, want: "PENDING"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := rollupState(test.checks); got != test.want { + t.Fatalf("rollupState() = %q, want %q", got, test.want) + } + }) + } +} + +func TestCachedPRListFiltersBotsWithoutMutatingCache(t *testing.T) { + limit := 9876 + storePRList(limit, []PR{{Number: 1, Author: "developer"}, {Number: 2, Author: "bot[bot]"}}) + + prs, fetchedAt := cachedPRList(limit) + if time.Since(fetchedAt) >= prCacheTTL { + t.Fatal("new cache entry is already expired") + } + if filtered := filterPRs(prs, false); len(filtered) != 1 || filtered[0].Number != 1 { + t.Fatalf("filtered PRs = %#v", filtered) + } + if unfiltered := filterPRs(prs, true); len(unfiltered) != 2 { + t.Fatalf("unfiltered PR count = %d, want 2", len(unfiltered)) + } +} + +func TestCommandErrorIncludesGitHubOutput(t *testing.T) { + err := commandError(errors.New("exit status 1"), []byte("GraphQL: API rate limit exceeded\n")) + if !strings.Contains(err.Error(), "API rate limit exceeded") { + t.Fatalf("error = %q", err) + } +} + +func TestListPRsExcludesBotsIntegration(t *testing.T) { + if os.Getenv("HOMARR_INTEGRATION_TEST") != "1" { + t.Skip("set HOMARR_INTEGRATION_TEST=1 to query GitHub") + } + + prs, err := ListPRs(t.Context(), 50, false) + if err != nil { + t.Fatal(err) + } + if len(prs) == 0 { + t.Fatal("expected at least one human-authored open PR") + } + for _, pr := range prs { + if isBot(pr.Author) { + t.Fatalf("bot PR was not filtered: #%d by %s", pr.Number, pr.Author) + } + if pr.HeadSHA == "" { + t.Fatalf("PR #%d has no head SHA", pr.Number) + } + } +} diff --git a/tools/homarr-dev/internal/platform/clipboard.go b/tools/homarr-dev/internal/platform/clipboard.go new file mode 100644 index 000000000..1dfdc7fdf --- /dev/null +++ b/tools/homarr-dev/internal/platform/clipboard.go @@ -0,0 +1,7 @@ +package platform + +import "github.com/atotto/clipboard" + +func CopyText(value string) error { + return clipboard.WriteAll(value) +} diff --git a/tools/homarr-dev/internal/platform/open.go b/tools/homarr-dev/internal/platform/open.go new file mode 100644 index 000000000..3be91d562 --- /dev/null +++ b/tools/homarr-dev/internal/platform/open.go @@ -0,0 +1,28 @@ +package platform + +import ( + "fmt" + "os/exec" + "runtime" +) + +func OpenURL(url string) error { + name, args, err := openCommand(runtime.GOOS, url) + if err != nil { + return err + } + return exec.Command(name, args...).Start() +} + +func openCommand(goos, url string) (string, []string, error) { + switch goos { + case "darwin": + return "open", []string{url}, nil + case "linux": + return "xdg-open", []string{url}, nil + case "windows": + return "rundll32", []string{"url.dll,FileProtocolHandler", url}, nil + default: + return "", nil, fmt.Errorf("opening URLs is not supported on %s", goos) + } +} diff --git a/tools/homarr-dev/internal/platform/open_test.go b/tools/homarr-dev/internal/platform/open_test.go new file mode 100644 index 000000000..413d9ee97 --- /dev/null +++ b/tools/homarr-dev/internal/platform/open_test.go @@ -0,0 +1,25 @@ +package platform + +import "testing" + +func TestOpenCommand(t *testing.T) { + tests := []struct { + goos string + name string + }{ + {goos: "darwin", name: "open"}, + {goos: "linux", name: "xdg-open"}, + {goos: "windows", name: "rundll32"}, + } + for _, test := range tests { + t.Run(test.goos, func(t *testing.T) { + name, args, err := openCommand(test.goos, "https://example.com") + if err != nil { + t.Fatal(err) + } + if name != test.name || len(args) == 0 { + t.Fatalf("command = %s %v", name, args) + } + }) + } +} diff --git a/tools/homarr-dev/internal/run/build.go b/tools/homarr-dev/internal/run/build.go new file mode 100644 index 000000000..de78fd291 --- /dev/null +++ b/tools/homarr-dev/internal/run/build.go @@ -0,0 +1,105 @@ +package run + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker" + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/gh" +) + +func BuildCurrentImage(tag, directory string) error { + return buildCurrentImage(context.Background(), tag, directory, os.Stdout, os.Stderr) +} + +func buildCurrentImage(ctx context.Context, tag, directory string, stdout, stderr io.Writer) error { + root, revision, err := checkoutDetails(ctx, directory) + if err != nil { + return err + } + return docker.BuildContext(ctx, docker.BuildOptions{Context: root, Tag: tag, Source: root, Revision: revision}, stdout, stderr) +} + +func BuildPRImage(number int, tag string) error { + return buildPRImage(context.Background(), number, tag, os.Stdout, os.Stderr) +} + +func buildPRImage(ctx context.Context, number int, tag string, stdout, stderr io.Writer) error { + if number <= 0 { + return fmt.Errorf("PR number must be positive") + } + temporaryRoot, err := os.MkdirTemp("", "homarr-pr-*") + if err != nil { + return fmt.Errorf("create temporary checkout: %w", err) + } + defer os.RemoveAll(temporaryRoot) + + checkout := filepath.Join(temporaryRoot, "homarr") + clone := exec.CommandContext(ctx, "gh", prCloneArgs(checkout)...) + clone.Stdout = stdout + clone.Stderr = stderr + if err := clone.Run(); err != nil { + return fmt.Errorf("clone %s: %w", gh.Repo, err) + } + command := exec.CommandContext(ctx, "gh", "pr", "checkout", strconv.Itoa(number), "--detach") + command.Dir = checkout + command.Stdout = stdout + command.Stderr = stderr + if err := command.Run(); err != nil { + return fmt.Errorf("checkout PR #%d: %w", number, err) + } + _, revision, err := checkoutDetails(ctx, checkout) + if err != nil { + return err + } + return docker.BuildContext(ctx, docker.BuildOptions{ + Context: checkout, + Tag: tag, + Source: fmt.Sprintf("https://github.com/%s/pull/%d", gh.Repo, number), + Revision: revision, + PRNumber: number, + }, stdout, stderr) +} + +func prCloneArgs(checkout string) []string { + return []string{"repo", "clone", gh.Repo, checkout, "--", "--depth=1", "--filter=blob:none"} +} + +func RebuildImage(image docker.Image) error { + return RebuildImageContext(context.Background(), image, os.Stdout, os.Stderr) +} + +func RebuildImageContext(ctx context.Context, image docker.Image, stdout, stderr io.Writer) error { + if image.PRNumber > 0 { + return buildPRImage(ctx, image.PRNumber, image.Tag, stdout, stderr) + } + if image.Source == "" { + return fmt.Errorf("homarr:%s has no build source metadata", image.Tag) + } + if info, err := os.Stat(image.Source); err != nil || !info.IsDir() { + return fmt.Errorf("build source %s is no longer available", image.Source) + } + return buildCurrentImage(ctx, image.Tag, image.Source, stdout, stderr) +} + +func checkoutDetails(ctx context.Context, directory string) (root string, revision string, err error) { + rootOutput, err := exec.CommandContext(ctx, "git", "-C", directory, "rev-parse", "--show-toplevel").CombinedOutput() + if err != nil { + return "", "", fmt.Errorf("find Homarr checkout: %w: %s", err, strings.TrimSpace(string(rootOutput))) + } + root = strings.TrimSpace(string(rootOutput)) + if _, err := os.Stat(filepath.Join(root, "Dockerfile")); err != nil { + return "", "", fmt.Errorf("%s is not a Homarr checkout", root) + } + revisionOutput, err := exec.CommandContext(ctx, "git", "-C", root, "rev-parse", "HEAD").CombinedOutput() + if err != nil { + return "", "", fmt.Errorf("read checkout revision: %w: %s", err, strings.TrimSpace(string(revisionOutput))) + } + return root, strings.TrimSpace(string(revisionOutput)), nil +} diff --git a/tools/homarr-dev/internal/run/run.go b/tools/homarr-dev/internal/run/run.go new file mode 100644 index 000000000..a144ee3df --- /dev/null +++ b/tools/homarr-dev/internal/run/run.go @@ -0,0 +1,119 @@ +package run + +import ( + "context" + "fmt" + "os" + + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker" + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/gh" +) + +type Options struct { + Context context.Context + PR int + Tag string + Demo bool + Env []string + FetchTitle bool + FindPort func(int) int +} + +type Plan struct { + Image string + Name string + Volume string + Label string + HostPort int + Env []string + Platform string + Pull bool + PRNumber int +} + +func BuildPlan(opts Options) (*Plan, error) { + p := &Plan{} + ctx := opts.Context + if ctx == nil { + ctx = context.Background() + } + findPort := opts.FindPort + if findPort == nil { + findPort = docker.FindFreePort + } + + if opts.PR > 0 { + p.PRNumber = opts.PR + p.Image = fmt.Sprintf("ghcr.io/homarr-labs/homarr-test:pr-%d", opts.PR) + p.Name = fmt.Sprintf("homarr_pr_%d", opts.PR) + p.Volume = fmt.Sprintf("homarr_pr_%d_data", opts.PR) + p.Label = fmt.Sprintf("PR #%d", opts.PR) + p.Pull = true + p.Platform = "linux/amd64" + if opts.FetchTitle { + if pr, err := gh.GetPR(ctx, opts.PR); err == nil && pr.Title != "" { + p.Label = fmt.Sprintf("PR #%d — %s", opts.PR, pr.Title) + } + } + } else if opts.Tag != "" { + p.Image = "homarr:" + opts.Tag + p.Name = "homarr_" + opts.Tag + p.Volume = "homarr_" + opts.Tag + "_data" + p.Label = "tag: " + opts.Tag + } else { + return nil, fmt.Errorf("need a tag or --pr") + } + + p.HostPort = findPort(7575) + if p.HostPort == 0 { + return nil, fmt.Errorf("no free host port is available") + } + p.Env = append(p.Env, opts.Env...) + if opts.Demo { + p.Env = append(p.Env, "DEMO_MODE=true", "UNSAFE_ENABLE_MOCK_INTEGRATION=true") + p.Label += " (demo)" + } + return p, nil +} + +func StartDetached(p *Plan) error { + return start(p, true) +} + +func StartForeground(p *Plan) error { + return start(p, false) +} + +func start(p *Plan, daemon bool) error { + const attempts = 5 + for attempt := 0; attempt < attempts; attempt++ { + err := docker.Start(docker.StartOptions{ + Name: p.Name, + Image: p.Image, + Volume: p.Volume, + HostPort: p.HostPort, + Env: p.Env, + Platform: p.Platform, + PullAlways: p.Pull && attempt == 0, + Daemon: daemon, + }) + if err == nil { + return nil + } + if !docker.IsPortConflict(err) || attempt == attempts-1 { + if docker.IsPortConflict(err) { + return fmt.Errorf("unable to bind a host port after %d attempts; restart Docker or OrbStack: %w", attempts, err) + } + return err + } + previousPort := p.HostPort + p.HostPort = docker.FindFreePort(p.HostPort + 1) + if p.HostPort == 0 { + return fmt.Errorf("no free host port is available: %w", err) + } + if !daemon { + fmt.Fprintf(os.Stderr, "Port %d unavailable; retrying on %d.\n", previousPort, p.HostPort) + } + } + return nil +} diff --git a/tools/homarr-dev/internal/run/run_test.go b/tools/homarr-dev/internal/run/run_test.go new file mode 100644 index 000000000..5bed7eac9 --- /dev/null +++ b/tools/homarr-dev/internal/run/run_test.go @@ -0,0 +1,59 @@ +package run + +import ( + "os" + "slices" + "strconv" + "testing" + + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker" +) + +func TestPRPlanUsesAMD64(t *testing.T) { + plan, err := BuildPlan(Options{PR: 1, FindPort: func(int) int { return 7575 }}) + if err != nil { + t.Fatal(err) + } + if plan.Platform != "linux/amd64" { + t.Fatalf("platform = %q, want linux/amd64", plan.Platform) + } +} + +func TestPRCloneIsShallow(t *testing.T) { + args := prCloneArgs("/tmp/checkout") + if !slices.Contains(args, "--depth=1") || !slices.Contains(args, "--filter=blob:none") { + t.Fatalf("clone args = %v", args) + } +} + +func TestBuildPlanFailsWhenNoPortIsAvailable(t *testing.T) { + _, err := BuildPlan(Options{Tag: "dev", FindPort: func(int) int { return 0 }}) + if err == nil { + t.Fatal("expected no-free-port error") + } +} + +func TestStartPRIntegration(t *testing.T) { + if os.Getenv("HOMARR_DOCKER_INTEGRATION_TEST") != "1" { + t.Skip("set HOMARR_DOCKER_INTEGRATION_TEST=1 and HOMARR_TEST_PR=") + } + pr, err := strconv.Atoi(os.Getenv("HOMARR_TEST_PR")) + if err != nil || pr <= 0 { + t.Fatal("HOMARR_TEST_PR must be a positive PR number") + } + + plan, err := BuildPlan(Options{PR: pr}) + if err != nil { + t.Fatal(err) + } + plan.Name += "_integration" + plan.Volume += "_integration" + defer docker.Remove(plan.Name) + + if err := StartDetached(plan); err != nil { + t.Fatal(err) + } + if !docker.IsRunning(plan.Name) { + t.Fatalf("container %s did not remain running", plan.Name) + } +} diff --git a/tools/homarr-dev/internal/tui/app.go b/tools/homarr-dev/internal/tui/app.go new file mode 100644 index 000000000..1af605250 --- /dev/null +++ b/tools/homarr-dev/internal/tui/app.go @@ -0,0 +1,122 @@ +package tui + +import ( + "charm.land/bubbles/v2/spinner" + tea "charm.land/bubbletea/v2" +) + +type appModel struct { + dev prsModel + dashboard dashModel + showDashboard bool + width int + height int +} + +func newAppModel(showDashboard bool) appModel { + return appModel{dev: newPRsModel(), dashboard: newDashModel(), showDashboard: showDashboard} +} + +func (m appModel) Init() tea.Cmd { + if m.showDashboard { + return m.dashboard.Init() + } + return m.dev.Init() +} + +func (m appModel) canSwitch() bool { + if m.showDashboard { + return !m.dashboard.filtering + } + return !m.dev.filtering && !m.dev.pulling +} + +func (m appModel) switchScreen() (tea.Model, tea.Cmd) { + m.showDashboard = !m.showDashboard + commands := make([]tea.Cmd, 0, 2) + if m.width > 0 && m.height > 0 { + resize := tea.WindowSizeMsg{Width: m.width, Height: m.height} + if m.showDashboard { + updated, command := m.dashboard.Update(resize) + m.dashboard = updated.(dashModel) + commands = append(commands, command) + } else { + updated, command := m.dev.Update(resize) + m.dev = updated.(prsModel) + commands = append(commands, command) + } + } + if m.showDashboard { + commands = append(commands, m.dashboard.Init()) + } else { + commands = append(commands, m.dev.Init()) + } + return m, tea.Batch(commands...) +} + +func (m appModel) updateDev(msg tea.Msg) (appModel, tea.Cmd) { + updated, command := m.dev.Update(msg) + m.dev = updated.(prsModel) + return m, command +} + +func (m appModel) updateDashboard(msg tea.Msg) (appModel, tea.Cmd) { + updated, command := m.dashboard.Update(msg) + m.dashboard = updated.(dashModel) + return m, command +} + +func (m appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + if window, ok := msg.(tea.WindowSizeMsg); ok { + m.width = window.Width + m.height = window.Height + m, devCommand := m.updateDev(msg) + m, dashboardCommand := m.updateDashboard(msg) + return m, tea.Batch(devCommand, dashboardCommand) + } + if key, ok := msg.(tea.KeyPressMsg); ok { + if key.String() == "d" && m.canSwitch() { + return m.switchScreen() + } + if m.showDashboard { + return m.updateDashboard(msg) + } + return m.updateDev(msg) + } + + switch msg.(type) { + case prsLoadedMsg, imageCheckedMsg, prActionMsg, rebuildDoneMsg, runningRefreshedMsg, pullReadyMsg, pullEvent, spinner.TickMsg: + return m.updateDev(msg) + case tickMsg, actionMsg, rowsMsg: + if m.showDashboard { + return m.updateDashboard(msg) + } + return m, nil + case prLogTickMsg: + if !m.showDashboard { + return m.updateDev(msg) + } + return m, nil + case logsMsg: + m, devCommand := m.updateDev(msg) + m, dashboardCommand := m.updateDashboard(msg) + return m, tea.Batch(devCommand, dashboardCommand) + } + if m.showDashboard { + return m.updateDashboard(msg) + } + return m.updateDev(msg) +} + +func (m appModel) View() tea.View { + if m.showDashboard { + return m.dashboard.View() + } + return m.dev.View() +} + +func runApp(showDashboard bool) error { + program := tea.NewProgram(newAppModel(showDashboard)) + _, err := program.Run() + return err +} diff --git a/tools/homarr-dev/internal/tui/app_test.go b/tools/homarr-dev/internal/tui/app_test.go new file mode 100644 index 000000000..1112ebb29 --- /dev/null +++ b/tools/homarr-dev/internal/tui/app_test.go @@ -0,0 +1,47 @@ +package tui + +import ( + "testing" + + tea "charm.land/bubbletea/v2" + + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/gh" +) + +func TestAppTogglePreservesDevelopmentState(t *testing.T) { + m := newAppModel(false) + m.dev.rows = []prRow{{kind: "remote", pr: gh.PR{Number: 6502}}} + + updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: 'd', Text: "d"})) + dashboard := updated.(appModel) + if !dashboard.showDashboard { + t.Fatal("d did not open the instance dashboard") + } + updated, _ = dashboard.Update(tea.KeyPressMsg(tea.Key{Code: 'd', Text: "d"})) + development := updated.(appModel) + if development.showDashboard || len(development.dev.rows) != 1 || development.dev.rows[0].pr.Number != 6502 { + t.Fatalf("development state was not preserved: %#v", development.dev.rows) + } +} + +func TestAppToggleDoesNotInterceptFilterInput(t *testing.T) { + m := newAppModel(false) + m.dev.filtering = true + m.dev.filter.Focus() + + updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: 'd', Text: "d"})) + got := updated.(appModel) + if got.showDashboard || got.dev.filter.Value() != "d" { + t.Fatalf("filter input was intercepted: dashboard=%v filter=%q", got.showDashboard, got.dev.filter.Value()) + } +} + +func TestAppCanOpenInstancesDuringBuild(t *testing.T) { + m := newAppModel(false) + m.dev.rebuilding = true + + updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: 'd', Text: "d"})) + if !updated.(appModel).showDashboard { + t.Fatal("background build blocked dashboard navigation") + } +} diff --git a/tools/homarr-dev/internal/tui/dashboard.go b/tools/homarr-dev/internal/tui/dashboard.go new file mode 100644 index 000000000..5cfe2d496 --- /dev/null +++ b/tools/homarr-dev/internal/tui/dashboard.go @@ -0,0 +1,415 @@ +package tui + +import ( + "context" + "fmt" + "os/exec" + "strings" + "time" + + "charm.land/bubbles/v2/table" + "charm.land/bubbles/v2/textinput" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker" + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/platform" +) + +var ( + titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")) + helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) + logHeadSt = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) + runningLegend = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render("● running") + stoppedLegend = lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("○ stopped") +) + +type dashModel struct { + table table.Model + allRows []table.Row + filter textinput.Model + filtering bool + logs viewport.Model + showLogs bool + follow bool + status string + confirmRemove string + width int + height int +} + +type tickMsg time.Time +type actionMsg string +type rowsMsg []table.Row +type logsMsg struct { + name string + content string +} + +func tick() tea.Cmd { + return tea.Tick(3*time.Second, func(t time.Time) tea.Msg { return tickMsg(t) }) +} + +func fetchLogs(name string) tea.Cmd { + return func() tea.Msg { + if name == "" { + return logsMsg{} + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + out, err := exec.CommandContext(ctx, "docker", "logs", "--tail", "200", name).CombinedOutput() + if err != nil { + return logsMsg{name: name, content: "logs unavailable: " + err.Error()} + } + return logsMsg{name: name, content: string(out)} + } +} + +func loadRows() tea.Cmd { + return func() tea.Msg { return rowsMsg(refreshRows()) } +} + +func refreshRows() []table.Row { + containers, _ := docker.List() + rows := make([]table.Row, 0, len(containers)) + for _, c := range containers { + port := c.HostPort() + url := "" + if port != "" && c.Running() { + url = "http://localhost:" + port + } + state := "● " + c.State + if !c.Running() { + state = "○ " + c.State + } + img := strings.TrimPrefix(c.Image, "ghcr.io/homarr-labs/") + rows = append(rows, table.Row{c.Name, state, img, port, url, c.Status}) + } + return rows +} + +func dashboardColumns(width int) []table.Column { + nameWidth := 26 + imageWidth := 30 + urlWidth := 24 + if width < 100 { + nameWidth = 22 + imageWidth = max(width-54, 18) + urlWidth = 0 + } + return []table.Column{ + {Title: "NAME", Width: nameWidth}, + {Title: "STATE", Width: 14}, + {Title: "IMAGE", Width: imageWidth}, + {Title: "PORT", Width: 6}, + {Title: "URL", Width: urlWidth}, + {Title: "STATUS", Width: 0}, + } +} + +func newDashModel() dashModel { + t := table.New( + table.WithColumns(dashboardColumns(104)), + table.WithFocused(true), + table.WithHeight(10), + table.WithWidth(104), + ) + s := table.DefaultStyles() + s.Header = s.Header.Bold(true) + s.Selected = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("230")).Background(lipgloss.Color("57")) + t.SetStyles(s) + logs := viewport.New(viewport.WithWidth(104), viewport.WithHeight(12)) + logs.SoftWrap = true + filter := textinput.New() + filter.Prompt = "/ " + filter.Placeholder = "filter name, image, port, state…" + filter.SetWidth(50) + styles := textinput.DefaultDarkStyles() + styles.Focused.Prompt = styles.Focused.Prompt.Foreground(lipgloss.Color("212")).Bold(true) + styles.Focused.Text = styles.Focused.Text.Foreground(lipgloss.Color("229")) + filter.SetStyles(styles) + return dashModel{table: t, filter: filter, logs: logs, showLogs: true, follow: true, width: 104, height: 30} +} + +func (m *dashModel) applyFilter() { + query := strings.ToLower(strings.TrimSpace(m.filter.Value())) + if query == "" { + m.table.SetRows(m.allRows) + return + } + rows := make([]table.Row, 0, len(m.allRows)) + for _, row := range m.allRows { + if strings.Contains(strings.ToLower(strings.Join(row, " ")), query) { + rows = append(rows, row) + } + } + m.table.SetRows(rows) +} + +func (m dashModel) selected() string { + row := m.table.SelectedRow() + if len(row) == 0 { + return "" + } + return row[0] +} + +func (m dashModel) selectedPort() string { + row := m.table.SelectedRow() + if len(row) < 4 { + return "" + } + return row[3] +} + +func (m dashModel) Init() tea.Cmd { + return tea.Batch(tick(), loadRows(), fetchLogs(m.selected())) +} + +func (m dashModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + m.table.SetColumns(dashboardColumns(msg.Width)) + m.table.SetWidth(msg.Width) + m.logs.SetWidth(msg.Width) + m.filter.SetWidth(max(min(msg.Width-4, 60), 10)) + m.layout() + return m, nil + case tickMsg: + return m, tea.Batch(tick(), loadRows()) + case rowsMsg: + m.allRows = []table.Row(msg) + m.applyFilter() + return m, fetchLogs(m.selected()) + case logsMsg: + if msg.name != "" && msg.name != m.selected() { + return m, nil + } + m.logs.SetContent(strings.TrimRight(msg.content, "\n")) + if m.follow { + m.logs.GotoBottom() + } + return m, nil + case actionMsg: + m.status = string(msg) + m.confirmRemove = "" + return m, loadRows() + case tea.KeyPressMsg: + if m.filtering { + switch msg.Key().Code { + case tea.KeyEscape: + m.filtering = false + m.filter.Reset() + m.filter.Blur() + m.applyFilter() + return m, fetchLogs(m.selected()) + case tea.KeyEnter: + m.filtering = false + m.filter.Blur() + return m, fetchLogs(m.selected()) + } + var cmd tea.Cmd + m.filter, cmd = m.filter.Update(msg) + m.applyFilter() + return m, tea.Batch(cmd, fetchLogs(m.selected())) + } + name := m.selected() + switch msg.Key().Code { + case tea.KeyUp, tea.KeyKpUp: + m.table.MoveUp(1) + return m, fetchLogs(m.selected()) + case tea.KeyDown, tea.KeyKpDown: + m.table.MoveDown(1) + return m, fetchLogs(m.selected()) + case tea.KeyPgUp, tea.KeyKpPgUp: + m.follow = false + m.logs.PageUp() + return m, nil + case tea.KeyPgDown, tea.KeyKpPgDown: + m.logs.PageDown() + m.follow = m.logs.AtBottom() + return m, nil + } + switch msg.String() { + case "q", "ctrl+c", "esc": + return m, tea.Quit + case "l": + m.showLogs = !m.showLogs + m.layout() + if m.showLogs { + return m, fetchLogs(name) + } + return m, nil + case "/": + m.filtering = true + return m, m.filter.Focus() + case "f": + m.follow = !m.follow + if m.follow { + m.logs.GotoBottom() + } + return m, nil + case "s": + if name != "" { + m.status = "stopping " + name + "…" + return m, func() tea.Msg { + if err := docker.Stop(name); err != nil { + return actionMsg("stop failed: " + err.Error()) + } + return actionMsg("stopped " + name) + } + } + case "x": + if name != "" { + if m.confirmRemove != name { + m.confirmRemove = name + m.status = "press x again to remove " + name + return m, nil + } + m.status = "removing " + name + "…" + return m, func() tea.Msg { + if err := docker.Remove(name); err != nil { + return actionMsg("remove failed: " + err.Error()) + } + return actionMsg("removed " + name) + } + } + case "R": + if name != "" { + m.status = "restarting " + name + "…" + return m, func() tea.Msg { + if err := docker.Restart(name); err != nil { + return actionMsg("restart failed: " + err.Error()) + } + return actionMsg("restarted " + name) + } + } + case "o": + if name != "" { + if port := m.selectedPort(); port != "" { + if err := platform.OpenURL("http://localhost:" + port); err != nil { + m.status = "open failed: " + err.Error() + } + } + } + case "c": + if name != "" { + if port := m.selectedPort(); port != "" { + url := "http://localhost:" + port + if err := platform.CopyText(url); err != nil { + m.status = "copy failed: " + err.Error() + } else { + m.status = "copied " + url + } + } + } + case "r": + return m, loadRows() + case "k": + m.table.MoveUp(1) + return m, fetchLogs(m.selected()) + case "j": + m.table.MoveDown(1) + return m, fetchLogs(m.selected()) + } + } + var cmd tea.Cmd + m.table, cmd = m.table.Update(msg) + return m, cmd +} + +func (m *dashModel) layout() { + available := m.height - 11 + if m.filtering || m.filter.Value() != "" { + available -= 2 + } + if !m.showLogs { + m.table.SetHeight(max(available, 5)) + return + } + tableH := min(12, max(available/2, 5)) + m.table.SetHeight(tableH) + logsH := available - tableH + if logsH < 3 { + logsH = 3 + } + m.logs.SetHeight(logsH) +} + +func (m dashModel) selectedDetailView() string { + row := m.table.SelectedRow() + if len(row) < 6 { + return "" + } + title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("229")).Render(row[0]) + meta := row[2] + " · " + row[1] + if row[3] != "" { + meta += " · port " + row[3] + } + if row[4] != "" { + meta += " · " + row[4] + } + if row[5] != "" { + meta += " · " + row[5] + } + return title + "\n" + helpStyle.Render(meta) +} + +func (m dashModel) View() tea.View { + var b strings.Builder + running := 0 + for _, row := range m.allRows { + if len(row) > 1 && strings.Contains(row[1], "running") { + running++ + } + } + b.WriteString(titleStyle.Render("🦞 homarr instances") + " " + helpStyle.Render(fmt.Sprintf("%d total · %d running · %d shown", len(m.allRows), running, len(m.table.Rows())))) + b.WriteString("\n\n") + if m.filtering || m.filter.Value() != "" { + b.WriteString(m.filter.View() + "\n\n") + } + if len(m.table.Rows()) == 0 { + message := "no homarr containers — start one with `homarr run ` or `homarr run --pr N`" + if len(m.allRows) > 0 { + message = "no instances match " + m.filter.Value() + } + b.WriteString(helpStyle.Render(" " + message + "\n")) + } else { + b.WriteString(m.table.View()) + b.WriteString("\n") + } + if detail := m.selectedDetailView(); detail != "" { + b.WriteString("\n" + detail + "\n") + } + if m.showLogs && len(m.table.Rows()) > 0 { + mode := "paused" + if m.follow { + mode = "following" + } + b.WriteString("\n" + logHeadSt.Render("── logs: "+m.selected()+" ("+mode+") ──") + "\n") + b.WriteString(m.logs.View()) + } + if m.status != "" { + statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("42")) + lower := strings.ToLower(m.status) + if strings.Contains(lower, "fail") || strings.Contains(lower, "error") { + statusStyle = statusStyle.Foreground(lipgloss.Color("196")) + } else if strings.Contains(lower, "press x") { + statusStyle = statusStyle.Foreground(lipgloss.Color("214")) + } + b.WriteString("\n" + statusStyle.Render(m.status)) + } + b.WriteString("\n\n" + helpStyle.Render("/ filter · ↑/↓ select · l logs · f follow · pgup/pgdn scroll · s stop · R restart · x remove · o open · c copy URL · r refresh · d development · q quit")) + b.WriteString("\n" + helpStyle.Render(runningLegend+" · "+stoppedLegend)) + v := tea.NewView(b.String()) + v.AltScreen = true + v.WindowTitle = "🦞 Homarr instances" + return v +} + +func RunDashboard() error { + return runApp(true) +} diff --git a/tools/homarr-dev/internal/tui/dashboard_test.go b/tools/homarr-dev/internal/tui/dashboard_test.go new file mode 100644 index 000000000..4456dc117 --- /dev/null +++ b/tools/homarr-dev/internal/tui/dashboard_test.go @@ -0,0 +1,62 @@ +package tui + +import ( + "strings" + "testing" + + "charm.land/bubbles/v2/table" + tea "charm.land/bubbletea/v2" +) + +func TestDashboardRendersRowsAndLogs(t *testing.T) { + m := newDashModel() + m.table.SetRows([]table.Row{{ + "homarr_pr_6441", + "running", + "homarr-test:pr-6441", + "7575", + "http://localhost:7575", + "Up 2 minutes", + }}) + m.logs.SetWidth(104) + m.logs.SetHeight(5) + m.logs.SetContent("server ready") + + got := m.View().Content + for _, want := range []string{"homarr_pr_6441", "server ready"} { + if !strings.Contains(got, want) { + t.Fatalf("dashboard omitted %q: %q", want, got) + } + } + if !m.View().AltScreen { + t.Fatal("dashboard must use the alternate screen") + } +} + +func TestDashboardArrowNavigation(t *testing.T) { + m := newDashModel() + m.table.SetRows([]table.Row{ + {"homarr_first", "running", "homarr:first", "7575", "", "Up 1 minute"}, + {"homarr_second", "running", "homarr:second", "7576", "", "Up 1 minute"}, + }) + + updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown})) + got := updated.(dashModel).selected() + if got != "homarr_second" { + t.Fatalf("down arrow selected %q, want homarr_second", got) + } +} + +func TestDashboardIgnoresStaleLogs(t *testing.T) { + m := newDashModel() + m.table.SetRows([]table.Row{{"homarr_current", "running", "homarr:current", "7575", "", "Up 1 minute"}}) + m.logs.SetWidth(104) + m.logs.SetHeight(5) + m.logs.SetContent("current logs") + + updated, _ := m.Update(logsMsg{name: "homarr_old", content: "stale logs"}) + got := updated.(dashModel).View().Content + if strings.Contains(got, "stale logs") || !strings.Contains(got, "current logs") { + t.Fatalf("stale log result replaced current content: %q", got) + } +} diff --git a/tools/homarr-dev/internal/tui/dev.go b/tools/homarr-dev/internal/tui/dev.go new file mode 100644 index 000000000..6a41d9d9a --- /dev/null +++ b/tools/homarr-dev/internal/tui/dev.go @@ -0,0 +1,1158 @@ +package tui + +import ( + "bufio" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "charm.land/bubbles/v2/progress" + "charm.land/bubbles/v2/spinner" + "charm.land/bubbles/v2/table" + "charm.land/bubbles/v2/textinput" + "charm.land/bubbles/v2/viewport" + tea "charm.land/bubbletea/v2" + "charm.land/lipgloss/v2" + + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker" + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/gh" + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/platform" + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/run" +) + +const ( + ciOK = "✓" + ciFail = "✗" + ciPend = "●" + ciNone = "–" + imgYes = "✓" + imgNo = "…" + imgCheck = "?" + imgError = "!" + imgLocal = "◆" + runMark = "▶" + idleMark = " " +) + +var ( + legendCIYes = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render(ciOK) + legendCINo = lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render(ciFail) + legendCIPend = lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Render(ciPend) + legendNone = lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(ciNone) + legendImageYes = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render(imgYes) + legendImageNo = lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render(imgNo) + legendLocal = lipgloss.NewStyle().Foreground(lipgloss.Color("81")).Render(imgLocal) +) + +type prRow struct { + kind string + pr gh.PR + local docker.Image + imageState string + running bool + port string +} + +func (row prRow) hasCurrentLocalImage() bool { + return row.kind == "remote" && row.local.Tag != "" && row.pr.HeadSHA != "" && row.local.Revision == row.pr.HeadSHA +} + +type prsModel struct { + table table.Model + filter textinput.Model + filtering bool + pullView viewport.Model + pulling bool + pullLines []string + pullLayers map[string]string + pullProgress progress.Model + pullCanceled bool + pullEvents <-chan pullEvent + pullPlan *run.Plan + pullCancel context.CancelFunc + rebuilding bool + rebuildCancel context.CancelFunc + logs viewport.Model + showLogs bool + followLogs bool + logsName string + spinner spinner.Model + rows []prRow + loading bool + status string + includeBots bool + demo bool + width int + height int + imageQueue []int + imageActive int + imageGen int +} + +type prsLoadedMsg struct { + prs []gh.PR + images []docker.Image + prErr error + imgErr error + refresh bool +} +type imageCheckedMsg struct { + number int + exists bool + err error + gen int +} +type prActionMsg struct { + text string +} +type rebuildDoneMsg struct { + image docker.Image + err error + canceled bool + action string +} +type runningRefreshedMsg struct { + rows []prRow + generation int +} +type pullReadyMsg struct{} +type prLogTickMsg struct{} +type pullEvent struct { + line string + done bool + err error +} + +type outputTail struct { + data []byte +} + +func (tail *outputTail) Write(data []byte) (int, error) { + const limit = 32 * 1024 + tail.data = append(tail.data, data...) + if len(tail.data) > limit { + tail.data = tail.data[len(tail.data)-limit:] + } + return len(data), nil +} + +func (tail *outputTail) lastLine() string { + lines := strings.FieldsFunc(string(tail.data), func(char rune) bool { return char == '\n' || char == '\r' }) + if len(lines) == 0 { + return "" + } + return strings.TrimSpace(lines[len(lines)-1]) +} + +func splitDockerProgress(data []byte, atEOF bool) (advance int, token []byte, err error) { + for i, b := range data { + if b == '\n' || b == '\r' { + return i + 1, data[:i], nil + } + } + if atEOF && len(data) > 0 { + return len(data), data, nil + } + return 0, nil, nil +} + +func startPull(ctx context.Context, cmd *exec.Cmd, events chan<- pullEvent) tea.Cmd { + return func() tea.Msg { + reader, writer, err := os.Pipe() + if err != nil { + return pullEvent{done: true, err: err} + } + cmd.Stdout = writer + cmd.Stderr = writer + if err := cmd.Start(); err != nil { + _ = reader.Close() + _ = writer.Close() + return pullEvent{done: true, err: err} + } + _ = writer.Close() + go func() { + defer close(events) + defer reader.Close() + scanner := bufio.NewScanner(reader) + scanner.Split(splitDockerProgress) + lastLine := "" + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line != "" { + lastLine = line + if ctx.Err() == nil && len(events) < cap(events)-1 { + events <- pullEvent{line: line} + } + } + } + err := cmd.Wait() + if err == nil { + err = scanner.Err() + } else { + err = commandErrorWithDetail(err, lastLine) + } + events <- pullEvent{done: true, err: err} + }() + return pullReadyMsg{} + } +} + +func commandErrorWithDetail(err error, lastLine string) error { + if lastLine == "" { + return err + } + return fmt.Errorf("%w: %s", err, lastLine) +} + +func waitPull(events <-chan pullEvent) tea.Cmd { + return func() tea.Msg { + event, ok := <-events + if !ok { + return nil + } + return event + } +} + +func prLogTick() tea.Cmd { + return tea.Tick(2*time.Second, func(time.Time) tea.Msg { return prLogTickMsg{} }) +} + +func loadDev(includeBots, refresh bool) tea.Cmd { + return func() tea.Msg { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + var prs []gh.PR + var prErr error + if refresh { + prs, prErr = gh.RefreshPRs(ctx, 50, includeBots) + } else { + prs, prErr = gh.ListPRs(ctx, 50, includeBots) + } + images, imgErr := docker.ListLocalImages(ctx) + return prsLoadedMsg{prs: prs, images: images, prErr: prErr, imgErr: imgErr, refresh: refresh} + } +} + +func checkImage(number, generation int) tea.Cmd { + return func() tea.Msg { + img := fmt.Sprintf("ghcr.io/homarr-labs/homarr-test:pr-%d", number) + exists, err := docker.CheckImage(img) + return imageCheckedMsg{number: number, exists: exists, err: err, gen: generation} + } +} + +func (m *prsModel) startImageChecks() tea.Cmd { + const concurrency = 6 + cmds := make([]tea.Cmd, 0, concurrency) + for m.imageActive < concurrency && len(m.imageQueue) > 0 { + number := m.imageQueue[0] + m.imageQueue = m.imageQueue[1:] + m.imageActive++ + cmds = append(cmds, checkImage(number, m.imageGen)) + } + return tea.Batch(cmds...) +} + +func ciIcon(state string) string { + switch state { + case "SUCCESS": + return ciOK + case "FAILURE": + return ciFail + case "PENDING": + return ciPend + default: + return ciNone + } +} + +func buildTableRows(rows []prRow) []table.Row { + out := make([]table.Row, 0, len(rows)) + for _, r := range rows { + mark := idleMark + port := "" + if r.running { + mark = runMark + port = r.port + } + img := imgCheck + switch r.imageState { + case "yes": + img = imgYes + case "no": + img = imgNo + case "error": + img = imgError + } + if r.hasCurrentLocalImage() { + img = imgLocal + } + source := "remote" + ref := fmt.Sprint(r.pr.Number) + title := r.pr.Title + author := r.pr.Author + ci := ciIcon(r.pr.CIState) + if r.kind == "local" { + source = "local" + ref = r.local.Tag + title = r.local.Reference() + author = "untracked build" + if r.local.Source != "" { + author = filepath.Base(r.local.Source) + } + ci = ciNone + } else if r.pr.IsDraft { + title = "DRAFT · " + title + } + if len([]rune(title)) > 44 { + title = string([]rune(title)[:43]) + "…" + } + out = append(out, table.Row{ + mark, + source, + ref, + title, + author, + ci, + img, + port, + }) + } + return out +} + +func developmentRows(prs []gh.PR, images []docker.Image, imageStates map[int]string) ([]prRow, []int) { + rows := make([]prRow, 0, len(prs)+len(images)) + prIndexes := make(map[int]int, len(prs)) + queue := make([]int, 0, len(prs)) + for _, pr := range prs { + imageState := imageStates[pr.Number] + prIndexes[pr.Number] = len(rows) + rows = append(rows, prRow{kind: "remote", pr: pr, imageState: imageState}) + } + for _, image := range images { + if index, exists := prIndexes[image.PRNumber]; exists && image.Revision != "" && image.Revision == rows[index].pr.HeadSHA { + if rows[index].local.Tag == "" { + rows[index].local = image + } + continue + } + rows = append(rows, prRow{kind: "local", local: image, imageState: "yes"}) + } + for index := range rows { + if rows[index].kind != "remote" || rows[index].imageState != "" { + continue + } + if rows[index].hasCurrentLocalImage() { + rows[index].imageState = "unchecked" + continue + } + rows[index].imageState = "checking" + queue = append(queue, rows[index].pr.Number) + } + return rows, queue +} + +func prColumns(width int) []table.Column { + authorWidth := 16 + portWidth := 6 + if width < 90 { + authorWidth = 0 + } + if width < 72 { + portWidth = 0 + } + titleWidth := max(min(width-60, 45), 18) + return []table.Column{ + {Title: "", Width: 2}, + {Title: "SOURCE", Width: 8}, + {Title: "REF", Width: 12}, + {Title: "TITLE", Width: titleWidth}, + {Title: "AUTHOR", Width: authorWidth}, + {Title: "CI", Width: 3}, + {Title: "IMAGE", Width: 5}, + {Title: "PORT", Width: portWidth}, + } +} + +func newPRsModel() prsModel { + t := table.New( + table.WithColumns(prColumns(110)), + table.WithFocused(true), + table.WithHeight(18), + table.WithWidth(110), + ) + s := table.DefaultStyles() + s.Header = s.Header.Bold(true) + s.Selected = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("230")).Background(lipgloss.Color("57")) + t.SetStyles(s) + + sp := spinner.New() + sp.Spinner = spinner.Dot + filter := textinput.New() + filter.Prompt = "/ " + filter.Placeholder = "filter source, PR, image, title, author, branch, CI, port…" + filter.SetWidth(65) + filterStyles := textinput.DefaultDarkStyles() + filterStyles.Focused.Prompt = filterStyles.Focused.Prompt.Foreground(lipgloss.Color("212")).Bold(true) + filterStyles.Focused.Text = filterStyles.Focused.Text.Foreground(lipgloss.Color("229")) + filter.SetStyles(filterStyles) + pullView := viewport.New(viewport.WithWidth(110), viewport.WithHeight(7)) + pullView.SoftWrap = true + pullView.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("250")) + pullProgress := progress.New(progress.WithWidth(60), progress.WithColors(lipgloss.Color("212"))) + logs := viewport.New(viewport.WithWidth(110), viewport.WithHeight(7)) + logs.SoftWrap = true + logs.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("250")) + + return prsModel{table: t, filter: filter, pullView: pullView, pullLayers: make(map[string]string), pullProgress: pullProgress, logs: logs, followLogs: true, spinner: sp, loading: true, status: "loading development images…", width: 110, height: 32} +} + +func (m *prsModel) layout() { + reserved := 12 + if m.filtering || m.filter.Value() != "" { + reserved += 2 + } + if m.pulling { + reserved += 9 + } + if m.showLogs { + reserved += 9 + } + m.table.SetHeight(max(m.height-reserved, 6)) + m.pullView.SetWidth(max(m.width-2, 20)) + m.pullView.SetHeight(7) + m.pullProgress.SetWidth(max(min(m.width-12, 70), 20)) + m.logs.SetWidth(max(m.width-2, 20)) + m.logs.SetHeight(7) +} + +func (m *prsModel) applyFilter() { + rendered := buildTableRows(m.rows) + query := strings.ToLower(strings.TrimSpace(m.filter.Value())) + if query == "" { + m.table.SetRows(rendered) + return + } + rows := make([]table.Row, 0, len(rendered)) + for i, row := range m.rows { + terms := []string{ + row.kind, + fmt.Sprint(row.pr.Number), + fmt.Sprintf("#%d", row.pr.Number), + fmt.Sprintf("pr-%d", row.pr.Number), + row.pr.Title, + row.pr.Author, + row.pr.HeadRef, + row.pr.CIState, + row.imageState, + row.port, + row.local.Tag, + row.local.Source, + row.local.Revision, + } + if row.pr.IsDraft { + terms = append(terms, "draft") + } + if row.running { + terms = append(terms, "running started") + } + if strings.Contains(strings.ToLower(strings.Join(terms, " ")), query) { + rows = append(rows, rendered[i]) + } + } + m.table.SetRows(rows) +} + +func (m prsModel) selectedRow() *prRow { + selected := m.table.SelectedRow() + if len(selected) < 3 { + return nil + } + for i := range m.rows { + if m.rows[i].kind == selected[1] && ((selected[1] == "remote" && fmt.Sprint(m.rows[i].pr.Number) == selected[2]) || (selected[1] == "local" && m.rows[i].local.Tag == selected[2])) { + return &m.rows[i] + } + } + return nil +} + +func (m prsModel) selectedPR() *prRow { + row := m.selectedRow() + if row == nil || row.kind != "remote" { + return nil + } + return row +} + +func localBuildForRow(row prRow) (docker.Image, string, string) { + if row.kind == "remote" { + image := docker.Image{Tag: fmt.Sprintf("pr-%d", row.pr.Number), PRNumber: row.pr.Number} + return image, "built", fmt.Sprintf("building PR #%d locally as %s…", row.pr.Number, image.Reference()) + } + return row.local, "rebuilt", "rebuilding " + row.local.Reference() + "…" +} + +func localPRPlan(row prRow, demo bool, findPort func(int) int) (*run.Plan, error) { + plan, err := run.BuildPlan(run.Options{Context: context.Background(), Tag: row.local.Tag, Demo: demo, FindPort: findPort}) + if err != nil { + return nil, err + } + plan.Name = fmt.Sprintf("homarr_pr_%d", row.pr.Number) + plan.Volume = fmt.Sprintf("homarr_pr_%d_data", row.pr.Number) + plan.Label = fmt.Sprintf("PR #%d · local %s", row.pr.Number, truncateText(row.pr.HeadSHA, 12)) + plan.PRNumber = row.pr.Number + return plan, nil +} + +func (m *prsModel) refreshSelectedLogs() tea.Cmd { + row := m.selectedRow() + if row == nil || !row.running { + m.logsName = "" + m.logs.SetContent("selected image is not running") + return nil + } + name := fmt.Sprintf("homarr_pr_%d", row.pr.Number) + if row.kind == "local" { + name = "homarr_" + row.local.Tag + } + if name != m.logsName { + m.logsName = name + m.logs.SetContent("loading logs…") + } + return fetchLogs(name) +} + +func (m *prsModel) beginPull(row prRow) tea.Cmd { + plan, err := run.BuildPlan(run.Options{Context: context.Background(), PR: row.pr.Number, Demo: m.demo}) + if err != nil { + m.status = err.Error() + return nil + } + if row.running && row.port != "" { + if port, err := strconv.Atoi(row.port); err == nil { + plan.HostPort = port + } + } + ctx, cancel := context.WithCancel(context.Background()) + events := make(chan pullEvent, 128) + m.pulling = true + m.pullPlan = plan + m.pullCancel = cancel + m.pullEvents = events + m.pullLines = nil + m.pullLayers = make(map[string]string) + m.pullCanceled = false + m.pullView.SetContent("") + m.showLogs = false + m.logsName = "" + if row.running { + m.status = fmt.Sprintf("pulling latest image and redeploying PR #%d…", row.pr.Number) + } else { + m.status = fmt.Sprintf("pulling latest image for PR #%d…", row.pr.Number) + } + m.layout() + return tea.Batch(startPull(ctx, docker.PullCommandContext(ctx, plan.Image, plan.Platform), events), m.spinner.Tick) +} + +func (m *prsModel) appendPullLine(line string) { + const retainedLines = 40 + m.pullLines = append(m.pullLines, line) + if len(m.pullLines) > retainedLines { + m.pullLines = m.pullLines[len(m.pullLines)-retainedLines:] + } + m.pullView.SetContent(strings.Join(m.pullLines, "\n")) + m.pullView.GotoBottom() + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + layer := strings.TrimSpace(parts[0]) + if len(layer) == 12 { + m.pullLayers[layer] = strings.TrimSpace(parts[1]) + } + } +} + +func (m prsModel) pullCompletion() (complete, total int, percent float64) { + for _, state := range m.pullLayers { + total++ + lower := strings.ToLower(state) + if strings.Contains(lower, "pull complete") || strings.Contains(lower, "already exists") { + complete++ + } + } + if total > 0 { + percent = float64(complete) / float64(total) + } + return complete, total, percent +} + +func (m prsModel) Init() tea.Cmd { + return tea.Batch(loadDev(m.includeBots, false), m.spinner.Tick) +} + +func refreshRunning(rows []prRow, generation int) tea.Cmd { + return func() tea.Msg { + containers, _ := docker.List() + updated := append([]prRow(nil), rows...) + for index, row := range updated { + name := fmt.Sprintf("homarr_pr_%d", row.pr.Number) + if row.kind == "local" { + name = "homarr_" + row.local.Tag + } + row.running = false + row.port = "" + for _, container := range containers { + if container.Name == name && container.Running() { + row.running = true + row.port = container.HostPort() + } + } + updated[index] = row + } + return runningRefreshedMsg{rows: updated, generation: generation} + } +} + +func (m prsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width = msg.Width + m.height = msg.Height + m.table.SetColumns(prColumns(msg.Width)) + m.table.SetWidth(msg.Width) + m.filter.SetWidth(max(min(msg.Width-4, 75), 10)) + m.layout() + return m, nil + case prsLoadedMsg: + if msg.prErr != nil && msg.imgErr != nil { + m.loading = false + m.status = "failed to load development images: " + msg.prErr.Error() + "; " + msg.imgErr.Error() + return m, nil + } + m.loading = false + m.status = fmt.Sprintf("%d remote PRs · %d local images", len(msg.prs), len(msg.images)) + if msg.prErr != nil { + m.status = "remote PRs unavailable: " + msg.prErr.Error() + } else if msg.imgErr != nil { + m.status = "local images unavailable: " + msg.imgErr.Error() + } + imageStates := make(map[int]string, len(m.rows)) + if !msg.refresh || msg.prErr != nil { + for _, row := range m.rows { + if row.kind == "remote" && (row.imageState == "yes" || row.imageState == "no") { + imageStates[row.pr.Number] = row.imageState + } + } + } + m.rows, m.imageQueue = developmentRows(msg.prs, msg.images, imageStates) + m.imageGen++ + m.imageActive = 0 + m.applyFilter() + return m, tea.Batch(m.startImageChecks(), refreshRunning(m.rows, m.imageGen)) + case imageCheckedMsg: + if msg.gen != m.imageGen { + return m, nil + } + m.imageActive-- + for i, r := range m.rows { + if r.pr.Number == msg.number { + if msg.err != nil { + m.rows[i].imageState = "error" + m.status = msg.err.Error() + } else if msg.exists { + m.rows[i].imageState = "yes" + } else { + m.rows[i].imageState = "no" + } + } + } + m.applyFilter() + return m, m.startImageChecks() + case prActionMsg: + m.status = msg.text + return m, refreshRunning(m.rows, m.imageGen) + case runningRefreshedMsg: + if msg.generation != m.imageGen { + return m, nil + } + m.rows = msg.rows + m.applyFilter() + return m, m.refreshSelectedLogs() + case rebuildDoneMsg: + m.rebuilding = false + if m.rebuildCancel != nil { + m.rebuildCancel() + } + m.rebuildCancel = nil + m.loading = true + action := msg.action + if action == "" { + action = "rebuilt" + } + if msg.canceled { + m.status = action + " canceled" + } else if msg.err != nil { + m.status = action + " failed: " + msg.err.Error() + } else { + m.status = action + " " + msg.image.Reference() + } + return m, tea.Batch(loadDev(m.includeBots, true), m.spinner.Tick) + case logsMsg: + if !m.showLogs || msg.name == "" || msg.name != m.logsName { + return m, nil + } + m.logs.SetContent(strings.TrimRight(msg.content, "\n")) + if m.followLogs { + m.logs.GotoBottom() + } + return m, nil + case prLogTickMsg: + if !m.showLogs { + return m, nil + } + return m, tea.Batch(m.refreshSelectedLogs(), prLogTick()) + case pullReadyMsg: + return m, waitPull(m.pullEvents) + case pullEvent: + if !msg.done { + m.appendPullLine(msg.line) + return m, waitPull(m.pullEvents) + } + m.pulling = false + m.pullEvents = nil + if m.pullCancel != nil { + m.pullCancel() + } + m.pullCancel = nil + m.layout() + if msg.err != nil { + if m.pullCanceled { + m.status = "pull canceled" + } else { + m.status = "pull failed: " + msg.err.Error() + } + m.pullCanceled = false + m.pullPlan = nil + return m, nil + } + m.pullCanceled = false + plan := m.pullPlan + m.pullPlan = nil + if plan == nil { + return m, nil + } + for i := range m.rows { + if m.rows[i].pr.Number == plan.PRNumber { + m.rows[i].imageState = "yes" + } + } + m.applyFilter() + m.status = fmt.Sprintf("starting PR #%d…", plan.PRNumber) + return m, func() tea.Msg { + plan.Pull = false + if err := run.StartDetached(plan); err != nil { + return prActionMsg{"start failed: " + err.Error()} + } + return prActionMsg{fmt.Sprintf("started PR #%d → http://localhost:%d", plan.PRNumber, plan.HostPort)} + } + case spinner.TickMsg: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + if m.loading || m.pulling || m.rebuilding { + return m, cmd + } + return m, nil + case tea.KeyPressMsg: + if m.rebuilding { + switch msg.String() { + case "q", "ctrl+c", "esc": + if m.rebuildCancel != nil { + m.rebuildCancel() + } + m.status = "canceling rebuild…" + return m, nil + } + } + if m.pulling { + switch msg.String() { + case "q", "ctrl+c", "esc": + if m.pullCancel != nil { + m.pullCanceled = true + m.pullCancel() + } + m.status = "canceling pull…" + } + return m, nil + } + if m.filtering { + switch msg.Key().Code { + case tea.KeyEscape: + m.filtering = false + m.filter.Reset() + m.filter.Blur() + m.applyFilter() + m.layout() + return m, nil + case tea.KeyEnter: + m.filtering = false + m.filter.Blur() + m.layout() + return m, nil + } + var cmd tea.Cmd + m.filter, cmd = m.filter.Update(msg) + m.applyFilter() + return m, cmd + } + switch msg.Key().Code { + case tea.KeyUp, tea.KeyKpUp: + m.table.MoveUp(1) + return m, m.refreshSelectedLogs() + case tea.KeyDown, tea.KeyKpDown: + m.table.MoveDown(1) + return m, m.refreshSelectedLogs() + case tea.KeyPgUp, tea.KeyKpPgUp: + if m.showLogs { + m.followLogs = false + m.logs.PageUp() + } + return m, nil + case tea.KeyPgDown, tea.KeyKpPgDown: + if m.showLogs { + m.logs.PageDown() + m.followLogs = m.logs.AtBottom() + } + return m, nil + } + switch msg.String() { + case "q", "ctrl+c", "esc": + return m, tea.Quit + case "enter", "space": + row := m.selectedRow() + if row == nil { + return m, nil + } + name := fmt.Sprintf("homarr_pr_%d", row.pr.Number) + label := fmt.Sprintf("PR #%d", row.pr.Number) + if row.kind == "local" { + name = "homarr_" + row.local.Tag + label = row.local.Reference() + } + if row.running { + return m, func() tea.Msg { + if err := docker.Stop(name); err != nil { + return prActionMsg{"stop failed: " + err.Error()} + } + return prActionMsg{"stopped " + label} + } + } + if row.kind == "remote" { + if row.hasCurrentLocalImage() { + plan, err := localPRPlan(*row, m.demo, nil) + if err != nil { + m.status = err.Error() + return m, nil + } + m.status = fmt.Sprintf("starting PR #%d from local build…", row.pr.Number) + return m, func() tea.Msg { + if err := run.StartDetached(plan); err != nil { + return prActionMsg{"start failed: " + err.Error()} + } + return prActionMsg{fmt.Sprintf("started PR #%d from local build → http://localhost:%d", row.pr.Number, plan.HostPort)} + } + } + if row.imageState == "checking" { + m.status = fmt.Sprintf("PR #%d image check is still in progress", row.pr.Number) + return m, nil + } + return m, m.beginPull(*row) + } + plan, err := run.BuildPlan(run.Options{Context: context.Background(), Tag: row.local.Tag, Demo: m.demo}) + if err != nil { + m.status = err.Error() + return m, nil + } + return m, func() tea.Msg { + if err := run.StartDetached(plan); err != nil { + return prActionMsg{"start failed: " + err.Error()} + } + return prActionMsg{fmt.Sprintf("started %s → http://localhost:%d", label, plan.HostPort)} + } + case "r": + m.loading = true + m.status = "refreshing…" + return m, tea.Batch(loadDev(m.includeBots, true), m.spinner.Tick) + case "/": + m.filtering = true + m.layout() + return m, m.filter.Focus() + case "o": + row := m.selectedPR() + if row != nil { + if err := platform.OpenURL(fmt.Sprintf("https://github.com/homarr-labs/homarr/pull/%d", row.pr.Number)); err != nil { + m.status = "open failed: " + err.Error() + } + } + case "a": + row := m.selectedRow() + if row != nil && row.running && row.port != "" { + if err := platform.OpenURL("http://localhost:" + row.port); err != nil { + m.status = "open failed: " + err.Error() + } + } else { + m.status = "selected image is not running" + } + case "l": + m.showLogs = !m.showLogs + m.layout() + if m.showLogs { + m.followLogs = true + return m, tea.Batch(m.refreshSelectedLogs(), prLogTick()) + } + return m, nil + case "f": + if m.showLogs { + m.followLogs = !m.followLogs + if m.followLogs { + m.logs.GotoBottom() + } + } else { + m.status = "open logs with l first" + } + case "p": + row := m.selectedPR() + if row != nil { + return m, m.beginPull(*row) + } else { + m.status = "pull is only available for remote PR images" + } + case "R": + if m.rebuilding { + m.status = "a local build is already running" + return m, nil + } + row := m.selectedRow() + if row == nil { + m.status = "select an image to build" + return m, nil + } + image, action, status := localBuildForRow(*row) + ctx, cancel := context.WithCancel(context.Background()) + m.rebuilding = true + m.rebuildCancel = cancel + m.status = status + rebuild := func() tea.Msg { + output := &outputTail{} + err := run.RebuildImageContext(ctx, image, output, output) + if err != nil { + err = commandErrorWithDetail(err, output.lastLine()) + } + return rebuildDoneMsg{image: image, err: err, canceled: ctx.Err() != nil, action: action} + } + return m, tea.Batch(rebuild, m.spinner.Tick) + case "m": + m.demo = !m.demo + if m.demo { + m.status = "demo mode enabled for the next launch" + } else { + m.status = "demo mode disabled" + } + case "k": + m.table.MoveUp(1) + return m, m.refreshSelectedLogs() + case "j": + m.table.MoveDown(1) + return m, m.refreshSelectedLogs() + case "b": + m.includeBots = !m.includeBots + m.loading = true + if m.includeBots { + m.status = "refreshing (bots shown)…" + } else { + m.status = "refreshing (bots hidden)…" + } + return m, tea.Batch(loadDev(m.includeBots, false), m.spinner.Tick) + } + } + var cmd tea.Cmd + m.table, cmd = m.table.Update(msg) + return m, cmd +} + +func coloredCI(state string) string { + switch state { + case "SUCCESS": + return lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render("✓ success") + case "FAILURE": + return lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render("✗ failed") + case "PENDING": + return lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Render("● pending") + default: + return lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("– no checks") + } +} + +func coloredImage(state string) string { + switch state { + case "yes": + return lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render("✓ available") + case "no": + return lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Render("… not built") + case "error": + return lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render("! check failed") + default: + return lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Render("? checking") + } +} + +func (m prsModel) selectedDetailView() string { + row := m.selectedRow() + if row == nil { + return "" + } + if row.kind == "local" { + source := row.local.Source + if source == "" { + source = "unknown source · rebuild unavailable" + } + meta := "local · " + source + if row.local.Revision != "" { + meta += " · " + truncateText(row.local.Revision, 12) + } + if row.local.PRNumber > 0 { + meta += fmt.Sprintf(" · PR #%d", row.local.PRNumber) + } + if row.running { + meta += " · " + lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render("running on :"+row.port) + } + title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("229")).Render(row.local.Reference()) + return title + "\n" + meta + } + branch := row.pr.HeadRef + if branch == "" { + branch = "unknown branch" + } + runSource := "run remote" + if row.hasCurrentLocalImage() { + runSource = "run local " + truncateText(row.local.Revision, 12) + } + meta := "@" + row.pr.Author + " · " + branch + " · CI " + coloredCI(row.pr.CIState) + " · " + runSource + if !row.hasCurrentLocalImage() { + meta += " · GHCR " + coloredImage(row.imageState) + } + if row.running { + meta += " · " + lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render("running on :"+row.port) + } + title := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("229")).Render(fmt.Sprintf("#%d %s", row.pr.Number, row.pr.Title)) + return title + "\n" + meta +} + +func truncateText(value string, length int) string { + runes := []rune(value) + if len(runes) <= length { + return value + } + return string(runes[:length]) +} + +func (m prsModel) View() tea.View { + var b strings.Builder + header := titleStyle.Render("🦞 homarr development") + if m.loading || m.rebuilding { + header += " " + m.spinner.View() + } + running := 0 + remote := 0 + local := 0 + for _, row := range m.rows { + if row.kind == "local" { + local++ + } else { + remote++ + if row.hasCurrentLocalImage() { + local++ + } + } + if row.running { + running++ + } + } + header += " " + helpStyle.Render(fmt.Sprintf("%d remote · %d local · %d running · %d shown", remote, local, running, len(m.table.Rows()))) + if m.demo { + header += " " + lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("214")).Render("DEMO") + } + b.WriteString(header + "\n\n") + if m.filtering || m.filter.Value() != "" { + b.WriteString(m.filter.View() + "\n\n") + } + if len(m.table.Rows()) > 0 { + b.WriteString(m.table.View() + "\n") + } else if len(m.rows) > 0 { + b.WriteString(helpStyle.Render("no development images match "+m.filter.Value()) + "\n") + } + if detail := m.selectedDetailView(); detail != "" { + b.WriteString("\n" + detail + "\n") + } + if m.pulling { + label := "Pulling image" + if m.pullPlan != nil { + label = fmt.Sprintf("Pulling PR #%d", m.pullPlan.PRNumber) + } + b.WriteString("\n" + lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("214")).Render(m.spinner.View()+" "+label) + "\n") + complete, total, percent := m.pullCompletion() + if total > 0 { + b.WriteString(m.pullProgress.ViewAs(percent) + " " + helpStyle.Render(fmt.Sprintf("%d/%d layers", complete, total)) + "\n") + } + b.WriteString(m.pullView.View() + "\n") + b.WriteString(helpStyle.Render("q/esc cancel download") + "\n") + } + if m.showLogs { + mode := "paused" + if m.followLogs { + mode = "following" + } + name := m.logsName + if name == "" { + name = "selected image" + } + b.WriteString("\n" + logHeadSt.Render("── logs: "+name+" ("+mode+") ──") + "\n") + b.WriteString(m.logs.View() + "\n") + } + if m.status != "" { + statusStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("42")) + lower := strings.ToLower(m.status) + if strings.Contains(lower, "fail") || strings.Contains(lower, "error") || strings.Contains(lower, "cannot") { + statusStyle = statusStyle.Foreground(lipgloss.Color("196")) + } else if m.pulling || m.rebuilding || strings.Contains(lower, "starting") || strings.Contains(lower, "refreshing") { + statusStyle = statusStyle.Foreground(lipgloss.Color("214")) + } + b.WriteString("\n" + statusStyle.Render(m.status)) + } + botState := "bots hidden" + if m.includeBots { + botState = "bots shown" + } + if !m.pulling { + b.WriteString("\n\n" + helpStyle.Render("/ filter · ↑/↓ select · enter start/stop · R build locally · p pull remote · m demo · o PR · a app")) + b.WriteString("\n" + helpStyle.Render("b "+botState+" · r refresh · d instances · q quit")) + b.WriteString("\n" + helpStyle.Render("l toggle logs · f follow · pgup/pgdn scroll")) + if m.rebuilding { + b.WriteString("\n" + helpStyle.Render("local build running in background · q/esc cancel build")) + } + } + b.WriteString("\n" + helpStyle.Render("SOURCE: local Docker / remote GHCR · CI: "+legendCIYes+" pass "+legendCINo+" fail "+legendCIPend+" pending "+legendNone+" none · IMAGE: "+legendLocal+" local "+legendImageYes+" remote "+legendImageNo+" not built")) + v := tea.NewView(b.String()) + v.AltScreen = true + v.WindowTitle = "🦞 Homarr Development" + return v +} + +func RunDev() error { + return runApp(false) +} diff --git a/tools/homarr-dev/internal/tui/dev_test.go b/tools/homarr-dev/internal/tui/dev_test.go new file mode 100644 index 000000000..ecd8d85bf --- /dev/null +++ b/tools/homarr-dev/internal/tui/dev_test.go @@ -0,0 +1,228 @@ +package tui + +import ( + "errors" + "strings" + "testing" + + tea "charm.land/bubbletea/v2" + + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker" + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/gh" +) + +func TestPRTableRendersRows(t *testing.T) { + m := newPRsModel() + m.rows = []prRow{{ + kind: "remote", + pr: gh.PR{Number: 6441, Title: "Human PR", Author: "ajnart", CIState: "SUCCESS"}, + imageState: "yes", + }} + m.table.SetRows(buildTableRows(m.rows)) + + if got := m.View().Content; !strings.Contains(got, "Human PR") { + t.Fatalf("table view omitted rows: %q", got) + } + if !m.View().AltScreen { + t.Fatal("development browser must use the alternate screen") + } +} + +func TestPRTableArrowNavigation(t *testing.T) { + m := newPRsModel() + m.rows = []prRow{ + {kind: "remote", pr: gh.PR{Number: 1, Title: "First"}}, + {kind: "remote", pr: gh.PR{Number: 2, Title: "Second"}}, + } + m.table.SetRows(buildTableRows(m.rows)) + + updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown})) + got := updated.(prsModel).table.SelectedRow() + if len(got) < 3 || got[2] != "2" { + t.Fatalf("down arrow selected %v, want PR 2", got) + } +} + +func TestPRImageChecksAreBounded(t *testing.T) { + m := newPRsModel() + prs := make([]gh.PR, 20) + for i := range prs { + prs[i] = gh.PR{Number: i + 1, Title: "PR"} + } + + updated, cmd := m.Update(prsLoadedMsg{prs: prs}) + got := updated.(prsModel) + if cmd == nil { + t.Fatal("expected image check commands") + } + if got.imageActive != 6 || len(got.imageQueue) != 14 { + t.Fatalf("active=%d queued=%d, want active=6 queued=14", got.imageActive, len(got.imageQueue)) + } +} + +func TestPRLoadedReusesImageChecksUnlessRefreshIsForced(t *testing.T) { + m := newPRsModel() + m.rows = []prRow{{kind: "remote", pr: gh.PR{Number: 1}, imageState: "yes"}} + + updated, _ := m.Update(prsLoadedMsg{prs: []gh.PR{{Number: 1}, {Number: 2}}}) + cached := updated.(prsModel) + if cached.rows[0].imageState != "yes" || len(cached.imageQueue) != 0 || cached.imageActive != 1 { + t.Fatalf("cached states were not reused: rows=%#v queue=%v active=%d", cached.rows, cached.imageQueue, cached.imageActive) + } + + updated, _ = cached.Update(prsLoadedMsg{prs: []gh.PR{{Number: 1}, {Number: 2}}, refresh: true}) + refreshed := updated.(prsModel) + if refreshed.rows[0].imageState != "checking" || refreshed.imageActive != 2 { + t.Fatalf("forced refresh reused image states: rows=%#v active=%d", refreshed.rows, refreshed.imageActive) + } +} + +func TestDevelopmentRowsFoldCurrentLocalPRImage(t *testing.T) { + prs := []gh.PR{{Number: 6502, HeadSHA: "current"}} + images := []docker.Image{ + {Tag: "pr-6502", PRNumber: 6502, Revision: "current"}, + {Tag: "pr-6502-stale", PRNumber: 6502, Revision: "stale"}, + } + + rows, queue := developmentRows(prs, images, nil) + if len(rows) != 2 || !rows[0].hasCurrentLocalImage() || rows[0].local.Tag != "pr-6502" { + t.Fatalf("current image was not folded into PR row: %#v", rows) + } + if rows[1].kind != "local" || rows[1].local.Tag != "pr-6502-stale" { + t.Fatalf("stale image should remain separate: %#v", rows[1]) + } + if got := buildTableRows(rows)[0][6]; got != imgLocal { + t.Fatalf("image marker = %q, want %q", got, imgLocal) + } + if len(queue) != 0 { + t.Fatalf("current local image queued unnecessary registry checks: %v", queue) + } +} + +func TestLocalPRPlanUsesPRContainerIdentity(t *testing.T) { + row := prRow{ + kind: "remote", + pr: gh.PR{Number: 6502, HeadSHA: "abcdef1234567890"}, + local: docker.Image{Tag: "pr-6502", PRNumber: 6502, Revision: "abcdef1234567890"}, + } + plan, err := localPRPlan(row, false, func(int) int { return 7575 }) + if err != nil { + t.Fatal(err) + } + if plan.Image != "homarr:pr-6502" || plan.Name != "homarr_pr_6502" || plan.Volume != "homarr_pr_6502_data" || plan.Pull || plan.PRNumber != 6502 { + t.Fatalf("unexpected local PR plan: %#v", plan) + } +} + +func TestRemotePRBuildUsesTemporaryPRWorkflow(t *testing.T) { + image, action, status := localBuildForRow(prRow{kind: "remote", pr: gh.PR{Number: 6441}}) + if image.Tag != "pr-6441" || image.PRNumber != 6441 || action != "built" || !strings.Contains(status, "building PR #6441 locally") { + t.Fatalf("image=%#v action=%q status=%q", image, action, status) + } +} + +func TestPRImageCheckInProgressDoesNotLaunch(t *testing.T) { + m := newPRsModel() + m.rows = []prRow{{kind: "remote", pr: gh.PR{Number: 6441, Title: "Pending image"}, imageState: "checking"}} + m.table.SetRows(buildTableRows(m.rows)) + + updated, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEnter})) + got := updated.(prsModel) + if cmd != nil { + t.Fatal("unavailable image returned a launch command") + } + if !strings.Contains(got.status, "still in progress") { + t.Fatalf("status = %q, want launch rejection", got.status) + } +} + +func TestPullErrorIncludesLastDockerMessage(t *testing.T) { + err := commandErrorWithDetail(errors.New("exit status 1"), "manifest unknown") + if !strings.Contains(err.Error(), "manifest unknown") { + t.Fatalf("pull error = %v", err) + } +} + +func TestOutputTailRetainsBuildFailure(t *testing.T) { + tail := &outputTail{} + _, _ = tail.Write([]byte("building\r\nERROR: Dockerfile not found\n")) + if got := tail.lastLine(); got != "ERROR: Dockerfile not found" { + t.Fatalf("last line = %q", got) + } +} + +func TestDevTableRendersLocalImageSource(t *testing.T) { + m := newPRsModel() + m.rows = []prRow{{ + kind: "local", + local: docker.Image{Tag: "feature", Source: "/tmp/homarr", Revision: "abcdef1234567890"}, + imageState: "yes", + }} + m.table.SetRows(buildTableRows(m.rows)) + + content := m.View().Content + for _, want := range []string{"local", "homarr:feature", "/tmp/homarr", "abcdef123456"} { + if !strings.Contains(content, want) { + t.Fatalf("development view omitted %q: %q", want, content) + } + } +} + +func TestRebuildCompletionRestoresLoadingState(t *testing.T) { + m := newPRsModel() + m.rebuilding = true + image := docker.Image{Tag: "feature"} + + updated, cmd := m.Update(rebuildDoneMsg{image: image}) + got := updated.(prsModel) + if got.rebuilding || !got.loading || got.status != "rebuilt homarr:feature" || cmd == nil { + t.Fatalf("unexpected rebuild state: rebuilding=%v loading=%v status=%q", got.rebuilding, got.loading, got.status) + } +} + +func TestRebuildCanBeCanceled(t *testing.T) { + m := newPRsModel() + canceled := false + m.rebuilding = true + m.rebuildCancel = func() { canceled = true } + + updated, cmd := m.Update(tea.KeyPressMsg(tea.Key{Code: 'q'})) + got := updated.(prsModel) + if !canceled || cmd != nil || got.status != "canceling rebuild…" { + t.Fatalf("cancel state: canceled=%v status=%q", canceled, got.status) + } +} + +func TestNavigationRemainsAvailableDuringBuild(t *testing.T) { + m := newPRsModel() + m.rebuilding = true + m.rows = []prRow{ + {kind: "remote", pr: gh.PR{Number: 1}}, + {kind: "remote", pr: gh.PR{Number: 2}}, + } + m.table.SetRows(buildTableRows(m.rows)) + + updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyDown})) + selected := updated.(prsModel).table.SelectedRow() + if len(selected) < 3 || selected[2] != "2" { + t.Fatalf("build blocked navigation: %v", selected) + } +} + +func TestCompletedPullWithoutPlanDoesNotPanic(t *testing.T) { + m := newPRsModel() + m.pulling = true + m.pullCancel = func() {} + + updated, cmd := m.Update(pullEvent{done: true}) + got := updated.(prsModel) + if got.pulling || cmd != nil { + t.Fatalf("pulling=%v cmd=%v", got.pulling, cmd) + } +} + +func TestTruncateTextPreservesUTF8(t *testing.T) { + if got := truncateText("🦞homarr", 2); got != "🦞h" { + t.Fatalf("truncateText() = %q", got) + } +} diff --git a/tools/homarr-dev/main.go b/tools/homarr-dev/main.go new file mode 100644 index 000000000..2380e41bd --- /dev/null +++ b/tools/homarr-dev/main.go @@ -0,0 +1,301 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/spf13/cobra" + + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker" + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/platform" + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/run" + "github.com/homarr-labs/homarr/tools/homarr-dev/internal/tui" +) + +var ( + flagDemo bool + flagPR int + flagEnv []string + flagDetach bool + flagBuildPR int + runMain = tui.RunDev +) + +func main() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +var rootCmd = &cobra.Command{ + Use: "homarr", + Short: "Launch and manage Homarr Docker instances", + SilenceUsage: true, + Long: `Launch and manage Homarr Docker containers. + +Start a local image, a PR test build from ghcr.io, browse open PRs, +or manage running instances from an interactive dashboard. + +Examples: + homarr run dev # launch local homarr:dev image + homarr run --pr 6390 # launch remote PR test image + homarr run --pr 6390 --demo # PR + demo mode + homarr run -e FOO=bar dev # extra env vars + homarr run --detach dev # start in background + homarr dash # interactive instance dashboard + homarr dev # browse remote PRs and local images + homarr build feature # build this checkout as homarr:feature + homarr build --pr 6390 # build PR #6390 locally as homarr:pr-6390 + homarr list # script-friendly instance list + homarr logs homarr_pr_6390 # follow logs + homarr doctor # check Docker and optional integrations`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return runMain() + }, +} + +var dashCmd = &cobra.Command{ + Use: "dash", + Short: "Interactive dashboard of running instances", + RunE: func(cmd *cobra.Command, args []string) error { + return tui.RunDashboard() + }, +} + +var runCmd = &cobra.Command{ + Use: "run [tag]", + Short: "Launch a local image or remote pull-request image", + Args: cobra.MaximumNArgs(1), + RunE: runLaunch, +} + +var devCmd = &cobra.Command{ + Use: "dev", + Short: "Browse remote PRs and local development images", + RunE: func(cmd *cobra.Command, args []string) error { + return tui.RunDev() + }, +} + +var buildCmd = &cobra.Command{ + Use: "build [name]", + Short: "Build a local Homarr image from this checkout or a pull request", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + tag := "" + if len(args) > 0 { + tag = args[0] + } + if flagBuildPR < 0 { + return fmt.Errorf("PR number must be positive") + } + if flagBuildPR > 0 { + if tag == "" { + tag = fmt.Sprintf("pr-%d", flagBuildPR) + } + return run.BuildPRImage(flagBuildPR, tag) + } + if tag == "" { + return fmt.Errorf("image name is required when --pr is not set") + } + return run.BuildCurrentImage(tag, ".") + }, +} + +var rebuildCmd = &cobra.Command{ + Use: "rebuild ", + Short: "Rebuild a local image from its recorded checkout or pull request", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + images, err := docker.ListLocalImages(cmd.Context()) + if err != nil { + return err + } + for _, image := range images { + if image.Tag == args[0] || image.Reference() == args[0] { + return run.RebuildImage(image) + } + } + return fmt.Errorf("local image homarr:%s was not found", strings.TrimPrefix(args[0], "homarr:")) + }, +} + +var logsCmd = &cobra.Command{ + Use: "logs ", + Short: "Follow logs of a running instance", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return docker.FollowLogs(args[0]) + }, +} + +var stopCmd = &cobra.Command{ + Use: "stop ", + Short: "Stop a running instance", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return docker.Stop(args[0]) + }, +} + +var restartCmd = &cobra.Command{ + Use: "restart ", + Short: "Restart a running instance", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return docker.Restart(args[0]) + }, +} + +var removeCmd = &cobra.Command{ + Use: "remove ", + Short: "Force-remove an instance", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return docker.Remove(args[0]) + }, +} + +var openCmd = &cobra.Command{ + Use: "open ", + Short: "Open a running instance in the browser", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + containers, err := docker.List() + if err != nil { + return err + } + for _, container := range containers { + if container.Name != args[0] { + continue + } + if !container.Running() { + return fmt.Errorf("container %q is not running", args[0]) + } + port := container.HostPort() + if port == "" { + return fmt.Errorf("container %q does not publish the Homarr port", args[0]) + } + return platform.OpenURL("http://localhost:" + port) + } + return fmt.Errorf("container %q was not found", args[0]) + }, +} + +var listCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List Homarr instances without opening the dashboard", + RunE: func(cmd *cobra.Command, args []string) error { + containers, err := docker.List() + if err != nil { + return err + } + if len(containers) == 0 { + fmt.Println("No Homarr instances found.") + return nil + } + fmt.Printf("%-28s %-10s %-7s %s\n", "NAME", "STATE", "PORT", "IMAGE") + for _, container := range containers { + fmt.Printf("%-28s %-10s %-7s %s\n", container.Name, container.State, container.HostPort(), container.Image) + } + return nil + }, +} + +var doctorCmd = &cobra.Command{ + Use: "doctor", + Short: "Check Docker and optional developer CLI integrations", + RunE: func(cmd *cobra.Command, args []string) error { + failed := false + output, err := exec.Command("docker", "info", "--format", "{{.ServerVersion}}").CombinedOutput() + if err != nil { + failed = true + fmt.Printf("✗ Docker daemon: %s\n", strings.TrimSpace(string(output))) + } else { + fmt.Printf("✓ Docker daemon: %s\n", strings.TrimSpace(string(output))) + } + + if _, err := exec.LookPath("gh"); err != nil { + fmt.Println("- GitHub CLI not found (optional; required for pull-request features)") + } else if output, err := exec.Command("gh", "auth", "status").CombinedOutput(); err != nil { + fmt.Printf("- GitHub CLI is not authenticated (optional; run `gh auth login` for pull-request features): %s\n", strings.TrimSpace(string(output))) + } else { + fmt.Println("✓ GitHub CLI authentication") + } + + if _, err := exec.LookPath("homarr"); err != nil { + fmt.Println("- homarr is not on PATH (optional; use `pnpm dev:cli -- ...` or run `pnpm dev:cli:install`)") + } else { + fmt.Println("✓ homarr is on PATH") + } + if failed { + return fmt.Errorf("one or more checks failed") + } + return nil + }, +} + +func init() { + runCmd.Flags().BoolVarP(&flagDemo, "demo", "m", false, "Demo mode (mock integrations)") + runCmd.Flags().IntVarP(&flagPR, "pr", "p", 0, "Launch PR test image from ghcr.io") + runCmd.Flags().StringArrayVarP(&flagEnv, "env", "e", nil, "Extra env vars (repeatable, KEY=VALUE)") + runCmd.Flags().BoolVar(&flagDetach, "detach", false, "Start in the background") + buildCmd.Flags().IntVarP(&flagBuildPR, "pr", "p", 0, "Build a pull request in a temporary checkout") + + rootCmd.AddCommand(runCmd, dashCmd, devCmd, buildCmd, rebuildCmd, listCmd, logsCmd, stopCmd, restartCmd, removeCmd, openCmd, doctorCmd) +} + +func runLaunch(cmd *cobra.Command, args []string) error { + tag := "" + if len(args) > 0 { + tag = args[0] + } + if flagPR < 0 { + return fmt.Errorf("PR number must be positive") + } + if flagPR > 0 && tag != "" { + return fmt.Errorf("tag and --pr cannot be used together") + } + if flagPR == 0 && tag == "" { + if err := cmd.Usage(); err != nil { + return err + } + return fmt.Errorf("provide a tag or --pr") + } + + plan, err := run.BuildPlan(run.Options{ + Context: cmd.Context(), + PR: flagPR, + Tag: tag, + Demo: flagDemo, + Env: flagEnv, + FetchTitle: flagPR > 0, + }) + if err != nil { + return err + } + + fmt.Printf("Starting Homarr (%s)\n", plan.Label) + fmt.Printf(" Image : %s\n", plan.Image) + fmt.Printf(" Container : %s\n", plan.Name) + fmt.Printf(" Volume : %s\n", plan.Volume) + fmt.Printf(" Port : %d:7575\n", plan.HostPort) + fmt.Printf(" URL : http://localhost:%d\n", plan.HostPort) + if plan.PRNumber > 0 { + fmt.Printf(" PR : https://github.com/homarr-labs/homarr/pull/%d\n", plan.PRNumber) + } + fmt.Println() + + if flagDetach { + if err := run.StartDetached(plan); err != nil { + return err + } + fmt.Printf("Started in background: http://localhost:%d\n", plan.HostPort) + return nil + } + return run.StartForeground(plan) +} diff --git a/tools/homarr-dev/main_test.go b/tools/homarr-dev/main_test.go new file mode 100644 index 000000000..1736d98b3 --- /dev/null +++ b/tools/homarr-dev/main_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "errors" + "strings" + "testing" +) + +func TestRootCommandLaunchesDevelopmentDashboard(t *testing.T) { + want := errors.New("dashboard started") + previous := runMain + t.Cleanup(func() { runMain = previous }) + runMain = func() error { return want } + + if err := rootCmd.RunE(rootCmd, nil); !errors.Is(err, want) { + t.Fatalf("root command error = %v", err) + } +} + +func TestRunLaunchRejectsTagWithPR(t *testing.T) { + previousPR := flagPR + t.Cleanup(func() { flagPR = previousPR }) + flagPR = 42 + + err := runLaunch(runCmd, []string{"feature"}) + if err == nil || !strings.Contains(err.Error(), "cannot be used together") { + t.Fatalf("error = %v", err) + } +} + +func TestRunLaunchRejectsNegativePR(t *testing.T) { + previousPR := flagPR + t.Cleanup(func() { flagPR = previousPR }) + flagPR = -1 + + err := runLaunch(runCmd, nil) + if err == nil || !strings.Contains(err.Error(), "must be positive") { + t.Fatalf("error = %v", err) + } +} + +func TestRunLaunchRejectsMissingImage(t *testing.T) { + previousPR := flagPR + t.Cleanup(func() { flagPR = previousPR }) + flagPR = 0 + + err := runLaunch(runCmd, nil) + if err == nil || !strings.Contains(err.Error(), "provide a tag or --pr") { + t.Fatalf("error = %v", err) + } +}