diff --git a/.claude/skills/copy-graham-blog-post/SKILL.md b/.claude/skills/copy-graham-blog-post/SKILL.md index 34e6d5a..18ae596 100644 --- a/.claude/skills/copy-graham-blog-post/SKILL.md +++ b/.claude/skills/copy-graham-blog-post/SKILL.md @@ -5,12 +5,13 @@ description: Copies blog posts from Graham Dumpleton's blog (grahamdumpleton.me # Copy Graham blog post to Educates site -Copy a post (and its images) from [Graham Dumpleton's blog repo](https://github.com/GrahamDumpleton/grahamdumpleton.me) into this repo's `blog/` folder, and rewrite the text from first person singular (I, my) to first person plural (we, our) as the Educates team. +Copy a post (and its images) from [Graham Dumpleton's blog repo](https://github.com/GrahamDumpleton/grahamdumpleton.me) into this repo's posts collection, `src/content/posts/`, and rewrite the text from first person singular (I, my) to first person plural (we, our) as the Educates team. ## Source and target - **Source**: GitHub repo `GrahamDumpleton/grahamdumpleton.me`, path like `src/posts/YYYY/MM/post-slug/` (e.g. `src/posts/2026/02/deploying-educates-yourself`). -- **Target**: This repo's `blog/` folder. Create a folder named `YYYY-MM-DD-/` (e.g. `2026-02-26-deploying-educates-yourself`) containing `index.md` and any image files. Use the post's `date` from frontmatter for the folder date, or the path date if needed. Add `` after first paragraph. If first paragraph is too short, do it after the second. Also, review the tags to align with those in `blog/tags.yaml` and the topic in the blog. +- **Target**: a folder `src/content/posts/YYYY-MM-DD-/` (e.g. `src/content/posts/2026-02-26-deploying-educates-yourself/`) holding `index.md` and the post's image files. The folder date is the post's `date` from its front matter, or the date in the source path if it has none. +- **Format**: `index.md`. Only a post that uses a component, such as ``, is `index.mdx`. ## Workflow @@ -49,36 +50,45 @@ In the **body** (not code blocks or image alt text unless they are first-person Preserve "you" when addressing the reader. Do not change technical wording, code blocks, or link URLs except internal post links. -### 4. Adapt frontmatter +### 4. Write the front matter -- Keep: `title`, `description`, `date`, `tags`, `image`, `draft`. -- Add: `slug: ` (e.g. `deploying-educates-yourself`). -- Add: `authors: [graham]`. -- Remove any frontmatter fields this site does not use (check existing posts in `blog/` for the schema). -- Remove `daft` and `date` frontmatter fields. +The posts collection's schema is in `src/content.config.ts`; the build fails on a post that breaks it. Write these keys: +- `title`: from the source. +- `slug`: the post's slug (e.g. `deploying-educates-yourself`). The post is served at `/blog/`, and the slug never changes once published. +- `description`: required. Take the source's description; if it has none, write one or two sentences saying what the reader gets from the post. +- `date`: the source's date, as `YYYY-MM-DD`. +- `tags`: keys defined in `src/content/tags.yml` that fit the post's topic (see step 6). A key missing from that file fails the build. +- `authors: [graham]`. Authors are keys of `src/content/authors.yml`. +- `draft: true` only for a post that must not publish yet; it then shows in the dev server only. + +Leave out every other key of the source, such as `image`. A post gets a generated cover; a picture of its own goes in `cover`, as the path of an image file next to the post. + +The post body carries no truncate marker: lists show the `description`. ### 5. Fix internal links -Replace Graham's post paths with this site's blog paths: +Replace Graham's post paths with this site's post paths, `/blog/`, with no trailing slash: -- `/posts/YYYY/MM/slug/` or `/posts/2026/02/teaching-an-ai-about-educates/` → `/blog/` (e.g. `/blog/teaching-an-ai-about-educates`). -- Use `/blog/` consistently (no trailing slash required; add one if the site expects it). +- `/posts/YYYY/MM/slug/` (e.g. `/posts/2026/02/teaching-an-ai-about-educates/`) → `/blog/` (e.g. `/blog/teaching-an-ai-about-educates`). + +GitHub Pages answers `/blog//` with a 404, so every internal link ends without a slash. ### 6. Adjust tags -- Review tags in frontmatter section to align with the contents of the blog post and tags already defined in `blog/tags.yaml` +Review the tags against the post's content and the tags defined in `src/content/tags.yml`, and keep only keys from that file. -### 6. Create target folder and files +### 7. Create target folder and files -- Create `blog/YYYY-MM-DD-/` (e.g. `blog/2026-02-26-deploying-educates-yourself/`). -- Write the rewritten content to `blog/YYYY-MM-DD-/index.md`. +- Create `src/content/posts/YYYY-MM-DD-/` (e.g. `src/content/posts/2026-02-26-deploying-educates-yourself/`). +- Write the rewritten content to `index.md` in that folder. - Download each image from `https://raw.githubusercontent.com/GrahamDumpleton/grahamdumpleton.me/main/src/posts/YYYY/MM//` into the same folder. Image references in the markdown are relative (e.g. `training-portal.png`), so they resolve correctly. -### 7. Verify +### 8. Verify - Confirm all images are present and paths in markdown match filenames. -- Spot-check that first-person singular in the body has been converted to plural and internal links use `/blog/...`. +- Spot-check that first-person singular in the body has been converted to plural, and that internal links are `/blog/` without a trailing slash. +- Run `npm run build`: it validates the front matter, renders the post at `/blog/`, and runs the site check. ## Example @@ -86,22 +96,18 @@ User provides: `https://github.com/GrahamDumpleton/grahamdumpleton.me/tree/main/ - Post slug: `deploying-educates-yourself`. - List contents → get `index.md`, `training-portal.png`, `workshop-dashboard.png`. -- Fetch `index.md`, rewrite I/my → we/our, add `slug` and `authors: [graham]`, change link to previous post to `/blog/teaching-an-ai-about-educates`. -- Create `blog/2026-02-26-deploying-educates-yourself/index.md` and download the two PNGs into that folder. +- Fetch `index.md`, rewrite I/my → we/our, write the front matter, change the link to the previous post to `/blog/teaching-an-ai-about-educates`. +- Create `src/content/posts/2026-02-26-deploying-educates-yourself/index.md` and download the two PNGs into that folder. -## Reference: existing Educates post frontmatter +## Reference: Educates post front matter ```yaml --- -title: "..." -slug: post-slug -description: "..." -date: YYYY-MM-DD -tags: ["educates", "ai"] +title: "Deploying Educates yourself" +slug: deploying-educates-yourself +description: "Educates is open source, not a SaaS. Deploy it yourself, wherever you want." +date: 2026-02-26 +tags: ["educates", "installation", "local"] authors: [graham] -image: https://opengraph.githubassets.com/1/educates/educates-training-platform -draft: false --- ``` - -Authors are defined in `blog/authors.yml`; `graham` exists. Use the same structure for new posts. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3970e86..39d16cc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -17,8 +17,7 @@ } }, "extensions": [ - "dbaeumer.vscode-eslint", - "orta.vscode-jest", + "astro-build.astro-vscode", "esbenp.prettier-vscode", "streetsidesoftware.code-spell-checker" ] @@ -26,11 +25,11 @@ }, // "forwardPorts": [3000], // "containerUser": "vscode", - "postCreateCommand": "yarn install", - "waitFor": "postCreateCommand", // otherwise automated jest tests fail + "postCreateCommand": "npm install", + "waitFor": "postCreateCommand", "features": { "node": { - "version": "18" + "version": "26" }, "github-cli": "latest" } diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..38cbfd9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +# The build installs its own dependencies and writes its own output. +node_modules/ +dist/ +.astro/ + +# Not part of the site. +.git +.github/ +.devcontainer/ +.claude/ +.scratch +Dockerfile +.dockerignore +npm-debug.log* +.DS_Store +.env +.env.* diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..559c0a4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,30 @@ +# Weekly dependency updates. Minor and patch updates arrive as one pull +# request per ecosystem; each major update gets a pull request of its own, so +# a new Astro major is never missed. +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + commit-message: + prefix: build + include: scope + groups: + npm-minor-and-patch: + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + commit-message: + prefix: ci + include: scope + groups: + actions-minor-and-patch: + update-types: + - minor + - patch diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 0000000..6703c39 --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,70 @@ +name: Checks + +on: + pull_request: + branches: + - develop + - main + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: checks-${{ github.ref }} + cancel-in-progress: true + +jobs: + checks: + name: Checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + # Node.js at the Volta pin in package.json. + - uses: actions/setup-node@v7 + with: + node-version-file: package.json + cache: npm + + - name: Install dependencies + id: install + run: npm ci + + # Each check runs even when an earlier one fails, so a pull request + # sees every failure at once. + - name: Type-check + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm run check + + - name: Check formatting + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm run format:check + + - name: Build the site + id: build + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npx astro build + + - name: Run the site check + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + run: npm run site-check + + - name: Run the unit tests + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm test + + # Offline, as `npm run link-check` runs it, with lychee.toml's settings. + - name: Check internal links + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + uses: lycheeverse/lychee-action@v2 + with: + args: --offline --root-dir "${{ github.workspace }}/dist" 'dist/**/*.html' + + # The pages and thresholds are in lighthouserc.yml. + - name: Run Lighthouse + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + uses: treosh/lighthouse-ci-action@v12 + with: + configPath: ./lighthouserc.yml + uploadArtifacts: true diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 9a0f563..8d8b54d 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -1,52 +1,48 @@ name: Deploy to GitHub Pages on: - workflow_dispatch: push: branches: - main - # Review gh actions docs if you want to further define triggers, paths, etc - # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on + workflow_dispatch: + +permissions: + contents: read + +# One deployment at a time; a running deployment finishes before the next. +concurrency: + group: pages + cancel-in-progress: false jobs: build: - name: Build Docusaurus + name: Build the site runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: yarn + - uses: actions/checkout@v7 - - name: Install dependencies - run: yarn install --frozen-lockfile - - name: Build website - run: yarn build + - name: Read the Node.js version from the Volta pin + id: node + run: echo "version=$(jq -r .volta.node package.json)" >> "$GITHUB_OUTPUT" - - name: Upload Build Artifact - uses: actions/upload-pages-artifact@v3 + # Installs the dependencies, runs `npm run build` (the build and the + # site check) and uploads dist/ as the Pages artifact. + - name: Build the site and upload it + uses: withastro/action@v6 with: - path: build + node-version: ${{ steps.node.outputs.version }} deploy: name: Deploy to GitHub Pages needs: build - - # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + runs-on: ubuntu-latest permissions: - pages: write # to deploy to Pages - id-token: write # to verify the deployment originates from an appropriate source - - # Deploy to the github-pages environment + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - - runs-on: ubuntu-latest steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/external-links.yaml b/.github/workflows/external-links.yaml new file mode 100644 index 0000000..be80253 --- /dev/null +++ b/.github/workflows/external-links.yaml @@ -0,0 +1,87 @@ +name: External links + +# Weekly, and by hand: checks the links from the built site to other sites, +# and keeps one issue listing the dead ones. It never blocks a pull request. +on: + schedule: + - cron: "17 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + issues: write + +concurrency: + group: external-links + cancel-in-progress: true + +env: + ISSUE_TITLE: Dead external links + +jobs: + external-links: + name: External links + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + # Node.js at the Volta pin in package.json. + - uses: actions/setup-node@v7 + with: + node-version-file: package.json + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build the site + run: npx astro build + + # Online, with lychee.toml's settings, for http and https links only. + # Links to educates.dev itself are left to the internal link check, + # which every pull request runs against the build. + - name: Check external links + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --root-dir "${{ github.workspace }}/dist" + --scheme https --scheme http + --exclude '^https?://educates\.dev(/|$)' + --accept '100..=103,200..=299,429' + 'dist/**/*.html' + fail: false + + - name: Find the open dead links issue + id: issue + env: + GH_TOKEN: ${{ github.token }} + run: | + number=$(gh issue list --state open --search "in:title \"$ISSUE_TITLE\"" \ + --json number,title \ + --jq "map(select(.title == \"$ISSUE_TITLE\")) | first | .number // empty") + echo "number=$number" >> "$GITHUB_OUTPUT" + + # lychee exits with 2 when a link is dead. + - name: Open or update the dead links issue + if: ${{ steps.lychee.outputs.exit_code == '2' }} + uses: peter-evans/create-issue-from-file@v6 + with: + title: ${{ env.ISSUE_TITLE }} + content-filepath: ./lychee/out.md + issue-number: ${{ steps.issue.outputs.number }} + + - name: Close the dead links issue + if: ${{ steps.lychee.outputs.exit_code == '0' && steps.issue.outputs.number != '' }} + env: + GH_TOKEN: ${{ github.token }} + NUMBER: ${{ steps.issue.outputs.number }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: gh issue close "$NUMBER" --comment "No dead external links found in $RUN_URL." + + # Any other exit code means lychee itself failed, not a link. + - name: Fail when lychee fails + if: ${{ steps.lychee.outputs.exit_code != '0' && steps.lychee.outputs.exit_code != '2' }} + run: | + echo "lychee exited with ${{ steps.lychee.outputs.exit_code }}" + exit 1 diff --git a/.github/workflows/test-deploy.yaml b/.github/workflows/test-deploy.yaml deleted file mode 100644 index aaa25d2..0000000 --- a/.github/workflows/test-deploy.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: Test deployment - -on: - workflow_dispatch: - pull_request: - branches: - - main - - develop - # Review gh actions docs if you want to further define triggers, paths, etc - # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on - -jobs: - test-deploy: - name: Test deployment - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: yarn - - - name: Install dependencies - run: yarn install --frozen-lockfile - - name: Test build website - run: yarn build \ No newline at end of file diff --git a/.gitignore b/.gitignore index b2d6de3..6a8ebd7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,17 @@ # Dependencies -/node_modules +node_modules/ -# Production -/build +# Build output +dist/ -# Generated files -.docusaurus -.cache-loader +# Types Astro generates +.astro/ + +# Lighthouse CI results +.lighthouseci/ # Misc .DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - +.env +.env.* npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a793654 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +# Prettier formats code files only. Content Markdown keeps the formatting +# its authors gave it. +*.md +*.mdx +docs/ + +# Generated +dist/ +.astro/ +package-lock.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..ceafe55 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "plugins": ["prettier-plugin-astro"], + "overrides": [ + { + "files": "*.astro", + "options": { "parser": "astro" } + } + ] +} diff --git a/Dockerfile b/Dockerfile index 2fda700..b534bb3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,52 +1,31 @@ # syntax=docker/dockerfile:1 -# TODO: Modify following examples at https://docusaurus.community/knowledge/deployment/docker/?package-managers=yarn +# The Node.js version; `npm run docker-build` passes the Volta pin from +# package.json. +ARG NODE_VERSION=26 -# Stage 1: Base image. -## Start with a base image containing NodeJS so we can build Docusaurus. -FROM node:lts AS base -## Disable colour output from yarn to make logs easier to read. +# The project's dependencies, installed from the lockfile. +FROM node:${NODE_VERSION}-slim AS deps +## Disable colour output to make logs easier to read. ENV FORCE_COLOR=0 -## Enable corepack. -RUN corepack enable -## Set the working directory to `/opt/docusaurus`. -WORKDIR /opt/docusaurus +WORKDIR /opt/site +COPY package.json package-lock.json ./ +RUN npm ci +# The Astro dev server over a source mounted at /opt/site. An anonymous volume +# at /opt/site/node_modules keeps this image's dependencies, which are built +# for Linux, in place of the host's. +FROM deps AS dev +EXPOSE 4321 +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] -# Stage 2a: Development mode. -FROM base AS dev -## Set the working directory to `/opt/docusaurus`. -WORKDIR /opt/docusaurus -## Expose the port that Docusaurus will run on. -EXPOSE 3000 -## Run the development server. -CMD [ -d "node_modules" ] && yarn start --host 0.0.0.0 --poll 1000 || yarn install && yarn start --host 0.0.0.0 --poll 1000 +# The site, built and checked. +FROM deps AS build +COPY . . +RUN npm run build -# Stage 2b: Production build mode. -FROM base AS prod -## Set the working directory to `/opt/docusaurus`. -WORKDIR /opt/docusaurus -## Copy over the source code. -COPY . /opt/docusaurus/ -## Install dependencies with `--immutable` to ensure reproducibility. -RUN yarn install --immutable -## Build the static site. -RUN yarn build - -# Stage 3a: Serve with `docusaurus serve`. -FROM prod AS serve -## Expose the port that Docusaurus will run on. -EXPOSE 3000 -## Run the production server. -CMD ["yarn", "serve", "--host", "0.0.0.0", "--no-open"] - -# Stage 3b: Serve with nginx -FROM nginx:alpine AS nginx -## Copy the nginx configuration. -COPY --from=prod /opt/docusaurus/nginx.conf /etc/nginx/conf.d/default.conf -## Copy the Docusaurus build output. -COPY --from=prod /opt/docusaurus/build /usr/share/nginx/html -# Expose port 80 +# The built site behind nginx, serving URLs the way GitHub Pages does. +FROM nginx:alpine AS serve +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /opt/site/dist /usr/share/nginx/html EXPOSE 80 -# Start nginx -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/GLOSSARY.md b/GLOSSARY.md new file mode 100644 index 0000000..4f54982 --- /dev/null +++ b/GLOSSARY.md @@ -0,0 +1,100 @@ +# educates.dev + +The public website of the Educates project: what Educates is, who it is for, +and everything the project publishes about it. + +## Language + +**Content**: +Anything the Educates project publishes for its audience: blog posts, +getting started guides, about pages, videos and talks, wherever they are +hosted. +_Avoid_: Blog (when meaning all content), resources, articles + +**Featured Content**: +The hand-picked subset of Content that the site promotes on its homepage, +drawn from any kind of Content. +_Avoid_: Highlights, featured posts (Featured Content is not limited to posts) + +**Learn page**: +The one page that lists Content of every kind, filterable by kind and +Topic, with Featured Content pinned at the top. The blog's own list is a +separate, posts-only view. +_Avoid_: Content hub, resources page, resource center, library, learning +center + +**Topic**: +A curated subject used to filter the Learn page, defined as a set of +blog tags. Content carries tags; a Topic groups them. +_Avoid_: Tag (when meaning the curated filter), category + +**Blog post**: +A dated, authored article published on the site's blog. One kind of Content. +_Avoid_: Article, news + +## Product + +**Feature**: +A capability of Educates that someone can see and use, such as the +workshop terminal or clickable actions, grouped by the job it serves: +authoring, delivering or operating workshops. +_Avoid_: Benefit, value claim (such as "Secure by Design") + +**Use case**: +A job a team does with Educates, presented on its own page of the site. A +use case combines several Features; it is not a Feature itself. +_Avoid_: Solution + +**Educates Hub**: +The project's public catalog of workshops, and of the extension packages +and policies that go with them, which people deploy on their own Educates +installation. It is not a place to run workshops. "The Hub" for short. +_Avoid_: EducatesHub, marketplace, hub in lower case + +## Demos + +**Demo Platform**: +A company's own system for giving its field teams one-click access to +curated demos and workshops of its products, with Educates as its core. +Educates is the core, not the whole platform. Educates is ready to use as +it is for an internal Demo Platform; a public-facing one adds its own front +end and integrations on top. +_Avoid_: Demo environment (Educates already uses "workshop environment" for +something else), sandbox platform + +**Session**: +One isolated copy of a workshop, given to a single Presenter or Attendee, +which can be prepared ahead of time so it starts instantly. +_Avoid_: Sandbox, demo environment, instance + +**Demo**: +A workshop that a Presenter delivers to Attendees. A way of delivering a +workshop, not a separate kind of thing. +_Avoid_: Demo workshop (redundant), showcase + +**Hands-on event**: +A one-off event with a set time and known Attendees, such as a conference +workshop, a customer workshop or a booth, where each Attendee works in their +own Session. +_Avoid_: Product demo (blurs it with Demo) + +## Demo roles + +**Sponsor**: +The person who funds a company's use of Educates for demos, typically the +head of sales engineering or field enablement. Not someone who funds the +Educates project itself: the site calls that "supporting the project". +_Avoid_: Buyer, customer + +**Builder**: +The team inside a company that builds and runs that company's demos and +workshops on Educates, typically a platform or DevRel team. +_Avoid_: Admin, operator (when meaning the whole team) + +**Presenter**: +The sales or solutions engineer who launches a demo in front of Attendees. +_Avoid_: Instructor, SE + +**Attendee**: +A person at a demo or event who watches or takes part in a workshop. +_Avoid_: Audience (that word means the site's readers), learner, user diff --git a/README.md b/README.md index c05a5b9..15b91b8 100644 --- a/README.md +++ b/README.md @@ -1,128 +1,125 @@ -# Website - -This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. - -### Installation - -``` -$ yarn -``` - -### Local Development - -``` -$ yarn start -``` - -This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. - -### Build - -``` -$ yarn build -``` - -This command generates static content into the `build` directory and can be served using any static contents hosting service. - -### Deployment - -Using SSH: - -``` -$ USE_SSH=true yarn deploy -``` - -Not using SSH: - -``` -$ GIT_USER= yarn deploy -``` - -If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. - +# educates.dev + +The Educates project's website, built with [Astro](https://astro.build/) as +a static site and published to GitHub Pages at . + +## Requirements + +Node.js 26, pinned in `package.json` through [Volta](https://volta.sh/), and +npm. + +## Commands + +| Command | What it does | +| --- | --- | +| `npm install` | Installs the dependencies. | +| `npm run dev` | Starts the dev server at `http://localhost:4321`. | +| `npm run build` | Builds the site into `dist/`, then runs the site check over it. | +| `npm run site-check` | Checks the build in `dist/` again without rebuilding it. | +| `npm run preview` | Serves the build in `dist/` locally. | +| `npm run check` | Type-checks the project with `astro check`. | +| `npm run format` | Formats the code files with Prettier, in place; content Markdown is excluded. | +| `npm run format:check` | Checks the formatting of code files with Prettier; content Markdown is excluded. | +| `npm test` | Runs the unit tests. | +| `npm run link-check` | Checks the internal links in the build in `dist/`, offline. Needs [lychee](https://lychee.cli.rs/), for example from `brew install lychee`. | +| `npm run url-check -- ` | Requests every URL of the must-resolve list from the site served at ``, such as `https://educates.dev` after a deploy; see below. | +| `npm run posters` | Downloads the poster of every video from the project's YouTube channel that has none, next to its outside Content entry in `src/content/outside-content/`. Run it after adding an entry for such a video, which fails the build until it has its poster, and commit each poster with its entry. | +| `npm run captures` | Takes the screenshots and loops on the Features pages from a running Educates, and wires them into the Feature entries; `npm run captures -- setup` deploys what it needs first. See [scripts/captures/README.md](scripts/captures/README.md). | +| `npm run docker-build` | Builds the Docker image; see below. | + +The site check, in `scripts/site-check/`, reads the build the way GitHub +Pages serves it and reports what is wrong: errors fail the build, warnings +are listed. Its rules are in `scripts/site-check/rules/`, and +`scripts/site-check/must-resolve.txt` lists every URL the site must keep +serving. It also fetches the live site's sitemap and fails when the build +does not serve a URL it lists, such as a blog post published since; when +the sitemap cannot be fetched, it skips that comparison with a warning. +`npm run site-check -- --live-sitemap ` compares with another +sitemap. + +The URL check, in `scripts/url-check/`, checks the same must-resolve list +against a served copy of the site instead of the build: every page and +file must answer 200 without an HTTP redirect, and every redirect source +must answer with a redirect page that names its target, which must answer +200 in turn. Run it once after a deploy that changes URLs, against +`https://educates.dev`, because GitHub does not document the Pages +behavior the URL form relies on. It also runs against the Docker image's +`serve` target at `http://localhost:8080`. + +The link check uses the settings in `lychee.toml`. ## Building the Docker image -To build the docker image you will need to run the following command: - ``` -docker build --target -t . +npm run docker-build [-- ] ``` -To deconstruct the above command: - -- docker build - This is the command to build a docker image. -- --target - This is the target to build. The target is the name of the stage in the dockerfile. Valid targets are dev, serve and caddy. -- -t - This is the name and tag of the image that will be built. The format is :. The name can be anything you want. The tag is optional. If you do not specify a tag, latest will be used. -- . - This is the path to the build context. In this case we are using the current directory as the build context. - -## Running the Docker Image -Depending on stage / target you will need to run the docker image differently. - -### Running Dev target -To run the dev target you will need to run the following command: - +The script builds a target of the Dockerfile, `serve` unless you name +another, as the image `educates-dev:`, with Node.js at the Volta +pin. Arguments after the target go to `docker buildx build`. The image is +for your machine's architecture; to build other platforms, list them in +`TARGET_PLATFORMS`: ``` -docker run --rm -d -p 3000:3000 -v $(pwd):/opt/docusaurus +TARGET_PLATFORMS=linux/amd64,linux/arm64 npm run docker-build ``` -If using PowerShell you will need to use ${pwd} instead of $(pwd). On some systems you may need to replace $(pwd) with . or the full path to the directory you want to mount. - -To deconstruct the above command: - -- docker run - This is the command to run a docker image. -- --rm - This is an optional flag that will remove the container when it exits. -- -d - This is an optional flag that will run the container in detached mode. -- -p 3000:3000 - This is an optional flag that will map port 3000 on the host to port 3000 in the container. -- -v $(pwd):/var/docusaurus - This is an optional flag that will mount the current directory as a volume in the container. -- - This is the name and tag of the image that will be run. Make sure to use the same tag that you used when building the image. +A platform other than your machine's builds under emulation, and an image +for more than one platform needs an image store that supports +multi-platform images, as the containerd image store, Docker Desktop's +default, does. -### Running Serve target +The Dockerfile has two targets: -To run the serve target you will need to run the following command: +- `serve` builds the site, runs the site check, and serves the result with + nginx the way GitHub Pages serves it: `/page` serves `page.html`, `/page/` + is a 404, a directory URL serves its `index.html`, and the redirect pages + work. Run it and open `http://localhost:8080`: -``` -docker run --rm -d -p 3000:3000 -``` - -To deconstruct the above command: - -- docker run - This is the command to run a docker image. -- --rm - This is an optional flag that will remove the container when it exits. -- -d - This is an optional flag that will run the container in detached mode. -- -p 3000:3000 - This is an optional flag that will map port 3000 on the host to port 3000 in the container. -- - This is the name and tag of the image that will be run. Make sure to use the same tag that you used when building the image. + ``` + docker run --rm -p 8080:80 educates-dev:serve + ``` -### Running Nginx target +- `dev` runs the Astro dev server over your checkout, mounted into the + container. The second volume keeps the image's dependencies, which are + built for Linux, in place of yours. Run it and open + `http://localhost:4321`: -To run the nginx target you will need to run the following command: + ``` + npm run docker-build -- dev + docker run --rm -it -p 4321:4321 -v "$(pwd)":/opt/site -v /opt/site/node_modules educates-dev:dev + ``` -``` -docker run --rm -d -p 8080:80 -``` +## Continuous integration -If using PowerShell you will need to use ${pwd} instead of $(pwd). On some systems you may need to replace $(pwd) with . or the full path to the directory you want to mount. +Workflows in `.github/workflows/`: -To deconstruct the above command: +- **Checks** runs on every pull request to `develop` and `main`: the type + check, the formatting check, the build, the site check, the unit tests, + the internal link check, and Lighthouse on a mobile profile, the median of + three runs, on the pages listed in `lighthouserc.yml`. Accessibility or + SEO below 90 fails the checks; performance below 90 warns. +- **Deploy to GitHub Pages** builds `main` and publishes it, on every push + to `main` and by hand. +- **External links** checks the links to other sites every week and keeps + one issue, "Dead external links", listing the dead ones. It never blocks + a pull request. -- docker run - This is the command to run a docker image. -- --rm - This is an optional flag that will remove the container when it exits. -- -d - This is an optional flag that will run the container in detached mode. -- -p 8080:80 - This is an optional flag that will map port 8080 on the host to port 80 in the container. -- - This is the name and tag of the image that will be run. Make sure to use the same tag that you used when building the image. +All of them run Node.js at the Volta pin in `package.json`. Dependabot +opens a weekly pull request with the minor and patch updates for npm, one +for GitHub Actions, and a pull request of its own for each major update. ## Publishing workflow for GitHub -- Develop your code in branch `develop` +- Develop your changes in branch `develop`. - Once you're done with your changes, commit them, push them and create a PR to incorporate the changes in `main`. ``` git commit -m "Message" git push origin develop ``` -- There's a workflow that will test that everything builds fine, and if everything is ok, you should be able to merge the PR into `main` -- There's a workflow that for the merged PR into `main` will also publish the generated site to `github pages` so that will become online -- You need to pull down `main` branch and merge it into `develop` locally for your next iteration. +- The checks workflow runs on the PR. Once it passes, you can merge the PR into `main`. +- A workflow publishes the site built from `main` to GitHub Pages. +- Pull `main` and merge it into `develop` locally for your next iteration. ``` git checkout main git pull origin main diff --git a/about-educates/_category_.json b/about-educates/_category_.json deleted file mode 100644 index f36dc58..0000000 --- a/about-educates/_category_.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "label": "About Educates", - "position": 2 -} - diff --git a/about-educates/deployment.md b/about-educates/deployment.md deleted file mode 100644 index 6acc76d..0000000 --- a/about-educates/deployment.md +++ /dev/null @@ -1,39 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Deployment - -Educates is a Kubernetes-based platform designed to provide interactive workshop environments. This section provides a comprehensive overview of the Educates architecture, its core concepts, and how the system works. - -## Deployment Components - -The Educates Training Platform has some requirements for it's configuration to properly work on a Kubernetes cluster on a production environment. - -These requirements are: - -- A **Kubernetes cluster**. Educates has been tested on **GKE**, **EKS** and **AKS**, **Talos**, **Minishift** and **Kind**. -- A **domain name** so that training portals will be accessible from the internet. In our cloud deployment we use **ExternalDNS** to create the DNS records, but you can use any other DNS provider. On local cloud deployments you can rely on external services like **nip.io** or **xip.io** or use Educates Local Resolver. -- A **wildcard certificate** for the domain name as we want access to the training portals to be secured. You can run Educates Training Platform over **http** but we don't recommend it. For local deployments, Educates CLI helps you manage your own certificates. For cloud deployments, you can use **cert-manager** to issue certificates from Let's Encrypt. -- A **policy engine**. Educates can enforce policies on the cluster and on every workshop session. It can use Kubernetes native policies or external policy engines like **Kyverno**, which is our recommended policy engine and the default one. - - -```mermaid -architecture-beta - group k8s(cloud)[Kubernetes Cluster] - - service educates(server)[Educates Training Platform] in k8s - service kyverno(internet)[Kyverno] in k8s - service kappcontroller(internet)[Kapp Controller] in k8s - service externaldns(internet)[ExternalDNS] in k8s - service certmanager(internet)[CertManager] in k8s - - educates:T -- B:kyverno - educates:T -- B:kappcontroller - educates:T -- B:externaldns - educates:T -- B:certmanager -``` - -**Educates Training Platform** does provide an installation mechanism via it's CLI that will provide oppinionated -deployments of the required components on local clusters, **GKE** and **EKS**. It does installation in an imperative way, -but you can also install Educates declaratively using **kapp-controller**. diff --git a/about-educates/history.md b/about-educates/history.md deleted file mode 100644 index eb7983a..0000000 --- a/about-educates/history.md +++ /dev/null @@ -1,137 +0,0 @@ ---- -sidebar_position: 6 -title: Educates History ---- - -This page documents the evolution of the Educates project from its inception as an internal tool to becoming an independent open-source project. - -## Project Timeline - -The following timeline shows the major milestones in Educates' history: - -```mermaid ---- -config: - timeline: - disableMulticolor: true ---- -timeline - title Educates History - Dec 2019: Educates 1.x - Sept 2020: Spring One Conference - Apr 2021: Tanzu Learning Center - Feb 2022: Educates 2.x - Nov 2023: Broadcom Acquisition - Aug 2024: Educates 3.x Release - Oct 2024: Independent OSS Project - Jun 2025: Educates Hub Launch -``` - -## Key Milestones - -### December 2019 - Educates 1.x - -Educates was originally created as an internal tool for the VMware Tanzu Developer Advocates team. The team needed a platform to train users in Kubernetes and showcase developer tools and applications running on Kubernetes. This initial version laid the foundation for what would become a comprehensive training platform. - -**Key Features:** -- Basic workshop hosting capabilities -- Kubernetes-based architecture -- Support for interactive training sessions - -### September 2020 - Spring One Conference - -Educates proved its scalability and reliability when it was used at the Spring One conference to run **over 5,000 workshop executions**. This large-scale deployment demonstrated that Educates could handle enterprise-level training scenarios and validated the platform's architecture and design decisions. - -**Achievements:** -- Successfully scaled to support hundreds of concurrent users -- Validated the platform's reliability and performance -- Demonstrated real-world applicability for large conferences - -### April 2021 - Tanzu Learning Center Launch - -VMware launched **Tanzu Learning Center** using Educates 1.x as its foundation. This marked Educates' transition from an internal tool to a production platform powering VMware's official learning initiatives. The Learning Center provided structured training paths for VMware Tanzu products and technologies. - -**Impact:** -- Educates became a core component of VMware's training infrastructure -- Enabled structured learning paths for Tanzu products -- Expanded the platform's user base significantly - -### February 2022 - Educates 2.x Launch - -The Developer Advocates team resumed active development on Educates to create version 2.x, which was designed to power multiple learning platforms: -- **[Tanzu.academy](https://tanzu.academy)** - VMware Tanzu training platform -- **[Kube.academy](https://kube.academy)** - Kubernetes training platform -- **[Spring.academy](https://spring.academy)** - Spring framework training platform - -**Version 2.x Improvements:** -- Enhanced workshop authoring capabilities -- Improved user experience -- Better integration with learning management systems -- Expanded support for different training scenarios - -### November 2023 - Broadcom Acquisition - -Broadcom acquired VMware and gained ownership of the Educates project. This transition period involved organizational changes and strategic decisions about the future of the platform. During this time, the project continued to be maintained and used across the various learning platforms. - -**Transition Period:** -- Project ownership transferred to Broadcom -- Continued maintenance and support -- Strategic evaluation of project direction - -### August 2024 - Educates 3.x Release - -Educates 3.x was released with significant improvements focused on usability and deployment: - -**Major Enhancements:** -- **Cloud Installers**: Simplified installation process for cloud providers -- **Improved Usability**: Enhanced user experience and workflow -- **Better Documentation**: Comprehensive guides and references -- **Enhanced Features**: New capabilities and integrations - -This release marked a significant step forward in making Educates more accessible and easier to deploy. - -### October 2024 - Independent Open Source Project - -In a significant milestone, Educates was **donated by Broadcom** and became an **independent open-source project**. This transition ensured the project's long-term sustainability and community-driven development. - -**Benefits of Independence:** -- Community-driven development and governance -- Open contribution model -- Independent project roadmap -- Broader adoption and ecosystem growth - -The project is now maintained by the Educates community, with contributions from developers, users, and organizations worldwide. - -### June 2025 - Educates Hub Launch - -The **Educates Hub** was launched, providing a centralized platform for: -- Discovering workshops -- Sharing workshop content -- Community collaboration -- Workshop marketplace - -**Hub Features:** -- Workshop catalog and discovery -- Community contributions -- Easy workshop sharing -- Integration with Educates deployments - -## Evolution Summary - -Educates has evolved from a small internal tool to a comprehensive, independent open-source platform: - -1. **Internal Tool (2019-2020)**: Created to solve specific needs of the Developer Advocates team -2. **Production Platform (2020-2023)**: Scaled to support enterprise training and multiple learning platforms -3. **Corporate Ownership (2023-2024)**: Maintained under VMware/Broadcom ownership -4. **Independent OSS (2024-present)**: Community-driven open-source project with broad adoption - -## Looking Forward - -As an independent open-source project, Educates continues to evolve with: -- Active community contributions -- Regular feature releases -- Expanding use cases and adoption -- Growing ecosystem of workshops and integrations - -The project's journey from an internal tool to an independent open-source platform demonstrates its value and the commitment of its community to making interactive Kubernetes training accessible to everyone. - diff --git a/about-educates/index.md b/about-educates/index.md deleted file mode 100644 index ca782a0..0000000 --- a/about-educates/index.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -sidebar_position: 1 -title: Architecture ---- - -Educates is a Kubernetes-based platform designed to provide interactive workshop environments. This section provides a comprehensive overview of the Educates architecture, its core concepts, and how the system works. - -## Architectural Components - -The core architecture consists of the **Session Manager**, **Secrets Manager**, which are the main Kubernetes Controllers. These controllers manages a set of **Custom Resource Definitions (CRDs)**, the **Training Portal** and **Workshop**, which are the main Kubernetes Resources a user will deal with. These resources work together to create **Workshop Environments** and **Workshop Sessions**. - -```mermaid -graph TB - subgraph "Kubernetes Cluster" - subgraph "Educates Operator" - SM[Session Manager] - SC[Secrets Manager] - end - - subgraph "Training Portal" - TP[Training Portal Service] - UI[Web UI] - API[REST API] - end - - subgraph "Workshop Environment" - WE[Workshop Environment Namespace] - WR[Workshop Resources] - end - - subgraph "Workshop Session" - WS[Workshop Session Namespace] - WT[Web Terminal] - VE[VS Code Editor] - KC[Kubernetes Console] - FS[File Server] - GS[Git Server] - IR[Image Registry] - DR[Docker Runtime] - VC[vCluster - Optional] - end - end - - User[User] -->|Access| UI - User -->|API Calls| API - UI --> TP - API --> TP - TP -->|Creates| WE - TP -->|Creates| WS - SM -->|Manages| WE - SM -->|Manages| WS - SC -->|Manages| WS - WS --> WT - WS --> VE - WS --> KC - WS --> FS - WS --> GS - WS --> IR - WS --> DR - WS -.->|Optional| VC -``` - -## Core Components - -```mermaid -graph TB - subgraph "Kubernetes Cluster" - subgraph "Educates Operator" - SM[Session Manager] - CM[Secrets Manager] - end - end -``` - -### Session Manager - -The Session Manager is responsible for: -- Creating and managing workshop session namespaces -- Allocating resources to sessions -- Managing session lifecycle (creation, updates, deletion) -- Ensuring proper RBAC and resource quotas are applied - -### Secrets Manager - -The Secrets Manager handles: -- Secret management across workshop sessions -- Secret injection into workshop environments -- Secret copying between namespaces -- Secure secret distribution - -### Workshop - -Provides the definition of a workshop. Preloaded by an administrator into the cluster, it defines where the workshop content is hosted, or the location of a container image which bundles the workshop content and any additional tools required for the workshop. The definition also lists additional resources that should be created which are to be shared between all workshop sessions, or for each session, along with details of resources quotas and access roles required by the workshop. - -```mermaid ---- - config: - class: - hideEmptyMembersBox: true ---- -classDiagram - direction LR - TrainingPortal *-- "many" Workshop - class TrainingPortal{ - Title - Logo - Authentication - Access Charactertistics - Capacity - Timeouts - Workshops[] - } - class Workshop{ - Title - Description - ContentLocation - Extensions - Budgets and resource quotas - Capabilities - Additional Session Resources - Additional Environment Resources - } -``` - - -### Training Portal - -Created by an administrator in the cluster to trigger the deployment of a training portal. The training portal can provide access to **one or more** distinct workshops defined by a **Workshop** resource. - -```mermaid -graph TB - TP1[Training Portal] - TP2[Training Portal] - W1[Workshop 1] - W2[Workshop 2] - W3[Workshop 3] - W4[Workshop 4] - W5[Workshop 5] - W6[Workshop 6] - - - TP1 --> W1 - TP1 --> W2 - TP1 --> W3 - TP1 --> W4 - TP1 --> W6 - - TP2 --> W2 - TP2 --> W3 - TP2 --> W5 -``` - -The training portal provides a **web based interface** for registering for workshops and accessing them. - -It also provides a **REST API** for requesting access to workshops, allowing custom front ends to be created which integrate with separate identity providers and which provide an alternate means for browsing and accessing workshops. - - -## Resource Flow - -1. **Workshop Definition**: A `Workshop` resource is created, defining the workshop content and requirements -2. **Training Portal**: A `TrainingPortal` resource is created, which sets up the web interface -3. **Workshop Environment**: The portal creates `WorkshopEnvironment` resources for each workshop -4. **Workshop Session**: When a user requests access, a `WorkshopSession` resource is created -5. **Session Resources**: The operator creates all necessary resources for the session (namespaces, services, etc.) diff --git a/about-educates/local-dev.md b/about-educates/local-dev.md deleted file mode 100644 index 702a108..0000000 --- a/about-educates/local-dev.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -sidebar_position: 5 -title: Local Development ---- - -For local development, Educates provides a streamlined experience using **kind** (Kubernetes in Docker): - -- **Fully Configured Cluster**: The Educates CLI creates a complete Kubernetes cluster with all necessary components. -- **Container Registry**: A local container registry is automatically set up for publishing and pulling workshop images. -- **Local DNS Resolver**: A local DNS resolver improves the development workflow by providing proper DNS resolution for local services. -- **Container Registry Mirrors**: Container registry mirrors can be configured to mirror upstream registries to optimize download speed times on local clusters. -- **Wildcard TLS certificate and Certificate Authority**: A local wildcard TLS certificate and Certificate Authority can be registered to allow secure communication within the cluster. - -This setup allows developers to: -- Test workshops locally before deploying to production -- Iterate quickly on workshop content -- Develop and test Educates features -- Experiment with different configurations - -The local development environment mirrors the production setup, making it easy to transition from development to production. - diff --git a/about-educates/workflows.md b/about-educates/workflows.md deleted file mode 100644 index bcc92e7..0000000 --- a/about-educates/workflows.md +++ /dev/null @@ -1,294 +0,0 @@ ---- -sidebar_position: 4 -title: Workflows ---- - -This page explains how Educates works from a workflow perspective, covering the lifecycle of workshops and sessions. - -## Workshop Deployment Workflow - -The following diagram illustrates how workshops are deployed and made available: - -```mermaid -sequenceDiagram - participant Admin - participant Portal as Training Portal - participant Operator as Educates Operator - participant Cluster as Kubernetes Cluster - - Admin->>Cluster: Create Workshop Resource - Operator->>Operator: Watch Workshop CR - Operator->>Operator: Validate Workshop Definition - - Admin->>Cluster: Create TrainingPortal Resource - Operator->>Operator: Watch TrainingPortal CR - Operator->>Cluster: Create WorkshopEnvironment - Operator->>Cluster: Deploy Shared Resources - Operator->>Portal: Register Workshop - - Portal->>Portal: Display Workshop in Catalog -``` - -### Step-by-Step Process - -1. **Workshop Definition**: An administrator creates a `Workshop` Custom Resource that defines: - - Workshop content location (Git repo or container image) - - Resource requirements - - Required capabilities - - RBAC permissions - -2. **Training Portal Creation**: An administrator creates a `TrainingPortal` resource that: - - References one or more workshops - - Configures authentication - - Sets up the web interface - -3. **Workshop Environment Setup**: The operator creates a `WorkshopEnvironment` for each workshop: - - Sets up a namespace for the workshop - - Deploys shared resources (common to all sessions) - - Configures the environment according to workshop requirements - -4. **Portal Registration**: The workshop becomes available in the Training Portal catalog - -## User Session Workflow - -When a user accesses a workshop, the following workflow is triggered: - -```mermaid -sequenceDiagram - participant User - participant Portal as Training Portal - participant Operator as Educates Operator - participant SM as Session Manager - participant SC as Secrets Manager - participant Session as Workshop Session - - User->>Portal: Request Workshop Access - Portal->>Portal: Check Available Sessions - alt Session Available - Portal->>Session: Allocate Existing Session - else No Session Available - Portal->>Operator: Create WorkshopSession CR - Operator->>SM: Create Session Namespace - SM->>Session: Create Session Resources - SM->>Session: Apply RBAC & Quotas - SC->>Session: Inject Required Secrets - Operator->>Session: Deploy Per-Session Resources - Operator->>Session: Start Capabilities (Terminal, Editor, etc.) - Operator->>Portal: Session Ready - Portal->>Session: Allocate Session to User - end - Portal->>User: Redirect to Workshop Session - User->>Session: Access Workshop Dashboard -``` - -### Session Creation Process - -1. **Session Request**: User selects a workshop from the Training Portal -2. **Session Allocation**: Portal checks for available pre-created sessions or creates a new one -3. **Namespace Creation**: Session Manager creates a dedicated namespace for the session -4. **Resource Setup**: - - RBAC policies are applied - - Resource quotas are set - - Required secrets are injected -5. **Capability Deployment**: Required capabilities are started: - - Web terminal - - VS Code editor - - Kubernetes console - - File server, Git server, etc. -6. **Per-Session Resources**: Workshop-specific resources are deployed to the session namespace -7. **Session Access**: User is redirected to the workshop session dashboard - -## Workshop Content Workflow - -The workflow for creating and publishing workshop content: - -```mermaid -graph LR - A[Write Markdown Content] --> B[Package as OCI Image] - B --> C[Push to Registry] - C --> D[Create Workshop CR] - D --> E[Workshop Available] - - style A fill:#e1f5ff - style B fill:#e1f5ff - style C fill:#fff4e1 - style D fill:#fff4e1 - style E fill:#e8f5e9 -``` - -### Content Development Process - -1. **Content Creation**: Workshop authors write content in Markdown (using Hugo format) -2. **Local Testing**: Content is tested using the local Educates environment -3. **Image Building**: Content is packaged into an OCI container image -4. **Image Publishing**: Image is pushed to a container registry -5. **Workshop Deployment**: Workshop CR is created referencing the image -6. **Content Updates**: For content-only changes, sessions can be updated without recreating - -## Session Lifecycle - -The lifecycle of a workshop session: - -```mermaid -stateDiagram-v2 - [*] --> Requested: User Requests Workshop - Requested --> Allocating: Portal Processes Request - Allocating --> Creating: No Session Available - Creating --> Starting: Resources Created - Starting --> Ready: Capabilities Started - Allocating --> Ready: Session Available - Ready --> Active: User Accesses Session - Active --> Idle: User Inactive - Idle --> Active: User Returns - Active --> Expired: Time Limit Reached - Expired --> Terminating: Cleanup Initiated - Ready --> Terminating: Session Not Used - Terminating --> [*]: Resources Deleted -``` - -### Session States - -- **Requested**: User has requested access to a workshop -- **Allocating**: Portal is finding or creating a session -- **Creating**: Session resources are being created -- **Starting**: Capabilities are being started -- **Ready**: Session is ready but not yet accessed -- **Active**: User is actively using the session -- **Idle**: Session is active but user is inactive -- **Expired**: Session has reached its time limit -- **Terminating**: Session is being cleaned up - -## Resource Management Workflow - -How resources are managed across the platform: - -```mermaid -graph TB - subgraph "Workshop Level" - WR[Workshop Resources] - WE[Workshop Environment] - end - - subgraph "Session Level" - SR[Session Resources] - SN[Session Namespace] - end - - subgraph "Shared Resources" - SH[Shared Applications] - SHN[Workshop Environment Namespace] - end - - WE --> SHN - WE --> SH - SN --> SR - SN -.->|Access| SHN - - style WR fill:#e1f5ff - style WE fill:#e1f5ff - style SR fill:#fff4e1 - style SN fill:#fff4e1 - style SH fill:#e8f5e9 - style SHN fill:#e8f5e9 -``` - -### Resource Hierarchy - -1. **Workshop Environment Resources**: Deployed once per workshop, shared by all sessions -2. **Session Resources**: Deployed per session, isolated to each user -3. **Resource Quotas**: Applied at the session level to limit resource usage -4. **RBAC Policies**: Applied at the session level to control access - -## Authentication and Authorization Workflow - -How users are authenticated and authorized: - -```mermaid -sequenceDiagram - participant User - participant Portal as Training Portal - participant Auth as Auth Provider - participant Operator as Educates Operator - - User->>Portal: Access Portal - Portal->>Auth: Authenticate User - Auth-->>Portal: Authentication Result - alt Authenticated - Portal->>Portal: Check User Permissions - Portal->>User: Show Available Workshops - User->>Portal: Request Workshop - Portal->>Operator: Create/Allocate Session - Operator->>Operator: Apply RBAC to Session - Operator-->>Portal: Session Ready - Portal->>User: Grant Access - else Not Authenticated - Portal->>User: Show Login/Register - end -``` - -## Content Update Workflow - -How workshop content updates are handled: - -```mermaid -graph LR - A[Update Content] --> B{Workshop Config Changed?} - B -->|Yes| C[Rebuild Image] - B -->|No| D[Update Content Only] - C --> E[Update Workshop CR] - E --> F[Recreate Environments] - D --> G[Update Running Sessions] - - style A fill:#e1f5ff - style C fill:#fff4e1 - style D fill:#e8f5e9 - style E fill:#fff4e1 - style F fill:#ffebee - style G fill:#e8f5e9 -``` - -### Update Scenarios - -1. **Content-Only Updates**: When only Markdown content changes: - - Content can be updated in running sessions - - No need to recreate workshop environments - - Faster iteration during development - -2. **Configuration Changes**: When workshop configuration changes: - - New container image must be built - - Workshop CR must be updated - - Workshop environments may need to be recreated - - Existing sessions continue until they expire - -## Local Development Workflow - -The workflow for developing workshops locally: - -```mermaid -graph LR - A[Write Content] --> B[Test Locally] - B --> C{Changes OK?} - C -->|No| A - C -->|Yes| D[Publish to Local Registry] - D --> E[Deploy to Local Cluster] - E --> F[Test Workshop] - F --> G{Workshop Works?} - G -->|No| A - G -->|Yes| H[Publish to Production Registry] - H --> I[Deploy to Production] - - style A fill:#e1f5ff - style B fill:#e8f5e9 - style D fill:#fff4e1 - style E fill:#fff4e1 - style H fill:#ffebee - style I fill:#ffebee -``` - -### Local Development Benefits - -- **Fast Iteration**: Content updates can be tested immediately -- **Isolated Environment**: No impact on production -- **Full Feature Set**: Local environment includes all capabilities -- **Easy Debugging**: Direct access to cluster and resources - diff --git a/about-educates/workshop-capabilities.md b/about-educates/workshop-capabilities.md deleted file mode 100644 index 4d0241c..0000000 --- a/about-educates/workshop-capabilities.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -sidebar_position: 3 -title: Workshop Capabilities ---- - -Educates provides a rich set of capabilities that can be enabled for workshops: - -### Web Terminal - -The **Web Terminal** provides a browser-based terminal interface that allows users to: -- Execute commands directly in the workshop environment -- Run scripts and tools -- Interact with Kubernetes clusters -- Execute commands from workshop instructions with a single click - -The terminal runs in a container within the workshop session namespace and provides full shell access to the workshop environment. - -### VS Code Editor - -The **VS Code Editor** (or compatible editor) provides a full-featured code editing experience: -- Edit files directly in the browser -- Syntax highlighting for multiple languages -- Support for VS Code extensions -- Integrated terminal access -- Git integration -- Debugging capabilities - -This allows users to write and modify code as part of the workshop without leaving the browser. - -### Kubernetes Web Console - -The **Kubernetes Web Console** provides a visual interface for: -- Viewing Kubernetes resources -- Managing deployments, services, and pods -- Inspecting logs and events -- Executing commands in containers -- Monitoring resource usage - -This makes it easier for users to understand and interact with Kubernetes resources during workshops. - -### vCluster (Virtual Cluster) - -**vCluster** provides isolated virtual Kubernetes clusters for each user session. This is useful for: -- Workshops requiring cluster-admin access -- Testing cluster-level operations -- Isolating users completely from each other -- Providing a full Kubernetes cluster experience - -vCluster runs as a lightweight virtual cluster within the host cluster, providing complete isolation while sharing the underlying infrastructure. - -### File Server - -The **File Server** provides HTTP access to files in the workshop environment: -- Serving static files and assets -- Downloading workshop resources -- Accessing generated files and outputs -- Sharing files between components - -### Git Server - -The **Git Server** provides Git repository access within the workshop: -- Cloning repositories -- Pushing and pulling changes -- Managing branches and tags -- Integrating with Git workflows - -This enables workshops that involve Git operations and version control. - -### Container Registry - -The **Container Registry** provides container image storage and distribution: -- Storing workshop-specific images -- Building and pushing images during workshops -- Pulling images for deployments -- Managing image versions - -For local development, Educates provides a local registry that simplifies the workflow. - -### Docker Runtime - -The **Docker Runtime** enables: -- Building container images -- Running containers -- Pushing images to registries -- Testing containerized applications - -This is essential for workshops that involve containerization and Docker operations. \ No newline at end of file diff --git a/astro.config.ts b/astro.config.ts new file mode 100644 index 0000000..79e8010 --- /dev/null +++ b/astro.config.ts @@ -0,0 +1,61 @@ +import { defineConfig } from "astro/config"; +import { satteri } from "@astrojs/markdown-satteri"; +import mdx from "@astrojs/mdx"; +import react from "@astrojs/react"; +import sitemap from "@astrojs/sitemap"; +import expressiveCode from "astro-expressive-code"; +import mermaid from "astro-mermaid"; +import { openGraphImages } from "./src/integrations/open-graph-images.ts"; +import { singleSitemap } from "./src/integrations/single-sitemap.ts"; +import { admonitions } from "./src/markdown/admonitions.ts"; +import { taskListLabels } from "./src/markdown/task-list-labels.ts"; +import { redirects } from "./src/redirects.ts"; +import { site } from "./src/site.ts"; + +// URL form (docs/adr/0002-keep-the-docusaurus-url-form.md): every page +// builds as `.html` and its canonical URL has no trailing slash, so +// GitHub Pages serves `/page` and `/page.html`, and `/page/` is a 404. +export default defineConfig({ + site: site.origin, + trailingSlash: "never", + build: { + format: "file", + }, + redirects: { ...redirects }, + // Two routes building the same URL fail the build, such as a redirect + // from a page that still exists. + prerenderConflictBehavior: "error", + // Markdown and MDX share this pipeline. + markdown: { + processor: satteri({ + features: { + // Admonitions, the `:::type` blocks. + directive: true, + // Content keeps its punctuation as written: `--` in prose stays two + // hyphens and quotes stay straight, as on the Docusaurus site. + smartPunctuation: false, + }, + mdastPlugins: [admonitions()], + // A task list's checkboxes, named by their items' text. + hastPlugins: [taskListLabels()], + }), + }, + // Integrations run in this order. expressiveCode(), configured in + // ec.config.mjs, must come before mdx() to render code blocks in MDX too. + // mermaid() turns each `mermaid` fence in Markdown into a + //
 that Mermaid renders in the browser, in the theme
+  // `data-theme` names; it comes after expressiveCode() and adds its plugin
+  // to the processor above. A `mermaid` fence in MDX fails the build unless
+  // the processor's `rawHtml` feature is on. react() renders the islands;
+  // singleSitemap() reads sitemap()'s output; openGraphImages() draws every
+  // page's Open Graph image from the built pages.
+  integrations: [
+    expressiveCode(),
+    mermaid({ enableLog: false }),
+    mdx(),
+    react(),
+    sitemap(),
+    singleSitemap(),
+    openGraphImages({ origin: site.origin }),
+  ],
+});
diff --git a/captures/custom-site/app.py b/captures/custom-site/app.py
new file mode 100644
index 0000000..4d4b7bf
--- /dev/null
+++ b/captures/custom-site/app.py
@@ -0,0 +1,520 @@
+"""A small front end of the kind a training team builds on Educates.
+
+It lists the workshops of a lookup service tenant and starts a Session of
+one when asked, embeds the training portal in a page of its own, receives
+the portal's analytics events, and runs an examiner check from outside the
+Session. Two more kinds of site live beside it: a catalog for the person
+signed in, built on one training portal's REST API alone, and the sites of
+two customers, each reaching only its own tenant of the lookup service. It
+uses only the Python standard library.
+
+Settings come from environment variables:
+
+- LOOKUP_URL, LOOKUP_TENANT, LOOKUP_USERNAME, LOOKUP_PASSWORD: the lookup
+  service, the tenant whose workshops it lists, and the client it logs in as.
+- PORTAL_URL: the training portal shown on the embedding page, whose REST
+  API the signed-in catalog uses.
+- ROBOT_CLIENT_ID, ROBOT_CLIENT_SECRET, ROBOT_USERNAME, ROBOT_PASSWORD: the
+  training portal's robot account, which the signed-in catalog logs in as.
+- CUSTOMERS: the customer sites, as a JSON list: each with its id, name,
+  initials and colour, its lookup service tenant, and the username and
+  password of the client it logs in as.
+- SITE_URL: this site's own URL, where a Session sends people back to.
+- EVENTS_URL: where the training portal posts a Session's analytics events
+  for this site, its URL inside the cluster.
+- SSL_CERT_FILE: the CA bundle to trust when calling the lookup service.
+"""
+
+import base64
+import html
+import json
+import os
+import ssl
+import threading
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+from collections import deque
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+
+LOOKUP_URL = os.environ.get("LOOKUP_URL", "")
+LOOKUP_TENANT = os.environ.get("LOOKUP_TENANT", "")
+LOOKUP_USERNAME = os.environ.get("LOOKUP_USERNAME", "")
+LOOKUP_PASSWORD = os.environ.get("LOOKUP_PASSWORD", "")
+PORTAL_URL = os.environ.get("PORTAL_URL", "")
+ROBOT_CLIENT_ID = os.environ.get("ROBOT_CLIENT_ID", "")
+ROBOT_CLIENT_SECRET = os.environ.get("ROBOT_CLIENT_SECRET", "")
+ROBOT_USERNAME = os.environ.get("ROBOT_USERNAME", "")
+ROBOT_PASSWORD = os.environ.get("ROBOT_PASSWORD", "")
+CUSTOMERS = {
+    customer["id"]: customer
+    for customer in json.loads(os.environ.get("CUSTOMERS") or "[]")
+}
+SITE_URL = os.environ.get("SITE_URL", "")
+EVENTS_URL = os.environ.get("EVENTS_URL", "")
+SITE_NAME = "Example Academy"
+SITE_INITIALS = "EA"
+
+# The person signed in to the signed-in catalog, as the site's own sign-in
+# knows them. The ID is the one the site passes to the portal for them.
+SIGNED_IN = {
+    "id": "dana-lee",
+    "first_name": "Dana",
+    "last_name": "Lee",
+    "email": "dana@example.com",
+}
+
+KUBERNETES_API = "https://kubernetes.default.svc"
+SERVICE_ACCOUNT = "/var/run/secrets/kubernetes.io/serviceaccount"
+
+events = deque(maxlen=200)
+token_lock = threading.Lock()
+# Access tokens, by the API and the client they are for.
+tokens = {}
+
+
+def cached_token(key, log_in):
+    """The access token kept for `key`, from `log_in` when there is none or
+    it has expired. `log_in` returns a token and how many seconds it lasts."""
+    with token_lock:
+        token = tokens.get(key)
+        if token is None or token["expires"] < time.time():
+            value, lifetime = log_in()
+            token = {"value": value, "expires": time.time() + lifetime}
+            tokens[key] = token
+        return token["value"]
+
+
+def lookup_request(method, path, body=None, client=(LOOKUP_USERNAME, LOOKUP_PASSWORD)):
+    """Calls the lookup service as a client, given by its username and
+    password, logging in first when it has no token."""
+    username, password = client
+
+    def log_in():
+        login = call_json(
+            "POST",
+            f"{LOOKUP_URL}/auth/login",
+            {"username": username, "password": password},
+        )
+        return login["access_token"], 3600
+
+    token = cached_token(("lookup", username), log_in)
+    headers = {"Authorization": f"Bearer {token}"}
+    return call_json(method, f"{LOOKUP_URL}{path}", body, headers)
+
+
+def portal_request(method, path, query=None):
+    """Calls the training portal's REST API as its robot account, logging in
+    first when it has no token."""
+
+    def log_in():
+        form = urllib.parse.urlencode(
+            {"grant_type": "password", "username": ROBOT_USERNAME, "password": ROBOT_PASSWORD}
+        ).encode()
+        request = urllib.request.Request(f"{PORTAL_URL}/oauth2/token/", data=form, method="POST")
+        client = base64.b64encode(f"{ROBOT_CLIENT_ID}:{ROBOT_CLIENT_SECRET}".encode()).decode()
+        request.add_header("Authorization", f"Basic {client}")
+        request.add_header("Content-Type", "application/x-www-form-urlencoded")
+        with urllib.request.urlopen(request, timeout=15) as response:
+            login = json.load(response)
+        # Renewed a minute early, so it does not expire during a request.
+        return login["access_token"], login.get("expires_in", 3600) - 60
+
+    token = cached_token(("portal", ROBOT_USERNAME), log_in)
+    url = f"{PORTAL_URL}{path}" + (f"?{urllib.parse.urlencode(query)}" if query else "")
+    return call_json(method, url, headers={"Authorization": f"Bearer {token}"})
+
+
+def call_json(method, url, body=None, headers=None, context=None):
+    data = json.dumps(body).encode() if body is not None else None
+    request = urllib.request.Request(url, data=data, method=method)
+    request.add_header("Content-Type", "application/json")
+    for name, value in (headers or {}).items():
+        request.add_header(name, value)
+    with urllib.request.urlopen(request, timeout=15, context=context) as response:
+        return json.load(response)
+
+
+def deployment_ready(namespace, name):
+    """Whether a deployment in the namespace has all its replicas ready."""
+    with open(f"{SERVICE_ACCOUNT}/token") as file:
+        bearer = file.read().strip()
+    context = ssl.create_default_context(cafile=f"{SERVICE_ACCOUNT}/ca.crt")
+    url = f"{KUBERNETES_API}/apis/apps/v1/namespaces/{namespace}/deployments/{name}"
+    try:
+        deployment = call_json(
+            "GET", url, headers={"Authorization": f"Bearer {bearer}"}, context=context
+        )
+    except urllib.error.HTTPError:
+        return False
+    wanted = deployment.get("spec", {}).get("replicas", 1)
+    ready = deployment.get("status", {}).get("readyReplicas", 0)
+    return ready >= wanted
+
+
+# Events about the plumbing rather than the learner, left off the page.
+QUIET_EVENTS = {"Terminal/Connect", "Terminal/Reconnect", "User/Create", "Workshop/Load"}
+
+
+def event_details(event):
+    """A line about an event: the page a learner viewed, or its data."""
+    data = event.get("data") or {}
+    if "current_page" in data:
+        number = data.get("page_number")
+        total = data.get("pages_total")
+        return f"Page {number} of {total}: {data['current_page']}"
+    if "event" in data:
+        return f"Action: {data['event']}"
+    return ", ".join(f"{key}: {value}" for key, value in data.items())
+
+
+STYLE = """
+* { box-sizing: border-box; }
+body { margin: 0; font: 16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
+  color: #1d2733; background: #f4f6f9; }
+header { display: flex; align-items: center; gap: 12px; padding: 14px 32px;
+  background: #1f3a5f; color: #fff; }
+header .logo { width: 30px; height: 30px; border-radius: 8px; background: #f2a541;
+  display: grid; place-items: center; font-weight: 700; color: #1f3a5f; }
+header nav { margin-left: auto; display: flex; gap: 20px; font-size: 15px; }
+header nav a { color: #dbe6f3; text-decoration: none; }
+header nav a.current { color: #fff; font-weight: 600; }
+main { max-width: 1040px; margin: 0 auto; padding: 32px; }
+h1 { margin: 0 0 6px; font-size: 28px; }
+p.lead { margin: 0 0 24px; color: #4a5868; }
+.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); gap: 20px; }
+.card { display: flex; flex-direction: column; gap: 8px; padding: 20px; border-radius: 12px;
+  background: #fff; box-shadow: 0 1px 3px rgba(20, 40, 70, 0.12); }
+.card h2 { margin: 0; font-size: 19px; }
+.card p { margin: 0; color: #4a5868; flex: 1; }
+.card button { align-self: flex-start; padding: 8px 16px; border: 0; border-radius: 8px;
+  background: #2f6fdf; color: #fff; font: 600 15px system-ui, sans-serif; cursor: pointer; }
+.card .meta { font-size: 13px; color: #6a7888; }
+table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 12px;
+  overflow: hidden; box-shadow: 0 1px 3px rgba(20, 40, 70, 0.12); font-size: 14px; }
+th, td { padding: 9px 14px; text-align: left; border-bottom: 1px solid #e6eaf0; vertical-align: top; }
+th { background: #eef2f7; font-weight: 600; }
+td code { font: 13px ui-monospace, Menlo, monospace; color: #33475b; white-space: nowrap; }
+iframe { width: 100%; height: 560px; border: 1px solid #d5dce6; border-radius: 12px; background: #fff; }
+header .account { margin-left: auto; display: flex; align-items: center; gap: 10px;
+  font-size: 15px; }
+header .avatar { width: 30px; height: 30px; border-radius: 50%; background: #dbe6f3;
+  color: #1f3a5f; display: grid; place-items: center; font: 700 13px system-ui, sans-serif; }
+.card .free { font-size: 14px; color: #4a5868; }
+.card .yours { font-size: 14px; font-weight: 600; color: #1f7a4d; }
+.card button.resume { background: #1f7a4d; }
+body.branded header { background: var(--brand); }
+body.branded header .logo { background: #fff; color: var(--brand); }
+body.branded .card button { background: var(--brand); }
+p.tenant { margin: 16px 0 0; font-size: 13px; color: #6a7888; }
+p.tenant code { font: 13px ui-monospace, Menlo, monospace; }
+"""
+
+
+def page(title, current, body):
+    links = [("/", "Workshops"), ("/embed", "Training portal"), ("/events", "Analytics")]
+    nav = "".join(
+        f'{label}'
+        for href, label in links
+    )
+    return layout(title, SITE_NAME, SITE_INITIALS, f"", body)
+
+
+def layout(title, site_name, initials, header_end, body, colour=None):
+    """A page under a header with the site's logo and name, and `header_end`
+    at the header's far end, in the site's own colour when it has one."""
+    brand = f' class="branded" style="--brand: {html.escape(colour)}"' if colour else ""
+    logo = f''
+    return f"""
+{html.escape(title)} | {html.escape(site_name)}
+
{logo}{html.escape(site_name)}{header_end}
+
{body}
""" + + +def minutes_left(seconds): + minutes = max(1, round(seconds / 60)) + return f"{minutes} min left" + + +def customer_at(path, page=""): + """The customer whose site has the page `path`, such as /customers/acme + with no `page`, or /customers/acme/start with `page` /start.""" + prefix = "/customers/" + if not path.startswith(prefix) or not path.endswith(page): + return None + return CUSTOMERS.get(path[len(prefix) : len(path) - len(page)]) + + +def client_of(customer): + """The lookup service client a customer's site logs in as.""" + return customer["username"], customer["password"] + + +def workshop_cards(tenant, client, action): + """The workshops a client reaches in a lookup service tenant, as cards + whose button posts to `action`, and a message when it cannot be reached.""" + try: + listing = lookup_request( + "GET", f"/api/v1/workshops?tenant={urllib.parse.quote(tenant)}", client=client + ) + except Exception as error: # Shown on the page, so the problem is visible. + return f"

Could not reach the lookup service: {html.escape(str(error))}

", "" + cards = "".join( + f"""
+

{html.escape(workshop.get("title", workshop["name"]))}

+

{html.escape(workshop.get("description", ""))}

+ +
""" + for workshop in listing.get("workshops", []) + ) + return "", cards + + +class Handler(BaseHTTPRequestHandler): + def send_text(self, status, content, content_type="text/html; charset=utf-8", headers=None): + payload = content.encode() + self.send_response(status) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(payload))) + for name, value in (headers or {}).items(): + self.send_header(name, value) + self.end_headers() + self.wfile.write(payload) + + def cors_headers(self): + return { + "Access-Control-Allow-Origin": self.headers.get("Origin", "*"), + "Access-Control-Allow-Methods": "POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type", + } + + def do_OPTIONS(self): + self.send_text(204, "", headers=self.cors_headers()) + + def do_GET(self): + path = urllib.parse.urlparse(self.path).path + if path == "/": + return self.workshops() + if path == "/portal": + return self.signed_in_catalog() + if customer := customer_at(path): + return self.customer_site(customer) + if path == "/embed": + return self.embed() + if path == "/events": + return self.events() + if path == "/healthz": + return self.send_text(200, "ok", "text/plain") + self.send_text(404, page("Not found", "", "

Not found

")) + + def do_POST(self): + parsed = urllib.parse.urlparse(self.path) + length = int(self.headers.get("Content-Length", "0") or 0) + body = self.rfile.read(length) if length else b"" + if parsed.path == "/start": + return self.start(urllib.parse.parse_qs(body.decode())) + if parsed.path == "/portal/start": + return self.start_signed_in(urllib.parse.parse_qs(body.decode())) + if customer := customer_at(parsed.path, "/start"): + return self.start_for_customer(customer, urllib.parse.parse_qs(body.decode())) + if parsed.path == "/events": + try: + events.appendleft(json.loads(body or b"{}")) + except json.JSONDecodeError: + pass + return self.send_text(200, "{}", "application/json") + if parsed.path.startswith("/check/"): + return self.check(parsed.path.removeprefix("/check/"), body) + self.send_text(404, "{}", "application/json") + + def workshops(self): + message, cards = workshop_cards( + LOOKUP_TENANT, (LOOKUP_USERNAME, LOOKUP_PASSWORD), "/start" + ) + body = f"""

Hands-on workshops

+

Pick a workshop, and a lab environment of your own opens in your browser.

+{message}
{cards}
""" + self.send_text(200, page("Workshops", "/", body)) + + def start(self, form): + self.start_session( + LOOKUP_TENANT, + (LOOKUP_USERNAME, LOOKUP_PASSWORD), + form, + { + "clientIndexUrl": f"{SITE_URL}/", + "clientUserId": "learner-0042", + "userFirstName": "Sam", + "userLastName": "Example", + "userEmailAddress": "sam@example.com", + "analyticsWebhookUrl": EVENTS_URL, + }, + ) + + def start_session(self, tenant, client, form, details): + """Asks the lookup service for a Session of the workshop in `form`, + with the request's other `details`, and sends the browser to it.""" + response = lookup_request( + "POST", + "/api/v1/workshops", + { + "tenantName": tenant, + "workshopName": form.get("workshop", [""])[0], + **details, + }, + client=client, + ) + self.send_text(303, "", headers={"Location": response["sessionActivationUrl"]}) + + def signed_in_catalog(self): + """The portal's workshops for the person signed in, from its REST API alone.""" + try: + catalog = portal_request("GET", "/workshops/catalog/environments/") + signed_in_sessions = portal_request( + "GET", f"/workshops/user/{SIGNED_IN['id']}/sessions/" + ) + except Exception as error: # Shown on the page, so the problem is visible. + catalog, signed_in_sessions = {}, {} + message = f"

Could not reach the training portal: {html.escape(str(error))}

" + else: + message = "" + sessions = { + session["environment"]: session + for session in signed_in_sessions.get("sessions", []) + } + cards = [] + for environment in catalog.get("environments", []): + workshop = environment["workshop"] + capacity = environment.get("capacity") + free_count = capacity - environment.get("allocated", 0) if capacity else 0 + free = ( + f'

{free_count} of {capacity} Sessions free

' if capacity else "" + ) + session = sessions.get(environment["name"]) + if session: + countdown = session.get("countdown") + status = ( + f'

Your Session: {minutes_left(countdown)}

' + if countdown + else "" + ) + resume = '' + action = f"{status}{resume}" + else: + action = '' + cards.append( + f"""
+

{html.escape(workshop.get("title") or workshop["name"])}

+

{html.escape(workshop.get("description", ""))}

{free} + +{action}
""" + ) + name = f"{SIGNED_IN['first_name']} {SIGNED_IN['last_name']}" + initials = SIGNED_IN["first_name"][0] + SIGNED_IN["last_name"][0] + account = ( + f'" + ) + body = f"""

Your workshops

+

Start a workshop and it opens in a new tab, or go back to the one you have open.

+{message}
{"".join(cards)}
""" + self.send_text(200, layout("Your workshops", SITE_NAME, SITE_INITIALS, account, body)) + + def start_signed_in(self, form): + """Asks the portal for a Session for the person signed in, the one they + have if any, and sends the browser to it.""" + environment = form.get("environment", [""])[0] + response = portal_request( + "POST", + f"/workshops/environment/{urllib.parse.quote(environment)}/request/", + query={ + "index_url": f"{SITE_URL}/portal", + "user": SIGNED_IN["id"], + "email": SIGNED_IN["email"], + "first_name": SIGNED_IN["first_name"], + "last_name": SIGNED_IN["last_name"], + }, + ) + self.send_text(303, "", headers={"Location": f"{PORTAL_URL}{response['url']}"}) + + def customer_site(self, customer): + """A customer's own site, listing the workshops of its tenant alone.""" + message, cards = workshop_cards( + customer["tenant"], client_of(customer), f"/customers/{customer['id']}/start" + ) + body = f"""

Your workshops

+

Hands-on labs, each in an environment of your own.

+{message}
{cards}
+

Lookup service tenant: {html.escape(customer["tenant"])}

""" + page_html = layout( + "Your workshops", + customer["name"], + customer["initials"], + "", + body, + customer["colour"], + ) + self.send_text(200, page_html) + + def start_for_customer(self, customer, form): + self.start_session( + customer["tenant"], + client_of(customer), + form, + { + "clientIndexUrl": f"{SITE_URL}/customers/{customer['id']}", + "clientUserId": f"{customer['id']}-learner-0001", + }, + ) + + def embed(self): + body = f"""

Training portal

+

The workshops of our training portal, embedded in this page.

+""" + self.send_text(200, page("Training portal", "/embed", body)) + + def events(self): + shown = [ + item.get("event", {}) + for item in events + if item.get("event", {}).get("name") not in QUIET_EVENTS + ] + rows = "".join( + f"""{html.escape(event.get("timestamp", "")[11:19])} +{html.escape(event.get("name", ""))} +{html.escape(event.get("session", "") or "")} +{html.escape(event_details(event))}""" + for event in shown[:40] + ) + body = f"""

Learner activity

+

Events the training portal posted to this site's webhook, newest first.

+ +{rows or ''}
TimeEventSessionDetails
No events yet.
+""" + self.send_text(200, page("Analytics", "/events", body)) + + def check(self, name, body): + try: + request = json.loads(body or b"{}") + except json.JSONDecodeError: + request = {} + args = request.get("args") or [] + if name == "deployment-ready" and len(args) == 2: + success = deployment_ready(args[0], args[1]) + message = "Deployment is ready" if success else "Deployment is not ready yet" + else: + success, message = False, f"Unknown check {name}" + self.send_text( + 200, + json.dumps({"success": success, "message": message}), + "application/json", + self.cors_headers(), + ) + + def log_message(self, format, *args): + pass + + +if __name__ == "__main__": + ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever() diff --git a/captures/workshop/README.md b/captures/workshop/README.md new file mode 100644 index 0000000..519c661 --- /dev/null +++ b/captures/workshop/README.md @@ -0,0 +1,9 @@ +# Educates Feature Tour + +The workshop the site's capture tool drives to take the screenshots and +recordings on the Features pages of educates.dev. Each instruction page +sets up one shot, or a group of them, so every capture is the same each +time it is taken. + +See `scripts/captures/README.md` in the repository for how to deploy it +and run the captures. diff --git a/captures/workshop/exercises/README.md b/captures/workshop/exercises/README.md new file mode 100644 index 0000000..0ac22b0 --- /dev/null +++ b/captures/workshop/exercises/README.md @@ -0,0 +1,4 @@ +# Exercises + +Files for the Educates Feature Tour: the web application it deploys and +the examples it opens in the editor. diff --git a/captures/workshop/exercises/ai-course/CLAUDE.md b/captures/workshop/exercises/ai-course/CLAUDE.md new file mode 100644 index 0000000..89ffce2 --- /dev/null +++ b/captures/workshop/exercises/ai-course/CLAUDE.md @@ -0,0 +1,18 @@ +# Kubernetes for Application Developers + +- `README.md` gives the course overview, its workshops and its layout. +- `planning/course-brief.md` holds the course vision and design principles. +- `planning/resources.md` lists the references gathered while planning; + read it before searching the web. + +## Skills + +- Use the **educates-course-design** skill to plan the course, add + workshops, or update the planning documents. +- Use the **educates-workshop-authoring** skill to create or change a + workshop from its plan in `planning/workshop-plans/`. + +## Design principles + +Fully guided: every command and every edit is a clickable action. See +`planning/course-brief.md`. diff --git a/captures/workshop/exercises/ai-course/README.md b/captures/workshop/exercises/ai-course/README.md new file mode 100644 index 0000000..c8aee26 --- /dev/null +++ b/captures/workshop/exercises/ai-course/README.md @@ -0,0 +1,19 @@ +# Kubernetes for Application Developers + +A short course for developers who build applications and now need to run +them on Kubernetes. It takes a learner from a first deployment to an +application that configures itself and recovers on its own. + +## Workshops + +| Workshop | Type | Title | +| --- | --- | --- | +| `lab-a01-first-deployment` | Core | Your First Deployment | +| `lab-a02-configuration` | Core | Configuration and Secrets | +| `lab-a03-health-checks` | Elective | Health Checks and Self-Healing | + +## Layout + +- `planning/` holds the course brief, the workshop breakdown, a plan for + each workshop and the list of outstanding tasks. +- `workshops/` holds one directory per workshop, each an Educates workshop. diff --git a/captures/workshop/exercises/ai-course/planning/course-brief.md b/captures/workshop/exercises/ai-course/planning/course-brief.md new file mode 100644 index 0000000..47ed11a --- /dev/null +++ b/captures/workshop/exercises/ai-course/planning/course-brief.md @@ -0,0 +1,32 @@ +# Course Brief: Kubernetes for Application Developers + +## Course Scope + +A focused course of three workshops, in one module. Two core workshops run +in sequence; one elective follows them. + +## Audience + +Developers who write and ship web applications, and know containers, but +have not deployed to Kubernetes themselves. + +## Goals + +By the end of the course, a learner can: + +- Deploy an application to Kubernetes and expose it to users. +- Move its configuration and credentials out of the image. +- Make it recover on its own when a pod fails. + +## Design Principles + +- Fully guided: commands run and files change through clickable actions, + so learners spend their time on Kubernetes, not on typing. +- One application throughout, a small bookshelf web application, so each + workshop builds on what the learner already deployed. +- Every workshop ends with checks that confirm the learner's work. + +## Environment + +Each Session gets a namespace with a medium budget, the editor and the +Kubernetes console. diff --git a/captures/workshop/exercises/ai-course/planning/course-module-a.md b/captures/workshop/exercises/ai-course/planning/course-module-a.md new file mode 100644 index 0000000..f28d337 --- /dev/null +++ b/captures/workshop/exercises/ai-course/planning/course-module-a.md @@ -0,0 +1,63 @@ +# Module A: From Container to Kubernetes + +The course's goals map onto three workshops. Deploying and exposing the +application come first, since every later step changes a running +deployment. Configuration follows, as the step most learners need next. +Health checks are an elective: useful, but no other workshop depends on +them. + +## Workshop Structure Conventions + +Each workshop described below includes: + +- **Type**: whether this is a core (mandatory prerequisite) or elective workshop. +- **Status**: current workshop status and link to tasks. +- **Prerequisites**: which workshops must be completed first. +- **Learning objectives**: what the learner will be able to do after completing it. +- **Narrative arc**: the progression of the workshop from start to finish. + +## Core Workshops + +These workshops form the mandatory core sequence. Each one builds directly +on the previous and cannot be skipped. + +### Workshop A01: Your First Deployment + +**Detailed plan:** [workshop-plans/lab-a01-first-deployment.md](workshop-plans/lab-a01-first-deployment.md) + +**Status:** Mostly complete, [tasks](tasks.md#workshop-a01-your-first-deployment) + +**Type:** Core + +**Prerequisites:** None (first workshop) + +**Learning objectives:** + +After completing this workshop, the learner will be able to: + +- Deploy an application from a manifest with `kubectl apply`. +- Expose it inside the cluster with a service. +- Scale it and watch Kubernetes keep the replicas running. + +### Workshop A02: Configuration and Secrets + +**Detailed plan:** [workshop-plans/lab-a02-configuration.md](workshop-plans/lab-a02-configuration.md) + +**Status:** Incomplete, [tasks](tasks.md#workshop-a02-configuration-and-secrets) + +**Type:** Core + +**Prerequisites:** Workshop A01: Your First Deployment + +## Elective Workshops + +These workshops branch off the core and can be taken in any order once +their prerequisites are met. + +### Workshop A03: Health Checks and Self-Healing + +**Detailed plan:** [workshop-plans/lab-a03-health-checks.md](workshop-plans/lab-a03-health-checks.md) + +**Type:** Elective + +**Prerequisites:** Workshop A01: Your First Deployment diff --git a/captures/workshop/exercises/ai-course/planning/tasks.md b/captures/workshop/exercises/ai-course/planning/tasks.md new file mode 100644 index 0000000..4b49649 --- /dev/null +++ b/captures/workshop/exercises/ai-course/planning/tasks.md @@ -0,0 +1,44 @@ +# Course Tasks + +Outstanding tasks for the course. Organized by workshop, with priorities: + +- **P1**: Must fix, broken functionality, missing core content, or incorrect syntax +- **P2**: Should fix, incomplete sections, redundant files, or weak guidance +- **P3**: Nice to have, cosmetic improvements, edge case handling + +--- + +## Course-Level Tasks + +- [ ] **P2**: Add the elective workshop to the training portal definition. +- [ ] **P3**: Add a course overview page that links the three workshops. + +--- + +## Workshop A01: Your First Deployment + +**Status:** Mostly complete + +**Plan:** [workshop-plans/lab-a01-first-deployment.md](workshop-plans/lab-a01-first-deployment.md) + +- [ ] **P2**: Add the examiner check after scaling on page 02. ([source](../workshops/lab-a01-first-deployment/workshop/content/02-scale-the-application.md)) +- [x] **P1**: Fix the service port in `service.yaml` to match the container. + +--- + +## Workshop A02: Configuration and Secrets + +**Status:** Incomplete + +**Plan:** [workshop-plans/lab-a02-configuration.md](workshop-plans/lab-a02-configuration.md) + +- [ ] **P1**: Write pages 02 and 03; only the overview exists. +- [ ] **P2**: Add the config map and secret exercise files listed in the plan. + +--- + +## Recommended Next + +1. **P1** in Workshop A02: write the missing pages, since it blocks the core sequence. +2. **P2** in Workshop A01: add the missing check. +3. Plan and build the elective, Workshop A03. diff --git a/captures/workshop/exercises/ai-course/planning/workshop-plans/lab-a01-first-deployment.md b/captures/workshop/exercises/ai-course/planning/workshop-plans/lab-a01-first-deployment.md new file mode 100644 index 0000000..2259439 --- /dev/null +++ b/captures/workshop/exercises/ai-course/planning/workshop-plans/lab-a01-first-deployment.md @@ -0,0 +1,66 @@ +# Workshop Plan: Your First Deployment + +## Workshop Metadata + +- **Name:** lab-a01-first-deployment +- **Title:** Your First Deployment +- **Description:** Deploy a web application to Kubernetes, expose it with a service, and scale it. +- **Duration:** 30m +- **Difficulty:** beginner +- **Type:** core +- **Prerequisites:** None +- **Status:** Mostly complete, [tasks](../tasks.md#workshop-a01-your-first-deployment) + +## Workshop Configuration + +- **Terminal:** Enabled (split layout) +- **Editor:** Enabled +- **Kubernetes access:** Enabled, with the console +- **Namespace budget:** medium + +## Learning Objectives + +After completing this workshop, the learner will be able to: + +- Deploy an application from a manifest with `kubectl apply`. +- Expose it inside the cluster with a service. +- Scale it and watch Kubernetes keep the replicas running. + +## Assumed Knowledge + +Building and running a container image locally. No Kubernetes experience. + +## Exercise Files to Create + +### exercises/deployment.yaml + +A deployment of the bookshelf web application, one replica, image +`nginxinc/nginx-unprivileged:1.27-alpine`, with a security context that +passes the restricted policy. + +### exercises/service.yaml + +A service named `bookshelf` on port 8080, selecting the deployment's pods. + +## Page Plan + +### 00-workshop-overview.md + +What the learner builds, and what Kubernetes gives them that `docker run` +does not. + +### 01-deploy-the-application.md + +1. Open `deployment.yaml` in the editor and select the container spec. +2. Apply it with `kubectl apply`, then watch the pod start in terminal 2. +3. Examiner check: the deployment has one ready replica. + +### 02-scale-the-application.md + +1. Set `spec.replicas` to 3 with `editor:set-yaml-value`. +2. Apply, and delete one pod to show it being replaced. +3. Examiner check: three ready replicas. + +### 99-workshop-summary.md + +Recap, and a pointer to Workshop A02. diff --git a/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/README.md b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/README.md new file mode 100644 index 0000000..74f7f6f --- /dev/null +++ b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/README.md @@ -0,0 +1,4 @@ +# Your First Deployment + +Deploy a web application to Kubernetes, expose it with a service, and scale +it. diff --git a/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/exercises/README.md b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/exercises/README.md new file mode 100644 index 0000000..c8639e4 --- /dev/null +++ b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/exercises/README.md @@ -0,0 +1 @@ +Exercise files for the Your First Deployment workshop. diff --git a/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/exercises/deployment.yaml b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/exercises/deployment.yaml new file mode 100644 index 0000000..1b121bb --- /dev/null +++ b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/exercises/deployment.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bookshelf + labels: + app: bookshelf +spec: + replicas: 1 + selector: + matchLabels: + app: bookshelf + template: + metadata: + labels: + app: bookshelf + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: web + image: nginxinc/nginx-unprivileged:1.27-alpine + ports: + - containerPort: 8080 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL diff --git a/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/exercises/service.yaml b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/exercises/service.yaml new file mode 100644 index 0000000..c53c496 --- /dev/null +++ b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/exercises/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: bookshelf + labels: + app: bookshelf +spec: + selector: + app: bookshelf + ports: + - name: http + port: 8080 + targetPort: 8080 diff --git a/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/resources/workshop.yaml b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/resources/workshop.yaml new file mode 100644 index 0000000..c6600b5 --- /dev/null +++ b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/resources/workshop.yaml @@ -0,0 +1,38 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: lab-a01-first-deployment +spec: + title: Your First Deployment + description: Deploy a web application to Kubernetes, expose it with a service, and scale it. + duration: 30m + difficulty: beginner + publish: + image: "$(image_repository)/lab-a01-first-deployment-files:$(workshop_version)" + files: + - directory: + path: . + includePaths: + - /workshop/** + - /exercises/** + - /README.md + workshop: + files: + - image: + url: "$(image_repository)/lab-a01-first-deployment-files:$(workshop_version)" + session: + namespaces: + budget: medium + security: + token: + enabled: true + applications: + terminal: + enabled: true + layout: split + editor: + enabled: true + console: + enabled: true + examiner: + enabled: true diff --git a/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/00-workshop-overview.md b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/00-workshop-overview.md new file mode 100644 index 0000000..2d9d5da --- /dev/null +++ b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/00-workshop-overview.md @@ -0,0 +1,14 @@ +--- +title: Workshop Overview +--- + +You have built a container image of your application. In this workshop you +run it on Kubernetes, where it keeps running even when a container fails. + +## What you will learn + +- How a deployment describes the application you want running. +- How a service gives its pods a single address. +- How Kubernetes scales the application and replaces pods that fail. + +Your namespace for this workshop is `{{< param session_namespace >}}`. diff --git a/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/01-deploy-the-application.md b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/01-deploy-the-application.md new file mode 100644 index 0000000..5419f46 --- /dev/null +++ b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/01-deploy-the-application.md @@ -0,0 +1,46 @@ +--- +title: Deploy the Application +--- + +A deployment describes the application you want running. Open it in the +editor: + +```editor:open-file +file: ~/exercises/deployment.yaml +``` + +The container section names the image and the port it listens on: + +```editor:select-matching-text +file: ~/exercises/deployment.yaml +text: "containers:" +after: 4 +``` + +Apply the deployment to your namespace: + +```terminal:execute +command: kubectl apply -f deployment.yaml +``` + +Watch its pod start in the second terminal: + +```terminal:execute +command: kubectl get pods --watch +session: 2 +``` + +When the pod shows `Running`, stop watching: + +```terminal:interrupt +session: 2 +``` + +Check that the deployment is ready: + +```examiner:execute-test +name: test-deployment-ready +title: Verify the bookshelf deployment is ready +args: +- bookshelf +``` diff --git a/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/02-scale-the-application.md b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/02-scale-the-application.md new file mode 100644 index 0000000..fd6bc08 --- /dev/null +++ b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/02-scale-the-application.md @@ -0,0 +1,23 @@ +--- +title: Scale the Application +--- + +One replica is not much. Ask for three: + +```editor:set-yaml-value +file: ~/exercises/deployment.yaml +path: spec.replicas +value: 3 +``` + +Apply the change: + +```terminal:execute +command: kubectl apply -f deployment.yaml && kubectl get pods +``` + +Delete one pod, and watch Kubernetes replace it: + +```terminal:execute +command: kubectl delete pod -l app=bookshelf --wait=false | head -1 && kubectl get pods +``` diff --git a/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/99-workshop-summary.md b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/99-workshop-summary.md new file mode 100644 index 0000000..94db545 --- /dev/null +++ b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/99-workshop-summary.md @@ -0,0 +1,9 @@ +--- +title: Workshop Summary +--- + +You deployed an application to Kubernetes, gave it a service, and scaled it +to three replicas that Kubernetes keeps running. + +In the next workshop, Configuration and Secrets, you move the application's +settings out of its image. diff --git a/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/examiner/tests/test-deployment-ready b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/examiner/tests/test-deployment-ready new file mode 100755 index 0000000..fd09893 --- /dev/null +++ b/captures/workshop/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/examiner/tests/test-deployment-ready @@ -0,0 +1,8 @@ +#!/bin/bash + +# Passes when every replica of the deployment named $1 is ready. + +wanted=$(kubectl get deployment "$1" -o jsonpath='{.spec.replicas}') || exit 1 +ready=$(kubectl get deployment "$1" -o jsonpath='{.status.readyReplicas}') + +[ "${ready:-0}" = "$wanted" ] diff --git a/captures/workshop/exercises/environments/lab-linux-vm/resources/workshop.yaml b/captures/workshop/exercises/environments/lab-linux-vm/resources/workshop.yaml new file mode 100644 index 0000000..98ef50b --- /dev/null +++ b/captures/workshop/exercises/environments/lab-linux-vm/resources/workshop.yaml @@ -0,0 +1,96 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: lab-linux-vm +spec: + title: Linux Administration + description: Administer a Fedora virtual machine of your own, with root access. + duration: 60m + difficulty: beginner + workshop: + files: + - image: + url: $(image_repository)/lab-linux-vm-files:$(workshop_version) + session: + namespaces: + budget: x-large + limits: + max: + memory: 2.5Gi + objects: + - apiVersion: secrets.educates.dev/v1beta1 + kind: SecretCopier + metadata: + name: $(session_name)-ssh-keys + spec: + rules: + - sourceSecret: + name: $(ssh_keys_secret) + namespace: $(workshop_namespace) + targetNamespaces: + nameSelector: + matchNames: + - $(session_namespace) + targetSecret: + name: ssh-keys + - apiVersion: kubevirt.io/v1 + kind: VirtualMachine + metadata: + name: fedora + labels: + kubevirt.io/vm: fedora + spec: + running: true + template: + metadata: + labels: + kubevirt.io/vm: fedora + spec: + accessCredentials: + - sshPublicKey: + source: + secret: + secretName: ssh-keys + propagationMethod: + configDrive: {} + domain: + cpu: + cores: 2 + resources: + requests: + memory: 2Gi + devices: + disks: + - name: disk1 + disk: + bus: virtio + - name: cloudinitdisk + disk: + bus: virtio + interfaces: + - name: default + masquerade: {} + networks: + - name: default + pod: {} + volumes: + - name: disk1 + containerDisk: + image: $(oci_image_cache)/containerdisks/fedora:42 + - name: cloudinitdisk + cloudInitConfigDrive: + userData: |- + #cloud-config + password: $(services_password) + chpasswd: { expire: False } + - apiVersion: v1 + kind: Service + metadata: + name: fedora + spec: + selector: + kubevirt.io/vm: fedora + ports: + - name: ssh + port: 22 + targetPort: 22 diff --git a/captures/workshop/exercises/environments/lab-spring-petclinic/resources/workshop.yaml b/captures/workshop/exercises/environments/lab-spring-petclinic/resources/workshop.yaml new file mode 100644 index 0000000..3bcacf7 --- /dev/null +++ b/captures/workshop/exercises/environments/lab-spring-petclinic/resources/workshop.yaml @@ -0,0 +1,62 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: lab-spring-petclinic +spec: + title: Spring PetClinic on Kubernetes + description: Build the Spring PetClinic application and deploy it with its database. + duration: 45m + difficulty: intermediate + workshop: + image: jdk21-environment:* + files: + - image: + url: $(image_repository)/lab-spring-petclinic-files:$(workshop_version) + packages: + - name: vscode-java-tools + files: + - path: . + image: + url: ghcr.io/educates/educates-extension-packages/vscode-java-tools:v0.29.2 + session: + resources: + memory: 2Gi + namespaces: + budget: large + applications: + terminal: + enabled: true + layout: split + editor: + enabled: true + console: + enabled: true + objects: + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: database + spec: + selector: + matchLabels: + app: database + template: + metadata: + labels: + app: database + spec: + containers: + - name: postgres + image: postgres:17 + env: + - name: POSTGRES_PASSWORD + value: $(services_password) + - apiVersion: v1 + kind: Service + metadata: + name: database + spec: + selector: + app: database + ports: + - port: 5432 diff --git a/captures/workshop/exercises/environments/lab-spring-petclinic/workshop/setup.d/01-build-petclinic.sh b/captures/workshop/exercises/environments/lab-spring-petclinic/workshop/setup.d/01-build-petclinic.sh new file mode 100755 index 0000000..01d89ad --- /dev/null +++ b/captures/workshop/exercises/environments/lab-spring-petclinic/workshop/setup.d/01-build-petclinic.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Clones the application and warms the Maven cache, so the first build in +# the workshop is quick. + +set -eo pipefail + +git clone -q https://github.com/spring-projects/spring-petclinic.git ~/exercises/spring-petclinic +cd ~/exercises/spring-petclinic && ./mvnw -q dependency:go-offline diff --git a/captures/workshop/exercises/frontend/Dockerfile b/captures/workshop/exercises/frontend/Dockerfile new file mode 100644 index 0000000..55e4df6 --- /dev/null +++ b/captures/workshop/exercises/frontend/Dockerfile @@ -0,0 +1,3 @@ +FROM nginxinc/nginx-unprivileged:1.27-alpine + +COPY index.html /usr/share/nginx/html/index.html diff --git a/captures/workshop/exercises/frontend/configmap.yaml b/captures/workshop/exercises/frontend/configmap.yaml new file mode 100644 index 0000000..a4060bb --- /dev/null +++ b/captures/workshop/exercises/frontend/configmap.yaml @@ -0,0 +1,49 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: frontend-config + labels: + app: frontend +data: + default.conf: | + server { + listen 8080; + location / { + root /usr/share/nginx/html; + index index.html; + ssi on; + } + } +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: frontend-content + labels: + app: frontend +data: + index.html: | + + + + + Bookshelf + + + +
+

Bookshelf

+

Your reading list, served from Kubernetes.

+

Answered by pod

+
+ + diff --git a/captures/workshop/exercises/frontend/deployment.yaml b/captures/workshop/exercises/frontend/deployment.yaml new file mode 100644 index 0000000..66c2d5e --- /dev/null +++ b/captures/workshop/exercises/frontend/deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: frontend + labels: + app: frontend +spec: + replicas: 2 + selector: + matchLabels: + app: frontend + template: + metadata: + labels: + app: frontend + spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: nginx + image: nginxinc/nginx-unprivileged:1.27-alpine + ports: + - containerPort: 8080 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + volumeMounts: + - name: content + mountPath: /usr/share/nginx/html + - name: config + mountPath: /etc/nginx/conf.d + volumes: + - name: content + configMap: + name: frontend-content + - name: config + configMap: + name: frontend-config diff --git a/captures/workshop/exercises/frontend/index.html b/captures/workshop/exercises/frontend/index.html new file mode 100644 index 0000000..20085be --- /dev/null +++ b/captures/workshop/exercises/frontend/index.html @@ -0,0 +1,11 @@ + + + + + Bookshelf + + +

Bookshelf

+

Your reading list, built into an image of its own.

+ + diff --git a/captures/workshop/exercises/frontend/service.yaml b/captures/workshop/exercises/frontend/service.yaml new file mode 100644 index 0000000..9594354 --- /dev/null +++ b/captures/workshop/exercises/frontend/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: frontend + labels: + app: frontend +spec: + selector: + app: frontend + ports: + - name: http + port: 8080 + targetPort: 8080 diff --git a/captures/workshop/exercises/lookup/tenants.yaml b/captures/workshop/exercises/lookup/tenants.yaml new file mode 100644 index 0000000..9f7722f --- /dev/null +++ b/captures/workshop/exercises/lookup/tenants.yaml @@ -0,0 +1,45 @@ +# Each customer gets a tenant, which picks the clusters and portals it +# reaches by name or by label. A client of the API reaches only the tenants +# it is granted. +apiVersion: lookup.educates.dev/v1beta1 +kind: TenantConfig +metadata: + name: example-academy + namespace: educates-config +spec: + clusters: + nameSelector: + matchNames: + - local-cluster + portals: + nameSelector: + matchNames: + - site-captures +--- +apiVersion: lookup.educates.dev/v1beta1 +kind: TenantConfig +metadata: + name: customer-1-production + namespace: educates-config +spec: + clusters: + labelSelector: + matchLabels: + customer: customer-1 + portals: + labelSelector: + matchLabels: + environment: production +--- +apiVersion: lookup.educates.dev/v1beta1 +kind: ClientConfig +metadata: + name: example-academy-site + namespace: educates-config +spec: + client: + password: change-me-to-a-long-password + roles: + - tenant + tenants: + - example-academy diff --git a/captures/workshop/exercises/pods/one.yaml b/captures/workshop/exercises/pods/one.yaml new file mode 100644 index 0000000..93cf79f --- /dev/null +++ b/captures/workshop/exercises/pods/one.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Pod +metadata: + name: one + labels: + app: one +spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: nginx + image: nginxinc/nginx-unprivileged:1.27-alpine + ports: + - containerPort: 8080 + readinessProbe: + httpGet: + path: / + port: 8080 + periodSeconds: 2 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL diff --git a/captures/workshop/exercises/pods/two.yaml b/captures/workshop/exercises/pods/two.yaml new file mode 100644 index 0000000..32cb754 --- /dev/null +++ b/captures/workshop/exercises/pods/two.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Pod +metadata: + name: two + labels: + app: two +spec: + securityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + containers: + - name: nginx + image: nginxinc/nginx-unprivileged:1.27-alpine + ports: + - containerPort: 8080 + readinessProbe: + httpGet: + path: / + port: 8080 + periodSeconds: 2 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL diff --git a/captures/workshop/exercises/terraform/educates-on-eks/environment.tfvars b/captures/workshop/exercises/terraform/educates-on-eks/environment.tfvars new file mode 100644 index 0000000..38c6365 --- /dev/null +++ b/captures/workshop/exercises/terraform/educates-on-eks/environment.tfvars @@ -0,0 +1,48 @@ +aws_region = "{{aws_region}}" +aws_account_id = "{{aws_account_id}}" +ami_type = "AL2023_x86_64_STANDARD" +kubernetes_version = "1.32" +cluster_name = "{{cluster_name}}" +deploy_educates = true +educates_version = "3.3.2" +TLD = "{{educates_ingress_domain}}" + +manage_aws_auth_configmap = true + +# Example of how to add a user to the aws-auth configmap +aws_auth_users = [ + { + groups = ["system:masters"] + userarn = "arn:aws:iam::{{aws_account_id}}:user/{{iams_principal}}" + username = "admin" + } +] + +# Example of how to add one or moreroles to the aws-auth configmap +aws_auth_roles = [ + { + groups = ["system:masters"] + rolearn = "arn:aws:iam::{{aws_account_id}}:role/TerraformPlanApply" + username = "admin" + }, + { + groups = ["system:masters"] + rolearn = "arn:aws:iam::{{aws_account_id}}:role/AWSReservedSSO_AdministratorAccess" + username = "admin" + } +] + +# Example of how to add a KMS key administrator from a role, otherwise implicit +kms_key_administrators = ["arn:aws:iam::{{aws_account_id}}:role/TerraformPlanApply"] + +node_groups = { + "node-group-workshops" : { + "name" : "node-group-workshops", + "desired_size" : 2 + "max_size" : 3, + "min_size" : 2, + "instance_types" : ["m5.large"] + "disk_size" : 100 + "use_custom_launch_template" : false + } +} diff --git a/captures/workshop/exercises/vcluster/books-crd.yaml b/captures/workshop/exercises/vcluster/books-crd.yaml new file mode 100644 index 0000000..47121fe --- /dev/null +++ b/captures/workshop/exercises/vcluster/books-crd.yaml @@ -0,0 +1,26 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: books.bookshelf.example.com +spec: + group: bookshelf.example.com + scope: Namespaced + names: + kind: Book + plural: books + singular: book + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + title: + type: string + author: + type: string diff --git a/captures/workshop/resources/workshop-no-kubernetes.yaml b/captures/workshop/resources/workshop-no-kubernetes.yaml new file mode 100644 index 0000000..2ced674 --- /dev/null +++ b/captures/workshop/resources/workshop-no-kubernetes.yaml @@ -0,0 +1,27 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: lab-site-captures-python +spec: + title: Python Without a Cluster + description: Start a Python project with uv, in a Session with the tools and no Kubernetes access at all. + duration: 15m + difficulty: beginner + workshop: + files: + - image: + url: "$(image_repository)/lab-site-captures-files:$(workshop_version)" + session: + env: + - name: PATHWAY_NAME + value: no-kubernetes + namespaces: + security: + token: + enabled: false + applications: + terminal: + enabled: true + layout: split + editor: + enabled: true diff --git a/captures/workshop/resources/workshop-virtual-cluster.yaml b/captures/workshop/resources/workshop-virtual-cluster.yaml new file mode 100644 index 0000000..445f10f --- /dev/null +++ b/captures/workshop/resources/workshop-virtual-cluster.yaml @@ -0,0 +1,29 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: lab-site-captures-vcluster +spec: + title: Kubernetes Cluster Admin + description: Install operators and manage namespaces as cluster admin, in a virtual cluster of your own. + duration: 30m + difficulty: intermediate + workshop: + files: + - image: + url: "$(image_repository)/lab-site-captures-files:$(workshop_version)" + session: + env: + - name: PATHWAY_NAME + value: virtual-cluster + namespaces: + budget: large + applications: + terminal: + enabled: true + layout: split + editor: + enabled: true + console: + enabled: true + vcluster: + enabled: true diff --git a/captures/workshop/resources/workshop.yaml b/captures/workshop/resources/workshop.yaml new file mode 100644 index 0000000..ff6a3ea --- /dev/null +++ b/captures/workshop/resources/workshop.yaml @@ -0,0 +1,83 @@ +apiVersion: training.educates.dev/v1beta1 +kind: Workshop +metadata: + name: lab-site-captures +spec: + title: Educates Feature Tour + description: Deploy a small web application and see what a Session gives you along the way, from clickable actions and checks to the editor, the console and the services beside them. + duration: 30m + difficulty: beginner + publish: + image: "$(image_repository)/lab-site-captures-files:$(workshop_version)" + files: + - directory: + path: . + includePaths: + - /workshop/** + - /exercises/** + - /README.md + workshop: + files: + - image: + url: "$(image_repository)/lab-site-captures-files:$(workshop_version)" + environment: + # On a local cluster with a certificate authority of its own, copy the + # ingress certificate's secret, whose CA certificate the Session mounts. + # On a cluster without it, nothing is copied and nothing is mounted. + secrets: + - name: educates-wildcard-tls + namespace: educates-installer + request: + parameters: + - name: TEAM_NAME + value: "" + session: + env: + - name: PATHWAY_NAME + value: tour + namespaces: + budget: large + security: + token: + enabled: true + volumes: + - name: ingress-ca + secret: + secretName: educates-wildcard-tls + optional: true + items: + - key: ca.crt + path: ca.crt + volumeMounts: + - name: ingress-ca + mountPath: /opt/ingress-ca + readOnly: true + ingresses: + - name: frontend + protocol: http + host: frontend.$(session_namespace).svc + port: 8080 + applications: + terminal: + enabled: true + layout: split + editor: + enabled: true + console: + enabled: true + slides: + enabled: true + reveal.js: + version: 4.x + examiner: + enabled: true + files: + enabled: true + uploads: + enabled: true + docker: + enabled: true + registry: + enabled: true + git: + enabled: true diff --git a/captures/workshop/workshop/config.yaml b/captures/workshop/workshop/config.yaml new file mode 100644 index 0000000..423d979 --- /dev/null +++ b/captures/workshop/workshop/config.yaml @@ -0,0 +1,37 @@ +pathways: + default: tour + + paths: + tour: + title: Educates Feature Tour + steps: + - 00-workshop-overview + - 01-deploy-the-application + - 02-change-the-configuration + - 03-open-the-application + - 04-share-files + - 05-at-your-own-pace + - 06-check-your-work + - 07-wait-for-it + - 08-one-step-at-a-time + - 09-answer-a-question + - 10-checked-from-outside + - 11-the-big-picture + - 12-build-and-push + - 13-your-namespace + - 14-portal-rest-api + - 15-lookup-service + - 16-workshop-environments + - 17-terraform-modules + - 18-ai-authoring + - 99-workshop-summary + + virtual-cluster: + title: Kubernetes Cluster Admin + steps: + - 50-virtual-cluster + + no-kubernetes: + title: Python Without a Cluster + steps: + - 60-no-kubernetes diff --git a/captures/workshop/workshop/content/00-workshop-overview.md b/captures/workshop/workshop/content/00-workshop-overview.md new file mode 100644 index 0000000..247f99e --- /dev/null +++ b/captures/workshop/workshop/content/00-workshop-overview.md @@ -0,0 +1,30 @@ +--- +title: Workshop overview +--- + +In this workshop you deploy Bookshelf, a small web application, to a +Kubernetes namespace of your own and check each step as you go. + +Everything you need is already here: + +* Your namespace is `{{< param session_namespace >}}`, and you have admin + access to it. +* Once deployed, the application answers at + `{{< param ingress_protocol >}}://frontend-{{< param session_hostname >}}`. +* The exercise files are in `~/exercises`, open in the terminals on the right. + +You will: + +1. Deploy the application with `kubectl`, from the instructions. +2. Change its configuration in the editor. +3. Watch it come up in the Kubernetes console. +4. Check your work before you move on. + +```terminal:execute +command: kubectl get all +``` + +{{< note >}} +Click an action like the one above to run it. You do not need to type +anything in this workshop. +{{< /note >}} diff --git a/captures/workshop/workshop/content/01-deploy-the-application.md b/captures/workshop/workshop/content/01-deploy-the-application.md new file mode 100644 index 0000000..29f0e48 --- /dev/null +++ b/captures/workshop/workshop/content/01-deploy-the-application.md @@ -0,0 +1,33 @@ +--- +title: Deploy the application +--- + +Bookshelf is two replicas of a web server, with its page and its settings +in config maps. The manifests are in `~/exercises/frontend`. + +Apply them to your namespace: + +```terminal:execute +command: kubectl apply -f frontend/ +event: bookshelf-deployed +``` + +Watch the pods start in the second terminal: + +```terminal:execute +command: kubectl get pods --watch +session: 2 +``` + +Once both pods show `Running`, stop watching: + +```terminal:interrupt +session: 2 +``` + +Then list what you created, in every terminal at once: + +```terminal:execute-all +command: kubectl get deployments,pods +clear: true +``` diff --git a/captures/workshop/workshop/content/02-change-the-configuration.md b/captures/workshop/workshop/content/02-change-the-configuration.md new file mode 100644 index 0000000..3aaceb5 --- /dev/null +++ b/captures/workshop/workshop/content/02-change-the-configuration.md @@ -0,0 +1,54 @@ +--- +title: Change the configuration +--- + +A new release of the web server is out. Open the deployment in the editor, +at the container definition: + +```editor:open-file +file: ~/exercises/frontend/deployment.yaml +line: 22 +``` + +Find the image it runs: + +```editor:select-matching-text +file: ~/exercises/frontend/deployment.yaml +text: "nginx-unprivileged:(.*)" +isRegex: true +group: 1 +``` + +Move it to the new release: + +```editor:replace-matching-text +file: ~/exercises/frontend/deployment.yaml +match: "1.27-alpine" +replacement: "1.28-alpine" +``` + +Ask for a third replica: + +```editor:set-yaml-value +file: ~/exercises/frontend/deployment.yaml +path: spec.replicas +value: 3 +``` + +Add a label that names the team that owns it: + +```editor:append-lines-after-match +file: ~/exercises/frontend/deployment.yaml +match: " app: frontend" +text: " team: bookshelf" +``` + +And record the change in a file of its own: + +```editor:create-file +file: ~/exercises/frontend/CHANGES.md +text: | + # Changes + + - Web server moved to 1.28, with three replicas. +``` diff --git a/captures/workshop/workshop/content/03-open-the-application.md b/captures/workshop/workshop/content/03-open-the-application.md new file mode 100644 index 0000000..432c314 --- /dev/null +++ b/captures/workshop/workshop/content/03-open-the-application.md @@ -0,0 +1,34 @@ +--- +title: Open the application +--- + +Bookshelf is running. Open it in a tab of its own beside the instructions: + +```dashboard:create-dashboard +name: Bookshelf +url: "{{< param ingress_protocol >}}://frontend-{{< param session_hostname >}}/" +``` + +Each time you reload it, a different pod may answer: + +```dashboard:reload-dashboard +name: Bookshelf +``` + +See the deployment and its pods in the Kubernetes console: + +```dashboard:open-dashboard +name: Console +``` + +Or open the application in a new browser tab: + +```dashboard:open-url +url: "{{< param ingress_protocol >}}://frontend-{{< param session_hostname >}}/" +``` + +When you are done with it, close the tab: + +```dashboard:delete-dashboard +name: Bookshelf +``` diff --git a/captures/workshop/workshop/content/04-share-files.md b/captures/workshop/workshop/content/04-share-files.md new file mode 100644 index 0000000..88eeb2e --- /dev/null +++ b/captures/workshop/workshop/content/04-share-files.md @@ -0,0 +1,29 @@ +--- +title: Share files with your machine +--- + +Copy the application's address to paste it anywhere: + +```workshop:copy +text: "{{< param ingress_protocol >}}://frontend-{{< param session_hostname >}}/" +``` + +Download the kubeconfig for your namespace, to use `kubectl` from your own +machine: + +```files:download-file +path: .kube/config +download: bookshelf-kubeconfig.yaml +``` + +Upload a reading list of your own, and Bookshelf serves it next: + +```files:upload-file +path: reading-list.json +``` + +The file arrives in your uploads directory: + +```terminal:execute +command: ls -l ~/uploads +``` diff --git a/captures/workshop/workshop/content/05-at-your-own-pace.md b/captures/workshop/workshop/content/05-at-your-own-pace.md new file mode 100644 index 0000000..9995028 --- /dev/null +++ b/captures/workshop/workshop/content/05-at-your-own-pace.md @@ -0,0 +1,60 @@ +--- +title: At your own pace +--- + +This page lists the pods as soon as it opens: + +```terminal:execute +command: kubectl get pods +autostart: true +``` + +```section:begin +name: scale +title: Scale the application, then check it +cascade: true +``` + +Opening this section scales Bookshelf, then waits for the rollout: + +```terminal:execute +command: kubectl scale deployment/frontend --replicas=3 +cascade: true +``` + +```terminal:execute +command: kubectl rollout status deployment/frontend +cascade: true +``` + +```section:end +name: scale +toggle: false +``` + +```section:begin +name: question +prefix: Question +title: Why does each reload show a different pod? +``` + +The service spreads requests over every ready pod of the deployment, so +consecutive requests can reach different pods. + +```section:end +name: question +``` + +```section:begin +name: optional +prefix: Optional +title: Look inside a pod +``` + +```terminal:execute +command: kubectl exec deploy/frontend -- nginx -v +``` + +```section:end +name: optional +``` diff --git a/captures/workshop/workshop/content/06-check-your-work.md b/captures/workshop/workshop/content/06-check-your-work.md new file mode 100644 index 0000000..798ea9b --- /dev/null +++ b/captures/workshop/workshop/content/06-check-your-work.md @@ -0,0 +1,32 @@ +--- +title: Check your work +--- + +Before you build on a step, check that it worked. Start a pod named `one`: + +```terminal:execute +command: kubectl apply -f pods/one.yaml && kubectl get pods one +``` + +Then check it is running. The check runs a test from the workshop, with the +pod's name as its argument: + +```examiner:execute-test +name: test-that-pod-exists +title: Verify that pod named "one" is running +args: +- one +timeout: 5 +``` + +This is the test it runs. It exits with 0 when the pod is running, and the +check passes: + +```editor:open-file +file: /opt/workshop/examiner/tests/test-that-pod-exists +``` + +```terminal:clear-all +hidden: true +autostart: true +``` diff --git a/captures/workshop/workshop/content/07-wait-for-it.md b/captures/workshop/workshop/content/07-wait-for-it.md new file mode 100644 index 0000000..21c888f --- /dev/null +++ b/captures/workshop/workshop/content/07-wait-for-it.md @@ -0,0 +1,29 @@ +--- +title: Wait for the pod +--- + +The check below started as this page opened. It tries again every second, +and passes once a pod named `two` is running: + +```examiner:execute-test +name: test-that-pod-exists +title: Verify that pod named "two" is running +args: +- two +retries: .INF +delay: 1 +autostart: true +``` + +Start the pod, and watch the check pass on its own: + +```terminal:execute +command: |- + kubectl apply -f pods/two.yaml && kubectl wait --for=condition=Ready pod/two --timeout=60s && kubectl get pods two +clear: true +``` + +```terminal:clear-all +hidden: true +autostart: true +``` diff --git a/captures/workshop/workshop/content/08-one-step-at-a-time.md b/captures/workshop/workshop/content/08-one-step-at-a-time.md new file mode 100644 index 0000000..693d8ea --- /dev/null +++ b/captures/workshop/workshop/content/08-one-step-at-a-time.md @@ -0,0 +1,33 @@ +--- +title: One step at a time +--- + +Check the whole application in one click. Each check that passes starts the +next, and the last one asks the application for its page: + +```examiner:execute-test +name: test-deployment-ready +title: Verify that every replica of the frontend is ready +args: +- frontend +timeout: 5 +cascade: true +``` + +```examiner:execute-test +name: test-service-exists +title: Verify that the frontend service has endpoints +args: +- frontend +timeout: 5 +cascade: true +``` + +```terminal:execute +command: curl -s frontend.$SESSION_NAMESPACE.svc:8080 | grep -o 'pod .*' +``` + +```terminal:clear-all +hidden: true +autostart: true +``` diff --git a/captures/workshop/workshop/content/09-answer-a-question.md b/captures/workshop/workshop/content/09-answer-a-question.md new file mode 100644 index 0000000..fd6293d --- /dev/null +++ b/captures/workshop/workshop/content/09-answer-a-question.md @@ -0,0 +1,33 @@ +--- +title: Answer a question +--- + +How many replicas of the frontend are running now? Enter your answer, and the +check compares it with the deployment: + +```examiner:execute-test +name: check-replicas +prefix: Question +title: Replicas of the frontend +inputs: + schema: + replicas: + type: integer + title: "Number of replicas:" + required: true + form: + - "*" + - type: submit + title: Check my answer +``` + +Look it up if you are not sure: + +```terminal:execute +command: kubectl get deployment frontend +``` + +```terminal:clear-all +hidden: true +autostart: true +``` diff --git a/captures/workshop/workshop/content/10-checked-from-outside.md b/captures/workshop/workshop/content/10-checked-from-outside.md new file mode 100644 index 0000000..efe0e1a --- /dev/null +++ b/captures/workshop/workshop/content/10-checked-from-outside.md @@ -0,0 +1,42 @@ +--- +title: Checked from outside +--- + +This check does not run in your Session. A service of the training team +runs it, with read access to the cluster, out of reach of the terminals on +the right: + +```examiner:execute-test +name: deployment-ready +title: Verify that the frontend is ready, checked by the training team +url: "{{< param ingress_protocol >}}://example-academy.{{< param ingress_domain >}}/check/deployment-ready" +args: +- "{{< param session_namespace >}}" +- frontend +timeout: 10 +``` + +The check names the service by its URL: + +```editor:open-file +file: /opt/workshop/content/10-checked-from-outside.md +line: 9 +``` + +```editor:select-matching-text +file: /opt/workshop/content/10-checked-from-outside.md +text: "name: deployment-ready" +after: 7 +``` + +It asks the cluster about your deployment, the same thing you can see from +here: + +```terminal:execute +command: kubectl get deployment frontend +``` + +```terminal:clear-all +hidden: true +autostart: true +``` diff --git a/captures/workshop/workshop/content/11-the-big-picture.md b/captures/workshop/workshop/content/11-the-big-picture.md new file mode 100644 index 0000000..ab195aa --- /dev/null +++ b/captures/workshop/workshop/content/11-the-big-picture.md @@ -0,0 +1,16 @@ +--- +title: The big picture +--- + +The slides for this step show how the pieces fit together: + +```dashboard:open-dashboard +name: Slides +``` + +A deployment keeps the pods you asked for running, and the service in front +of them spreads the requests: + +```terminal:execute +command: kubectl get deployment,replicaset,pods,service -l app=frontend +``` diff --git a/captures/workshop/workshop/content/12-build-and-push.md b/captures/workshop/workshop/content/12-build-and-push.md new file mode 100644 index 0000000..dbe1fc0 --- /dev/null +++ b/captures/workshop/workshop/content/12-build-and-push.md @@ -0,0 +1,25 @@ +--- +title: Build, push and commit +--- + +Your Session has a Docker daemon, an image registry and a Git server of its +own. Build Bookshelf into an image and push it to your registry, +`{{< param registry_host >}}`: + +```terminal:execute +command: |- + docker build -q -t bookshelf:v1 frontend/ && skopeo copy docker-daemon:bookshelf:v1 docker://$REGISTRY_HOST/bookshelf:v1 && skopeo list-tags docker://$REGISTRY_HOST/bookshelf +clear: true +``` + +Then keep the source in your Git server, at +`{{< param git_protocol >}}://{{< param git_host >}}`: + +```terminal:execute +command: |- + (cd frontend && git init -q -b main && git add . && git commit -q -m "Add Bookshelf" && git push $GIT_PROTOCOL://$GIT_HOST/bookshelf.git main) +session: 2 +clear: true +``` + +Both stay with your Session, and are deleted with it. diff --git a/captures/workshop/workshop/content/13-your-namespace.md b/captures/workshop/workshop/content/13-your-namespace.md new file mode 100644 index 0000000..71fdd18 --- /dev/null +++ b/captures/workshop/workshop/content/13-your-namespace.md @@ -0,0 +1,39 @@ +--- +title: Your own namespace +--- + +Everything you deploy lives in your namespace, `{{< param session_namespace >}}`. +Deploy Bookshelf there, and list your pods: + +```terminal:execute +command: |- + echo "Namespace: $SESSION_NAMESPACE" && kubectl apply -f frontend/ && kubectl get pods +clear: true +``` + +Other Sessions have namespaces of their own, which you cannot see: + +```terminal:execute +command: kubectl get pods --all-namespaces +``` + +In your namespace you are an admin, and outside it you are nobody: + +```terminal:execute +command: |- + kubectl auth can-i create deployments && kubectl auth can-i list pods --all-namespaces +session: 2 +clear: true +``` + +The namespace has a budget, which caps what all your pods together may use: + +```terminal:execute +command: kubectl describe resourcequota compute-resources object-counts +clear: true +``` + +Clear the terminals before you move on: + +```terminal:clear-all +``` diff --git a/captures/workshop/workshop/content/14-portal-rest-api.md b/captures/workshop/workshop/content/14-portal-rest-api.md new file mode 100644 index 0000000..edafb63 --- /dev/null +++ b/captures/workshop/workshop/content/14-portal-rest-api.md @@ -0,0 +1,38 @@ +--- +title: The portal's REST API +--- + +A site of your own can list the training portal's workshops and start +Sessions through its REST API. Log in with the portal's robot account, in +both terminals: + +```terminal:execute-all +command: |- + TOKEN=$(curl -s -X POST -u "$ROBOT_CLIENT_ID:$ROBOT_CLIENT_SECRET" -d "grant_type=password&username=$ROBOT_USERNAME&password=$ROBOT_PASSWORD" $PORTAL_URL/oauth2/token/ | jq -r .access_token) +clear: true +``` + +List the workshops it serves, with how many Sessions each can run: + +```terminal:execute +command: |- + curl -s -H "Authorization: Bearer $TOKEN" $PORTAL_URL/workshops/catalog/environments/ | jq -r '.environments[] | [.name, .workshop.title, .capacity] | @tsv' +``` + +Ask for a Session for one of your users, `learner-0042`: + +```terminal:execute +command: |- + curl -s -H "Authorization: Bearer $TOKEN" "$PORTAL_URL/workshops/environment/$WORKSHOP_NAMESPACE/request/?user=learner-0042&index_url=https://academy.example.com/" | jq '{name, user, url}' +session: 2 +clear: true +``` + +Ask again for the same user, and the portal hands back the Session they +already have: + +```terminal:execute +command: |- + curl -s -H "Authorization: Bearer $TOKEN" "$PORTAL_URL/workshops/environment/$WORKSHOP_NAMESPACE/request/?user=learner-0042&index_url=https://academy.example.com/" | jq -c '{name, user}' +session: 2 +``` diff --git a/captures/workshop/workshop/content/15-lookup-service.md b/captures/workshop/workshop/content/15-lookup-service.md new file mode 100644 index 0000000..4929fbc --- /dev/null +++ b/captures/workshop/workshop/content/15-lookup-service.md @@ -0,0 +1,61 @@ +--- +title: One API for every portal +--- + +The lookup service puts one REST API in front of the training portals on +every cluster it watches. Log in, as a platform admin in the first terminal +and as the Example Academy site in the second: + +```terminal:execute +command: |- + TOKEN=$(curl -s -X POST $LOOKUP_URL/auth/login -H "Content-Type: application/json" -d "{\"username\": \"$LOOKUP_ADMIN_USERNAME\", \"password\": \"$LOOKUP_ADMIN_PASSWORD\"}" | jq -r .access_token) +clear: true +``` + +```terminal:execute +command: |- + TOKEN=$(curl -s -X POST $LOOKUP_URL/auth/login -H "Content-Type: application/json" -d "{\"username\": \"$LOOKUP_USERNAME\", \"password\": \"$LOOKUP_PASSWORD\"}" | jq -r .access_token) +session: 2 +clear: true +``` + +The admin sees every cluster and the portals on each: + +```terminal:execute +command: |- + curl -s -H "Authorization: Bearer $TOKEN" $LOOKUP_URL/api/v1/clusters | jq && curl -s -H "Authorization: Bearer $TOKEN" $LOOKUP_URL/api/v1/portals | jq +``` + +Tenants pick clusters and portals by name or by label: + +```editor:open-file +file: ~/exercises/lookup/tenants.yaml +``` + +The site sees only the workshops of its tenant: + +```terminal:execute +command: |- + curl -s -H "Authorization: Bearer $TOKEN" "$LOOKUP_URL/api/v1/workshops?tenant=$LOOKUP_TENANT" | jq +session: 2 +``` + +And asks for a Session for one of its learners, with their details, a +parameter for the workshop and a webhook for its events: + +```terminal:execute +command: |- + curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" $LOOKUP_URL/api/v1/workshops -d '{ + "tenantName": "'$LOOKUP_TENANT'", + "workshopName": "'$WORKSHOP_NAME'", + "clientUserId": "learner-0042", + "userFirstName": "Sam", + "userLastName": "Example", + "userEmailAddress": "sam@example.com", + "clientIndexUrl": "https://academy.example.com/", + "workshopParams": [{"name": "TEAM_NAME", "value": "bookshelf"}], + "analyticsWebhookUrl": "'$EVENTS_URL'" + }' | jq +session: 2 +clear: true +``` diff --git a/captures/workshop/workshop/content/16-workshop-environments.md b/captures/workshop/workshop/content/16-workshop-environments.md new file mode 100644 index 0000000..d44ee6f --- /dev/null +++ b/captures/workshop/workshop/content/16-workshop-environments.md @@ -0,0 +1,52 @@ +--- +title: Environments for other workshops +--- + +Not every workshop runs on the standard image. This one, for a Spring +application, starts from the Java image, adds the Java tools for the editor +as an extension package, and creates a database in each Session's +namespace: + +```editor:open-file +file: ~/exercises/environments/lab-spring-petclinic/resources/workshop.yaml +line: 10 +``` + +```editor:select-matching-text +file: ~/exercises/environments/lab-spring-petclinic/resources/workshop.yaml +text: " image: jdk21-environment:*" +after: 9 +``` + +A setup script clones the application and warms the build cache before the +learner arrives: + +```editor:open-file +file: ~/exercises/environments/lab-spring-petclinic/workshop/setup.d/01-build-petclinic.sh +``` + +A workshop that needs a whole Linux machine creates a virtual machine for +each Session, with KubeVirt: + +```editor:open-file +file: ~/exercises/environments/lab-linux-vm/resources/workshop.yaml +line: 35 +``` + +```editor:select-matching-text +file: ~/exercises/environments/lab-linux-vm/resources/workshop.yaml +text: " - apiVersion: kubevirt.io/v1" +after: 12 +``` + +```editor:execute-command +command: workbench.action.closeAllEditors +hidden: true +autostart: true +``` + +```editor:execute-command +command: workbench.files.action.collapseExplorerFolders +hidden: true +autostart: true +``` diff --git a/captures/workshop/workshop/content/17-terraform-modules.md b/captures/workshop/workshop/content/17-terraform-modules.md new file mode 100644 index 0000000..b7b4acd --- /dev/null +++ b/captures/workshop/workshop/content/17-terraform-modules.md @@ -0,0 +1,35 @@ +--- +title: A cluster of your own, with Terraform +--- + +The Educates Terraform modules create an EKS or GKE cluster with what +Educates needs, and install Educates on it. Start from the root module for +your cloud, and put your settings in `environment.tfvars`: + +```editor:open-file +file: ~/exercises/terraform/educates-on-eks/environment.tfvars +``` + +Then create the cluster, with Educates on it: + +```workshop:copy +text: terraform apply -var-file environment.tfvars -auto-approve +``` + +And remove it all when you are done: + +```workshop:copy +text: terraform destroy -var-file environment.tfvars -auto-approve +``` + +```editor:execute-command +command: workbench.action.closeAllEditors +hidden: true +autostart: true +``` + +```editor:execute-command +command: workbench.files.action.collapseExplorerFolders +hidden: true +autostart: true +``` diff --git a/captures/workshop/workshop/content/18-ai-authoring.md b/captures/workshop/workshop/content/18-ai-authoring.md new file mode 100644 index 0000000..ae3350b --- /dev/null +++ b/captures/workshop/workshop/content/18-ai-authoring.md @@ -0,0 +1,48 @@ +--- +title: A course written with an AI agent +--- + +This course was planned with the course design skill, and its first +workshop built with the workshop authoring skill. Start from the workshop +the skill created, with its definition: + +```editor:open-file +file: ~/exercises/ai-course/workshops/lab-a01-first-deployment/resources/workshop.yaml +``` + +And its first instruction page, with an action for each step: + +```editor:open-file +file: ~/exercises/ai-course/workshops/lab-a01-first-deployment/workshop/content/01-deploy-the-application.md +``` + +The course plan sorts its workshops into core and elective: + +```editor:open-file +file: ~/exercises/ai-course/planning/course-module-a.md +line: 24 +``` + +Each workshop has a detailed plan, which the authoring skill built it from: + +```editor:open-file +file: ~/exercises/ai-course/planning/workshop-plans/lab-a01-first-deployment.md +``` + +And the task list tracks what is left, with what to do next: + +```editor:open-file +file: ~/exercises/ai-course/planning/tasks.md +``` + +```editor:execute-command +command: workbench.action.closeAllEditors +hidden: true +autostart: true +``` + +```editor:execute-command +command: workbench.files.action.collapseExplorerFolders +hidden: true +autostart: true +``` diff --git a/captures/workshop/workshop/content/50-virtual-cluster.md b/captures/workshop/workshop/content/50-virtual-cluster.md new file mode 100644 index 0000000..7bb32f0 --- /dev/null +++ b/captures/workshop/workshop/content/50-virtual-cluster.md @@ -0,0 +1,26 @@ +--- +title: A cluster of your own +--- + +Your Session has a virtual cluster of its own, where you are cluster admin. +It has nodes and namespaces like any other cluster: + +```terminal:execute +command: kubectl get nodes && kubectl get namespaces +clear: true +``` + +You can do anything a cluster admin can: + +```terminal:execute +command: kubectl auth can-i '*' '*' --all-namespaces +``` + +Such as create a namespace, and add a custom resource type of your own: + +```terminal:execute +command: |- + kubectl create namespace operators && kubectl apply -f vcluster/books-crd.yaml && kubectl get crds +session: 2 +clear: true +``` diff --git a/captures/workshop/workshop/content/60-no-kubernetes.md b/captures/workshop/workshop/content/60-no-kubernetes.md new file mode 100644 index 0000000..e113e01 --- /dev/null +++ b/captures/workshop/workshop/content/60-no-kubernetes.md @@ -0,0 +1,27 @@ +--- +title: Just the tools +--- + +This workshop is about Python, so it needs no cluster. Its Session is a +container with the tools, and nothing more: + +```terminal:execute +command: python3 --version && uv --version +clear: true +``` + +Create a script and run it: + +```terminal:execute +command: |- + uv init -q --script hello.py && uv run -q hello.py +``` + +There is no Kubernetes cluster to reach from here at all: + +```terminal:execute +command: |- + kubectl get pods 2>&1 | tail -1 +session: 2 +clear: true +``` diff --git a/captures/workshop/workshop/content/99-workshop-summary.md b/captures/workshop/workshop/content/99-workshop-summary.md new file mode 100644 index 0000000..460df33 --- /dev/null +++ b/captures/workshop/workshop/content/99-workshop-summary.md @@ -0,0 +1,8 @@ +--- +title: Workshop summary +--- + +You deployed Bookshelf to a namespace of your own, changed it in the editor, +watched it in the console, and checked each step along the way. + +When you finish the workshop, your Session and everything in it is deleted. diff --git a/captures/workshop/workshop/examiner/tests/check-replicas b/captures/workshop/workshop/examiner/tests/check-replicas new file mode 100755 index 0000000..fc2cd52 --- /dev/null +++ b/captures/workshop/workshop/examiner/tests/check-replicas @@ -0,0 +1,9 @@ +#!/bin/bash + +# Passes when the answer, sent as JSON on standard input, matches the number +# of replicas the frontend deployment runs. + +answer=$(jq -r '.replicas // empty') +replicas=$(kubectl get deployment frontend -o jsonpath='{.spec.replicas}') || exit 1 + +[ "$answer" = "$replicas" ] diff --git a/captures/workshop/workshop/examiner/tests/test-deployment-ready b/captures/workshop/workshop/examiner/tests/test-deployment-ready new file mode 100755 index 0000000..fd09893 --- /dev/null +++ b/captures/workshop/workshop/examiner/tests/test-deployment-ready @@ -0,0 +1,8 @@ +#!/bin/bash + +# Passes when every replica of the deployment named $1 is ready. + +wanted=$(kubectl get deployment "$1" -o jsonpath='{.spec.replicas}') || exit 1 +ready=$(kubectl get deployment "$1" -o jsonpath='{.status.readyReplicas}') + +[ "${ready:-0}" = "$wanted" ] diff --git a/captures/workshop/workshop/examiner/tests/test-service-exists b/captures/workshop/workshop/examiner/tests/test-service-exists new file mode 100755 index 0000000..1af1eca --- /dev/null +++ b/captures/workshop/workshop/examiner/tests/test-service-exists @@ -0,0 +1,9 @@ +#!/bin/bash + +# Passes when a service named $1 exists and has at least one ready endpoint. + +kubectl get service "$1" > /dev/null || exit 1 + +addresses=$(kubectl get endpoints "$1" -o jsonpath='{.subsets[*].addresses[*].ip}' 2> /dev/null) + +[ -n "$addresses" ] diff --git a/captures/workshop/workshop/examiner/tests/test-that-pod-exists b/captures/workshop/workshop/examiner/tests/test-that-pod-exists new file mode 100755 index 0000000..97ae41e --- /dev/null +++ b/captures/workshop/workshop/examiner/tests/test-that-pod-exists @@ -0,0 +1,9 @@ +#!/bin/bash + +kubectl get pods --field-selector=status.phase=Running -o name | egrep -e "^pod/$1$" + +if [ "$?" != "0" ]; then + exit 1 +fi + +exit 0 diff --git a/captures/workshop/workshop/setup.d/01-ingress-ca.sh b/captures/workshop/workshop/setup.d/01-ingress-ca.sh new file mode 100755 index 0000000..f57e090 --- /dev/null +++ b/captures/workshop/workshop/setup.d/01-ingress-ca.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Trusts the certificate authority that issued the cluster's ingress +# certificate, when the workshop definition mounts it at +# /opt/ingress-ca/ca.crt, as it does on a local cluster with a CA of its +# own. curl, git, skopeo and Python then reach the training portal, the +# lookup service and the Session's own services over HTTPS. On a cluster +# with a publicly trusted certificate there is nothing to mount, and +# nothing to do. + +set -eo pipefail + +if [ -s /opt/ingress-ca/ca.crt ]; then + bundle=$HOME/.local/share/ingress-ca/bundle.pem + mkdir -p "$(dirname "$bundle")" + cat /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /opt/ingress-ca/ca.crt > "$bundle" + for name in SSL_CERT_FILE CURL_CA_BUNDLE GIT_SSL_CAINFO REQUESTS_CA_BUNDLE; do + echo "$name=$bundle" >> "$WORKSHOP_ENV" + done +fi diff --git a/captures/workshop/workshop/setup.d/02-git-identity.sh b/captures/workshop/workshop/setup.d/02-git-identity.sh new file mode 100755 index 0000000..cc930a9 --- /dev/null +++ b/captures/workshop/workshop/setup.d/02-git-identity.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Gives git an identity for the commits the workshop makes. + +set -eo pipefail + +git config --global user.name > /dev/null || git config --global user.name "Workshop learner" +git config --global user.email > /dev/null || git config --global user.email "learner@example.com" diff --git a/captures/workshop/workshop/slides/index.html b/captures/workshop/workshop/slides/index.html new file mode 100644 index 0000000..5843419 --- /dev/null +++ b/captures/workshop/workshop/slides/index.html @@ -0,0 +1,40 @@ + + + + + Bookshelf on Kubernetes + + + + + + +
+
+
+

From manifest to running pods

+
+ Deployment → ReplicaSet → Pods +
+

Apply the manifest, and Kubernetes keeps the pods you asked for running.

+
+
+

A service in front

+

The service spreads requests over every ready pod.

+
+
+

Check before you move on

+

Each step ends with a check of the work you just did.

+
+
+
+ + + + diff --git a/docs/adr/0001-astro-for-educates-dev.md b/docs/adr/0001-astro-for-educates-dev.md new file mode 100644 index 0000000..daea082 --- /dev/null +++ b/docs/adr/0001-astro-for-educates-dev.md @@ -0,0 +1,45 @@ +# Astro for educates.dev, without Starlight + +educates.dev is built with Astro 7 as a plain Astro site: no Starlight, +React 19 only for the islands that need the browser, and plain CSS with +design tokens as CSS custom properties alongside Astro's scoped component +styles. The site is mostly a marketing site with a custom look, +data-driven use case and Feature pages, a Content hub across every kind of +Content, and covers generated at build time. In Astro those are content +collections, static paths and endpoints. In Docusaurus they would be custom +plugins and swizzled theme components, which sit outside Docusaurus's +stability promise, with a Docusaurus 4 migration close behind. + +## Considered Options + +- **Docusaurus 3.10, then 4.** It keeps every Content file and URL with no + migration. It was rejected because the custom design, the data-driven + pages, the Content hub and the covers would all be custom code against + Docusaurus internals. Performance did not decide it: the Docusaurus site + scored 93 to 100 for Lighthouse mobile performance in October 2026, so + either stack can meet the site's bar. +- **Starlight for About Educates and Getting Started Guides.** It would + cover only those two sections, about a third of the site's URLs. It + brings its own visual language to restyle, is still before 1.0, pins the + Astro version it runs on, and turns on site search, which the site does + not offer. One docs layout in the site's own design serves both + sections. Starlight is the fallback if that layout proves costly, or if + the sections grow into real documentation. +- **Tailwind CSS 4, or Bootstrap 5.3 as hub.educates.dev uses.** Plain CSS + with tokens adds no dependency with its own release pace, and its custom + properties are a layer the Hub can adopt without dropping Bootstrap. + Tailwind is the fallback if the design system proves slow to write. + +## Consequences + +- The blog machinery is the site's own code: the post, tag, author, + archive and pagination routes, the RSS and Atom feeds, and + `/sitemap.xml`, all reproducing the URLs and feeds the Docusaurus site + published. +- The site is never more than one Astro major behind the current one. + Astro ships security fixes for the previous major only, and its majors + have come between 3.5 and 15 months apart. +- React islands are limited to the Asciinema player and the Learn page's + filters, so React loads only on the pages that use them. The theme + toggle, the phone menu and click-to-enlarge on diagrams are small inline + scripts. diff --git a/docs/adr/0002-keep-the-docusaurus-url-form.md b/docs/adr/0002-keep-the-docusaurus-url-form.md new file mode 100644 index 0000000..7d9e7e3 --- /dev/null +++ b/docs/adr/0002-keep-the-docusaurus-url-form.md @@ -0,0 +1,43 @@ +# Keep the Docusaurus URL form and feed identity + +educates.dev keeps the URL form its Docusaurus site published: canonical +URLs without a trailing slash, each page built as `.html` so that +GitHub Pages serves both `/page` and `/page.html`, and `/page/` a 404. In +Astro that is `build.format: 'file'` with `trailingSlash: 'never'`, not +Astro's default of directory URLs. The blog feeds keep their identity: +`/blog/rss.xml` and `/blog/atom.xml`, each item's RSS `guid` and Atom `id` +equal to `https://educates.dev/blog/`, and the Atom feed `id` +`https://educates.dev/blog`. Every URL the old site served is public, linked +from elsewhere and indexed, and a feed item whose ID changes reappears as a +new post for every subscriber. + +## Considered Options + +- **Astro's default directory URLs (`/page/`).** It would change all 67 + canonical page URLs and leave 67 HTML redirect pages, the only kind of + redirect GitHub Pages serves, which carry no status code. +- **`@astrojs/rss` for the RSS feed.** It writes RSS only, so Atom would be + a second, hand-written endpoint, and it adds a trailing slash to item + links unless told not to. The `feed` library, which the Docusaurus site + used, writes both formats from one source; given each post's `link` and + no `id`, it produces the same `guid` and entry `id` as before. + +## Consequences + +- The site builds its own canonical URLs and `og:url`: under + `build.format: 'file'`, `Astro.url.pathname` ends in `.html`. +- `@astrojs/sitemap` always writes an index plus numbered files, so a step + at the end of the build renames `sitemap-0.xml` to `sitemap.xml` and + deletes the index, keeping `/sitemap.xml` a single file. +- Astro's `redirects` cannot express a source with a trailing slash, so the + predecessor site's `/posts/` URLs are redirect pages in `public/`. +- GitHub does not document the Pages behavior this URL form relies on, so + a check of every published URL runs in CI against the build and once + against the live site after a cutover. +- The old feeds' XSLT stylesheets, `/blog/rss.xsl` and `/blog/atom.xsl` + with their CSS, are not kept, and the feeds link no stylesheet: only the + feeds linked them, and Chrome stops running XSLT in November 2026, with + Firefox and WebKit planning to follow. +- `/Downloads`, renamed to `/downloads` in 2025, has no redirect: + `Downloads.html` and `downloads.html` are the same file on macOS's + case-insensitive file system. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index e7a59ff..0000000 --- a/docs/index.md +++ /dev/null @@ -1 +0,0 @@ -# No docs should exist as they are external \ No newline at end of file diff --git a/docusaurus.config.ts b/docusaurus.config.ts deleted file mode 100644 index 813f2b0..0000000 --- a/docusaurus.config.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { themes as prismThemes } from "prism-react-renderer"; -import type { Config } from "@docusaurus/types"; -import type * as Preset from "@docusaurus/preset-classic"; - -// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) - -const config: Config = { - title: "Educates Training Platform", - tagline: "Interactive workshop environments on Kubernetes", - favicon: "img/favicon.ico", - - // Set the production url of your site here - url: "https://educates.dev/", - // Set the // pathname under which your site is served - // For GitHub pages deployment, it is often '//' - baseUrl: "/", - - // GitHub pages deployment config. - // If you aren't using GitHub pages, you don't need these. - organizationName: "educates", // Usually your GitHub org/user name. - projectName: "educates.github.io", // Usually your repo name. - trailingSlash: false, // See: https://docusaurus.io/docs/deployment#docusaurusconfigjs-settings - deploymentBranch: "main", // The branch your GitHub pages site is deployed from. - - customFields: { - educatesProject: { - projectGitHubUrl: - "https://github.com/educates/educates-training-platform", - projectSlackUrl: "https://kubernetes.slack.com/archives/C05UWT4SKRV", - contributingUrl: - "https://github.com/educates/educates-training-platform/blob/develop/CONTRIBUTING.md", - sponsorshipUrl: "https://github.com/sponsors/educates", - downloadsUrl: - "https://github.com/educates/educates-training-platform/releases", - descriptionTitle: "Interactive Training Platform", - description: - "The Educates project provides a system for hosting interactive workshop environments in Kubernetes, " + - "or on top of a local container runtime. It can be used for self paced or supervised workshops. " + - "It can also be useful where you need to package up demos of applications hosted in Kubernetes " + - "or a local container runtime.", - screenshot: "/img/screenshot.png", - youtubeUrl: "https://www.youtube.com/@EducatesTrainingPlatform", - }, - }, - - onBrokenLinks: "throw", - onBrokenMarkdownLinks: "warn", - // Set to "ignore" because navbar/footer link to homepage section anchors - // (e.g. /#use-cases, /#features) which are rendered by React components - // with id props. Docusaurus's static checker can't detect these. - onBrokenAnchors: "ignore", - - markdown: { - mermaid: true, - }, - themes: ["@docusaurus/theme-mermaid"], - - // Even if you don't use internationalization, you can use this field to set - // useful metadata like html lang. For example, if your site is Chinese, you - // may want to replace "en" with "zh-Hans". - i18n: { - defaultLocale: "en", - locales: ["en"], - }, - - headTags: [ - { - tagName: "script", - attributes: { type: "application/ld+json" }, - innerHTML: JSON.stringify({ - "@context": "https://schema.org", - "@type": "SoftwareApplication", - name: "Educates Training Platform", - description: - "A system for hosting interactive workshop environments in Kubernetes, or on top of a local container runtime.", - applicationCategory: "DeveloperApplication", - operatingSystem: "Kubernetes", - url: "https://educates.dev", - offers: { - "@type": "Offer", - price: "0", - priceCurrency: "USD", - }, - sourceOrganization: { - "@type": "Organization", - name: "Educates", - url: "https://educates.dev", - logo: "https://educates.dev/img/educates-social-card.png", - sameAs: [ - "https://github.com/educates/educates-training-platform", - "https://www.youtube.com/@EducatesTrainingPlatform", - ], - }, - }), - }, - ], - - plugins: [ - [ - "@docusaurus/plugin-content-docs", - { - id: "getting-started-guides", - sidebarPath: "./sidebars.ts", - path: "./getting-started-guides", - routeBasePath: "getting-started-guides", - // Please change this to your repo. - // Remove this to remove the "edit this page" links. - editUrl: "https://github.com/educates/educates.github.io/tree/develop/", - }, - ], - [ - "@docusaurus/plugin-content-docs", - { - id: "about-educates", - sidebarPath: "./sidebars.ts", - path: "./about-educates", - routeBasePath: "about-educates", - // Please change this to your repo. - // Remove this to remove the "edit this page" links. - editUrl: "https://github.com/educates/educates.github.io/tree/develop/", - }, - ], - [ - "@docusaurus/plugin-ideal-image", - { - quality: 70, - max: 1030, // max resized image's size. - min: 640, // min resized image's size. if original is lower, use that size. - steps: 2, // the max number of images generated between min and max (inclusive) - disableInDev: false, - }, - ], - ], - - presets: [ - [ - "@docusaurus/preset-classic", - { - docs: { - // Default docs folder - path: "docs", - routeBasePath: "docs", - sidebarPath: require.resolve("./sidebars.js"), - }, - blog: { - blogTitle: "Educates Training Platform blog!", - blogDescription: "A blog about all things Educates!", - blogSidebarTitle: "All posts", - blogSidebarCount: "ALL", - // postsPerPage: "ALL", - showReadingTime: true, - feedOptions: { - type: ["rss", "atom"], - xslt: true, - }, - // Please change this to your repo. - // Remove this to remove the "edit this page" links. - editUrl: - "https://github.com/educates/educates.github.io/tree/develop/", - // Useful options to enforce blogging best practices - onInlineTags: "warn", - onInlineAuthors: "warn", - onUntruncatedBlogPosts: "warn", - }, - theme: { - customCss: "./src/css/custom.css", - }, - gtag: { - trackingID: "G-6E8JHKBWNX", - anonymizeIP: true, - }, - } satisfies Preset.Options, - ], - ], - - themeConfig: { - // Replace with your project's social card - image: "img/educates-social-card.png", - metadata: [ - { name: "keywords", content: "kubernetes, training, workshops, interactive learning, hands-on labs, developer education, cloud native, containers" }, - { name: "og:type", content: "website" }, - { name: "twitter:card", content: "summary_large_image" }, - ], - colorMode: { - defaultMode: "light", - disableSwitch: true, - respectPrefersColorScheme: false, - }, - // announcementBar: { - // id: 'support_us', - // content: - // 'We are looking for support to help us maintain the project, please sponsor us', - // backgroundColor: '#fafbfc', - // textColor: '#091E42', - // isCloseable: true, - // }, - navbar: { - title: "Educates Training Platform", - logo: { - alt: "Educates Training Platform", - src: "img/logo.svg", - }, - items: [ - { - type: "dropdown", - label: "Project", - position: "left", - items: [ - { to: "/#use-cases", label: "Use Cases" }, - { to: "/#features", label: "Features" }, - { to: "/#team", label: "Team" }, - // { to: '/#references', label: 'References', position: 'left' }, - { to: "/#pricing", label: "Pricing" }, - { to: "/#featured-content", label: "Featured Content" }, - ], - }, - { to: "/downloads", label: "Downloads", position: "left" }, - { - type: "dropdown", - label: "About", - position: "left", - items: [ - { to: "/about-educates", label: "Architecture" }, - { to: "/about-educates/deployment", label: "Deployment" }, - { to: "/about-educates/workshop-capabilities", label: "Workshop Capabilities" }, - { to: "/about-educates/workflows", label: "Workflows" }, - { to: "/about-educates/local-dev", label: "Local Development" }, - { to: "/about-educates/history", label: "Educates History" }, - ], - }, - { - type: "dropdown", - label: "Guides", - position: "left", - items: [ - { to: "/getting-started-guides", label: "Getting Started" }, - { to: "/getting-started-guides/setup", label: "Setup" }, - { to: "/getting-started-guides/about", label: "About" }, - { to: "/getting-started-guides/authoring", label: "Authoring" }, - ], - }, - { to: "/blog", label: "Blog", position: "left" }, - { href: "https://hub.educates.dev", label: "Hub", position: "right" }, - { href: "https://docs.educates.dev", label: "Docs", position: "right" }, - { - href: "https://github.com/educates/educates-training-platform", - label: "GitHub", - position: "right", - }, - // { href: '/login', label: 'Login / Sign Up', position: 'right', className: 'navbar-login-button' }, - ], - }, - footer: { - style: "dark", - links: [ - { - title: "Project", - items: [ - { label: "Use Cases", to: "/#use-cases" }, - { label: "Features", to: "/#features" }, - { label: "Team", to: "/#team" }, - // { label: "References", to: "/#references" }, - { label: "Pricing", to: "/#pricing" }, - { label: "Downloads", to: "/downloads" }, - ], - }, - { - title: "Docs", - items: [ - { label: "Featured Content", to: "/#featured-content" }, - { label: "Blog", to: "/blog" }, - { - label: "Getting Started Guides", - to: "/getting-started-guides", - }, - { - label: "Reference docs", - href: "https://docs.educates.dev", - }, - ], - }, - { - title: "Community", - items: [ - { - label: "Educates Hub", - href: "https://hub.educates.dev", - }, - { - label: "Slack", - href: "https://kubernetes.slack.com/archives/C05UWT4SKRV", - }, - { - label: "GitHub", - href: "https://github.com/educates/educates-training-platform", - }, - { - label: "YouTube", - href: "https://www.youtube.com/@EducatesTrainingPlatform", - }, - ], - }, - ], - copyright: `Copyright © ${new Date().getFullYear()} Educates Team. Built with Docusaurus.`, - }, - prism: { - theme: prismThemes.github, - darkTheme: prismThemes.dracula, - }, - } satisfies Preset.ThemeConfig, -}; - -export default config; diff --git a/ec.config.mjs b/ec.config.mjs new file mode 100644 index 0000000..23d5d07 --- /dev/null +++ b/ec.config.mjs @@ -0,0 +1,52 @@ +// @ts-check +// Expressive Code renders every fenced code block in Markdown and MDX, and +// the component, with the site's fonts and both themes: a frame with +// the fence's `title="..."`, a terminal frame for shell languages, line +// markers such as `{2-3}`, and a copy button. The Astro integration in +// astro.config.ts reads this file; options that hold functions, such as +// `themeCssSelector`, must live here rather than in the Astro config. +import { defineEcConfig } from "astro-expressive-code"; + +export default defineEcConfig({ + // The light theme is the base. The dark one applies when `data-theme` is + // dark, which the theme script sets from the visitor's choice or the + // system theme, and without JavaScript when the system prefers dark: the + // same rules as src/styles/tokens.css. + themes: ["github-light", "github-dark"], + themeCssSelector: (theme) => `[data-theme='${theme.type}']`, + shiki: { + // Docusaurus's name for a block left unhighlighted. + langAlias: { "no-highlight": "txt" }, + }, + frames: { + // A comment naming a file at the top of a block stays code; a title + // comes from the fence's `title="..."` only. + extractFileNameFromCode: false, + }, + // Frames take their colours from the design tokens, which follow the + // theme on their own. + styleOverrides: { + codeFontFamily: "var(--font-mono)", + uiFontFamily: "var(--font-sans)", + borderRadius: "var(--radius-control)", + borderColor: "var(--color-line)", + focusBorder: "var(--color-accent)", + codeBackground: "var(--color-surface)", + frames: { + shadowColor: "var(--color-shadow)", + editorTabBarBackground: "var(--color-chrome)", + editorTabBarBorderBottomColor: "var(--color-line)", + editorActiveTabBackground: "var(--color-surface)", + editorActiveTabForeground: "var(--color-ink)", + editorActiveTabIndicatorTopColor: "var(--color-accent)", + terminalTitlebarBackground: "var(--color-chrome)", + terminalTitlebarForeground: "var(--color-ink-muted)", + terminalTitlebarBorderBottomColor: "var(--color-line)", + terminalBackground: "var(--color-surface)", + }, + textMarkers: { + markBackground: "var(--color-accent-soft)", + markBorderColor: "var(--color-accent)", + }, + }, +}); diff --git a/getting-started-guides/_category_.json b/getting-started-guides/_category_.json deleted file mode 100644 index d5fbf5d..0000000 --- a/getting-started-guides/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Getting Started Guides", - "position": 1 -} diff --git a/getting-started-guides/about/_category_.json b/getting-started-guides/about/_category_.json deleted file mode 100644 index eaa78dc..0000000 --- a/getting-started-guides/about/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "About Educates", - "position": 20 -} diff --git a/getting-started-guides/about/components.md b/getting-started-guides/about/components.md deleted file mode 100644 index fe76578..0000000 --- a/getting-started-guides/about/components.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -sidebar_position: 2 ---- -# Educates Components - -Educates is a complex platform, handling **workshop creation, user sessions, runtime security**, and -many more things for us. This of course means that additional tooling is required, which has already -been installed when bootstrapping the platform. - -Let's take a first look. - -## Overview - -We can display all the workloads deployed to our cluster using `kubectl`: - -```sh title="Listing all Pods in our cluster" -k get pods -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name -A -``` - -The output presents itself similar to this - the workloads deployed as part of the -Educates platform are highlighted: - -``` -NAMESPACE NAME -educates image-puller-4qzph -educates secrets-manager-5f78fb8599-kkkw8 -educates session-manager-5d86cdf4cb-h2flm -kube-system coredns-7db6d8ff4d-4rf8f -kube-system coredns-7db6d8ff4d-tb9tx -kube-system etcd-educates-control-plane -kube-system kindnet-7g59l -kube-system kube-apiserver-educates-control-plane -kube-system kube-controller-manager-educates-control-plane -kube-system kube-proxy-rpwb8 -kube-system kube-scheduler-educates-control-plane -kyverno kyverno-admission-controller-d49646b75-6qhf2 -kyverno kyverno-background-controller-6f9b5b9d57-ljm66 -kyverno kyverno-cleanup-admission-reports-28816380-zt8b2 -kyverno kyverno-cleanup-cluster-admission-reports-28816380-cf5m9 -kyverno kyverno-cleanup-controller-5d44984995-dghtb -kyverno kyverno-reports-controller-7b4c74c6c5-k7gkk -local-path-storage local-path-provisioner-988d74bc-gt7xr -projectcontour contour-7fb9b8fd87-9w4mc -projectcontour contour-certgen-v1-28-5-d9hqt -projectcontour envoy-2fzc7 -``` - -On first glance, we can spot three major components: **Educates** itself, **Kyverno**, -and **Contour**. - -## Educates - -For now, we see three workloads deployed to the `educates` namespace: an **image-puller**, -a **secrets-manager**, and a **sessions-manager**. - -Each of them serves a specific purpose, which can be derived from their names: - -- **image-puller**: This workload **prefetches** images needed for the workshops once they - are created into the cluster: the workshop environment, the web platform, etc. -- **secrets-manager**: This workload **copies** secrets needed within workshop environments - upon their creation; this can be **session secrets** as well as information needed for specific - workshops, e.g. **registry credentials** -- **sessions-manager**: This workload creates and manages the workshop sessions created by/for users - -As we deploy our first workshop later on, we will see additional workloads being spun up as part -of the Educates platform. - -## Kyverno - -[Kyverno](https://kyverno.io) offers _Kubernetes Native Policy Management_ and is used by Educates to -**properly sandbox** workshop sessions from the cluster and each other. - -With a set of **predefined policies** Educates ensures that workshop users can't escalate -their privileges, disturb other users' sessions, or harm the cluster in any other way. - -It does so by deploying an **admission controller** that intercepts each request towards the Kubernetes API -before it gets acted upon and checks it against the configured policies. -Other deployed parts are tasked with cleaning up and reporting on admissions and other -Kyverno-related things. - -We can list all `Policies` and `ClusterPolicies` deployed by Educates like this: - -```sh title="Display all Kyverno policies" -kubectl clusterpolicies -``` - -There are quite a few of them: - -``` { .text .no-copy title="List of configured Kyverno policies" } -kubectl get clusterpolicies - -NAME ADMISSION BACKGROUND VALIDATE ACTION READY AGE MESSAGE -educates-baseline-disallow-capabilities true true Enforce True 22h Ready -educates-baseline-disallow-host-namespaces true true Enforce True 22h Ready -educates-baseline-disallow-host-path true true Enforce True 22h Ready -educates-baseline-disallow-host-ports true true Enforce True 22h Ready -educates-baseline-disallow-host-ports-range true true Enforce True 22h Ready -educates-baseline-disallow-host-process true true Enforce True 22h Ready -educates-baseline-disallow-privileged-containers true true Enforce True 22h Ready -educates-baseline-disallow-proc-mount true true Enforce True 22h Ready -educates-baseline-disallow-selinux true true Enforce True 22h Ready -educates-baseline-restrict-apparmor-profiles true true Enforce True 22h Ready -educates-baseline-restrict-seccomp true true Enforce True 22h Ready -educates-baseline-restrict-sysctls true true Enforce True 22h Ready -educates-restricted-disallow-capabilities-strict true true Enforce True 22h Ready -educates-restricted-disallow-privilege-escalation true true Enforce True 22h Ready -educates-restricted-require-run-as-non-root-user true true Enforce True 22h Ready -educates-restricted-require-run-as-nonroot true true Enforce True 22h Ready -educates-restricted-restrict-volume-types true true Enforce True 22h Ready -``` - -## Contour - -[Contour](https://projectcontour.io/) is a _High performance ingress controller for Kubernetes_, used for -routing traffic from the outside of our cluster to the right workloads, e.g. **workshop sessions**. -It is based on the [Envoy](https://www.envoyproxy.io/) proxy, and allows for fine-grained and -extensive configuration. - -Upon creation of workshop sessions, Educates will automatically create matching `Ingress` resources to -route traffic to the sessions based the on global platform configuration. For our local environment, the -`Ingress` resources will have [nip.io](https://nip.io) subdomains that always resolve to your own machine. diff --git a/getting-started-guides/about/crds.md b/getting-started-guides/about/crds.md deleted file mode 100644 index dd9aedf..0000000 --- a/getting-started-guides/about/crds.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -sidebar_position: 3 ---- -# Educates CRDs - -As Educates has been written with Kubernetes as deployment target in mind, it has been based largely -on the concept of _Custom Resource Definitions_ (CRDs). This concept allows 3rd party tools to add new -functionality to the Kubernetes API by defining new resource specifications. - -The CRDs added by Educates upon installation of the platform can be listed with `kubectl`: - -```sh title="Listing Educates' CRDs" -kubectl get crds -o custom-columns=NAME:.metadata.name | grep educates -``` - -The result is a list of 10 CRDs: - -```{ .text .no-copy title="Educates CRDs" } -secretcopiers.secrets.educates.dev -secretexporters.secrets.educates.dev -secretimporters.secrets.educates.dev -secretinjectors.secrets.educates.dev -trainingportals.training.educates.dev -workshopallocations.training.educates.dev -workshopenvironments.training.educates.dev -workshoprequests.training.educates.dev -workshops.training.educates.dev -workshopsessions.training.educates.dev -``` - -These CRDs can be grouped into 3 areas: **secret management**, **training portals**, and **workshop management**. Let's look at the ones we might interact with manually. - -## SecretCopiers - -In the [components overview](./components.md) we already talked about the `secrets-manager` deployed -by Educates. One of its task is to constantly look for `SecretCopier` resources within the cluster -and copy secrets from namespace A to namespace B. - -This way, secrets can be distributed dynamically to multiple workshops at once, which comes in handy for -things like **certificates, registry credentials**, or other sensitive data. - -A `SecretCopier` might look like this: - -```yaml title="Example SecretCopier definition" -apiVersion: secrets.educates.dev/v1beta1 -kind: SecretCopier -metadata: - name: internal-tls-certs -spec: - rules: - - sourceSecret: - name: wildcard-workshop-cert - namespace: kube-system - targetSecret: - name: wildcard-workshop - targetNamespaces: - nameSelector: - matchNames: - - workshop-* - - sourceSecret: - name: wildcard-website-cert - namespace: kube-system - targetSecret: - name: wildcard-website - targetNamespaces: - nameSelector: - matchNames: - - workshop-* -``` - -This definition will copy two secrets from the `kube-system` namespace to any namespace starting with -`workshop-` whenever they get created, changing the secrets' names in the target namespace in the process. - -## TrainingPortals - -`TrainingPortals` declaratively define the web apps workshop users will end up navigating. From **defaults** -for all **contained workshops**, to **authentication** and **styling** of the web app, configuration -happens within this CustomResource. - -Note that this implies that Educates can manage **multiple indipendent** learning platforms within the same -cluster. - -A `TrainingPortal` might look like this: - -```yaml title="Example TrainingPortal definition" -apiVersion: training.educates.dev/v1beta1 -kind: TrainingPortal -metadata: - name: example-portal -spec: - portal: - logo: data:image/svg+xml;base64,[...] - title: "Example Portal" - ingress: - hostname: portal.example.com - tlsCertificateRef: - name: wildcard-website-cert - namespace: kube-system - cookies: - domain: "" - password: keepthisasecret - registration: - type: anonymous - sessions: - maximum: 10 - updates: - workshop: true - workshop: - defaults: - reserved: 1 - workshops: - - expires: 3h - initial: 1 - name: workshop-1 - orphaned: 10m - overdue: 5m - reserved: 1 -``` - -Note how this CustomResource defines **cosmetic things** like the website's logo as well as title in -the browser, **authentication and network settings**, as well as **defaults** for workshops and a specific -listing of workshops to be hosted by this `TrainingPortal`. - -## Workshops - -`Workshops` define actual workshops in the same way `TrainingPortals` define the hosting web apps - they -contain information of a workshop's configuration, such as **enabled features** for the workshop -environment, **sources** to pull **workshop resources** from, workshop-specific **overrides** for the -defaults set per `TrainingPortal`, and additional **metadata**. - -A `Workshop` might look like this: - -```yaml title="Example Workshop definition" -apiVersion: training.educates.dev/v1beta1 -kind: Workshop -metadata: - name: security-workshop -spec: - authors: - - John Doe B(Build instructions); - B --workshop.yaml changed?-->D(Deploy workshop); - B --workshop.yaml unchanged?--> C(Update workshop in-session); - C --repeat--> A; - D --repeat--> A; -``` - -But what happens in each stage? - -## Writing Instructions - -Educates' workshop instructions are written in **Markdown**. This allows for a great editing experience, especially when writing about technical topics. In the background, [Hugo](https://gohugo.io) is used to serve the Markdown documents as HTML pages from the workshop session in the web app. - -## Building Instructions - -When done writing, the instructions need to be **bundled and published** to a container registry as an **OCI image**. - -**In your terminal**, the Educates CLI can take care of this process by publishing the workshop files to the registry spun up by Educates on `localhost:5000`: - -```sh title="Publishing a workshop to the local Educates registry" -educates publish-workshop -``` - -For production, you will probably want to have some form of **CI/CD pipeline** building and publishing the OCI image and pushing it to a _real_ registry. - -## Deploying a Workshop - -Once the workshop is bundled and published to an OCI registry, we will need to **deploy it** to our Educates -platform. - -We already learned that Educates manages deployed workshops via its `Workshop` CRD. Thus, we will -have to create and deploy a `Workshop` CustomResource to our cluster referencing the OCI image built from -our workshop's instructions. - -Again, we can do so **from our terminals** using another built-in Educates CLI command: - -```sh title="Deploying a workshop to the local Educates cluster" -educates deploy-workshop -``` - -This will create a `Workshop` CustomResource, push it to the cluster, and - if necessary - even create a `TrainingPortal` for us referencing the workshop in case there isn't any to begin with. - -With all this theoretical knowledge in our heads, we can finally move on to create our own first workshop! - -## Updating a Workshop - -Once you start iterating over the instructions of an already existing workshop, you can get an even faster feedback loop than having to **publish, deploy,** and **restart** the workshop session in your browser: - -As long as you only changed **content**, not the configuration of the workshop itself in -`resources/workshop.yaml`, it's sufficient to just **publish changes** with `educates -publish-workshop` and then pull the published resources into your running workshop session -using `update-workshop` from within your session's terminal. diff --git a/getting-started-guides/authoring/_category_.json b/getting-started-guides/authoring/_category_.json deleted file mode 100644 index 4af23d0..0000000 --- a/getting-started-guides/authoring/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Creating Workshops", - "position": 30 -} diff --git a/getting-started-guides/authoring/explore.md b/getting-started-guides/authoring/explore.md deleted file mode 100644 index bac8cd2..0000000 --- a/getting-started-guides/authoring/explore.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -sidebar_position: 3 ---- -# Exploring a Workshop Session... - -We managed to deploy our first workshop, and it's about time we had a look -at it from a user's perspective! - -In the web app, click on **Start Workshop** and wait a few seconds for the -session to get started. You should end up in a web interface looking like this: - -![Screenshot of a workshop session](img/session.png) - -## ...From a User's Perspective - -Take your time and look around! Explore the session using the terminal(s), and -get to know the environment your users will be using, too. - -Things that might be of interest could be... - -- ...what is the OS in use by the environment? -- ...what is my user called? -- ...am I root? -- ...what's in my home directory? -- ...what tools do I have available? - -## From an Admin's Perspective - -After getting to know the workshop environment from a user's perspective, -let's have a look at it from the admin's side, using `kubectl` from your -local terminal! - -Here, try to find out... - -- ...which pods have been spun up for the workshop session? -- ...how do they persist data? -- ...how is connectivity into the session via internet established? -- ...how do the pods start up? -- ...which other resources reside in the session's namespace? - -Once you're done with your personal reconnaissance mission, **terminate your session** -in the top-right corner - it's time to learn about the features that make Educates -so great for interactive workshops! diff --git a/getting-started-guides/authoring/img/session.png b/getting-started-guides/authoring/img/session.png deleted file mode 100644 index 8fc4fa4..0000000 Binary files a/getting-started-guides/authoring/img/session.png and /dev/null differ diff --git a/getting-started-guides/authoring/live-edit.md b/getting-started-guides/authoring/live-edit.md deleted file mode 100644 index 3699507..0000000 --- a/getting-started-guides/authoring/live-edit.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -sidebar_position: 5 ---- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Live-Editing Workshops - -While the authoring workflow is already quite fast by using the -`publish-workshop` and `deploy-workshop` cmdlets of the Educates CLI -or `update-workshop` from within a running session, an even better -experience would be live previews _while_ we edit our workshop -instructions. - -With the Educates CLI, we can **patch** a deployed workshop to render -**local instructions** instead of the ones bundled and published with the -workshop. For this to work, however, we need [Hugo](https://gohugo.io) -installed locally. - -## Installing Hugo - -Hugo is a single Go binary that can be installed via package managers for -most systems. - -### Installing Hugo on Linux - - - - ``` sh - sudo apt install hugo - ``` - - - ``` sh - sudo dnf install hugo - ``` - - - - ``` sh - sudo zypper install hugo - ``` - - - -For other distributions, please look -[at the installation docs](https://gohugo.io/installation/linux/#package-managers). - -### Installing Hugo ond MacOS and Windows - - - - ``` sh - brew install hugo - ``` - - - ``` sh - choco install hugo-extended - ``` - - - ``` sh - scoop install hugo-extended - ``` - - - ``` sh - winget install hugo-extended - ``` - - - -## Patching a Workshop - -Once Hugo is installed, we can patch a deployed workshop and start -hosting its instructions directly from our repository like this: - -```sh title="Start live-editing workshops" -educates serve-workshop --patch-workshop -``` - -All we need to do afterwards is to start a new workshop session - it -will have its instructions updated in realtime as we edit them locally. diff --git a/getting-started-guides/index.md b/getting-started-guides/index.md deleted file mode 100644 index 25b27d3..0000000 --- a/getting-started-guides/index.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -sidebar_position: 5 ---- -# Getting Started Guides - -Let's take a look at [Educates Training Platform](https://educates.dev), -an educational framework for building workshops powered by [Kubernetes](https://kubernetes.io). - -The outline of these guides look like this: - -1. [Prerequisites and Setup](/getting-started-guides/setup) -2. [How does Educates work?](/getting-started-guides/about) -3. [Creating workshops](/getting-started-guides/authoring) -4. Publishing and deploying our first workshop - -Let's get hacking! 🚀 diff --git a/getting-started-guides/setup/_category_.json b/getting-started-guides/setup/_category_.json deleted file mode 100644 index 5932eca..0000000 --- a/getting-started-guides/setup/_category_.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "label": "Setup Educates", - "position": 10 -} diff --git a/getting-started-guides/setup/create-cluster.md b/getting-started-guides/setup/create-cluster.md deleted file mode 100644 index 5296e13..0000000 --- a/getting-started-guides/setup/create-cluster.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -sidebar_label: Create an Educates Cluster -sidebar_position: 5 ---- -# Creating an Educates Cluster - -Once we got our prerequisites - Docker Desktop and Educates - installed we can proceed -with creating our Educates environment by issuing a single bootstrapping command: - -```sh -educates create-cluster -``` - -This will do the following, in sequence: - -1. Create a purposefully crafted KinD configuration and save it to your local system. -2. Create a local Kubernetes cluster using KinD and the configuration created in step 1. -3. Set the context of `kubectl` to the created cluster. -4. Create a local image registry to publish workshop images and resources to on `localhost:5001` -5. Configure KinD cluster nodes to use the created registry on a node and cluster level. -6. Deploy the Educates framework to the cluster using [`kapp`](https://carvel.dev/kapp/). - -The output of this command will look like this - see the annotations for references to each of the -outlined steps. - -```{ .sh .no-copy title="Installation progress" } -Cluster config used is saved to: -~/Library/Application Support/educates/educates-cluster-config.yaml -Creating cluster "educates" ... -✓ Ensuring node image (kindest/node:v1.30.0) 🖼 -✓ Preparing nodes 📦 - ✓ Writing configuration 📜 -✓ Starting control-plane 🕹️ -✓ Installing CNI 🔌 -✓ Installing StorageClass 💾 -✓ Waiting ≤ 1m0s for control-plane = Ready ⏳ -• Ready after 14s 💚 -Set kubectl context to "kind-educates" -You can now use your cluster with: - -kubectl cluster-info --context kind-educates --kubeconfig /Users/you/.kube/config - -Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂 -Deploying local image registry -Linking local image registry to cluster -Adding local image registry config (localhost:5001) to Kind nodes -Adding local image registry config (registry.default.svc.cluster.local) to Kind nodes -9:15:15AM: ---- applying 7 changes [0/137 done] ---- -[...] -9:17:41AM: ---- applying complete [137/137 done] ---- -9:17:41AM: ---- waiting complete [137/137 done] ---- -Educates cluster has been created succesfully -``` - -**This command might take several minutes to spin up the cluster, download all the needed -container images, and bootstrap the environment. Enough time to get some coffee! ☕** diff --git a/getting-started-guides/setup/educates.md b/getting-started-guides/setup/educates.md deleted file mode 100644 index a7ecee5..0000000 --- a/getting-started-guides/setup/educates.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -sidebar_position: 4 ---- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Installing the Educates CLI - -Next, we are going to install the Educates CLI from the available release binaries -[on GitHub](https://github.com/educates/educates-training-platform/releases). - - - - ``` sh - # For AMD64 / x86_64 - [ $(uname -m) = x86_64 ] && curl -sSLo educates https://github.com/educates/educates-training-platform/releases/latest/download/educates-linux-amd64 - # For ARM64 - [ $(uname -m) = aarch64 ] && curl -sSLo educates https://github.com/educates/educates-training-platform/releases/latest/download/educates-linux-arm64 - chmod +x educates - sudo mv educates /usr/local/bin/educates - ``` - - - ``` sh - # For AMD64 / x86_64 - [ $(uname -m) = x86_64 ] && curl -sSLo educates https://github.com/educates/educates-training-platform/releases/latest/download/educates-darwin-amd64 - # For ARM64 - [ $(uname -m) = arm64 ] && curl -sSLo educates https://github.com/educates/educates-training-platform/releases/latest/download/educates-darwin-arm64 - chmod +x educates - sudo mv educates /usr/local/bin/educates - ``` - - - -## Testing the Educates CLI - -To test if the installation of the Educates CLI has been successful, spin up a terminal and run the following command: - -```sh title="Testing the Educates CLI" -educates version -``` - -The output should look like this: - -```{ .text .no-copy title="Output" } -educates version - -3.3.2 -``` diff --git a/getting-started-guides/setup/index.md b/getting-started-guides/setup/index.md deleted file mode 100644 index e4b1614..0000000 --- a/getting-started-guides/setup/index.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -sidebar_position: 1 ---- -# Setup Educates - -As [Educates](https://educates.dev) is powered by [Kubernetes](https://kubernetes.io), we will -need a way to run a cluster locally - luckily, the Educates CLI wraps [KinD](https://kind.sigs.k8s.io/) -and can do the heavy-lifting for us. - -However, for this setup to work, we will need a **container runtime**. Our choice will be -[Docker Desktop](https://www.docker.com/products/docker-desktop/), which is also the recommended runtime from KinD's/Educates' perspective. - -We will also install [`kubectl`](https://kubernetes.io/docs/reference/kubectl/), the official Kubernetes -CLI, in order to interact with our cluster in a more fine-grained way than is possible with the Educates -CLI. diff --git a/getting-started-guides/setup/kubectl.md b/getting-started-guides/setup/kubectl.md deleted file mode 100644 index 2946e8f..0000000 --- a/getting-started-guides/setup/kubectl.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -sidebar_position: 3 ---- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Installing kubectl - -Next, we are going to install [kubectl](https://kubernetes.io/docs/reference/kubectl/), -the official Kubernetes CLI. - -Despite the Educates CLI wrapping most of the needed functionality to interact with our -dedicated Educates cluster later-on, `kubectl` will come in handy whenever we want -to have a closer look at how things work within the platform. - -`kubectl` is available from package managers for almost all common operating systems. - -## Installing kubectl on Linux - -On the **Debian, RedHat**, and **SUSE** families of operating systems, `kubectl` -can be installed via native package management. On other Linux flavors, the release -binaries can be downloaded instead. - -### Installing kubectl with Native Package Managers - - - - - ``` sh - sudo apt-get update - sudo apt-get install -y apt-transport-https ca-certificates curl gnupg - curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg - sudo chmod 644 /etc/apt/keyrings/kubernetes-apt-keyring.gpg - echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list - sudo chmod 644 /etc/apt/sources.list.d/kubernetes.list - sudo apt-get update - sudo apt-get install -y kubectl - ``` - - - ``` sh - cat < - - ``` sh - cat < - - - -### Installing kubectl from GitHub - -`kubectl` can be installed from release binaries: - -```sh title="Installing kubectl from release binaries" -# For AMD64 / x86_64 -[ $(uname -m) = x86_64 ] && curl -sSLO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" -# For ARM64 -[ $(uname -m) = aarch64 ] && curl -sSLO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/arm64/kubectl" -chmod +x kubectl -sudo mv kubectl /usr/local/bin/kubectl -``` - -## Installing kubectl on MacOS and Windows - -On MacOS and Windows, `kubectl` can be installed with multiple available package managers: - - - - ```sh - brew install kubectl - ``` - - - ```sh - sudo port selfupdate - sudo port install kubectl - ``` - - - ```ps1 - choco install kubernetes-cli - cd ~ - mkdir .kube - cd .kube - New-Item config -type file - ``` - - - ```ps1 - scoop install kubectl - cd ~ - mkdir .kube - cd .kube - New-Item config -type file - ``` - - - ```ps1 - winget install -e --id Kubernetes.kubectl - cd ~ - mkdir .kube - cd .kube - New-Item config -type file - ``` - - - -## Testing kubectl - -To test if the installation `kubectl` has been successful, **spin up a terminal** and -run the following command: - -```sh title="Testing kubectl" -kubectl version --client -``` - -The output should look like this: - -```{ .text .no-copy title="Output" } -kubectl version --client - -Client Version: v1.31.1 -Kustomize Version: v5.4.2 -``` diff --git a/lighthouserc.yml b/lighthouserc.yml new file mode 100644 index 0000000..8407082 --- /dev/null +++ b/lighthouserc.yml @@ -0,0 +1,31 @@ +# Lighthouse CI settings for the checks workflow. Lighthouse's default +# settings audit a mobile device on a throttled connection. +ci: + collect: + # Lighthouse CI serves dist/ with its own static server, which maps a URL + # to a file name only, so each page is named by its .html file. + staticDistDir: ./dist + url: + - http://localhost/ + - http://localhost/use-cases/hands-on-events.html + - http://localhost/features/clickable-actions.html + - http://localhost/blog/getting-started-on-kind.html + - http://localhost/learn.html + numberOfRuns: 3 + assert: + # The median of the three runs. Accessibility and SEO below 90 fail the + # checks; performance below 90 only warns, because shared runners vary + # too much to gate on it. + assertions: + categories:accessibility: + - error + - minScore: 0.9 + aggregationMethod: median + categories:seo: + - error + - minScore: 0.9 + aggregationMethod: median + categories:performance: + - warn + - minScore: 0.9 + aggregationMethod: median diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 0000000..36eb555 --- /dev/null +++ b/lychee.toml @@ -0,0 +1,12 @@ +# lychee settings for checking links in the built site, `dist/`. The checks +# workflow and `npm run link-check` run lychee offline for internal links; the +# external links workflow runs it online. Both pass `--root-dir` with the +# absolute path of `dist/`, which root-relative links resolve against. + +# GitHub Pages serves `/page` from `page.html`, and a directory URL from the +# directory's `index.html`. The `.` accepts a link such as `/blog` that names +# a directory too: `blog/` holds the posts and `blog.html` is the page. +fallback_extensions = ["html"] +index_files = ["index.html", "."] + +no_progress = true diff --git a/nginx.conf b/nginx.conf index adb2c5f..8bc1585 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,22 +1,38 @@ +# Serves the built site the way GitHub Pages serves it, so the `serve` image +# previews the URLs the live site has (docs/adr/0002-keep-the-docusaurus-url-form.md): +# +# - `/page` serves the file `page`, else `page.html`. +# - A URL ending in `/` serves its directory's `index.html` or is a 404, so +# `/page/` is a 404 even when `page.html` exists. +# - A directory URL without the `/` redirects to add it. +# - Anything else is a 404 with the site's `404.html`. server { listen 80; server_name localhost; root /usr/share/nginx/html; - index index.html; + charset utf-8; + + # A relative Location keeps the host and port the browser used. + absolute_redirect off; + + error_page 404 /404.html; + + location ~ /$ { + try_files ${uri}index.html =404; + } - # Support for client-side routing location / { - try_files $uri $uri/ /index.html; + try_files $uri $uri.html $uri/ =404; } - # Cache control for static assets - location /assets/ { - expires 1y; - add_header Cache-Control "public, no-transform"; + # The feeds and the sitemap, as GitHub Pages types them. + location ~ \.xml$ { + types { } + default_type application/xml; + try_files $uri =404; } - # Enable gzip compression gzip on; - gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; -} \ No newline at end of file + gzip_types text/plain text/css text/xml application/xml application/json application/javascript image/svg+xml; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..26968d5 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,7951 @@ +{ + "name": "educates-dev", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "educates-dev", + "version": "0.0.0", + "dependencies": { + "@astrojs/markdown-satteri": "^0.4.3", + "@astrojs/mdx": "^8.0.3", + "@astrojs/react": "^7.0.1", + "@astrojs/sitemap": "^3.7.4", + "@fontsource/jetbrains-mono": "^5.3.0", + "@fontsource/onest": "^5.3.1", + "asciinema-player": "^3.17.0", + "astro": "^7.3.6", + "astro-expressive-code": "^0.44.2", + "astro-mermaid": "^2.1.0", + "feed": "^6.0.0", + "mermaid": "^11.17.2", + "node-html-parser": "^9.0.4", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "satori": "0.35.1", + "satteri": "^0.10.5", + "sharp": "^0.35.5" + }, + "devDependencies": { + "@astrojs/check": "^0.9.10", + "@types/node": "^26.6.4", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "prettier": "^3.9.9", + "prettier-plugin-astro": "^1.1.0", + "puppeteer-core": "^25.12.0", + "typescript": "^6.0.3", + "vitest": "^5.0.3", + "yaml": "^2.9.1" + }, + "engines": { + "node": ">=24" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.1.0.tgz", + "integrity": "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@astrojs/astro2tsx": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@astrojs/astro2tsx/-/astro2tsx-0.1.2.tgz", + "integrity": "sha512-Uj/M9zbrhu5cIZEvWAGTbQJRKSxXWJ/E59BZwt7F+chrKhbfttW+9rkS/l+1QxoJP1KU3UnpM62M7yszfAI0WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emnapi/core": "1.11.3", + "@napi-rs/wasm-runtime": "1.2.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/astro2tsx/node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", + "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@astrojs/check": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.10.tgz", + "integrity": "sha512-zgx/UQMozdjOa3bOxjgeCFdtpE3c9rRX6xHwa+2QXvy8z8Akifu2AtubHyv/zzC2znO8dl8fFWL4K+Ba9kS8HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/language-server": "^2.16.7", + "chokidar": "^4.0.3", + "kleur": "^4.1.5", + "yargs": "^18.0.0" + }, + "bin": { + "astro-check": "bin/astro-check.js" + }, + "peerDependencies": { + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/@astrojs/check/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@astrojs/check/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@astrojs/compiler-binding": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.5.1.tgz", + "integrity": "sha512-oczyolLUl9mNrtuBuNjuvn5thpHrFLXAHAWGTRg0JDHQ8cGVsVeFekOu2uSoQGgainJIi+A3/9m0DyYwz3Nlnw==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@astrojs/compiler-binding-android-arm64": "0.5.1", + "@astrojs/compiler-binding-darwin-arm64": "0.5.1", + "@astrojs/compiler-binding-darwin-x64": "0.5.1", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.5.1", + "@astrojs/compiler-binding-linux-arm64-musl": "0.5.1", + "@astrojs/compiler-binding-linux-x64-gnu": "0.5.1", + "@astrojs/compiler-binding-linux-x64-musl": "0.5.1", + "@astrojs/compiler-binding-wasm32-wasi": "0.5.1", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.5.1", + "@astrojs/compiler-binding-win32-x64-msvc": "0.5.1" + } + }, + "node_modules/@astrojs/compiler-binding-android-arm64": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.5.1.tgz", + "integrity": "sha512-nMb2nMIJtcWLNH+fV75Enmi+jp4tIOYOfsv4XtpgdOWkqc9P4uSSdK5u1xb340AzrXo5V18SxLl/nc3vcqtamQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-arm64": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.5.1.tgz", + "integrity": "sha512-5k4+nZ/WRt+dcVow4z7rBuI7iv9A58h/uQ91YJe1PrTivbcCkq3u2Ynx/AZJwII4CsZawBCeGpojkFjutZBhBQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-x64": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.5.1.tgz", + "integrity": "sha512-0Buw+YST9Ff2EqQyhRMQujuFq/dijLw3zXWhsvI0dJ2pGrT38iVoxTEca2eBX/PfhlwkVWSRJEKO5gMqKHyeHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.5.1.tgz", + "integrity": "sha512-x5LRpyCoMKuu3INr18cn42YhwSOaRpp9b5Ny3eJJ/sWVeejC16Zeshu1JlvlSmEZACLeWxU5JM0RX6T6nI+YyA==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.5.1.tgz", + "integrity": "sha512-mL7tQEuDk3rGfaKmrO63nw6WBy1TPVioQGmVyjcAr+C+a/k3SVMYBytNSMqH9mZPT/Swj30JaZ2N842Z1NnOEQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.5.1.tgz", + "integrity": "sha512-uLGc9jtxwiDe4cQyhNPQim4ECiB/YzbWlXssfLAv8r374kArfTwBHjwR2+j+XVyF71yCx5mF9YNRclRqdNBgeg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-musl": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.5.1.tgz", + "integrity": "sha512-G0cywinaQUc8v1MrytvYKUX7svTDMmYtOCZfxjwKPbJi59zIqM+Ll0rWgO0GQWLZH3XoPYo19ZwlP3Xt9ep9YA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-wasm32-wasi": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.5.1.tgz", + "integrity": "sha512-yMhLL8asmLydoAdnEZxAw6eLfNDs3YJl6jqn0pjPt6zrb2U5qIDLnii4/twThpIFODKkQ6YDpBRTOz/vSfZCxw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.3", + "@emnapi/runtime": "1.11.3", + "@napi-rs/wasm-runtime": "^1.2.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.5.1.tgz", + "integrity": "sha512-aClIG6poFt3Wf/bZykrEAX2nAnKvBHW6RXiKUa69QjBtWH0/FOlOAjnFcxRNlhej2AMWNYFx0o+LGjwMcH8P7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.5.1.tgz", + "integrity": "sha512-/E6uBCagY70YpWqssBC+/FuniNSloeQPRgj7l3LDWiqjxIyTLnIgX1sTt18FU7jrigetpSnC9cHAM6b6IxSgPw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-rs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.5.1.tgz", + "integrity": "sha512-1k1acnX7DepIiea9U9/Y+d1hdAFfylu1ZQQkkYgRSwBOBAsXInolyZuh/2tax+83vu/A+hOYP6P3+265RvMTvw==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-binding": "0.5.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.12.0.tgz", + "integrity": "sha512-pKPuwCDtO0L7zgmt1JGd9X4Aes1+0SX3IEBSqBNPHsScYdPMlfmed3Y7DTbXyF7E+8ylnb563o/0OzGQPOFehw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.8.0", + "unified": "^11.0.5", + "yaml": "^2.9.1" + } + }, + "node_modules/@astrojs/language-server": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/@astrojs/language-server/-/language-server-2.17.1.tgz", + "integrity": "sha512-gG7bvMVNIXgoSe1pM9dI1HsSDLU1EIc/dSAYQ0DuReLyLkOBjsiqvaRYgQltWaldLxh0bNEqjgreLfjd9OnUxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/astro2tsx": "^0.1.0", + "@astrojs/yaml2ts": "^0.2.4", + "@volar/kit": "~2.4.28", + "@volar/language-core": "~2.4.28", + "@volar/language-server": "~2.4.28", + "@volar/language-service": "~2.4.28", + "muggle-string": "^0.4.1", + "tinyglobby": "^0.2.17", + "volar-service-css": "0.0.71", + "volar-service-emmet": "0.0.71", + "volar-service-html": "0.0.71", + "volar-service-prettier": "0.0.71", + "volar-service-typescript": "0.0.71", + "volar-service-typescript-twoslash-queries": "0.0.71", + "volar-service-yaml": "0.0.71", + "vscode-html-languageservice": "^5.6.2", + "vscode-uri": "^3.2.0" + }, + "bin": { + "astro-ls": "bin/nodeServer.js" + }, + "peerDependencies": { + "prettier": "^3.0.0", + "prettier-plugin-astro": ">=0.11.0" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + } + } + }, + "node_modules/@astrojs/markdown-satteri": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.3.tgz", + "integrity": "sha512-PFbEHPnwUlLvpXd7RXrrKM+j12akF4bG+DSuNuoN+3eiwjOwp4HaPi+QQdbkBV7QEYXmzyKca50OJsmGxhNIUA==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.12.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "satteri": "^0.10.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-8.0.3.tgz", + "integrity": "sha512-SmwnVrGZSohDG0B9LXiAPbR6fe9TOumli9Vw/9zo815xGTeB1T3Fqt6HEClmBsVwIKFg/ccos4TgjZAedvg7zA==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.12.0", + "@astrojs/markdown-satteri": "0.4.3", + "es-module-lexer": "^3.0.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0", + "@astrojs/markdown-satteri": "^0.4.0", + "astro": "^7.2.10" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", + "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/react": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@astrojs/react/-/react-7.0.1.tgz", + "integrity": "sha512-ggKxfvq5KHDveZd/3TGbxvdA4O0ClwMG0eiSJh92dpHJxqfGjRudOqKW2SLFTmKfZAJD+hV+soPgT9WJArO0dQ==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.12.0", + "@vitejs/plugin-react": "^6.1.1", + "devalue": "^5.9.4", + "ultrahtml": "^1.7.0", + "vite": "^8.3.0" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@types/react": "^17.0.50 || ^18.0.21 || ^19.0.0", + "@types/react-dom": "^17.0.17 || ^18.0.6 || ^19.0.0", + "oxc-transform-react": "^0.145.0", + "react": "^17.0.2 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.4.tgz", + "integrity": "sha512-LbKNC24bdUWcQf/pThB6qLlSqHojxGjZDURIzFocY8rlWnAn2t74nnhnK6S5x0NHriHoAduLEpVjRykmeGiVvA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.3.tgz", + "integrity": "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "package-manager-detector": "^1.6.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@astrojs/yaml2ts": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@astrojs/yaml2ts/-/yaml2ts-0.2.4.tgz", + "integrity": "sha512-8oddpOae35pJsXPQXhTkM0ypfKPskVsh2bCxRtbf7e+/Epw2nReakFYpLKjZMEr75CsoF203PMnCocpfz0s69A==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.8.3" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.10.tgz", + "integrity": "sha512-NpugTzbKgGrNe45Ui32cN8vCFFMxVtP/9rtfwv0nP5FRDAjvYBxJcLVpYA9mr1jxP6cnIT7x9LUzuDTbWYYNRA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz", + "integrity": "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-darwin-x64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.10.5.tgz", + "integrity": "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz", + "integrity": "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.10.5.tgz", + "integrity": "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.10.5.tgz", + "integrity": "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.10.5.tgz", + "integrity": "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.10.5.tgz", + "integrity": "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.10.5.tgz", + "integrity": "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.10.5.tgz", + "integrity": "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, + "node_modules/@clack/core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", + "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.5.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz", + "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@emmetio/abbreviation": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@emmetio/abbreviation/-/abbreviation-2.3.3.tgz", + "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-abbreviation": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@emmetio/css-abbreviation/-/css-abbreviation-2.1.8.tgz", + "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/scanner": "^1.0.4" + } + }, + "node_modules/@emmetio/css-parser": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@emmetio/css-parser/-/css-parser-0.4.1.tgz", + "integrity": "sha512-2bC6m0MV/voF4CTZiAbG5MWKbq5EBmDPKu9Sb7s7nVcEzNQlrZP6mFFFlIaISM8X6514H9shWMme1fCm8cWAfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/stream-reader": "^2.2.0", + "@emmetio/stream-reader-utils": "^0.1.0" + } + }, + "node_modules/@emmetio/html-matcher": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@emmetio/html-matcher/-/html-matcher-1.3.0.tgz", + "integrity": "sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@emmetio/scanner": "^1.0.0" + } + }, + "node_modules/@emmetio/scanner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emmetio/scanner/-/scanner-1.0.4.tgz", + "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader/-/stream-reader-2.2.0.tgz", + "integrity": "sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emmetio/stream-reader-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@emmetio/stream-reader-utils/-/stream-reader-utils-0.1.0.tgz", + "integrity": "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@expressive-code/core": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.2.tgz", + "integrity": "sha512-Yw5KonQCD1HoOO2P89y4wifqS9qrJcGVY0LkVMUsVZjhi++bhz4WwQ37inenKFNOEVo50P7cjoVsCdaVnUbq8w==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.0.4", + "hast-util-select": "^6.0.2", + "hast-util-to-html": "^9.0.1", + "hast-util-to-text": "^4.0.1", + "hastscript": "^9.0.0", + "postcss": "^8.4.38", + "postcss-nested": "^6.0.1", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/@expressive-code/plugin-frames": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.2.tgz", + "integrity": "sha512-gVLm6TQMNIRWIaM7o+mCtxkGNjLZY5/2faSTLoXeo9wv/Pkj/0QAtZyXPVPrRsPARqiipATNFmfvQFLJudxPHQ==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2" + } + }, + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.2.tgz", + "integrity": "sha512-ebWIvCDzXvjyD8rH6T6DfytYJw1GoArLR4nusIAE4VxXrCQf3d+owytiv010KU/dUg+SRLZskDn0N+HaFXeuSg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2", + "shiki": "^4.0.2" + } + }, + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.2.tgz", + "integrity": "sha512-SNI7EgUSiADqbqUzzMbj1ZeC/r2pa5eg3KyFOGAd3a7EsIA4qzjXQLp+ODWQtHVDKFCdGs4J3ovSJUqvKIK96g==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2" + } + }, + "node_modules/@fontsource/jetbrains-mono": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.3.0.tgz", + "integrity": "sha512-fqDfB5I9f1p1TV486aUgB9t8zP84P0O1FtQR5Ol9vjwPy+S+EIGlVYm1cvj2W5shcZMTg2nZFdVMoH5wFu8a1A==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/onest": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@fontsource/onest/-/onest-5.3.1.tgz", + "integrity": "sha512-2uA9HUzQHBB8H7T2R6inmrnqPofB6+svATHM/huIG640duJr4rm+uanmAIheTTlbZvRC0ujMtf2T4U4RX51oEQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.7.tgz", + "integrity": "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^2.0.1", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.1.tgz", + "integrity": "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", + "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@prettier/parse-srcset": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@prettier/parse-srcset/-/parse-srcset-3.1.0.tgz", + "integrity": "sha512-FIRv2rZotO9NP/r66taYqD1zICpPiBbrECqjKV/xqNotqDxF7fLzzUeK+1RSRx9Tenk0DajR/GcRmTJ2qtHaKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@puppeteer/browsers": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.3.tgz", + "integrity": "sha512-2Bt3m6dDAJqmZehn0wiXSYyQmuhyiHHZ7FjvrVaS1W2RoFGV6eCK71UvKYkNJjkwC1Q9nobjhvTMhLQztFVINA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.8.4", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1", + "yauzl": "^2.10.0 || ^3.4.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + }, + "yauzl": { + "optional": true + } + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@shikijs/core": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.5.0.tgz", + "integrity": "sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.5.0", + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.5.0.tgz", + "integrity": "sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.5.0.tgz", + "integrity": "sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.5.0.tgz", + "integrity": "sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.5.0.tgz", + "integrity": "sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.5.0.tgz", + "integrity": "sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.5.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@solid-primitives/refs": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@solid-primitives/refs/-/refs-1.1.4.tgz", + "integrity": "sha512-bLjwIs6ZPu8NQnuw04sU3Zc8qKSpbc0umUU/O4SHf6oWOdO4+dHY8vb1T7C4b/Tg103+9WGr6sEE0+NFlbaB/A==", + "license": "MIT", + "dependencies": { + "@solid-primitives/utils": "^6.4.1" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/transition-group": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@solid-primitives/transition-group/-/transition-group-1.1.2.tgz", + "integrity": "sha512-gnHS0OmcdjeoHN9n7Khu8KNrOlRc8a2weETDt2YT6o1zeW/XtUC6Db3Q9pkMU/9cCKdEmN4b0a/41MKAHRhzWA==", + "license": "MIT", + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@solid-primitives/utils": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.4.1.tgz", + "integrity": "sha512-ISSB5QX1qP2ynrheIpYwc4oKR5Ny4siNuUyf1qZniy+Il+p/PtDB0QK1Dnle8noiHpwRD3gpPdubOC3qI/Zamg==", + "license": "MIT", + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", + "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", + "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz", + "integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==", + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.152.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.3", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/kit": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/kit/-/kit-2.4.28.tgz", + "integrity": "sha512-cKX4vK9dtZvDRaAzeoUdaAJEew6IdxHNCRrdp5Kvcl6zZOqb6jTOfk3kXkIkG3T7oTFXguEMt5+9ptyqYR84Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-service": "2.4.28", + "@volar/typescript": "2.4.28", + "typesafe-path": "^0.2.2", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/language-server": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-server/-/language-server-2.4.28.tgz", + "integrity": "sha512-NqcLnE5gERKuS4PUFwlhMxf6vqYo7hXtbMFbViXcbVkbZ905AIVWhnSo0ZNBC2V127H1/2zP7RvVOVnyITFfBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@volar/language-service": "2.4.28", + "@volar/typescript": "2.4.28", + "path-browserify": "^1.0.1", + "request-light": "^0.7.0", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/language-service": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-service/-/language-service-2.4.28.tgz", + "integrity": "sha512-Rh/wYCZJrI5vCwMk9xyw/Z+MsWxlJY1rmMZPsxUoJKfzIRjS/NF1NmnuEcrMbEVGja00aVpCsInJfixQTMdvLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "vscode-languageserver-protocol": "^3.17.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/emmet-helper": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@vscode/emmet-helper/-/emmet-helper-2.11.0.tgz", + "integrity": "sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "emmet": "^2.4.3", + "jsonc-parser": "^2.3.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.15.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vscode/emmet-helper/node_modules/jsonc-parser": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/l10n": { + "version": "0.0.18", + "resolved": "https://registry.npmjs.org/@vscode/l10n/-/l10n-0.0.18.tgz", + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-i18n": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ajv-i18n/-/ajv-i18n-4.2.0.tgz", + "integrity": "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^8.0.0-beta.0" + } + }, + "node_modules/am-i-vibing": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.5.0.tgz", + "integrity": "sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==", + "license": "MIT", + "dependencies": { + "process-ancestry": "^0.1.0" + }, + "bin": { + "am-i-vibing": "dist/cli.mjs" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asciinema-player": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.17.0.tgz", + "integrity": "sha512-JbjNJmA2TLIeYNaOEja+kVSzXadKoqpIzVVmfBGNj2DmdtE/vExBCnkE8NYEcpaQcDvUYg8Ltt0urT80frACmw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.21.0", + "solid-js": "^1.3.0", + "solid-transition-group": "^0.2.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astro": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.3.6.tgz", + "integrity": "sha512-3q0MKxfE4cpr5IoQ9EWrN0gwxeT5tkCaSVe3SrGlLR5fnTIywHqjVqFokQkQiZexfwpNtB9dtBdUcXV9FuD+4w==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-rs": "^0.5.1", + "@astrojs/internal-helpers": "0.12.0", + "@astrojs/markdown-satteri": "0.4.3", + "@astrojs/telemetry": "3.3.3", + "@capsizecss/unpack": "^4.0.1", + "@clack/prompts": "^1.8.1", + "@oslojs/encoding": "^1.1.0", + "am-i-vibing": "^0.5.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^2.0.1", + "devalue": "^5.9.4", + "diff": "^9.0.0", + "dset": "^3.1.4", + "es-module-lexer": "^3.0.2", + "esbuild": "^0.28.0", + "find-proc": "0.2.0", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.6", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "jsonc-parser": "^3.3.1", + "magic-string": "^1.4.2", + "magicast": "^0.5.5", + "mrmime": "^2.0.1", + "neotraverse": "^1.0.1", + "obug": "^3.0.0", + "p-limit": "^7.3.3", + "p-queue": "^9.3.3", + "package-manager-detector": "^1.8.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.7", + "rolldown": "^1.2.11", + "shiki": "^4.4.3", + "smol-toml": "^1.9.0", + "svgo": "^4.1.0", + "tinyclip": "^1.0.3", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "ultrahtml": "^1.7.0", + "unifont": "~0.7.5", + "unstorage": "^1.17.5", + "verkit": "^0.5.0", + "vite": "^8.3.1", + "vitefu": "^1.1.3", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.6.5" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/astro-expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.2.tgz", + "integrity": "sha512-avop7nZcRwC7quvIxVguhwEZ9Tp6IoTzKUCLwP30Fb7+er+pWQgcysP01dcuzRIJo81kLGg30q/YAI2Yb1Sdzg==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.44.2", + "url-extras": "^0.1.0" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" + } + }, + "node_modules/astro-mermaid": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/astro-mermaid/-/astro-mermaid-2.1.0.tgz", + "integrity": "sha512-fFRUN0BTZh+DZhDiLyblXoO26XqJ1Rr+qK3JGgSu7OBspKHDm59jkztg/aHsrdo1vO/tIq/+xhP/vgT8Mp92XA==", + "license": "MIT", + "dependencies": { + "import-meta-resolve": "^4.2.0", + "mdast-util-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "peerDependencies": { + "@mermaid-js/layout-elk": "^0.2.0", + "astro": ">=4", + "mermaid": "^10.0.0 || ^11.0.0" + }, + "peerDependenciesMeta": { + "@mermaid-js/layout-elk": { + "optional": true + } + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chromium-bidi": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz", + "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "engines": { + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz", + "integrity": "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.34.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", + "integrity": "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1687809", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1687809.tgz", + "integrity": "sha512-t7kSb+UKYdyxcmqy98U2P86ne6rCve3DIJ4+lp54i/hhClgFuG35JrLj23rO14Jw3sWDsp7i+22QhNU0+/qk8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/emmet": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/emmet/-/emmet-2.4.11.tgz", + "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "./packages/scanner", + "./packages/abbreviation", + "./packages/css-abbreviation", + "./" + ], + "dependencies": { + "@emmetio/abbreviation": "^2.3.3", + "@emmetio/css-abbreviation": "^2.1.8" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.3.tgz", + "integrity": "sha512-yWGe1O/j+almnV4hOYrmZo8u58JdnmXEMneRQAYkWtqSiT86yoMIEKF4rpqEDj52tbYl3xceBD3DQIBwcsC9Pw==", + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.2.tgz", + "integrity": "sha512-MLNghkvhyFgVW4oCD5DxBuYnnTXIf0PygzGmfT6OZeOOYWh/D4sH1j931biElkA5RG/J9wY/iuSMGAPF5vzHOg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2", + "@expressive-code/plugin-frames": "^0.44.2", + "@expressive-code/plugin-shiki": "^0.44.2", + "@expressive-code/plugin-text-markers": "^0.44.2" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fastdom": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fastdom/-/fastdom-1.0.12.tgz", + "integrity": "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg==", + "license": "MIT", + "dependencies": { + "strictdom": "^1.0.1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/feed": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/feed/-/feed-6.0.0.tgz", + "integrity": "sha512-rvFOF1POce0ijAKy3tZN5T8PuprgIit/VbMyXJWcoYwcMOTSIJ7NqZb8DTLPJr77yiEO27kidm/fw0kcSIV+ww==", + "license": "MIT", + "dependencies": { + "xml-js": "^1.6.11" + }, + "engines": { + "node": ">=20", + "pnpm": ">=10" + } + }, + "node_modules/fflate": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.3.tgz", + "integrity": "sha512-0Zz1jOzJWERhyhsimS54VTqOteCNwRtIlh8isdL0AXLo0g7xNTfTL7oWrkmCnPhZGocKIkWHBistBrrpoNH3aw==", + "license": "MIT" + }, + "node_modules/find-proc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/find-proc/-/find-proc-0.2.0.tgz", + "integrity": "sha512-a6h2nTrgB3g5Wrn4au9Ux4kMk7IJtyF6oA8uGn+mk3weLZssUtc1cp6meHIzzuW2o1srrBAxzkmXLbqjRkgB6w==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.6.tgz", + "integrity": "sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/harfbuzzjs": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/harfbuzzjs/-/harfbuzzjs-0.10.0.tgz", + "integrity": "sha512-SN8LVwCOzvTq3OPNd0+EAghgXugItz56wst567D1vs7LnnKGWprhi3EG58aVOBwhN8HEbQxL4I9NDWkcXUutRw==", + "license": "MIT" + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", + "license": "BSD-2-Clause" + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-docker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/mermaid": { + "version": "11.17.2", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.17.2.tgz", + "integrity": "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.1", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.34.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.21", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "fastdom": "1.0.12", + "katex": "^0.16.47", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/modern-tar": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.5.tgz", + "integrity": "sha512-snEhs+6G5Tjd4I7tLCDOaoln2RgE0bD19RzEKgvgK2hZ5VKy3MpLhLTZ2fWpXSTg4K2cyPwp+VHATFJhxfnOeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", + "integrity": "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-html-parser": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-9.0.4.tgz", + "integrity": "sha512-reUSrZkF1rO7hEYe5mJwtbVcQ6mB5QrViCZH6Wv+iz3er/PA9Q50gol15dMLQRDfdli4VzqPMPR24HNRFgVKFw==", + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "entities": "^8.0.0" + } + }, + "node_modules/node-html-parser/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/node-html-parser/node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/node-html-parser/node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/node-mock-http": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", + "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.3.tgz", + "integrity": "sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", + "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-astro": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-astro/-/prettier-plugin-astro-1.1.0.tgz", + "integrity": "sha512-ujJ1VX+iin4eYJPHVCEbmXHzrEVsi1SaVehHtbAteTZv8VTrZyBiZPmYc6Ud6+fkr/UBMXuEu3N3+d49h7YfKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/compiler-rs": "^0.4.0", + "@prettier/parse-srcset": "^3.1.0", + "sass-formatter": "^0.8.0" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "prettier": "^3.5.3" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.4.1.tgz", + "integrity": "sha512-ZYVDW1P58OXyxqZbOMp+/2U9KA7y2esIx9r4pWdpZ/l6fCZhaw4ul+D7EgpRVxYLkyTWqzANbTTUFGPFP8zDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@astrojs/compiler-binding-android-arm64": "0.4.1", + "@astrojs/compiler-binding-darwin-arm64": "0.4.1", + "@astrojs/compiler-binding-darwin-x64": "0.4.1", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.4.1", + "@astrojs/compiler-binding-linux-arm64-musl": "0.4.1", + "@astrojs/compiler-binding-linux-x64-gnu": "0.4.1", + "@astrojs/compiler-binding-linux-x64-musl": "0.4.1", + "@astrojs/compiler-binding-wasm32-wasi": "0.4.1", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.4.1", + "@astrojs/compiler-binding-win32-x64-msvc": "0.4.1" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding-android-arm64": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.4.1.tgz", + "integrity": "sha512-Wtx2DZORuNTLASLLZ8hlrjFaJe1LUloatcCn9EPq7knBhRpjMGXeJANIJMeBZG4igvz7x+3nyAJM2iwCSW9xog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding-darwin-arm64": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.4.1.tgz", + "integrity": "sha512-7iwXcU+hB60Y1SvOfnWHNioT72gDsDQKaDipXh2Onwp/TvQMozhbvhy13wJR8Af940a0h6PbddCeJFpj791UDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding-darwin-x64": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.4.1.tgz", + "integrity": "sha512-pifjICos49RXhSVKSDu5gDB0/8j0O7DmxSp1DaWW2PyLG5ekwSkcrfOZto5JqhdI7QYMZB3XOiXpsdOXpS4npA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.4.1.tgz", + "integrity": "sha512-hsZMlNWc6LqWHh9LlSwTq1/XCY7pxA5rkkKIdZRV8mgbOz9OwLvjEoe5Jry13eIZjMUQO8XnQyzk8o/Pjw2YFA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding-linux-arm64-musl": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.4.1.tgz", + "integrity": "sha512-a3EW27f+Z9uf57vYmpLgUriA1y9wDhKz57PT8D8XlFZt2HZEWhUxKuxI1jvKVI9p8okYee11WXEm/FHY1G9yoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding-linux-x64-gnu": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.4.1.tgz", + "integrity": "sha512-XruPTKB/SS4DFzeAjQ1ZyM0ieHDjapHkOSnnzRApZ4FXY2YAI7UY3mETjfJiIS/acyZyrnPH5jObv1gATTdUJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding-linux-x64-musl": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.4.1.tgz", + "integrity": "sha512-E8j7lGanqNpudtkUbsFaOLJhmwFe4Ecqux7K9hkj4wULIPNZRM2bYZNcMSj8r1oWvaUiwZl8LaaNwv3+14jqhw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding-wasm32-wasi": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.4.1.tgz", + "integrity": "sha512-vNLxIv41ZE+nmPFtw24cRW5ZXG+ZPcAGtgigtjCP8lvcxKI7u2GrXoMsCgEH5fMw8WCEJpwLvsVBg15rJKAkyA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.2.4" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.4.1.tgz", + "integrity": "sha512-/xTDgRsFDoTh0/+KFXJ74DY4Jqn8CoSHwa8UP5RAblHfnRHkenwfSl4zYDET61O9vXKLk09UzHjjq09yP20/AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-binding-win32-x64-msvc": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.4.1.tgz", + "integrity": "sha512-0x5J6iHZO0Fjo/CVzoIbXKzWnGESftDbEUQ/oXQ61HhUAFoR7+T1jfToZSNAS+gYyBXHSTpMWYC7lzarbhClcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/prettier-plugin-astro/node_modules/@astrojs/compiler-rs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.4.1.tgz", + "integrity": "sha512-//NmAuRhy7eU9iP0ceI5Pymy55M+nUZ//kS6XeOyhBWdDib2KbJOyFaVJ52k7sDkJwTKVox7oPu3SpYiVnGm5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@astrojs/compiler-binding": "0.4.1" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process-ancestry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/process-ancestry/-/process-ancestry-0.1.0.tgz", + "integrity": "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/puppeteer-core": { + "version": "25.12.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.12.0.tgz", + "integrity": "sha512-z6LQUt5SH7jwGtGA+nMtJuEjZIsqPiwQ+EFHCYuVDQrJpvifNmmGuyDvTzAKPyFzihxgHc5f3aMzu2kn8Ps37A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.2.3", + "chromium-bidi": "17.0.2", + "devtools-protocol": "0.0.1687809", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.3", + "ws": "^8.21.3" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype-expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.2.tgz", + "integrity": "sha512-jvjQpsfWKBquDrEl5YQA3CJybRRIuDOYsui1BJTs5oKDUK8pPkJSSxY8FjxNlIFIdtgrTjC1zuahuY4Cc1j9SQ==", + "license": "MIT", + "dependencies": { + "expressive-code": "^0.44.2" + } + }, + "node_modules/request-light": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.7.0.tgz", + "integrity": "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/s.color": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/s.color/-/s.color-0.0.15.tgz", + "integrity": "sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sass-formatter": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.8.0.tgz", + "integrity": "sha512-Nf4eMGE2O5vnJG7KPCRyksWS9gVKSYoUWtPeMXRRxwZSPVQhSGtL602xVYS65LvMkHmkvKa7xpauF0+/5pPwrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "suf-log": "^2.5.3" + }, + "bin": { + "sass-formatter": "dist/cli.js" + } + }, + "node_modules/satori": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.35.1.tgz", + "integrity": "sha512-y2GHiXg2o+WNdH2kcjC30kW6tzSLRLOG+gJ7YGkZM8fC7Atj7C+otpGRH7H/c0GLRGWUKivd5dv50vqg6m1aQA==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.17", + "css-to-react-native": "^3.0.0", + "emoji-regex-xs": "^2.0.1", + "escape-html": "^1.0.3", + "fflate": "0.7.3", + "harfbuzzjs": "0.10.0", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-layout": "^3.2.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/satteri": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.10.5.tgz", + "integrity": "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" + }, + "optionalDependencies": { + "@bruits/satteri-darwin-arm64": "0.10.5", + "@bruits/satteri-darwin-x64": "0.10.5", + "@bruits/satteri-linux-arm64-gnu": "0.10.5", + "@bruits/satteri-linux-arm64-musl": "0.10.5", + "@bruits/satteri-linux-x64-gnu": "0.10.5", + "@bruits/satteri-linux-x64-musl": "0.10.5", + "@bruits/satteri-wasm32-wasi": "0.10.5", + "@bruits/satteri-win32-arm64-msvc": "0.10.5", + "@bruits/satteri-win32-x64-msvc": "0.10.5" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.6.8.tgz", + "integrity": "sha512-HlSgSAkTk4EqHcje1ptJjfZi1YDv5KbhVJ/d3P7T/nAXua2VmDu+AKDX5VTdFfZf48nDkWB2TKYt0DrCSa+3wg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.6.8.tgz", + "integrity": "sha512-N7mWAMydj89EnYTHtRpS3LBrAz3J9O6oVrqpuXuvlAHfAUG8WEKqtbAd7SBiJNY5NfCAFhSdyAh7wI4Shjqy4g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/sharp": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/shiki": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.5.0.tgz", + "integrity": "sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.5.0", + "@shikijs/engine-javascript": "4.5.0", + "@shikijs/engine-oniguruma": "4.5.0", + "@shikijs/langs": "4.5.0", + "@shikijs/themes": "4.5.0", + "@shikijs/types": "4.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", + "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/esm/cli.js" + }, + "engines": { + "node": ">=20.19.5", + "npm": ">=10.8.2" + } + }, + "node_modules/sitemap/node_modules/@types/node": { + "version": "24.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz", + "integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/sitemap/node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/smol-toml": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz", + "integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/solid-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.16.tgz", + "integrity": "sha512-vESESWrNRebA0sQv+Vr9/2wOFOL6NK2QQow7F5/WCBH4c+OVoDyQNEbiVKVL7YqE1M7MPd7z7p27KElLkaD0bw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.6.8", + "seroval-plugins": "~1.6.8" + } + }, + "node_modules/solid-transition-group": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/solid-transition-group/-/solid-transition-group-0.2.3.tgz", + "integrity": "sha512-iB72c9N5Kz9ykRqIXl0lQohOau4t0dhel9kjwFvx81UZJbVwaChMuBuyhiZmK24b8aKEK0w3uFM96ZxzcyZGdg==", + "license": "MIT", + "dependencies": { + "@solid-primitives/refs": "^1.0.5", + "@solid-primitives/transition-group": "^1.0.2" + }, + "engines": { + "node": ">=18.0.0", + "pnpm": ">=8.6.0" + }, + "peerDependencies": { + "solid-js": "^1.6.12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/strictdom": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strictdom/-/strictdom-1.0.1.tgz", + "integrity": "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/suf-log": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz", + "integrity": "sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==", + "dev": true, + "license": "MIT", + "dependencies": { + "s.color": "0.0.15" + } + }, + "node_modules/svgo": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.1.0.tgz", + "integrity": "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^6.0.0", + "css-tree": "^3.0.1", + "css-what": "^7.0.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "1.6.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.1.tgz", + "integrity": "sha512-IAbQaPJIIdhVxi0mqy9lez1wEwje0UQf9F1vwtbelEG5Nbgn0nPOxCvU+mNeNS9gyJTAhrHp74CbUaSynQpZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyclip": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-1.0.3.tgz", + "integrity": "sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/typesafe-path": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/typesafe-path/-/typesafe-path-0.2.2.tgz", + "integrity": "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-auto-import-cache": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/typescript-auto-import-cache/-/typescript-auto-import-cache-0.3.6.tgz", + "integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.8" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz", + "integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.5.tgz", + "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ohash": "^2.0.11", + "undici": "^8.0.0" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/url-extras": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/url-extras/-/url-extras-0.1.0.tgz", + "integrity": "sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", + "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/verkit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", + "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz", + "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.3", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/volar-service-css": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-css/-/volar-service-css-0.0.71.tgz", + "integrity": "sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-css-languageservice": "^6.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-emmet": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-emmet/-/volar-service-emmet-0.0.71.tgz", + "integrity": "sha512-zqjzt6bN95e3CUstBm0PBFAJnrfz0ZAARka87fart46/gNCLLuP3Vujy8V/J8HEziTFLnfkgIASLFYPUhonJcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@emmetio/css-parser": "^0.4.1", + "@emmetio/html-matcher": "^1.3.0", + "@vscode/emmet-helper": "^2.9.3", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-html": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-html/-/volar-service-html-0.0.71.tgz", + "integrity": "sha512-e8tHPhgQ7ooLfudAEIku+kgd9pWkq3SSz8RbnQDI1+Eb8wbenkLGHqoirLqz5ORLV6wIMr2Iv08RWBG5eOcgpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-html-languageservice": "^5.3.0", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-prettier": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-prettier/-/volar-service-prettier-0.0.71.tgz", + "integrity": "sha512-Rz7JVH3qD108UCdmIEiZvOBNljMt2nLFdbN8AXcDfn7xD9F5I2aCIsDVqBbXw21PsnxG0b7MfwtNF+zPS/NKUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0", + "prettier": "^2.2 || ^3.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + }, + "prettier": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-typescript/-/volar-service-typescript-0.0.71.tgz", + "integrity": "sha512-yTtM/BVT6hoyEYnDtaCyAtNhdNeS/mhTTABlBOdw3NNiRBUin3IznFJpgfjer4c6RYopiPjjQjc9VFhxVl1mLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-browserify": "^1.0.1", + "semver": "^7.6.2", + "typescript-auto-import-cache": "^0.3.5", + "vscode-languageserver-textdocument": "^1.0.11", + "vscode-nls": "^5.2.0", + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-typescript-twoslash-queries": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-typescript-twoslash-queries/-/volar-service-typescript-twoslash-queries-0.0.71.tgz", + "integrity": "sha512-9K2k72s4n7rV9s4bX0MyjbX9iBribvKZbBJKuEmTCZfeWJXs6Yh7bGpY4eoc7UufAjvpheBqwyZCOIPBvxCv0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/volar-service-yaml": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/volar-service-yaml/-/volar-service-yaml-0.0.71.tgz", + "integrity": "sha512-qYGWGuVpUTnZGu5P/CR4KLK4aIR8RrcVnmfZ2eRcj9q/I8VZCoC5yy9FtEvfNvnDp4MU17yhdJcvpQPIqhJS2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-uri": "^3.0.8", + "yaml-language-server": "~1.23.0" + }, + "peerDependencies": { + "@volar/language-service": "~2.4.0" + }, + "peerDependenciesMeta": { + "@volar/language-service": { + "optional": true + } + } + }, + "node_modules/vscode-css-languageservice": { + "version": "6.3.10", + "resolved": "https://registry.npmjs.org/vscode-css-languageservice/-/vscode-css-languageservice-6.3.10.tgz", + "integrity": "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-css-languageservice/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-html-languageservice": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/vscode-html-languageservice/-/vscode-html-languageservice-5.6.2.tgz", + "integrity": "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "vscode-languageserver-textdocument": "^1.0.12", + "vscode-languageserver-types": "^3.17.5", + "vscode-uri": "^3.1.0" + } + }, + "node_modules/vscode-json-languageservice": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vscode-json-languageservice/-/vscode-json-languageservice-4.1.8.tgz", + "integrity": "sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonc-parser": "^3.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-nls": "^5.0.0", + "vscode-uri": "^3.0.2" + }, + "engines": { + "npm": ">=7.0.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-9.0.3.tgz", + "integrity": "sha512-qETYf9PCl1lVfwR+oBmMfc4vP2nqLNA35ABxorw3j06+xlKRgDVxbLgKm4Y/xPL0YWdeDjLoI4ImICIT3nB0zQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.18.4.tgz", + "integrity": "sha512-CCXbyr99dafRJzJH/0Ryk7ly5T8XPmSR0//OuF2w4m+9JiOin7zcAT5iET8GRE7Z9taFBejb3GbM0a0mbjdbrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "9.0.3", + "vscode-languageserver-types": "3.18.4" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.15.tgz", + "integrity": "sha512-Gsfr9S8LU3S6885cS+NaleasJ0n3V5y/SOTqHCifQiXWFw3G9w9/YItDfwHrlCz+dSZWWZ8ilYVMcJe7Sz7Sxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.18.4", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.18.4.tgz", + "integrity": "sha512-nQsacoiijvl0fRf2q28uFDrAVwRhdCa3QveiF7/CX0r18mLR5FU/1WskSsCKOjmEbtteKUnY8x8Yzxop+k0zwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver/node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver/node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-nls": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/vscode-nls/-/vscode-nls-5.2.0.tgz", + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.2.0.tgz", + "integrity": "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.3.tgz", + "integrity": "sha512-uuN0goWfxP22B7J/uAgBpOYNPttC+XVseYE+rSY5+rQ+YBeVz/VORw8WbmLVcqW78zNg5A4qnjNXYUWR3il2ig==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yaml-language-server": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/yaml-language-server/-/yaml-language-server-1.23.0.tgz", + "integrity": "sha512-3qVyCOexLCWw06PQa5kRPwvMWMZ/eZeCRWUvgD6a0OkqL/4iCnxy2WumbWifa937Uo5xhyWJ0uxlU39ljhNh7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vscode/l10n": "^0.0.18", + "ajv": "^8.17.1", + "ajv-draft-04": "^1.0.0", + "ajv-i18n": "^4.2.0", + "prettier": "^3.8.1", + "request-light": "^0.5.7", + "vscode-json-languageservice": "4.1.8", + "vscode-languageserver": "^9.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-languageserver-types": "^3.16.0", + "vscode-uri": "^3.0.2", + "yaml": "2.8.3" + }, + "bin": { + "yaml-language-server": "bin/yaml-language-server" + } + }, + "node_modules/yaml-language-server/node_modules/request-light": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/request-light/-/request-light-0.5.8.tgz", + "integrity": "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml-language-server/node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json index b31a034..4a8d53e 100644 --- a/package.json +++ b/package.json @@ -1,67 +1,59 @@ { - "name": "educates-docusaurus", + "name": "educates-dev", "version": "0.0.0", "private": true, + "type": "module", "scripts": { - "docusaurus": "docusaurus", - "start": "docusaurus start --port 3000 --host 0.0.0.0", - "build": "docusaurus build", - "swizzle": "docusaurus swizzle", - "deploy": "docusaurus deploy", - "clear": "docusaurus clear", - "serve": "docusaurus serve", - "write-translations": "docusaurus write-translations", - "write-heading-ids": "docusaurus write-heading-ids", - "typecheck": "tsc", - "docker-build": "docker buildx build --platform linux/amd64,linux/arm64 -t educates-dev ." + "dev": "astro dev", + "build": "astro build && npm run site-check", + "preview": "astro preview", + "check": "astro check", + "site-check": "node scripts/site-check/cli.ts dist", + "link-check": "lychee --offline --root-dir \"$PWD/dist\" 'dist/**/*.html'", + "docker-build": "sh scripts/docker-build.sh", + "format": "prettier --write \"**/*.{astro,ts,tsx,js,mjs,css}\"", + "format:check": "prettier --check \"**/*.{astro,ts,tsx,js,mjs,css}\"", + "test": "vitest run", + "posters": "node scripts/posters/cli.ts", + "url-check": "node scripts/url-check/cli.ts", + "captures": "node scripts/captures/cli.ts" + }, + "engines": { + "node": ">=24" }, "dependencies": { - "@docusaurus/core": "^3.9.2", - "@docusaurus/plugin-google-gtag": "^3.9.2", - "@docusaurus/plugin-ideal-image": "^3.9.2", - "@docusaurus/preset-classic": "^3.9.2", - "@docusaurus/theme-mermaid": "^3.9.2", - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.1", - "@mdx-js/react": "^3.1.1", - "@mui/icons-material": "^7.1.0", - "@mui/material": "^7.1.0", - "asciinema-player": "^3.15.0", - "clsx": "^2.1.1", - "ityped": "^1.0.3", - "prism-react-renderer": "^2.4.1", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-icons": "^5.5.0", - "sharp": "^0.34.5" + "@astrojs/markdown-satteri": "^0.4.3", + "@astrojs/mdx": "^8.0.3", + "@astrojs/react": "^7.0.1", + "@astrojs/sitemap": "^3.7.4", + "@fontsource/jetbrains-mono": "^5.3.0", + "@fontsource/onest": "^5.3.1", + "asciinema-player": "^3.17.0", + "astro": "^7.3.6", + "astro-expressive-code": "^0.44.2", + "astro-mermaid": "^2.1.0", + "feed": "^6.0.0", + "mermaid": "^11.17.2", + "node-html-parser": "^9.0.4", + "react": "^19.3.0", + "react-dom": "^19.3.0", + "satori": "0.35.1", + "satteri": "^0.10.5", + "sharp": "^0.35.5" }, "devDependencies": { - "@docusaurus/module-type-aliases": "^3.9.2", - "@docusaurus/tsconfig": "^3.9.2", - "@docusaurus/types": "^3.9.2", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", - "typescript": "~5.9.3" - }, - "resolutions": { - "lodash-es": ">=4.17.23", - "seroval": ">=1.4.1", - "seroval-plugins": ">=1.4.1" - }, - "browserslist": { - "production": [ - ">0.5%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 3 chrome version", - "last 3 firefox version", - "last 5 safari version" - ] - }, - "engines": { - "node": ">=18.0" + "@astrojs/check": "^0.9.10", + "@types/node": "^26.6.4", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", + "prettier": "^3.9.9", + "prettier-plugin-astro": "^1.1.0", + "puppeteer-core": "^25.12.0", + "typescript": "^6.0.3", + "vitest": "^5.0.3", + "yaml": "^2.9.1" }, - "packageManager": "yarn@1.22.22" + "volta": { + "node": "26.10.0" + } } diff --git a/static/.nojekyll b/public/.nojekyll similarity index 100% rename from static/.nojekyll rename to public/.nojekyll diff --git a/static/asciinemas/getting-started-on-kind.cast b/public/asciinemas/getting-started-on-kind.cast similarity index 100% rename from static/asciinemas/getting-started-on-kind.cast rename to public/asciinemas/getting-started-on-kind.cast diff --git a/static/asciinemas/install-educates-on-gcloud.cast b/public/asciinemas/install-educates-on-gcloud.cast similarity index 100% rename from static/asciinemas/install-educates-on-gcloud.cast rename to public/asciinemas/install-educates-on-gcloud.cast diff --git a/static/asciinemas/terraform-apply.cast b/public/asciinemas/terraform-apply.cast similarity index 100% rename from static/asciinemas/terraform-apply.cast rename to public/asciinemas/terraform-apply.cast diff --git a/static/asciinemas/terraform-destroy.cast b/public/asciinemas/terraform-destroy.cast similarity index 100% rename from static/asciinemas/terraform-destroy.cast rename to public/asciinemas/terraform-destroy.cast diff --git a/static/asciinemas/terraform-init.cast b/public/asciinemas/terraform-init.cast similarity index 100% rename from static/asciinemas/terraform-init.cast rename to public/asciinemas/terraform-init.cast diff --git a/static/asciinemas/terraform-plan.cast b/public/asciinemas/terraform-plan.cast similarity index 100% rename from static/asciinemas/terraform-plan.cast rename to public/asciinemas/terraform-plan.cast diff --git a/static/asciinemas/terraform-verify-k8s-pods.cast b/public/asciinemas/terraform-verify-k8s-pods.cast similarity index 100% rename from static/asciinemas/terraform-verify-k8s-pods.cast rename to public/asciinemas/terraform-verify-k8s-pods.cast diff --git a/blog/2026-02-24-teaching-an-ai-about-educates/air-framework-workshop.png b/public/assets/images/air-framework-workshop-8ca71a831f82d614ac23141da7f5bf8b.png similarity index 100% rename from blog/2026-02-24-teaching-an-ai-about-educates/air-framework-workshop.png rename to public/assets/images/air-framework-workshop-8ca71a831f82d614ac23141da7f5bf8b.png diff --git a/blog/2025-05-02-verify-educates-cloud-install/arch.png b/public/assets/images/arch-be5e0caa423b4a512d9a52af5ec629c2.png similarity index 100% rename from blog/2025-05-02-verify-educates-cloud-install/arch.png rename to public/assets/images/arch-be5e0caa423b4a512d9a52af5ec629c2.png diff --git a/blog/2025-03-21-how-to-best-work-locally/ca_certificate.png b/public/assets/images/ca_certificate-9df963f62f1778720ce2faf257ce3a4d.png similarity index 100% rename from blog/2025-03-21-how-to-best-work-locally/ca_certificate.png rename to public/assets/images/ca_certificate-9df963f62f1778720ce2faf257ce3a4d.png diff --git a/blog/2025-03-21-how-to-best-work-locally/certificate_not_trusted.png b/public/assets/images/certificate_not_trusted-211dd4ab441d5442c6523543c6ecbf39.png similarity index 100% rename from blog/2025-03-21-how-to-best-work-locally/certificate_not_trusted.png rename to public/assets/images/certificate_not_trusted-211dd4ab441d5442c6523543c6ecbf39.png diff --git a/blog/2025-04-30-install-educates-cloud-cli/cluster-basics.png b/public/assets/images/cluster-basics-68cfe6bfdd65b7ab0aa7720bb07f7990.png similarity index 100% rename from blog/2025-04-30-install-educates-cloud-cli/cluster-basics.png rename to public/assets/images/cluster-basics-68cfe6bfdd65b7ab0aa7720bb07f7990.png diff --git a/blog/2025-04-30-install-educates-cloud-cli/cluster-networking.png b/public/assets/images/cluster-networking-578f02aaa87a5ea5171f9f542afe8467.png similarity index 100% rename from blog/2025-04-30-install-educates-cloud-cli/cluster-networking.png rename to public/assets/images/cluster-networking-578f02aaa87a5ea5171f9f542afe8467.png diff --git a/blog/2025-04-30-install-educates-cloud-cli/cluster-storage.png b/public/assets/images/cluster-storage-cc481bf179af1718b8853db8f9d6674d.png similarity index 100% rename from blog/2025-04-30-install-educates-cloud-cli/cluster-storage.png rename to public/assets/images/cluster-storage-cc481bf179af1718b8853db8f9d6674d.png diff --git a/blog/2025-04-30-install-educates-cloud-cli/dns-admin-role.png b/public/assets/images/dns-admin-role-b8bfce697e60f37fdf15c3f298352b17.png similarity index 100% rename from blog/2025-04-30-install-educates-cloud-cli/dns-admin-role.png rename to public/assets/images/dns-admin-role-b8bfce697e60f37fdf15c3f298352b17.png diff --git a/blog/2026-02-20-clickable-actions-in-workshops/editor-replace-example.png b/public/assets/images/editor-replace-example-b5030836bb6ce40fde38a8c37c05694b.png similarity index 100% rename from blog/2026-02-20-clickable-actions-in-workshops/editor-replace-example.png rename to public/assets/images/editor-replace-example-b5030836bb6ce40fde38a8c37c05694b.png diff --git a/blog/2026-02-26-reviewing-workshops-with-ai/initial-request-prompt.png b/public/assets/images/initial-request-prompt-069903bb5efb9514c8235c3efb0f95a2.png similarity index 100% rename from blog/2026-02-26-reviewing-workshops-with-ai/initial-request-prompt.png rename to public/assets/images/initial-request-prompt-069903bb5efb9514c8235c3efb0f95a2.png diff --git a/blog/2025-05-02-verify-educates-cloud-install/network-trace.png b/public/assets/images/network-trace-3c7bc5c29456557bf70ddecf11bf9cd8.png similarity index 100% rename from blog/2025-05-02-verify-educates-cloud-install/network-trace.png rename to public/assets/images/network-trace-3c7bc5c29456557bf70ddecf11bf9cd8.png diff --git a/blog/2025-05-02-verify-educates-cloud-install/portal.png b/public/assets/images/portal-787c66c9219ddc1d53bbe6796fd36eb9.png similarity index 100% rename from blog/2025-05-02-verify-educates-cloud-install/portal.png rename to public/assets/images/portal-787c66c9219ddc1d53bbe6796fd36eb9.png diff --git a/blog/2025-04-30-install-educates-cloud-cli/root-dns-zone.png b/public/assets/images/root-dns-zone-b6add9d7ceadf53b22c8a0bb226f7a98.png similarity index 100% rename from blog/2025-04-30-install-educates-cloud-cli/root-dns-zone.png rename to public/assets/images/root-dns-zone-b6add9d7ceadf53b22c8a0bb226f7a98.png diff --git a/blog/2025-03-21-how-to-best-work-locally/secured_workshop.png b/public/assets/images/secured_workshop-0e56c65cb82aa3004714cd42a2e32dd1.png similarity index 100% rename from blog/2025-03-21-how-to-best-work-locally/secured_workshop.png rename to public/assets/images/secured_workshop-0e56c65cb82aa3004714cd42a2e32dd1.png diff --git a/blog/2025-05-02-verify-educates-cloud-install/spinner.png b/public/assets/images/spinner-32dcb86f0224ca8b73ce066efa071d0f.png similarity index 100% rename from blog/2025-05-02-verify-educates-cloud-install/spinner.png rename to public/assets/images/spinner-32dcb86f0224ca8b73ce066efa071d0f.png diff --git a/blog/2025-04-30-install-educates-cloud-cli/subdomain-dns-zone.png b/public/assets/images/subdomain-dns-zone-eaabcc03f4495a6a1d4fb05739a39077.png similarity index 100% rename from blog/2025-04-30-install-educates-cloud-cli/subdomain-dns-zone.png rename to public/assets/images/subdomain-dns-zone-eaabcc03f4495a6a1d4fb05739a39077.png diff --git a/blog/2026-02-26-reviewing-workshops-with-ai/summary-and-review.png b/public/assets/images/summary-and-review-54ddcd33d4fdbede5a112ddec6b2fc85.png similarity index 100% rename from blog/2026-02-26-reviewing-workshops-with-ai/summary-and-review.png rename to public/assets/images/summary-and-review-54ddcd33d4fdbede5a112ddec6b2fc85.png diff --git a/blog/2026-02-23-fixed-ip-bridge-macos/swiftbar-plugin.png b/public/assets/images/swiftbar-plugin-07da4379e84ecbec878d2005950f4458.png similarity index 100% rename from blog/2026-02-23-fixed-ip-bridge-macos/swiftbar-plugin.png rename to public/assets/images/swiftbar-plugin-07da4379e84ecbec878d2005950f4458.png diff --git a/blog/2026-02-20-clickable-actions-in-workshops/terminal-execute-example.png b/public/assets/images/terminal-execute-example-b27034651e7ac08ba256e38d738ee3e8.png similarity index 100% rename from blog/2026-02-20-clickable-actions-in-workshops/terminal-execute-example.png rename to public/assets/images/terminal-execute-example-b27034651e7ac08ba256e38d738ee3e8.png diff --git a/blog/2026-02-25-deploying-educates-yourself/training-portal.png b/public/assets/images/training-portal-a2d751f02d602f94c754eb87858dde39.png similarity index 100% rename from blog/2026-02-25-deploying-educates-yourself/training-portal.png rename to public/assets/images/training-portal-a2d751f02d602f94c754eb87858dde39.png diff --git a/blog/2026-02-26-reviewing-workshops-with-ai/trigger-clickable-actions.png b/public/assets/images/trigger-clickable-actions-d9f7e8dbe68494fd14b6913a840377dd.png similarity index 100% rename from blog/2026-02-26-reviewing-workshops-with-ai/trigger-clickable-actions.png rename to public/assets/images/trigger-clickable-actions-d9f7e8dbe68494fd14b6913a840377dd.png diff --git a/blog/2026-02-25-deploying-educates-yourself/workshop-dashboard.png b/public/assets/images/workshop-dashboard-f829cad0e16049aa7f31485c48693347.png similarity index 100% rename from blog/2026-02-25-deploying-educates-yourself/workshop-dashboard.png rename to public/assets/images/workshop-dashboard-f829cad0e16049aa7f31485c48693347.png diff --git a/blog/2025-05-02-verify-educates-cloud-install/workshop-overview.png b/public/assets/images/workshop-overview-1f160d4e24f46060019ee33157cbb857.png similarity index 100% rename from blog/2025-05-02-verify-educates-cloud-install/workshop-overview.png rename to public/assets/images/workshop-overview-1f160d4e24f46060019ee33157cbb857.png diff --git a/blog/2025-05-02-verify-educates-cloud-install/workshop-session-url.png b/public/assets/images/workshop-session-url-6d012eaa0b9c0d45321d5ddf1fa50c93.png similarity index 100% rename from blog/2025-05-02-verify-educates-cloud-install/workshop-session-url.png rename to public/assets/images/workshop-session-url-6d012eaa0b9c0d45321d5ddf1fa50c93.png diff --git a/static/img/favicon.ico b/public/favicon.ico similarity index 100% rename from static/img/favicon.ico rename to public/favicon.ico diff --git a/static/img/educates-social-card.png b/public/img/educates-social-card.png similarity index 100% rename from static/img/educates-social-card.png rename to public/img/educates-social-card.png diff --git a/public/img/favicon.ico b/public/img/favicon.ico new file mode 100644 index 0000000..31e4ff6 Binary files /dev/null and b/public/img/favicon.ico differ diff --git a/static/img/logo.svg b/public/img/logo.svg similarity index 100% rename from static/img/logo.svg rename to public/img/logo.svg diff --git a/public/posts/index.html b/public/posts/index.html new file mode 100644 index 0000000..a63a627 --- /dev/null +++ b/public/posts/index.html @@ -0,0 +1 @@ +Redirecting to: /blog Redirecting from /posts/ to /blog diff --git a/public/posts/installation-kind/index.html b/public/posts/installation-kind/index.html new file mode 100644 index 0000000..256720e --- /dev/null +++ b/public/posts/installation-kind/index.html @@ -0,0 +1 @@ +Redirecting to: /blog/getting-started-on-kind Redirecting from /posts/installation-kind/ to /blog/getting-started-on-kind diff --git a/static/robots.txt b/public/robots.txt similarity index 100% rename from static/robots.txt rename to public/robots.txt diff --git a/scripts/captures/README.md b/scripts/captures/README.md new file mode 100644 index 0000000..c17bc22 --- /dev/null +++ b/scripts/captures/README.md @@ -0,0 +1,149 @@ +# Captures + +The screenshots and the short muted loops on the Features pages come from a +running Educates, not from mock-ups. This folder holds the tool that takes +them, and `captures/` at the repository root holds what it drives: + +- `captures/workshop/` is the capture workshop, Educates Feature Tour. Each + instruction page sets up one shot or a group of them: clickable actions, + examiner checks, the editor, the console, the slides, the Session's own + Docker daemon, registry and Git server, its namespace and quota, the + portal's REST API, the lookup service, workshop definitions, the + Terraform modules' usage and a course written with the AI authoring + skills. Two more definitions in its `resources/` folder run pages of + the same content on a virtual cluster and with no Kubernetes access. +- `captures/custom-site/app.py` is Example Academy, a small front end of + the kind a training team builds: it lists workshops from the lookup + service and starts Sessions, embeds the training portal, receives the + portal's analytics events, and runs an examiner check from outside a + Session. It also serves a catalog for the person signed in, built on + the portal's REST API alone, and the sites of two customers, Acme + Training and Globex Academy, each reaching only its own lookup service + tenant. + +The guides' command outputs are recaptured by hand; see +[guide-outputs.md](guide-outputs.md). + +## What the current captures come from + +The captures in `src/content/features/` were taken on 7 and 8 October 2026 from +Educates 4.0 as built from the `develop` branch of +`jorgemoralespou/educates-training-platform` at commit `a648c25`, on a +local Kind cluster with HTTPS at `educates.test`: + +| Part | Build | +| --- | --- | +| Operator, session manager, secrets manager, training portal, lookup service | images built 2 October 2026 | +| Workshop base image | built 5 October 2026 | +| educates CLI | built 20 September 2026 | +| Air-gapped install's image list | `educates-images-4.0.0-alpha.10.txt`, from the 4.0.0-alpha.10 release, in `fixtures/` | + +The site's 4.0 setting, `site.educates4Released`, was off; 4.0-only +Features are captured all the same, so their visuals are ready when it +turns on. Once Educates 4.0 is released, take the captures again from the +release. + +## What you need + +- A local Educates cluster, from `educates create-cluster` (or + `educates local cluster create`), with an ingress domain of its own and + HTTPS, as the [local environment docs](https://docs.educates.dev/en/stable/getting-started/local-environment.html) + describe. The Session's image registry needs HTTPS. The lookup service + must be turned on. On a cluster without a domain of its own, the shots + of the registry and the lookup service fail. +- The `educates` CLI on your `PATH`, or its path in `EDUCATES_CLI` for + `setup` and `teardown`, and `kubectl` pointing at the cluster. +- Google Chrome. The tool looks for it at its macOS location, or at + `CHROME_PATH`. +- ffmpeg, with libx264, and Docker. +- `tree` and Hugo, for the local authoring shots (`brew install tree hugo`). +- About 6 CPUs and 12 GiB of memory free on the cluster: up to six + Sessions run at once, three of them with a Docker daemon. + +The cluster's certificate comes from a CA of its own, so the browser the +tool starts accepts certificates it cannot verify. Only that browser does, +and only for the run. + +## Taking the captures + +1. Deploy the capture workshop and everything beside it: + + ``` + npm run captures -- setup + ``` + + This publishes `captures/workshop` to the cluster's registry, creates a + training portal of its own, `site-captures`, and deploys the three + workshop definitions to it. It registers the cluster with the lookup + service, with a tenant and two clients, and deploys Example Academy in + the `site-captures-example-academy` namespace. For the two customers it + creates a portal each, `site-captures-acme` and `site-captures-globex`, + listing different capture workshops, and a tenant and a client each. + Running it again publishes the workshop again and updates the rest. + +2. Take every shot, or only some, by shot id or Feature: + + ``` + npm run captures + npm run captures -- examiner-checks lookup-service/request + ``` + + `npm run captures -- list` lists the shots. Each is defined in + `manifest.ts`: the Feature entry and slot it fills, where it is taken + (a Session's dashboard, two side by side, the portal, a page of + Example Academy or two side by side, or a terminal on this machine), + the steps that set it up, its window and its alt text. Shots of the + same Session run in the order of the manifest, so a shot can rely on + the steps of those before it. A shot taken alone gets a new Session, + so take a shot that needs an earlier step, such as the application + deployed, together with the shots before it: + `npm run captures -- clickable-actions examiner-checks`. + + Screenshots are saved as WebP, next to their entry, such as + `src/content/features/examiner-checks/question.webp`, at twice the + window's size and under about 300 KB. Loops are recorded through the + DevTools screencast, encoded as muted H.264 MP4 of 6 to 10 seconds + under about 1 MB, with a WebP poster from their first frame. The local + authoring shots run the `educates` CLI on this machine, in a scratch + folder under the system's temporary folder, and show it in a terminal + window drawn with asciinema-player. + +3. Look at every capture before you commit it: nothing cropped, no + errors, no spinners, nothing personal. Take a shot again by naming it. + +4. The tool wires each capture into its Feature entry as it writes it: + the Feature's `visual`, its deep page's `page.loop`, or a thing's + `visual` or `loop`, with the alt text from the manifest. Check that + every slot is filled and wired with: + + ``` + npm run captures -- check + ``` + + The unit tests run the same check, and the site check warns about any + placeholder left. + +5. Remove the portal and everything `setup` created: + + ``` + npm run captures -- teardown + ``` + + It deletes the `site-captures` portal with its Sessions, the workshop + definitions deployed to it, the customers' portals, the portal the + branding shot creates for itself, Example Academy's namespace, and the + lookup service resources and cluster-wide resources labelled + `app.kubernetes.io/part-of=site-captures`, among them the + `educates-config` namespace when `setup` created it. It leaves other + portals alone. The workshop content images it published stay in the + local registry, `localhost:5001/lab-site-captures-files` and + `localhost:5001/lab-new-workshop-files`, as do the images the cluster + pulled. + +## Changing a shot + +Change its steps in `manifest.ts`, and the instruction page it uses in +`captures/workshop/workshop/content/`. After changing the workshop, run +`setup` again so new Sessions get the new content. A shot that needs a new +slot on a Feature page starts with the entry: the manifest check fails +until every slot has exactly one shot of its kind. diff --git a/scripts/captures/browser.ts b/scripts/captures/browser.ts new file mode 100644 index 0000000..d8f8e95 --- /dev/null +++ b/scripts/captures/browser.ts @@ -0,0 +1,468 @@ +// Drives Chrome against the capture portal: opens Sessions as anonymous +// learners, works through the capture workshop's instructions, and takes +// pages of the portal and of Example Academy. +// +// The cluster's ingress certificate comes from a local certificate +// authority, so this browser, and only this browser, accepts certificates +// it cannot verify. + +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { setTimeout as sleep } from "node:timers/promises"; +import puppeteer, { type Browser, type Frame, type Page } from "puppeteer-core"; +import type { SessionName, Step } from "./manifest.ts"; + +/** Chrome on macOS, unless `CHROME_PATH` names another. */ +export const chromePath = + process.env.CHROME_PATH ?? + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; + +/** The window most shots are taken in, in CSS pixels. */ +export const defaultViewport = { width: 1152, height: 720 }; + +/** How much sharper than CSS pixels screenshots are. */ +export const screenshotScale = 2; + +export interface Portal { + url: string; + accessCode: string; +} + +/** Where shots of the cluster are taken: the capture portal and Example Academy. */ +export interface Targets { + portal: Portal; + siteUrl: string; +} + +export class Capture { + readonly #browser: Browser; + readonly #findTargets: () => Targets; + #targets: Targets | undefined; + readonly #pages = new Map(); + + private constructor(browser: Browser, findTargets: () => Targets) { + this.#browser = browser; + this.#findTargets = findTargets; + } + + /** + * Starts the browser. `findTargets` is asked for the portal and Example + * Academy the first time a shot needs them, so shots that need neither, + * such as a terminal's, run without the capture portal. + */ + static async launch(findTargets: () => Targets): Promise { + const browser = await puppeteer.launch({ + executablePath: chromePath, + headless: true, + acceptInsecureCerts: true, + defaultViewport: { ...defaultViewport, deviceScaleFactor: 1 }, + args: ["--hide-scrollbars", "--force-color-profile=srgb"], + }); + return new Capture(browser, findTargets); + } + + get #portal(): Portal { + this.#targets ??= this.#findTargets(); + return this.#targets.portal; + } + + get #siteUrl(): string { + this.#targets ??= this.#findTargets(); + return this.#targets.siteUrl; + } + + async close() { + await this.#browser.close(); + } + + /** + * The page of a browser context of its own, created the first time it is + * asked for, so each name is a different anonymous learner. + */ + async page(name: string): Promise { + const existing = this.#pages.get(name); + if (existing) return existing; + const context = await this.#browser.createBrowserContext(); + const page = await context.newPage(); + const cdp = await page.createCDPSession(); + await cdp.send("Browser.setDownloadBehavior", { behavior: "deny" }); + this.#pages.set(name, page); + return page; + } + + /** Signs a page's learner in to the portal, past its access code. */ + async signIn(page: Page) { + await page.goto(`${this.#portal.url}/`, { waitUntil: "load" }); + if (page.url().includes("/workshops/access/")) { + await page.type("#id_password", this.#portal.accessCode); + await Promise.all([ + page.waitForNavigation({ waitUntil: "load" }), + page.click("button[type=submit]"), + ]); + } + } + + /** A page of the portal, for a learner signed in past its access code. */ + async portalPage(path: string, name = "portal"): Promise { + const page = await this.page(name); + // The portal restarts for a minute or two after its workshops change. + await waitFor( + async () => { + if (!page.url().startsWith(this.#portal.url)) await this.signIn(page); + const response = await page.goto(`${this.#portal.url}${path}`, { + waitUntil: "load", + }); + return ( + (response?.status() ?? 500) < 500 && + !page.url().includes("/workshops/access/") + ); + }, + 300_000, + `the portal page ${path}`, + ); + return page; + } + + /** A page of Example Academy, for a learner signed in to the portal. */ + async sitePage(path: string, name = "portal"): Promise { + const page = await this.page(name); + if (page.url() === "about:blank") await this.signIn(page); + await page.goto(`${this.#siteUrl}${path}`, { waitUntil: "load" }); + return page; + } + + /** + * A Session of the workshop the portal lists as `title`, started the + * first time `name` is asked for, with its dashboard open. + */ + async session(name: SessionName, title: string): Promise { + const known = this.#pages.has(name); + const page = await this.page(name); + if (known) return page; + // The portal restarts for a minute or two after its workshops change, + // so the catalog is read until it lists the workshop. + const findStart = async () => { + if (!page.url().includes("/workshops/catalog/")) await this.signIn(page); + await page.goto(`${this.#portal.url}/workshops/catalog/`, { + waitUntil: "load", + }); + const start = await page.evaluateHandle((workshop) => { + const cards = [...document.querySelectorAll(".card")]; + const card = cards.find( + (element) => + element + .querySelector(".card-title, h5, h4") + ?.textContent?.trim() === workshop, + ); + return ( + card?.querySelector("a.start-workshop, a.btn") ?? null + ); + }, title); + return start.asElement(); + }; + let link = await findStart().catch(() => null); + const deadline = Date.now() + 600_000; + while (!link && Date.now() < deadline) { + await sleep(5000); + link = await findStart().catch(() => null); + } + if (!link) + throw new Error(`the portal lists no workshop titled "${title}"`); + await Promise.all([ + page.waitForNavigation({ waitUntil: "load", timeout: 300_000 }), + (link as unknown as { click(): Promise }).click(), + ]); + let attempts = 1; + await waitFor( + async () => { + // A Session deleted as it was handed out sends the learner back to + // the catalog, so the workshop is started again. + if (page.url().includes("/workshops/catalog/") && attempts < 3) { + attempts += 1; + const again = await findStart(); + if (again) { + await Promise.all([ + page.waitForNavigation({ waitUntil: "load", timeout: 300_000 }), + (again as unknown as { click(): Promise }).click(), + ]); + } + return false; + } + // The dashboard, or the instructions in it, can fail to load while + // a new Session starts. + const session = page + .frames() + .find((candidate) => candidate.name() === "session"); + if (session?.url().startsWith("chrome-error:")) { + await page.reload({ waitUntil: "load" }); + return false; + } + const frame = instructions(page); + if (!frame) return false; + if (frame.url().startsWith("chrome-error:")) { + await frame.goto( + new URL("/workshop/content/", dashboard(page).url()).href, + ); + return false; + } + return frame.evaluate( + () => document.querySelector(".page-content") !== null, + ); + }, + 600_000, + `the dashboard of the ${name} Session`, + ).catch(async (error: Error) => { + const screenshot = join(tmpdir(), `captures-${name}-session.png`); + await page + .screenshot({ path: screenshot as `${string}.png` }) + .catch(() => undefined); + throw new Error(`${error.message} (at ${page.url()}, see ${screenshot})`); + }); + // Gives the terminals time to connect. + await sleep(4000); + return page; + } +} + +/** Polls `check` until it is true, or fails after `timeoutMs`. */ +export async function waitFor( + check: () => Promise, + timeoutMs: number, + what: string, +) { + const deadline = Date.now() + timeoutMs; + while (!(await check().catch(() => false))) { + if (Date.now() > deadline) throw new Error(`timed out waiting for ${what}`); + await sleep(500); + } +} + +/** The frame of the Session's dashboard, inside the portal's page. */ +export function dashboard(page: Page): Frame { + // A frame's name is fixed when the frame is created, and the portal + // creates the Session's frame before it names it, so its URL tells too. + return ( + page + .frames() + .find( + (frame) => + frame.name() === "session" || + new URL(frame.url()).pathname === "/dashboard/", + ) ?? page.mainFrame() + ); +} + +/** The frame of the workshop instructions. */ +export function instructions(page: Page): Frame | undefined { + return page + .frames() + .find( + (frame) => + frame.name() === "workshop-iframe" || + frame.url().includes("/workshop/content/"), + ); +} + +function requireInstructions(page: Page): Frame { + const frame = instructions(page); + if (!frame) throw new Error("the page has no workshop instructions"); + return frame; +} + +/** Runs the steps of a shot on a page, in order. */ +export async function runSteps( + page: Page, + steps: readonly Step[], + fixtures: string, + options: { front?: boolean } = {}, +) { + // Chrome holds back pages in the background, so the page comes first, + // unless two pages are being recorded side by side. + if (steps.length > 0 && options.front !== false) await page.bringToFront(); + for (const step of steps) await runStep(page, step, fixtures); +} + +async function runStep(page: Page, step: Step, fixtures: string) { + if ("page" in step) { + // The dashboard can still be redirecting its instructions to the first + // page, or replace their frame, so the frame is looked up on each try. + // A frame that failed to load shows Chrome's error page, so the page's + // URL comes from the Session's own dashboard. + const path = `/workshop/content/${step.page}/`; + const url = new URL(path, dashboard(page).url()).href; + // A page that stays blank is loaded again. + let loaded = 0; + await waitFor( + async () => { + const frame = requireInstructions(page); + if (frame.url() !== url || Date.now() - loaded > 10_000) { + loaded = Date.now(); + await frame.goto(url, { waitUntil: "load" }); + return false; + } + return frame.evaluate( + () => document.querySelector(".page-content") !== null, + ); + }, + 60_000, + `page ${step.page}`, + ).catch(async (error: Error) => { + const screenshot = join(tmpdir(), `captures-page-${step.page}.png`); + await page + .screenshot({ path: screenshot as `${string}.png` }) + .catch(() => undefined); + const frames = page + .frames() + .map((frame) => `${frame.name() || "-"} ${frame.url()}`); + throw new Error( + `${error.message}, expected ${url}; frames: ${frames.join(", ")}; see ${screenshot}`, + ); + }); + await sleep(1200); + } else if ("click" in step) { + await clickAction(page, step.click, step.wait ?? 30_000); + } else if ("fill" in step) { + const field = `#clickable-action-${step.fill} [name="${step.field}"]`; + const frame = requireInstructions(page); + await frame.click(field, { count: 3 }); + await frame.type(field, step.value, { delay: 60 }); + } else if ("submit" in step) { + await clickAction( + page, + step.submit, + 30_000, + `#clickable-action-${step.submit} [type=submit]`, + ); + } else if ("upload" in step) { + const frame = requireInstructions(page); + const input = await frame.$( + `#clickable-action-${step.upload} input[type=file]`, + ); + if (!input) throw new Error(`action ${step.upload} has no file to upload`); + await ( + input as unknown as { uploadFile(path: string): Promise } + ).uploadFile(`${fixtures}/${step.file}`); + await clickAction( + page, + step.upload, + 30_000, + `#clickable-action-${step.upload} button, #clickable-action-${step.upload} [type=submit]`, + ); + } else if ("tab" in step) { + await openTab(page, step.tab); + } else if ("console" in step) { + const frame = page + .frames() + .find((candidate) => candidate.url().includes("://console-")); + if (!frame) throw new Error("the Session has no console open"); + const url = new URL(frame.url()); + const namespace = + url.hash.match(/namespace=([^&]+)/)?.[1] ?? + url.hostname.split(".")[0]!.replace(/^console-/, ""); + await frame.goto(`${url.origin}/#/${step.console}?namespace=${namespace}`, { + waitUntil: "load", + }); + } else if ("press" in step) { + await page.click(step.press); + } else if ("text" in step) { + await waitFor( + async () => { + if ( + await page.evaluate( + (text) => document.body.innerText.includes(text), + step.text, + ) + ) { + return true; + } + await sleep(5000); + await page.reload({ waitUntil: "load" }); + return false; + }, + 600_000, + `the page to show "${step.text}"`, + ); + } else if ("hover" in step) { + await dashboard(page).hover(step.hover); + await sleep(600); + } else if ("pause" in step) { + await sleep(step.pause); + } else if ("scroll" in step) { + const frame = requireInstructions(page); + if (step.scroll === "top") { + await frame.evaluate(() => { + for (const element of [ + document.scrollingElement, + ...document.querySelectorAll("*"), + ]) { + if (element && element.scrollTop > 0) element.scrollTop = 0; + } + }); + } else { + await frame.$eval(`#clickable-action-${step.scroll}`, (element) => + element.scrollIntoView({ block: "start" }), + ); + } + await sleep(400); + } +} + +/** Brings a dashboard tab to the front, by its label. */ +async function openTab(page: Page, label: string) { + const clicked = await dashboard(page).evaluate((wanted) => { + const tab = [ + ...document.querySelectorAll("a[id$='-tab'], [role=tab]"), + ].find((element) => element.textContent?.trim() === wanted); + tab?.click(); + return tab !== undefined; + }, label); + if (!clicked) throw new Error(`the dashboard has no ${label} tab`); + await sleep(800); +} + +/** + * Clicks clickable action `number` (or `target` inside it), and waits for + * it to finish, for up to `waitMs`. A wait of 0 does not wait. + */ +async function clickAction( + page: Page, + number: number, + waitMs: number, + target?: string, +) { + const frame = requireInstructions(page); + const selector = `#clickable-action-${number}`; + const before = await frame.$eval( + selector, + (element) => (element as HTMLElement).dataset.actionCompleted ?? "", + ); + await frame.$eval(selector, (element) => + element.scrollIntoView({ block: "nearest" }), + ); + await sleep(250); + await frame.click(target ?? `${selector} .clickable-action__header`); + if (waitMs === 0) { + await sleep(400); + return; + } + await waitFor( + () => + frame.$eval( + selector, + (element, previous) => { + const data = (element as HTMLElement).dataset; + return ( + (data.actionResult === "success" || + data.actionResult === "failure") && + (data.actionCompleted ?? "") !== previous + ); + }, + before, + ), + waitMs, + `action ${number} to finish`, + ).catch((error: Error) => + console.warn(`captures: ${error.message}; carrying on`), + ); + await sleep(600); +} diff --git a/scripts/captures/cli.ts b/scripts/captures/cli.ts new file mode 100644 index 0000000..0ba2252 --- /dev/null +++ b/scripts/captures/cli.ts @@ -0,0 +1,26 @@ +// Takes the screenshots and recordings on the Features pages from a running +// Educates. Usage: +// +// npm run captures -- setup deploy the capture workshop and its portal +// npm run captures [-- ...] take every shot, or only those named +// npm run captures -- list list the shots +// npm run captures -- check check the manifest against the entries +// npm run captures -- teardown remove everything setup created +// +// See scripts/captures/README.md. + +import { setup, teardown } from "./cluster.ts"; + +const [command, ...rest] = process.argv.slice(2); + +try { + if (command === "setup") setup(); + else if (command === "teardown") teardown(); + else { + const { main } = await import("./run.ts"); + await main(command === undefined ? [] : [command, ...rest]); + } +} catch (error) { + console.error(`captures: ${(error as Error).message}`); + process.exitCode = 1; +} diff --git a/scripts/captures/cluster.ts b/scripts/captures/cluster.ts new file mode 100644 index 0000000..863743c --- /dev/null +++ b/scripts/captures/cluster.ts @@ -0,0 +1,797 @@ +// Sets up and removes everything the captures need on the cluster: the +// capture workshop in a training portal of its own, the lookup service's +// cluster, tenant and clients, two customers kept apart by the lookup +// service, each with a portal, a tenant and a client of its own, and +// Example Academy, the small front end in captures/custom-site that stands +// in for a training team's own site and for the customers' sites. +// +// Every resource it creates carries the label `partOf` (or is named for +// the portal), so `teardown()` removes exactly what `setup()` created and +// nothing else. + +import { execFileSync } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { stringify } from "yaml"; + +/** The training portal the capture workshop is deployed to. */ +export const portal = "site-captures"; + +/** The label on every other resource the captures create. */ +export const partOf = { "app.kubernetes.io/part-of": portal }; +const partOfSelector = "app.kubernetes.io/part-of=site-captures"; + +/** Where Example Academy runs. */ +export const siteNamespace = "site-captures-example-academy"; +/** The hostname prefix of Example Academy, under the ingress domain. */ +export const siteHost = "example-academy"; +/** + * Where the training portal posts its analytics events: Example Academy's + * service inside the cluster, which the portal reaches directly. + */ +export const eventsUrl = `http://${siteHost}.${siteNamespace}.svc.cluster.local:8080/events`; + +/** The lookup service tenant Example Academy reaches. */ +export const lookupTenant = "example-academy"; + +/** + * Two customers one lookup service keeps apart. Each has a training portal + * listing some of the capture workshops, by their definitions' names, a + * tenant that picks that portal, and a client granted only that tenant, + * which its site in Example Academy logs in as. + */ +export const customers = [ + { + id: "acme", + name: "Acme Training", + initials: "AT", + colour: "#7a2e1f", + workshops: ["lab-site-captures", "lab-site-captures-vcluster"], + }, + { + id: "globex", + name: "Globex Academy", + initials: "GA", + colour: "#1f5f4a", + workshops: ["lab-site-captures-python"], + }, +] as const; + +type Customer = (typeof customers)[number]; + +/** A customer's training portal. */ +function customerPortal(customer: Customer) { + return `${portal}-${customer.id}`; +} + +/** A customer's tenant of the lookup service. */ +function customerTenant(customer: Customer) { + return customer.id; +} + +/** A customer's client of the lookup service, granted only its tenant. */ +function customerClient(customer: Customer) { + return `${customer.id}-site`; +} + +const workshopDir = resolve("captures/workshop"); +const siteApp = resolve("captures/custom-site/app.py"); + +/** + * The capture workshop's definitions, in the order the portal lists them, + * with the options each is deployed with. The first holds most pages, and + * keeps a Session in reserve so a new one opens at once. + */ +export const workshops = [ + { + file: "resources/workshop.yaml", + options: [ + "--initial", + "1", + "--reserved", + "1", + "--expires", + "60m", + "--deadline", + "90m", + ], + withEnv: true, + }, + { + file: "resources/workshop-virtual-cluster.yaml", + options: [], + withEnv: false, + }, + { + file: "resources/workshop-no-kubernetes.yaml", + options: [], + withEnv: false, + }, +] as const; + +function run( + command: string, + args: string[], + options: { cwd?: string; input?: string } = {}, +) { + return execFileSync(command, args, { + cwd: options.cwd, + input: options.input, + encoding: "utf8", + stdio: [options.input === undefined ? "ignore" : "pipe", "pipe", "inherit"], + }); +} + +function kubectl(args: string[], input?: string) { + return run("kubectl", args, input === undefined ? {} : { input }); +} + +function educates(args: string[]) { + const cli = process.env.EDUCATES_CLI ?? "educates"; + return run(cli, args, { cwd: workshopDir }); +} + +function apply(objects: object[]) { + kubectl( + ["apply", "-f", "-"], + objects.map((object) => stringify(object)).join("---\n"), + ); +} + +function json(args: string[]) { + return JSON.parse(kubectl([...args, "-o", "json"])); +} + +/** The cluster's ingress: its domain, protocol and wildcard certificate. */ +export function clusterIngress() { + const status = json(["get", "educatesclusterconfig", "cluster"]).status + .ingress; + return { + domain: status.domain as string, + protocol: status.protocol as string, + certificate: status.wildcardCertificateSecretRef as { + name: string; + namespace: string; + }, + }; +} + +/** The training portal's URL, access code and robot account. */ +export function portalDetails(name = portal) { + const resource = json(["get", "trainingportal", name]); + const educatesStatus = resource.status.educates; + return { + url: educatesStatus.url as string, + accessCode: (resource.spec.portal?.password ?? "") as string, + robot: { + username: educatesStatus.credentials.robot.username as string, + password: educatesStatus.credentials.robot.password as string, + clientId: educatesStatus.clients.robot.id as string, + clientSecret: educatesStatus.clients.robot.secret as string, + }, + }; +} + +function password() { + return randomBytes(18).toString("base64url"); +} + +function lookupConfig( + sitePassword: string, + adminPassword: string, + customerPasswords: Record, +) { + const metadata = (name: string) => ({ + name, + namespace: "educates-config", + labels: partOf, + }); + return [ + { + apiVersion: "lookup.educates.dev/v1beta1", + kind: "ClusterConfig", + metadata: metadata("local-cluster"), + }, + { + apiVersion: "lookup.educates.dev/v1beta1", + kind: "TenantConfig", + metadata: metadata(lookupTenant), + spec: { + clusters: { nameSelector: { matchNames: ["local-cluster"] } }, + portals: { nameSelector: { matchNames: [portal] } }, + }, + }, + { + apiVersion: "lookup.educates.dev/v1beta1", + kind: "ClientConfig", + metadata: metadata("example-academy-site"), + spec: { + client: { password: sitePassword }, + roles: ["tenant"], + tenants: [lookupTenant], + }, + }, + { + apiVersion: "lookup.educates.dev/v1beta1", + kind: "ClientConfig", + metadata: metadata("platform-admin"), + spec: { + client: { password: adminPassword }, + roles: ["admin"], + tenants: ["*"], + }, + }, + ...customers.flatMap((customer) => [ + { + apiVersion: "lookup.educates.dev/v1beta1", + kind: "TenantConfig", + metadata: metadata(customerTenant(customer)), + spec: { + clusters: { nameSelector: { matchNames: ["local-cluster"] } }, + portals: { + nameSelector: { matchNames: [customerPortal(customer)] }, + }, + }, + }, + { + apiVersion: "lookup.educates.dev/v1beta1", + kind: "ClientConfig", + metadata: metadata(customerClient(customer)), + spec: { + client: { password: customerPasswords[customer.id] }, + roles: ["tenant"], + tenants: [customerTenant(customer)], + }, + }, + ]), + ]; +} + +function exampleAcademy( + ingress: ReturnType, + details: ReturnType, + sitePassword: string, + customerPasswords: Record, +) { + const labels = { ...partOf, app: siteHost }; + const metadata = (name: string) => ({ + name, + namespace: siteNamespace, + labels, + }); + const host = `${siteHost}.${ingress.domain}`; + return [ + { + apiVersion: "v1", + kind: "Namespace", + metadata: { name: siteNamespace, labels: partOf }, + }, + { + // The secrets manager copies the ingress certificate in, for the + // site's HTTPS and for the CA it trusts when calling the lookup service. + apiVersion: "secrets.educates.dev/v1beta1", + kind: "SecretCopier", + metadata: { name: siteNamespace, labels: partOf }, + spec: { + rules: [ + { + sourceSecret: ingress.certificate, + targetNamespaces: { nameSelector: { matchNames: [siteNamespace] } }, + }, + ], + }, + }, + { + apiVersion: "v1", + kind: "ConfigMap", + metadata: metadata("example-academy-app"), + data: { "app.py": readFileSync(siteApp, "utf8") }, + }, + { + apiVersion: "v1", + kind: "Secret", + metadata: metadata("example-academy-lookup"), + stringData: { password: sitePassword }, + }, + { + // The portal's robot account, for the catalog built on its REST API. + apiVersion: "v1", + kind: "Secret", + metadata: metadata("example-academy-portal"), + stringData: { + "client-id": details.robot.clientId, + "client-secret": details.robot.clientSecret, + username: details.robot.username, + password: details.robot.password, + }, + }, + { + // The customers' sites, each with the client it logs in as. + apiVersion: "v1", + kind: "Secret", + metadata: metadata("example-academy-customers"), + stringData: { + customers: JSON.stringify( + customers.map((customer) => ({ + id: customer.id, + name: customer.name, + initials: customer.initials, + colour: customer.colour, + tenant: customerTenant(customer), + username: customerClient(customer), + password: customerPasswords[customer.id], + })), + ), + }, + }, + { apiVersion: "v1", kind: "ServiceAccount", metadata: metadata(siteHost) }, + { + // Read access to deployments, for the check it runs from outside a + // Session. + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "ClusterRole", + metadata: { name: siteNamespace, labels: partOf }, + rules: [ + { apiGroups: ["apps"], resources: ["deployments"], verbs: ["get"] }, + ], + }, + { + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "ClusterRoleBinding", + metadata: { name: siteNamespace, labels: partOf }, + roleRef: { + apiGroup: "rbac.authorization.k8s.io", + kind: "ClusterRole", + name: siteNamespace, + }, + subjects: [ + { kind: "ServiceAccount", name: siteHost, namespace: siteNamespace }, + ], + }, + { + apiVersion: "apps/v1", + kind: "Deployment", + metadata: metadata(siteHost), + spec: { + selector: { matchLabels: { app: siteHost } }, + template: { + metadata: { labels }, + spec: { + serviceAccountName: siteHost, + securityContext: { + runAsNonRoot: true, + seccompProfile: { type: "RuntimeDefault" }, + }, + containers: [ + { + name: "site", + image: "python:3.13-alpine", + command: ["python3", "/opt/app/app.py"], + env: [ + { + name: "LOOKUP_URL", + value: `${ingress.protocol}://lookup.${ingress.domain}`, + }, + { name: "LOOKUP_TENANT", value: lookupTenant }, + { name: "LOOKUP_USERNAME", value: "example-academy-site" }, + { + name: "LOOKUP_PASSWORD", + valueFrom: { + secretKeyRef: { + name: "example-academy-lookup", + key: "password", + }, + }, + }, + { name: "PORTAL_URL", value: details.url }, + ...( + [ + ["ROBOT_CLIENT_ID", "client-id"], + ["ROBOT_CLIENT_SECRET", "client-secret"], + ["ROBOT_USERNAME", "username"], + ["ROBOT_PASSWORD", "password"], + ] as const + ).map(([name, key]) => ({ + name, + valueFrom: { + secretKeyRef: { name: "example-academy-portal", key }, + }, + })), + { + name: "CUSTOMERS", + valueFrom: { + secretKeyRef: { + name: "example-academy-customers", + key: "customers", + }, + }, + }, + { name: "SITE_URL", value: `${ingress.protocol}://${host}` }, + { name: "EVENTS_URL", value: eventsUrl }, + { name: "SSL_CERT_FILE", value: "/opt/ingress-ca/ca.crt" }, + ], + ports: [{ containerPort: 8080 }], + readinessProbe: { httpGet: { path: "/healthz", port: 8080 } }, + securityContext: { + runAsUser: 1000, + allowPrivilegeEscalation: false, + capabilities: { drop: ["ALL"] }, + }, + volumeMounts: [ + { name: "app", mountPath: "/opt/app" }, + { name: "ingress-ca", mountPath: "/opt/ingress-ca" }, + ], + }, + ], + volumes: [ + { name: "app", configMap: { name: "example-academy-app" } }, + { + name: "ingress-ca", + secret: { + secretName: ingress.certificate.name, + optional: true, + items: [{ key: "ca.crt", path: "ca.crt" }], + }, + }, + ], + }, + }, + }, + }, + { + apiVersion: "v1", + kind: "Service", + metadata: metadata(siteHost), + spec: { + selector: { app: siteHost }, + ports: [{ port: 8080, targetPort: 8080 }], + }, + }, + { + apiVersion: "networking.k8s.io/v1", + kind: "Ingress", + metadata: metadata(siteHost), + spec: { + ingressClassName: json(["get", "educatesclusterconfig", "cluster"]) + .status.ingress.ingressClassName, + tls: [{ hosts: [host], secretName: ingress.certificate.name }], + rules: [ + { + host, + http: { + paths: [ + { + path: "/", + pathType: "Prefix", + backend: { + service: { name: siteHost, port: { number: 8080 } }, + }, + }, + ], + }, + }, + ], + }, + }, + ]; +} + +/** + * Publishes the capture workshop, deploys it to its own training portal, + * and sets up the lookup service, the customers' portals and Example + * Academy beside it. + */ +export function setup(log: (message: string) => void = console.log) { + const ingress = clusterIngress(); + log(`setup: publishing the capture workshop from ${workshopDir}`); + educates(["publish-workshop"]); + + if (!exists("trainingportal", portal)) { + // The portal takes its analytics webhook and the sites allowed to embed + // it when it is created, so it is created here with them, as + // `educates create-portal` would create it otherwise. + log(`setup: creating the training portal ${portal}`); + kubectl(["create", "-f", "-"], stringify(trainingPortal(portal, ingress))); + } + + log("setup: configuring the lookup service"); + const sitePassword = password(); + const adminPassword = password(); + const customerPasswords = Object.fromEntries( + customers.map((customer) => [customer.id, password()]), + ); + if (!namespaceExists("educates-config")) { + apply([ + { + apiVersion: "v1", + kind: "Namespace", + metadata: { name: "educates-config", labels: partOf }, + }, + ]); + } + apply(lookupConfig(sitePassword, adminPassword, customerPasswords)); + + const details = waitForPortal(); + log( + `setup: deploying Example Academy at ${ingress.protocol}://${siteHost}.${ingress.domain}`, + ); + apply(exampleAcademy(ingress, details, sitePassword, customerPasswords)); + kubectl([ + "-n", + siteNamespace, + "rollout", + "restart", + `deployment/${siteHost}`, + ]); + + const env = { + PORTAL_URL: details.url, + ROBOT_USERNAME: details.robot.username, + ROBOT_PASSWORD: details.robot.password, + ROBOT_CLIENT_ID: details.robot.clientId, + ROBOT_CLIENT_SECRET: details.robot.clientSecret, + LOOKUP_URL: `${ingress.protocol}://lookup.${ingress.domain}`, + LOOKUP_TENANT: lookupTenant, + LOOKUP_USERNAME: "example-academy-site", + LOOKUP_PASSWORD: sitePassword, + LOOKUP_ADMIN_USERNAME: "platform-admin", + LOOKUP_ADMIN_PASSWORD: adminPassword, + EVENTS_URL: eventsUrl, + }; + for (const workshop of workshops) { + log(`setup: deploying ${workshop.file}`); + const envArgs = workshop.withEnv + ? Object.entries(env).flatMap(([name, value]) => [ + "--env", + `${name}=${value}`, + ]) + : []; + educates([ + "deploy-workshop", + "--portal", + portal, + "--workshop-file", + workshop.file, + ...workshop.options, + ...envArgs, + ]); + } + + waitForPortal(); + // A customer's portal lists workshop definitions deployed above, so it is + // created once they exist. + for (const customer of customers) { + const name = customerPortal(customer); + if (!exists("trainingportal", name)) { + log(`setup: creating the training portal ${name} for ${customer.name}`); + const resource = trainingPortal(name, ingress); + // `educates deploy-workshop` names a definition after the portal, the + // workshop, and a hash of where the workshop's files are. + const listed = captureDefinitions(); + resource.spec.workshops = customer.workshops.map((workshop) => { + const definition = listed.find((candidate) => + new RegExp(`^${portal}--${workshop}-[0-9a-f]+$`).test(candidate), + ); + if (!definition) + throw new Error(`the capture portal lists no workshop ${workshop}`); + return { name: definition, capacity: 1 }; + }) as never[]; + kubectl(["create", "-f", "-"], stringify(resource)); + } + } + for (const customer of customers) waitForPortal(customerPortal(customer)); + kubectl([ + "-n", + siteNamespace, + "rollout", + "status", + `deployment/${siteHost}`, + "--timeout=180s", + ]); + log("setup: done"); +} + +/** + * A training portal for the captures, as `educates create-portal` makes + * one, with an access code, anonymous learners and room for 12 Sessions, + * plus Example Academy's webhook for analytics and permission to embed the + * portal in its pages. `branding` gives it a title and logo of its own. + */ +export function trainingPortal( + name: string, + ingress: ReturnType, + branding?: { title: string; logo: string }, +) { + return { + apiVersion: "training.educates.dev/v1beta1", + kind: "TrainingPortal", + metadata: { name, labels: partOf }, + spec: { + portal: { + password: randomBytes(9).toString("base64url"), + registration: { type: "anonymous" }, + sessions: { maximum: 12 }, + updates: { workshop: true }, + theme: { + frame: { + ancestors: [`${ingress.protocol}://${siteHost}.${ingress.domain}`], + }, + }, + ...branding, + }, + analytics: { webhook: { url: eventsUrl } }, + workshops: [], + }, + }; +} + +function exists(kind: string, name: string) { + const names = kubectl(["get", kind, "-o", "name"]).split("\n"); + return names.some((line) => line.endsWith(`/${name}`)); +} + +function namespaceExists(name: string) { + return exists("namespaces", name); +} + +/** + * Waits until the portal is running and answers in the browser, which it + * does a minute or two after it starts, and returns its details. + */ +export function waitForPortal(name = portal, timeoutMs = 600_000) { + const deadline = Date.now() + timeoutMs; + for (;;) { + if (Date.now() > deadline) + throw new Error(`training portal ${name} is not answering`); + try { + const resource = json(["get", "trainingportal", name]); + const url = resource.status?.educates?.url as string | undefined; + if (resource.status?.educates?.phase === "Running" && url) { + // The cluster's ingress certificate comes from a local CA, so + // this check does not verify it. + const status = execFileSync( + "curl", + [ + "-sk", + "-m", + "10", + "-o", + "/dev/null", + "-w", + "%{http_code}", + `${url}/`, + ], + { encoding: "utf8" }, + ); + if (status === "302" || status === "200") return portalDetails(name); + } + } catch { + // Not there yet. + } + execFileSync("sleep", ["5"]); + } +} + +/** + * Restarts the portal, which then picks up workshop environments it failed + * to activate, as it can when its database is busy, and waits until it + * answers again. + */ +export function restartPortal() { + const namespace = `${portal}-ui`; + kubectl([ + "-n", + namespace, + "rollout", + "restart", + "deployment/training-portal", + ]); + kubectl([ + "-n", + namespace, + "rollout", + "status", + "deployment/training-portal", + "--timeout=300s", + ]); + waitForPortal(); +} + +/** + * The capture workshops' definitions, which `educates deploy-workshop` + * created for the capture portal, in the order it lists them. + */ +function captureDefinitions(): string[] { + const listed = json(["get", "trainingportal", portal]).spec.workshops as { + name: string; + }[]; + return listed + .map(({ name }) => name) + .filter((name) => name.startsWith(`${portal}--lab-site-captures`)); +} + +/** The portal with a title and logo of its own, for the branding shot. */ +export const brandedPortal = `${portal}-academy`; + +/** + * Creates the branded portal, listing the capture workshops, and returns + * its details once it answers. It takes its title and logo when created. + */ +export function createBrandedPortal(branding: { title: string; logo: string }) { + const ingress = clusterIngress(); + const resource = trainingPortal(brandedPortal, ingress, branding); + resource.spec.workshops = captureDefinitions().map((name) => ({ + name, + capacity: 1, + })) as never[]; + kubectl(["delete", "trainingportal", brandedPortal, "--ignore-not-found"]); + kubectl(["create", "-f", "-"], stringify(resource)); + return waitForPortal(brandedPortal); +} + +/** Deletes the branded portal. */ +export function deleteBrandedPortal() { + kubectl([ + "delete", + "trainingportal", + brandedPortal, + "--ignore-not-found", + "--wait=false", + ]); +} + +/** + * Removes the portal, with its workshops and Sessions, and everything else + * `setup()` created. + */ +export function teardown(log: (message: string) => void = console.log) { + log(`teardown: deleting the training portal ${portal}`); + try { + educates(["delete-portal", "--portal", portal]); + } catch { + log(`teardown: no training portal ${portal}`); + } + // The customers' portals, and the portal with a title and logo of its + // own, for its shot. + kubectl([ + "delete", + "trainingportals", + "-l", + partOfSelector, + "--ignore-not-found", + ]); + // The workshop definitions `educates deploy-workshop` created for the + // portal, which it names after it, and which outlive the portal. + const definitions = kubectl(["get", "workshops", "-o", "name"]) + .split("\n") + .filter((name) => + name.startsWith(`workshop.training.educates.dev/${portal}--`), + ); + if (definitions.length > 0) { + log(`teardown: deleting ${definitions.length} workshop definitions`); + kubectl(["delete", ...definitions, "--ignore-not-found"]); + } + log( + "teardown: deleting Example Academy and the lookup service configuration", + ); + kubectl(["delete", "namespace", siteNamespace, "--ignore-not-found"]); + for (const kind of ["secretcopier", "clusterrolebinding", "clusterrole"]) { + kubectl(["delete", kind, "-l", partOfSelector, "--ignore-not-found"]); + } + for (const kind of ["clientconfigs", "tenantconfigs", "clusterconfigs"]) { + if (namespaceExists("educates-config")) { + kubectl([ + "-n", + "educates-config", + "delete", + kind, + "-l", + partOfSelector, + "--ignore-not-found", + ]); + } + } + kubectl(["delete", "namespace", "-l", partOfSelector, "--ignore-not-found"]); + log("teardown: done"); +} diff --git a/scripts/captures/entries.ts b/scripts/captures/entries.ts new file mode 100644 index 0000000..8232dc3 --- /dev/null +++ b/scripts/captures/entries.ts @@ -0,0 +1,28 @@ +// Reads the Feature entries the way the capture tool needs them: their ids +// and the frontmatter that holds their visuals, without Astro's content +// layer. + +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { parse } from "yaml"; +import type { FeatureEntry } from "./slots.ts"; + +/** The folder of the Feature entries, from the repository root. */ +export const featuresDir = "src/content/features"; + +/** The path of a Feature entry's Markdown file. */ +export function entryFile(id: string, dir = featuresDir): string { + return join(dir, `${id}.md`); +} + +/** Every Feature entry in `dir`, in file name order. */ +export function readFeatureEntries(dir = featuresDir): FeatureEntry[] { + return readdirSync(dir) + .filter((name) => name.endsWith(".md")) + .sort() + .map((name) => { + const markdown = readFileSync(join(dir, name), "utf8"); + const frontmatter = /^---\n([\s\S]*?)\n---\n/.exec(markdown)?.[1] ?? ""; + return { id: name.replace(/\.md$/, ""), data: parse(frontmatter) ?? {} }; + }); +} diff --git a/scripts/captures/fixtures/educates-images-4.0.0-alpha.10.txt b/scripts/captures/fixtures/educates-images-4.0.0-alpha.10.txt new file mode 100644 index 0000000..40f627a --- /dev/null +++ b/scripts/captures/fixtures/educates-images-4.0.0-alpha.10.txt @@ -0,0 +1,42 @@ +debian:sid-20230502-slim@sha256:25095ed790e32ed43236eb0c793a9959deda9d623bd527aa44a11d51c371397f +docker.io/envoyproxy/envoy:v1.31.5@sha256:2ef22f7a72d3008d68bd4912f52b9a3d466d00db31a567dffa1b103e42eb9198 +docker.io/envoyproxy/envoy:v1.35.10@sha256:8537189cdf7beb9369ed3584dc263141b8eeb556576d297182ba747591722ae5 +docker:27.5.1-dind@sha256:aa3df78ecf320f5fafdce71c659f1629e96e9de0968305fe1de670e0ca9176ce +ghcr.io/educates/educates-assets-server:4.0.0-alpha.10@sha256:0991779b241308d8551edb4235e71d84f8e7b084132a7e55415ede4fceed634d +ghcr.io/educates/educates-base-environment:4.0.0-alpha.10@sha256:204baca8578765fc735b0ade5013f2fa7f8953eec0d7d197e224ededd76b994b +ghcr.io/educates/educates-conda-environment:4.0.0-alpha.10@sha256:b8242f41eacedd31a0f65a6d40e094680473f6fde325d596b0600f630b576e9c +ghcr.io/educates/educates-docker-registry:4.0.0-alpha.10@sha256:0116843f1f621f1ebcbb315d9b250c9b994bc157708ac68334450035092760be +ghcr.io/educates/educates-image-cache:4.0.0-alpha.10@sha256:21ef91987b15d8a4ef6aeedc90fad1230fbd14707122bf607587900afa5744b9 +ghcr.io/educates/educates-jdk11-environment:4.0.0-alpha.10@sha256:c76ae3f4d45f9a6c8d54dd2b1c591075b515e209b6415721af26418964dfe5ea +ghcr.io/educates/educates-jdk17-environment:4.0.0-alpha.10@sha256:53e9020e8aab6b1fbe30c25867b67c36b3241f8e41fc0fec3f6fce4de0954df1 +ghcr.io/educates/educates-jdk21-environment:4.0.0-alpha.10@sha256:28d88eacb3745e82c9f56e9e576dc580c42729aa23b0b20dd53fe49fc3ad849a +ghcr.io/educates/educates-jdk8-environment:4.0.0-alpha.10@sha256:c3de40cc7135e41f5ea2fa64901f042a162467eb39e445acc572fa17aad869aa +ghcr.io/educates/educates-lookup-service:4.0.0-alpha.10@sha256:b249cd0bf1dcc8b03944804f1605d2f460676c8d1ba3cf502aad4384d0442f74 +ghcr.io/educates/educates-node-ca-injector:4.0.0-alpha.10@sha256:a3275be908c6e00cc00f9f19e6f30a0db3fda39d31df83e62ee52d12615d8c15 +ghcr.io/educates/educates-operator:4.0.0-alpha.10@sha256:6b69646befd78e2be7e8da8d9fb09607392518d15304eb1926d2198640889f09 +ghcr.io/educates/educates-pause-container:4.0.0-alpha.10@sha256:6174c07753a3dec00405cd2573e2a7f02c036125df0f1f5c8a5ede5b672dab4b +ghcr.io/educates/educates-secrets-manager:4.0.0-alpha.10@sha256:6f992416c9e27c495ffa58c4f631addda026efe12711a8016206f440d9cbfcf1 +ghcr.io/educates/educates-session-manager:4.0.0-alpha.10@sha256:d9f216353533cef7dd79cc81bd6730c209978f73fe70c92828970f9422181b31 +ghcr.io/educates/educates-training-portal:4.0.0-alpha.10@sha256:17e1c9c1883a00b164c957443448c6c754c3ddae05ee361806a9a3cbf0920a70 +ghcr.io/educates/educates-tunnel-manager:4.0.0-alpha.10@sha256:e4fb7cde8fa87c2bdfa5aa7a78276c68b5fa571a97dd15f480e501570f6ca74f +ghcr.io/kyverno/readiness-checker:latest@sha256:56f19b2ed9dd7f2ad1176026dfb51c20478659d737761d5314b22268666fa705 +ghcr.io/kyverno/readiness-checker:v1.18.1@sha256:455400758235cf448dd9be3374ecb9e8bf543ee0c39adc880d25ed28cc7eb69d +ghcr.io/loft-sh/kubernetes:v1.33.4@sha256:47a6cc7a605e29018e504ef268829ac002364f685d2bdb4a38336b15cd54dabc +ghcr.io/loft-sh/kubernetes:v1.34.0@sha256:2c5c4154208cc566085df4c16744eeec8b4e155b71168566e7be654c80dcf959 +ghcr.io/loft-sh/kubernetes:v1.35.0@sha256:90097a08b87cd988e3c48f3885e9a459f25e9ea584bb8e77f13aff9471204fd0 +ghcr.io/loft-sh/kubernetes:v1.36.0@sha256:003a79a8b919489b1cd2adbc091aac8de489b60732747c5f2b919b4b5166b246 +ghcr.io/loft-sh/vcluster-oss:0.35.2@sha256:9c6fcf5bfb4d696ad3885c1609656c7b1a32c3c6c56244597702cef8f9476264 +ghcr.io/projectcontour/contour:v1.30.2@sha256:ba4fd19911bd603e3857fafdd17bd44c687ea1ab52b12f8ddc7971ba68f9b532 +ghcr.io/projectcontour/contour:v1.33.5@sha256:fc005b50254df59fab555fc008501df4552ae2208783663f6c28931b3e5849e4 +quay.io/jetstack/cert-manager-acmesolver:v1.20.3@sha256:d8d948a9fa0fb9eeb9fd2432dee4945cc202c840ee25a733f0b238fd89fe5cbb +quay.io/jetstack/cert-manager-cainjector:v1.20.3@sha256:06ad347fe0dc2eb84cc355c26f6752e05e87dceb6447f5cd29b963dd66dfd8bd +quay.io/jetstack/cert-manager-controller:v1.20.3@sha256:6c13d61e0348a5bc3477f8ea9a928624300b30d19b1c72a7d2b90372fc713db4 +quay.io/jetstack/cert-manager-startupapicheck:v1.20.3@sha256:b0c9a2c18d02f3c40a9e091a6fe9afdf39cbf7fb2d9acc18c9881c21c625f2dc +quay.io/jetstack/cert-manager-webhook:v1.20.3@sha256:a61e817632cebed3bb59a189327e786fa3fdd7597167d994a1848d98fd55848f +reg.kyverno.io/kyverno/background-controller:v1.18.1@sha256:33a19d2ae18ee0b8e650172f65985f35acd8909d6eeca4032465cca1ba022831 +reg.kyverno.io/kyverno/cleanup-controller:v1.18.1@sha256:e9b3342264c82ae35f4fe3abd0e563e613bb54eae6ca493134f0fd4cbacfc0d1 +reg.kyverno.io/kyverno/kyverno-cli:v1.18.1@sha256:b7e272572d244ddec0b83469f7200ba883555bf69de4b294cee52a197c8c6590 +reg.kyverno.io/kyverno/kyverno:v1.18.1@sha256:dcd8cf6de2158cd8334fc728f9c4eb521e2c006320a59d69a9b91af87ac8f41c +reg.kyverno.io/kyverno/kyvernopre:v1.18.1@sha256:e72b2309cde1d77de494c17e519dfe1dff9c1ee4d64327e80298a018bb612628 +reg.kyverno.io/kyverno/reports-controller:v1.18.1@sha256:4972c80bccbafecc65406ec938abf029f0a428c56ea86f4e5cd11f63d951aad9 +registry.k8s.io/external-dns/external-dns:v0.21.0@sha256:f53faaf71cb270d1ca9dce6ea0c94bfebf1a18696263487f0fbc74b9bf2bd7ff diff --git a/scripts/captures/fixtures/eks.yaml b/scripts/captures/fixtures/eks.yaml new file mode 100644 index 0000000..2eac213 --- /dev/null +++ b/scripts/captures/fixtures/eks.yaml @@ -0,0 +1,9 @@ +apiVersion: cli.educates.dev/v1alpha1 +kind: EducatesEKSConfig +aws: + accountId: "123456789012" + region: eu-west-1 + route53HostedZoneId: Z0123456789ABCDEF +domain: educates.example.com +acme: + email: admin@example.com diff --git a/scripts/captures/fixtures/gke.yaml b/scripts/captures/fixtures/gke.yaml new file mode 100644 index 0000000..1e6d40e --- /dev/null +++ b/scripts/captures/fixtures/gke.yaml @@ -0,0 +1,7 @@ +apiVersion: cli.educates.dev/v1alpha1 +kind: EducatesGKEConfig +gcp: + project: my-gcp-project +domain: educates.example.com +acme: + email: admin@example.com diff --git a/scripts/captures/fixtures/openshift.yaml b/scripts/captures/fixtures/openshift.yaml new file mode 100644 index 0000000..fe134a5 --- /dev/null +++ b/scripts/captures/fixtures/openshift.yaml @@ -0,0 +1,8 @@ +apiVersion: cli.educates.dev/v1alpha1 +kind: EducatesInlineConfig +domain: educates.apps.example.com +ingressClassName: openshift-default +wildcardCertificateSecret: wildcard-tls +policyEnforcement: + clusterEngine: OpenShiftSCC + workshopEngine: Kyverno diff --git a/scripts/captures/fixtures/reading-list.json b/scripts/captures/fixtures/reading-list.json new file mode 100644 index 0000000..cdc836a --- /dev/null +++ b/scripts/captures/fixtures/reading-list.json @@ -0,0 +1,7 @@ +{ + "books": [ + { "title": "The Pragmatic Programmer", "status": "reading" }, + { "title": "Designing Data-Intensive Applications", "status": "next" }, + { "title": "Kubernetes in Action", "status": "done" } + ] +} diff --git a/scripts/captures/frames.ts b/scripts/captures/frames.ts new file mode 100644 index 0000000..fc7fe96 --- /dev/null +++ b/scripts/captures/frames.ts @@ -0,0 +1,39 @@ +// Turns the frames of a DevTools screencast into a video. Chrome sends a +// frame only when the page changes, so each frame is shown for as long as +// the screen stayed that way: until the next frame arrived, or the +// recording ended. + +/** A frame saved from the screencast, with when Chrome painted it, in seconds. */ +export interface Frame { + file: string; + timestamp: number; +} + +/** + * An ffmpeg concat list showing the frames from `start` to `end`, each + * until the next. The screen at `start` is the last frame painted before + * it; frames painted after `end` are left out. + */ +export function frameList( + frames: readonly Frame[], + recording: { start: number; end: number }, +): string { + const sorted = [...frames].sort((a, b) => a.timestamp - b.timestamp); + const firstShown = Math.max( + 0, + sorted.findLastIndex((frame) => frame.timestamp <= recording.start), + ); + const shown = sorted + .slice(firstShown) + .filter((frame) => frame.timestamp < recording.end); + const lines = ["ffconcat version 1.0"]; + shown.forEach((frame, index) => { + const from = Math.max(frame.timestamp, recording.start); + const until = shown[index + 1]?.timestamp ?? recording.end; + lines.push(`file '${frame.file}'`, `duration ${(until - from).toFixed(3)}`); + }); + // ffmpeg ignores the duration of the last entry unless it is repeated. + const last = shown.at(-1); + if (last) lines.push(`file '${last.file}'`); + return `${lines.join("\n")}\n`; +} diff --git a/scripts/captures/guide-outputs.md b/scripts/captures/guide-outputs.md new file mode 100644 index 0000000..81f05b2 --- /dev/null +++ b/scripts/captures/guide-outputs.md @@ -0,0 +1,262 @@ +# Release-specific strings in the Getting Started Guides + +The Getting Started Guides (`src/content/guides/`) show versions and +command outputs that belong to one Educates release: the release the +guides' install commands download. Those commands fetch +`releases/latest`, so the guides follow the latest released CLI, GitHub's +"Latest" on `educates/educates-training-platform`, not an unreleased build. +The guides currently follow **3.8.0**. + +This file lists, page by page, which strings are release-specific, where +each value comes from, and how to refresh them for a new release. + +## Ground rules + +- **Find the release.** `gh release list -R educates/educates-training-platform --limit 3` + shows which tag is "Latest". +- **Read the source at the tag, never from memory, and never check it + out.** From a local clone of the platform: + `git -C show :`. Without a clone: + `gh api "repos/educates/educates-training-platform/contents/?ref=" -H "Accept: application/vnd.github.raw"`. + The same `gh api` form reads Kind, Kubernetes, kapp and imgpkg at their + own tags. +- **Run the released CLI only for what is safe.** Download it into a new, + empty temporary directory, check it against the release's + `checksums.txt`, and run only `version` and `--help`: + + ```sh + dir=$(mktemp -d) + gh release download -R educates/educates-training-platform \ + -p educates-darwin-arm64 -p checksums.txt -D "$dir" + (cd "$dir" && grep educates-darwin-arm64 checksums.txt | shasum -a 256 -c) + chmod +x "$dir/educates-darwin-arm64" + "$dir/educates-darwin-arm64" version + "$dir/educates-darwin-arm64" deploy-workshop --help + ``` + +- **Outputs that need a cluster** (pod names, ages, timings, digests, + change counts) come only from a real run on a fresh machine or VM. When + you cannot run one, keep the output's shape, change only what the source + at the tag fixes (versions, image and component names, columns), and + list what you could not verify. +- **Keep colon-joined values in code.** The Markdown pipeline parses + something like `kindest/node:v1.36.1` in prose as a directive and drops + it silently, so versions with a colon stay in code spans or code blocks. + +## Values for 3.8.0 + +| Value | 3.8.0 | Where it comes from | +| --- | --- | --- | +| Educates CLI version | `3.8.0` | `educates version` of the released binary | +| Kind library | `v0.32.0` | `sigs.k8s.io/kind` in `client-programs/go.mod` at the tag | +| Kind node image | `kindest/node:v1.36.1` | `pkg/apis/config/defaults/image.go` in Kind at `v0.32.0` | +| Kubernetes minor for the kubectl repositories | `v1.36` | the node image's Kubernetes version | +| kubectl patch release | `v1.36.5` | `https://dl.k8s.io/release/stable-1.36.txt` | +| Kustomize in that kubectl | `v5.8.1` | `sigs.k8s.io/kustomize/kustomize/v5` in Kubernetes' `go.mod` at `v1.36.5` | +| Kyverno | `v1.15.1` | `vendir.yml` at the tag | +| Contour | `v1.30.2` | `vendir.yml` at the tag | +| kapp library | `v0.64.2` | `carvel.dev/kapp` in `client-programs/go.mod` at the tag | + +The installer bundle's paths below are relative to +`carvel-packages/installer/bundle/config/ytt/_ytt_lib/` in the platform +repository, written as `/`. + +## Set up + +### Installing Docker (`setup/docker.md`) + +Nothing here follows the Educates release. The `hello-world` output is +Docker's own; its `(arm64v8)` line names the architecture of the machine +it was captured on. + +### Installing kubectl (`setup/kubectl.mdx`) + +Release-specific strings: + +- `v1.36` in the six `pkgs.k8s.io/core:/stable:/v1.36/...` URLs of the + Debian, Red Hat and SUSE tabs. +- `Client Version: v1.36.5` and `Kustomize Version: v5.8.1` in the + sample output. + +The repositories follow the Kubernetes minor of the Kind node image the +CLI creates its cluster from (see the next page), so kubectl matches the +cluster it talks to. To refresh: + +```sh +# The minor's package repository exists +curl -fsSL -o /dev/null -w '%{http_code}\n' https://pkgs.k8s.io/core:/stable:/v/deb/Release.key +# Its current patch release +curl -fsSL https://dl.k8s.io/release/stable-.txt +# The Kustomize that kubectl reports +gh api "repos/kubernetes/kubernetes/contents/go.mod?ref=" \ + -H "Accept: application/vnd.github.raw" | grep kustomize/kustomize/v5 +``` + +Not pinned on purpose: the release binary command reads `stable.txt`, +the newest Kubernetes release, and Homebrew and MacPorts install their +newest kubectl, so on those paths a reader may get a newer minor than the +sample output shows. + +### Installing the Educates CLI (`setup/educates.mdx`) + +Release-specific string: `3.8.0` in the `educates version` output. The +command prints the version alone +(`client-programs/pkg/cmd/project_version_cmd.go`); take it from the +released binary as in the ground rules. + +### Creating an Educates cluster (`setup/create-cluster.md`) + +Release-specific strings and where they come from: + +- **`kindest/node:v1.36.1`** in "Ensuring node image". The CLI's + `--kind-cluster-image` defaults to empty (see `create-cluster --help`), + so Kind uses its default node image. Read the Kind version from + `client-programs/go.mod`, then the image from Kind: + + ```sh + gh api "repos/kubernetes-sigs/kind/contents/pkg/apis/config/defaults/image.go?ref=" \ + -H "Accept: application/vnd.github.raw" + ``` + + Kind shows the image without its `@sha256:` digest. +- **Kind's lines** (`Creating cluster`, the ` ✓ ` status lines, ` • Ready + after`, the kubectl context lines and the closing salutation) come from + Kind at that version: `pkg/cluster/internal/create/create.go`, the + actions under `pkg/cluster/internal/create/actions/`, and + `pkg/internal/cli/status.go` for the ` ✓ %s` format. The salutation is + one of four, picked at random. +- **`Cluster config used is saved to: `** comes from + `client-programs/pkg/cluster/kindcluster.go`. It prints on one line + with two spaces, because `Println` adds a space between its two + operands. The directory is the XDG data home plus `educates` + (`client-programs/pkg/utils/dirs.go`): `~/Library/Application Support` + on macOS, `~/.local/share` on Linux. +- **The registry lines** come from `client-programs/pkg/registry/registry.go`. +- **The kapp lines.** 3.8.0 installs Educates with the kapp library + (`client-programs/pkg/installer/installer.go`), which prints + `